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 (
-