@atlaskit/atlassian-navigation
Version:
A horizontal navigation component for Atlassian apps.
133 lines (126 loc) • 5.22 kB
JavaScript
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line @atlassian/tangerine/import/entry-points
// oxlint-disable-next-line @atlassian/no-restricted-imports
import throttle from 'lodash/throttle';
import noop from '@atlaskit/ds-lib/noop';
// Prevent width detector from triggering too many re-renders
const THROTTLE_INTERVAL = 16 * 4;
// Approx min width of items (based of "More" size)
const ITEM_APPROX_MINWIDTH = 70;
const calculateHash = (w, n) => w + '#' + n;
const updateHashRef = (currentRef, value) => {
currentRef.unshift(value);
currentRef.length = 3;
};
const OverflowContext = /*#__PURE__*/createContext({
isVisible: true,
openOverflowMenu: noop,
closeOverflowMenu: noop
});
// eslint-disable-next-line @repo/internal/react/require-jsdoc
export const OverflowProvider = ({
children,
isVisible,
openOverflowMenu,
closeOverflowMenu
}) => {
const {
Provider
} = OverflowContext;
const value = useMemo(() => ({
isVisible,
openOverflowMenu,
closeOverflowMenu
}), [isVisible, openOverflowMenu, closeOverflowMenu]);
return /*#__PURE__*/React.createElement(Provider, {
value: value
}, children);
};
/**
* __useOverFlowStatus__
*
* Returns the current context value for the nearest OverflowProvider.
*
* - [Example](https://atlassian.design/components/atlassian-navigation/examples#responsive)
*
* @deprecated `@atlaskit/atlassian-navigation` is deprecated. Use `@atlaskit/navigation-system` instead.
*/
export const useOverflowStatus = () => useContext(OverflowContext);
export const useOverflowController = nodes => {
const items = React.Children.toArray(nodes);
const [width, setWidth] = useState(9999);
const [itemsLimit, setItemsLimit] = useState(items.length);
const [forceEffectValue, triggerForceEffect] = useState({});
// Storing items approximate width so we can try expanding when there is enough room
const itemsWidths = useRef([]).current;
// Storing a couple of width + items count in order to stabilize
const hashRef = useRef([]);
// AFP-2511 TODO: Fix automatic suppressions below
// eslint-disable-next-line react-hooks/exhaustive-deps
const throttleSetWidth = useCallback(throttle(setWidth, THROTTLE_INTERVAL), [setWidth]);
useEffect(() => {
const lastItemWidth = itemsWidths[itemsLimit] || 0;
const wasJustLimited = lastItemWidth < 0;
const currentHash = calculateHash(width, itemsLimit);
if (hashRef.current[0] === currentHash) {
// After removing an item, if width has not changed yet we schedule a force update
// to handle case where removing an item does not actually trigger width change
const t = setTimeout(() => {
updateHashRef(hashRef.current, '');
triggerForceEffect({});
}, THROTTLE_INTERVAL * 1.5);
return () => clearTimeout(t);
}
if (wasJustLimited) {
// Width was updated either via resize or after changing the limit
// we cap the width between ITEM_APPROX_MINWIDTH and 2*ITEM_APPROX_MINWIDTH
// because width is throttled as when fast expanding/resizing partialWidth
// will not be reliable (edge case)
const partialWidth = Math.max(Math.min(width + lastItemWidth, ITEM_APPROX_MINWIDTH * 2), ITEM_APPROX_MINWIDTH);
itemsWidths[itemsLimit] = partialWidth;
}
if (width < ITEM_APPROX_MINWIDTH * 0.9 && itemsLimit) {
// If current width is less than an item approx width we remove an item
// marking the width as negative so we will calculate it on width update
// plus we set the hash to stabilise and not removing more than one element
// until we are sure width was updated
const nextHash = calculateHash(width, itemsLimit - 1);
if (hashRef.current.indexOf(nextHash) === -1) {
setItemsLimit(itemsLimit - 1);
itemsWidths[itemsLimit - 1] = -(width || 1);
updateHashRef(hashRef.current, nextHash);
}
return;
}
/**
* This is not necessarily equal to `lastItemWidth` because the
* ```js
* if (wasJustLimited) {}
* ```
* branch above modifies `itemsWidths`.
*
* Using `lastItemWidth` here can cause collapsing behavior to fail,
* such as the issue reported in DSP-7329.
*/
const currentLastItemWidth = itemsWidths[itemsLimit] || 0;
if (width - currentLastItemWidth > ITEM_APPROX_MINWIDTH * 1.1 && itemsLimit < items.length) {
// If we have enough room to accomodate next item width we increase the limit
// unless it has been recently removed
const nextHash = calculateHash(width, itemsLimit + 1);
if (hashRef.current.indexOf(nextHash) === -1) {
setItemsLimit(itemsLimit + 1);
updateHashRef(hashRef.current, nextHash);
}
return;
}
return;
}, [width, hashRef, itemsLimit, itemsWidths, forceEffectValue, items.length]);
return {
visibleItems: items.slice(0, itemsLimit),
overflowItems: items.slice(itemsLimit),
updateWidth: throttleSetWidth
};
};
// Used to extract props for useOverflowStatus();
// eslint-disable-next-line import/no-anonymous-default-export
export default (_props => {});