194 lines
7.6 KiB
JavaScript
194 lines
7.6 KiB
JavaScript
"use strict";
|
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.DescendantProvider = DescendantProvider;
|
|
exports.useDescendant = useDescendant;
|
|
var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
|
|
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
|
|
var React = _interopRequireWildcard(require("react"));
|
|
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
var _useEnhancedEffect = _interopRequireDefault(require("@mui/utils/useEnhancedEffect"));
|
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
const _excluded = ["element"];
|
|
/** 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.
|
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
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.
|
|
*/
|
|
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
|
|
(0, _useEnhancedEffect.default)(() => {
|
|
if (descendant.element) {
|
|
registerDescendant((0, _extends2.default)({}, descendant, {
|
|
index
|
|
}));
|
|
return () => {
|
|
unregisterDescendant(descendant.element);
|
|
};
|
|
}
|
|
forceUpdate({});
|
|
return undefined;
|
|
}, [registerDescendant, unregisterDescendant, index, someDescendantsHaveChanged, descendant]);
|
|
return {
|
|
parentId,
|
|
index
|
|
};
|
|
}
|
|
function DescendantProvider(props) {
|
|
const {
|
|
children,
|
|
id
|
|
} = props;
|
|
const [items, set] = React.useState([]);
|
|
const registerDescendant = React.useCallback(_ref => {
|
|
let {
|
|
element
|
|
} = _ref,
|
|
other = (0, _objectWithoutPropertiesLoose2.default)(_ref, _excluded);
|
|
set(oldItems => {
|
|
if (oldItems.length === 0) {
|
|
// If there are no items, register at index 0 and bail.
|
|
return [(0, _extends2.default)({}, 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 = (0, _extends2.default)({}, 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__*/(0, _jsxRuntime.jsx)(DescendantContext.Provider, {
|
|
value: value,
|
|
children: children
|
|
});
|
|
}
|
|
process.env.NODE_ENV !== "production" ? DescendantProvider.propTypes = {
|
|
children: _propTypes.default.node,
|
|
id: _propTypes.default.string
|
|
} : void 0; |