Skip to content

Commit

Permalink
fix: observe container resizing in useIndexOfLastVisibleChild hook in…
Browse files Browse the repository at this point in the history
…stead of window resize
  • Loading branch information
viktorrusakov committed Dec 22, 2023
1 parent 9a16ba1 commit a734575
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 57 deletions.
32 changes: 19 additions & 13 deletions src/Tabs/index.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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
Expand All @@ -50,24 +56,24 @@ 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) {
overflowElementRef.current.parentNode.tabIndex = -1;
}
}, [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;
Expand Down Expand Up @@ -165,10 +171,10 @@ function Tabs({
/>
));
return childrenList;
}, [activeKey, children, defaultActiveKey, indexOfLastVisibleChild, moreTabText]);
}, [activeKey, children, defaultActiveKey, indexOfLastVisibleChild, moreTabText, handleDropdownTabClick]);

return (
<div ref={containerElementRef}>
<div ref={setContainerElementRef}>
<BaseTabs
defaultActiveKey={defaultActiveKey}
activeKey={activeKey}
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/tests/useIndexOfLastVisibleChild.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ window.ResizeObserver = window.ResizeObserver
}));

function TestComponent() {
const containerElementRef = React.useRef(null);
const [containerElementRef, setContainerElementRef] = React.useState(null);
const overflowElementRef = React.useRef(null);
const indexOfLastVisibleChild = useIndexOfLastVisibleChild(containerElementRef.current, overflowElementRef.current);
const indexOfLastVisibleChild = useIndexOfLastVisibleChild(containerElementRef, overflowElementRef.current);

return (
<div ref={containerElementRef} style={{ display: 'flex' }}>
<div ref={setContainerElementRef} style={{ display: 'flex' }}>
<div style={{ width: '250px' }} className="element">Element 1</div>
<div style={{ width: '250px' }} className="element">Element 2</div>
<div style={{ width: '250px' }} className="element">Element 3</div>
Expand Down
74 changes: 36 additions & 38 deletions src/hooks/useIndexOfLastVisibleChild.jsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
};
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useIndexOfLastVisibleChild.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -71,7 +71,7 @@ of the children until they exceed the width of the container.
}, [indexOfLastVisibleChild]);

return (
<div className="d-flex" ref={containerElementRef}>
<div className="d-flex" ref={setContainerElementRef}>
{children}
</div>
)
Expand Down

0 comments on commit a734575

Please sign in to comment.