);
@@ -33,7 +34,6 @@ function CourseTabsNavigation({
CourseTabsNavigation.propTypes = {
activeTabSlug: PropTypes.string,
- className: PropTypes.string,
tabs: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired,
@@ -45,7 +45,6 @@ CourseTabsNavigation.propTypes = {
CourseTabsNavigation.defaultProps = {
activeTabSlug: undefined,
- className: null,
};
export default injectIntl(CourseTabsNavigation);
diff --git a/src/courseware/course/NavTab.jsx b/src/courseware/course/NavTab.jsx
deleted file mode 100644
index 1bb3f1e5..00000000
--- a/src/courseware/course/NavTab.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import { getConfig } from '@edx/frontend-platform';
-
-
-export default function NavTab(props) {
- const {
- isActive, url, title, ...attrs
- } = props;
-
- const className = classNames(
- 'nav-item nav-link',
- { active: isActive },
- attrs.className,
- );
-
- // TODO: We probably don't want to blindly add LMS_BASE_URL here. I think it's more likely
- // that the course metadata API should provide us fully qualified URLs.
- return {title};
-}
-
-NavTab.propTypes = {
- className: PropTypes.string,
- isActive: PropTypes.bool,
- title: PropTypes.string.isRequired,
- url: PropTypes.string.isRequired,
-};
-
-NavTab.defaultProps = {
- className: undefined,
- isActive: false,
-};
diff --git a/src/index.scss b/src/index.scss
index 918d21aa..995bd2d6 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -46,21 +46,26 @@ $primary: #1176B2;
}
.course-tabs-navigation {
- margin-top: 4px;
border-bottom: solid 1px #EAEAEA;
}
.nav-underline-tabs {
- margin: 0;
+ margin: 0 0 -1px;
.nav-link {
border-bottom: 4px solid transparent;
+ border-top: 4px solid transparent;
color: theme-color('gray', 400);
+ // temporary until we can remove .btn class from dropdowns
+ border-left: 0;
+ border-right: 0;
+ border-radius: 0;
+
&:hover,
&:focus,
&.active {
font-weight: $font-weight-normal;
color: theme-color('primary', 500);
- border-color: theme-color('primary', 500);
+ border-bottom-color: theme-color('primary', 500);
}
}
}
diff --git a/src/tabs/Tabs.jsx b/src/tabs/Tabs.jsx
new file mode 100644
index 00000000..32e7a58d
--- /dev/null
+++ b/src/tabs/Tabs.jsx
@@ -0,0 +1,119 @@
+import React, {
+ useLayoutEffect, useRef, useMemo, useState,
+} from 'react';
+import PropTypes from 'prop-types';
+import { Dropdown } from '@edx/paragon';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import classNames from 'classnames';
+import useWindowSize from './useWindowSize';
+
+const invisibleStyle = {
+ position: 'absolute',
+ left: 0,
+ pointerEvents: 'none',
+ visibility: 'hidden',
+};
+export default function Tabs({ children, className, ...attrs }) {
+ const [cutOffIndex, setCutOffIndex] = useState(React.Children.count(children));
+ const windowSize = useWindowSize();
+ const navElementRef = useRef(null);
+ const tabsRectRef = useRef({});
+ const overflowEl = useRef(null);
+
+ // eslint-disable-next-line prefer-arrow-callback
+ useLayoutEffect(function findCutOffIndex() {
+ const tabsRect = navElementRef.current.getBoundingClientRect();
+
+ // No-op if the width is unchanged.
+ // (Assumes tabs themselves don't change count or width).
+ if (!tabsRect.width === tabsRectRef.current.width) {
+ return;
+ }
+ // Update for future comparison
+ tabsRectRef.current = tabsRect;
+
+ // Get array of child nodes from NodeList form
+ const childNodesArray = Array.prototype.slice.call(navElementRef.current.children);
+ // Use reduce to sum the widths of child nodes and determine the new cutoff index
+ const { lastFittingChildIndex } = childNodesArray.reduce((acc, childNode) => {
+ const isOverflowElement = childNode === overflowEl.current;
+ if (isOverflowElement) {
+ return acc;
+ }
+
+ acc.sumWidth += childNode.getBoundingClientRect().width;
+
+ if (acc.sumWidth <= tabsRect.width) {
+ acc.lastFittingChildIndex += 1;
+ }
+
+ return acc;
+ }, {
+ // Include the overflow element's width to begin with. Doing this means
+ // sometimes we'll show a dropdown with one item in it when it would fit,
+ // but allowing this case dramatically simplifies the calculations we need
+ // to do above.
+ sumWidth: overflowEl.current.getBoundingClientRect().width,
+ lastFittingChildIndex: 0,
+ });
+
+ setCutOffIndex(lastFittingChildIndex);
+ }, [windowSize]);
+
+ const tabChildren = useMemo(() => {
+ const childrenArray = React.Children.toArray(children);
+
+ // All tabs will be rendered. Those that would overflow are set to invisible.
+ const wrappedChildren = childrenArray.map((child, index) => (
+
+ {React.cloneElement(child)}
+
+ ));
+
+ // Build the list of items to put in the overflow menu
+ const overflowChildren = childrenArray.slice(cutOffIndex)
+ .map((overflowChild) => React.cloneElement(overflowChild, { className: 'dropdown-item' }));
+
+ // Insert the overflow menu at the cut off index (even if it will be hidden
+ // it so it can be part of measurements)
+ wrappedChildren.splice(cutOffIndex, 0, (
+