189 lines
6.4 KiB
JavaScript
189 lines
6.4 KiB
JavaScript
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
|
|
import _extends from "@babel/runtime/helpers/esm/extends";
|
|
const _excluded = ["element"];
|
|
import * as React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
|
|
|
|
/** Credit: https://github.com/reach/reach-ui/blob/86a046f54d53b6420e392b3fa56dd991d9d4e458/packages/descendants/README.md
|
|
* Modified slightly to suit our purposes.
|
|
*/
|
|
|
|
// To replace with .findIndex() once we stop IE11 support.
|
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
function findIndex(array, comp) {
|
|
for (let i = 0; i < array.length; i += 1) {
|
|
if (comp(array[i])) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
function binaryFindElement(array, element) {
|
|
let start = 0;
|
|
let end = array.length - 1;
|
|
while (start <= end) {
|
|
const middle = Math.floor((start + end) / 2);
|
|
if (array[middle].element === element) {
|
|
return middle;
|
|
}
|
|
|
|
// eslint-disable-next-line no-bitwise
|
|
if (array[middle].element.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING) {
|
|
end = middle - 1;
|
|
} else {
|
|
start = middle + 1;
|
|
}
|
|
}
|
|
return start;
|
|
}
|
|
const DescendantContext = /*#__PURE__*/React.createContext({});
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
DescendantContext.displayName = 'DescendantContext';
|
|
}
|
|
function usePrevious(value) {
|
|
const ref = React.useRef(null);
|
|
React.useEffect(() => {
|
|
ref.current = value;
|
|
}, [value]);
|
|
return ref.current;
|
|
}
|
|
const noop = () => {};
|
|
|
|
/**
|
|
* This hook registers our descendant by passing it into an array. We can then
|
|
* search that array by to find its index when registering it in the component.
|
|
* We use this for focus management, keyboard navigation, and typeahead
|
|
* functionality for some components.
|
|
*
|
|
* The hook accepts the element node
|
|
*
|
|
* Our main goals with this are:
|
|
* 1) maximum composability,
|
|
* 2) minimal API friction
|
|
* 3) SSR compatibility*
|
|
* 4) concurrent safe
|
|
* 5) index always up-to-date with the tree despite changes
|
|
* 6) works with memoization of any component in the tree (hopefully)
|
|
*
|
|
* * As for SSR, the good news is that we don't actually need the index on the
|
|
* server for most use-cases, as we are only using it to determine the order of
|
|
* composed descendants for keyboard navigation.
|
|
*/
|
|
export function useDescendant(descendant) {
|
|
const [, forceUpdate] = React.useState();
|
|
const {
|
|
registerDescendant = noop,
|
|
unregisterDescendant = noop,
|
|
descendants = [],
|
|
parentId = null
|
|
} = React.useContext(DescendantContext);
|
|
|
|
// This will initially return -1 because we haven't registered the descendant
|
|
// on the first render. After we register, this will then return the correct
|
|
// index on the following render, and we will re-register descendants
|
|
// so that everything is up-to-date before the user interacts with a
|
|
// collection.
|
|
const index = findIndex(descendants, item => item.element === descendant.element);
|
|
const previousDescendants = usePrevious(descendants);
|
|
|
|
// We also need to re-register descendants any time ANY of the other
|
|
// descendants have changed. My brain was melting when I wrote this and it
|
|
// feels a little off, but checking in render and using the result in the
|
|
// effect's dependency array works well enough.
|
|
const someDescendantsHaveChanged = descendants.some((newDescendant, position) => {
|
|
return previousDescendants && previousDescendants[position] && previousDescendants[position].element !== newDescendant.element;
|
|
});
|
|
|
|
// Prevent any flashing
|
|
useEnhancedEffect(() => {
|
|
if (descendant.element) {
|
|
registerDescendant(_extends({}, descendant, {
|
|
index
|
|
}));
|
|
return () => {
|
|
unregisterDescendant(descendant.element);
|
|
};
|
|
}
|
|
forceUpdate({});
|
|
return undefined;
|
|
}, [registerDescendant, unregisterDescendant, index, someDescendantsHaveChanged, descendant]);
|
|
return {
|
|
parentId,
|
|
index
|
|
};
|
|
}
|
|
export function DescendantProvider(props) {
|
|
const {
|
|
children,
|
|
id
|
|
} = props;
|
|
const [items, set] = React.useState([]);
|
|
const registerDescendant = React.useCallback(_ref => {
|
|
let {
|
|
element
|
|
} = _ref,
|
|
other = _objectWithoutPropertiesLoose(_ref, _excluded);
|
|
set(oldItems => {
|
|
if (oldItems.length === 0) {
|
|
// If there are no items, register at index 0 and bail.
|
|
return [_extends({}, other, {
|
|
element,
|
|
index: 0
|
|
})];
|
|
}
|
|
const index = binaryFindElement(oldItems, element);
|
|
let newItems;
|
|
if (oldItems[index] && oldItems[index].element === element) {
|
|
// If the element is already registered, just use the same array
|
|
newItems = oldItems;
|
|
} else {
|
|
// When registering a descendant, we need to make sure we insert in
|
|
// into the array in the same order that it appears in the DOM. So as
|
|
// new descendants are added or maybe some are removed, we always know
|
|
// that the array is up-to-date and correct.
|
|
//
|
|
// So here we look at our registered descendants and see if the new
|
|
// element we are adding appears earlier than an existing descendant's
|
|
// DOM node via `node.compareDocumentPosition`. If it does, we insert
|
|
// the new element at this index. Because `registerDescendant` will be
|
|
// called in an effect every time the descendants state value changes,
|
|
// we should be sure that this index is accurate when descendent
|
|
// elements come or go from our component.
|
|
|
|
const newItem = _extends({}, other, {
|
|
element,
|
|
index
|
|
});
|
|
|
|
// If an index is not found we will push the element to the end.
|
|
newItems = oldItems.slice();
|
|
newItems.splice(index, 0, newItem);
|
|
}
|
|
newItems.forEach((item, position) => {
|
|
item.index = position;
|
|
});
|
|
return newItems;
|
|
});
|
|
}, []);
|
|
const unregisterDescendant = React.useCallback(element => {
|
|
set(oldItems => oldItems.filter(item => element !== item.element));
|
|
}, []);
|
|
|
|
|
|
const value = React.useMemo(() => ({
|
|
descendants: items,
|
|
registerDescendant,
|
|
unregisterDescendant,
|
|
parentId: id
|
|
}), [items, registerDescendant, unregisterDescendant, id]);
|
|
|
|
return /*#__PURE__*/_jsx(DescendantContext.Provider, {
|
|
value: value,
|
|
children: children
|
|
});
|
|
}
|
|
process.env.NODE_ENV !== "production" ? DescendantProvider.propTypes = {
|
|
children: PropTypes.node,
|
|
id: PropTypes.string
|
|
} : void 0; |