diff --git a/src/courseware/course/CourseTabsNavigation.jsx b/src/courseware/course/CourseTabsNavigation.jsx index a4ed69cd..ce0da856 100644 --- a/src/courseware/course/CourseTabsNavigation.jsx +++ b/src/courseware/course/CourseTabsNavigation.jsx @@ -20,7 +20,7 @@ function CourseTabsNavigation({ {tabs.map(({ url, title, slug }) => ( {title} diff --git a/src/courseware/sequence/SequenceNavigation.jsx b/src/courseware/sequence/SequenceNavigation.jsx index 19cb280f..d3be70d4 100644 --- a/src/courseware/sequence/SequenceNavigation.jsx +++ b/src/courseware/sequence/SequenceNavigation.jsx @@ -7,6 +7,7 @@ import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons import { FormattedMessage } from '@edx/frontend-platform/i18n'; import UnitButton from './UnitButton'; +import SequenceNavigationTabs from './SequenceNavigationTabs'; export default function SequenceNavigation({ activeUnitId, @@ -20,16 +21,6 @@ export default function SequenceNavigation({ showCompletion, unitIds, }) { - const unitButtons = unitIds.map(unitId => ( - - )); - return ( ); } diff --git a/src/tabs/useIndexOfLastVisibleChild.js b/src/tabs/useIndexOfLastVisibleChild.js new file mode 100644 index 00000000..94dfcc00 --- /dev/null +++ b/src/tabs/useIndexOfLastVisibleChild.js @@ -0,0 +1,77 @@ +import { useLayoutEffect, useRef, useState } from 'react'; +import useWindowSize from './useWindowSize'; + +const invisibleStyle = { + position: 'absolute', + left: 0, + pointerEvents: 'none', + visibility: 'hidden', +}; + +/** + * This hook will find the index of the last child of a containing element + * that fits within its bounding rectangle. This is done by summing the widths + * of the children until they exceed the width of the container. + * + * The hook returns an array containing: + * [indexOfLastVisibleChild, containerElementRef, invisibleStyle, overflowElementRef] + * + * indexOfLastVisibleChild - the index of the last visible child + * containerElementRef - a ref to be added to the containing html node + * invisibleStyle - a set of styles to be applied to child of the containing node + * if it needs to be hidden. These styles remove the element visually, from + * screen readers, and from normal layout flow. But, importantly, these styles + * preserve the width of the element, so that future width calculations will + * still be accurate. + * overflowElementRef - a ref to be added to an html node inside the container + * that is likely to be used to contain a "More" type dropdown or other + * mechanism to reveal hidden children. The width of this element is always + * included when determining which children will fit or not. Usage of this ref + * is optional. + */ +export default function useIndexOfLastVisibleChild() { + const containerElementRef = useRef(null); + const overflowElementRef = useRef(null); + const containingRectRef = useRef({}); + const [indexOfLastVisibleChild, setIndexOfLastVisibleChild] = useState(-1); + const windowSize = useWindowSize(); + + useLayoutEffect(() => { + const containingRect = containerElementRef.current.getBoundingClientRect(); + + // No-op if the width is unchanged. + // (Assumes tabs themselves don't change count or width). + if (!containingRect.width === containingRectRef.current.width) { + return; + } + // Update for future comparison + containingRectRef.current = containingRect; + + // Get array of child nodes from NodeList form + const childNodesArr = Array.prototype.slice.call(containerElementRef.current.children); + const { nextIndexOfLastVisibleChild } = childNodesArr + // filter out the overflow element + .filter(childNode => childNode !== overflowElementRef.current) + // sum the widths to find the last visible element's index + .reduce((acc, childNode, index) => { + // use floor to prevent rounding errors + acc.sumWidth += Math.floor(childNode.getBoundingClientRect().width); + if (acc.sumWidth <= containingRect.width) { + acc.nextIndexOfLastVisibleChild = index; + } + 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: overflowElementRef.current ? overflowElementRef.current.getBoundingClientRect().width : 0, + nextIndexOfLastVisibleChild: -1, + }); + + + setIndexOfLastVisibleChild(nextIndexOfLastVisibleChild); + }, [windowSize, containerElementRef.current]); + + return [indexOfLastVisibleChild, containerElementRef, invisibleStyle, overflowElementRef]; +}