From dc38d63d1427a559602950e1f02c0e53e6fd652d Mon Sep 17 00:00:00 2001 From: Viktor Rusakov <52399399+viktorrusakov@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:43:08 +0200 Subject: [PATCH] fix: observe container resizing in useIndexOfLastVisibleChild hook instead of window resize (#2962) --- src/Tabs/index.jsx | 32 ++++---- .../tests/useIndexOfLastVisibleChild.test.jsx | 6 +- src/hooks/useIndexOfLastVisibleChild.jsx | 74 +++++++++---------- src/hooks/useIndexOfLastVisibleChild.mdx | 6 +- 4 files changed, 61 insertions(+), 57 deletions(-) diff --git a/src/Tabs/index.jsx b/src/Tabs/index.jsx index 409315c146..140593c48a 100644 --- a/src/Tabs/index.jsx +++ b/src/Tabs/index.jsx @@ -1,4 +1,10 @@ -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { + useEffect, + useMemo, + useRef, + useState, + useCallback, +} from 'react'; import classNames from 'classnames'; import PropTypes from 'prop-types'; import BaseTabs from 'react-bootstrap/Tabs'; @@ -18,15 +24,15 @@ function Tabs({ activeKey, ...props }) { - const containerElementRef = useRef(null); + const [containerElementRef, setContainerElementRef] = useState(null); const overflowElementRef = useRef(null); const indexOfLastVisibleChild = useIndexOfLastVisibleChild( - containerElementRef.current?.children[0], + containerElementRef?.firstChild, overflowElementRef.current?.parentNode, ); useEffect(() => { - if (containerElementRef.current) { + if (containerElementRef) { const observer = new MutationObserver((mutations => { mutations.forEach(mutation => { // React-Bootstrap attribute 'data-rb-event-key' is responsible for the tab identification @@ -35,8 +41,8 @@ function Tabs({ const isActive = mutation.target.getAttribute('aria-selected') === 'true'; // datakey attribute is added manually to the dropdown // elements so that they correspond to the native tabs' eventKey - const element = containerElementRef.current.querySelector(`[datakey='${eventKey}']`); - const moreTab = containerElementRef.current.querySelector('.pgn__tab_more'); + const element = containerElementRef.querySelector(`[datakey='${eventKey}']`); + const moreTab = containerElementRef.querySelector('.pgn__tab_more'); if (isActive) { element?.classList.add('active'); // Here we add active class to the 'More Tab' if element exists in the dropdown @@ -50,13 +56,13 @@ function Tabs({ } }); })); - observer.observe(containerElementRef.current, { + observer.observe(containerElementRef, { attributes: true, subtree: true, attributeFilter: ['aria-selected'], }); return () => observer.disconnect(); } return undefined; - }, []); + }, [containerElementRef]); useEffect(() => { if (overflowElementRef.current?.parentNode) { @@ -64,10 +70,10 @@ function Tabs({ } }, [overflowElementRef.current?.parentNode]); - const handleDropdownTabClick = (eventKey) => { - const hiddenTab = containerElementRef.current.querySelector(`[data-rb-event-key='${eventKey}']`); + const handleDropdownTabClick = useCallback((eventKey) => { + const hiddenTab = containerElementRef.querySelector(`[data-rb-event-key='${eventKey}']`); hiddenTab.click(); - }; + }, [containerElementRef]); const tabsChildren = useMemo(() => { const indexOfOverflowStart = indexOfLastVisibleChild + 1; @@ -165,10 +171,10 @@ function Tabs({ /> )); return childrenList; - }, [activeKey, children, defaultActiveKey, indexOfLastVisibleChild, moreTabText]); + }, [activeKey, children, defaultActiveKey, indexOfLastVisibleChild, moreTabText, handleDropdownTabClick]); return ( -
+
+
Element 1
Element 2
Element 3
diff --git a/src/hooks/useIndexOfLastVisibleChild.jsx b/src/hooks/useIndexOfLastVisibleChild.jsx index 458b1b3d82..786375cdbf 100644 --- a/src/hooks/useIndexOfLastVisibleChild.jsx +++ b/src/hooks/useIndexOfLastVisibleChild.jsx @@ -1,7 +1,5 @@ import { useLayoutEffect, useState } from 'react'; -import useWindowSize from './useWindowSize'; - /** * 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 @@ -10,48 +8,48 @@ import useWindowSize from './useWindowSize'; * @param {Element} containerElementRef - container element * @param {Element} overflowElementRef - overflow element * - * The hook returns an array containing: - * [indexOfLastVisibleChild, containerElementRef, overflowElementRef] - * - * indexOfLastVisibleChild - the index of the last visible child - * containerElementRef - a ref to be added to the containing html node - * 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. + * The hook returns the index of the last visible child. */ const useIndexOfLastVisibleChild = (containerElementRef, overflowElementRef) => { const [indexOfLastVisibleChild, setIndexOfLastVisibleChild] = useState(-1); - const windowSize = useWindowSize(); useLayoutEffect(() => { - if (!containerElementRef) { - return; + function updateLastVisibleChildIndex() { + // Get array of child nodes from NodeList form + const childNodesArr = Array.prototype.slice.call(containerElementRef.children); + const { nextIndexOfLastVisibleChild } = childNodesArr + // filter out the overflow element + .filter(childNode => childNode !== overflowElementRef) + // sum the widths to find the last visible element's index + .reduce((acc, childNode, index) => { + acc.sumWidth += childNode.getBoundingClientRect().width; + if (acc.sumWidth <= containerElementRef.getBoundingClientRect().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 ? overflowElementRef.getBoundingClientRect().width : 0, + nextIndexOfLastVisibleChild: -1, + }); + + setIndexOfLastVisibleChild(nextIndexOfLastVisibleChild); + } + + if (containerElementRef) { + updateLastVisibleChildIndex(); + + const resizeObserver = new ResizeObserver(() => updateLastVisibleChildIndex()); + resizeObserver.observe(containerElementRef); + + return () => resizeObserver.disconnect(); } - // Get array of child nodes from NodeList form - const childNodesArr = Array.prototype.slice.call(containerElementRef.children); - const { nextIndexOfLastVisibleChild } = childNodesArr - // filter out the overflow element - .filter(childNode => childNode !== overflowElementRef) - // sum the widths to find the last visible element's index - .reduce((acc, childNode, index) => { - acc.sumWidth += childNode.getBoundingClientRect().width; - if (acc.sumWidth <= containerElementRef.getBoundingClientRect().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 ? overflowElementRef.getBoundingClientRect().width : 0, - nextIndexOfLastVisibleChild: -1, - }); - - setIndexOfLastVisibleChild(nextIndexOfLastVisibleChild); - }, [windowSize, containerElementRef, overflowElementRef]); + + return undefined; + }, [containerElementRef, overflowElementRef]); return indexOfLastVisibleChild; }; diff --git a/src/hooks/useIndexOfLastVisibleChild.mdx b/src/hooks/useIndexOfLastVisibleChild.mdx index 59a9fc417c..7da3e5d693 100644 --- a/src/hooks/useIndexOfLastVisibleChild.mdx +++ b/src/hooks/useIndexOfLastVisibleChild.mdx @@ -25,10 +25,10 @@ of the children until they exceed the width of the container. pointerEvents: 'none', visibility: 'hidden', }; - const containerElementRef = React.useRef(null); + const [containerElementRef, setContainerElementRef] = React.useState(null); const overflowElementRef = React.useRef(null); const indexOfLastVisibleChild = useIndexOfLastVisibleChild( - containerElementRef.current, + containerElementRef, overflowElementRef.current, ); const elements = ['Element 1', 'Element 2', 'Element 3', 'Element 4', 'Element 5', 'Element 6', 'Element 7']; @@ -71,7 +71,7 @@ of the children until they exceed the width of the container. }, [indexOfLastVisibleChild]); return ( -
+
{children}
)