@primer/react
Version:
An implementation of GitHub's Primer Design System using React
82 lines (78 loc) • 3.33 kB
JavaScript
import { useContext, useRef, useEffect, useCallback } from 'react';
import { AutocompleteContext } from './AutocompleteContext.js';
import { useMergedRefs } from '../hooks/useMergedRefs.js';
import VisuallyHidden from '../_VisuallyHidden.js';
import classes from './AutocompleteOverlay.module.css.js';
import { clsx } from 'clsx';
import { jsx } from 'react/jsx-runtime';
import { useAnchoredPosition } from '../hooks/useAnchoredPosition.js';
import Overlay from '../Overlay/Overlay.js';
// TODO: consider making 'aria-labelledby' required
function AutocompleteOverlay({
menuAnchorRef,
overlayProps: oldOverlayProps,
children,
className,
...newOverlayProps
}) {
const autocompleteContext = useContext(AutocompleteContext);
if (autocompleteContext === null) {
throw new Error('AutocompleteContext returned null values');
}
const overlayProps = {
...oldOverlayProps,
...newOverlayProps
};
const {
inputRef,
scrollContainerRef,
selectedItemLength,
setShowMenu,
showMenu = false
} = autocompleteContext;
const computedAnchorRef = useRef(null);
useEffect(() => {
var _menuAnchorRef$curren, _tokensContainer$pare, _ref;
const explicit = (_menuAnchorRef$curren = menuAnchorRef === null || menuAnchorRef === void 0 ? void 0 : menuAnchorRef.current) !== null && _menuAnchorRef$curren !== void 0 ? _menuAnchorRef$curren : null;
const tokensContainer = inputRef.current ? inputRef.current.closest('[data-prevent-token-wrapping]') : null;
const tokensRoot = (_tokensContainer$pare = tokensContainer === null || tokensContainer === void 0 ? void 0 : tokensContainer.parentElement) !== null && _tokensContainer$pare !== void 0 ? _tokensContainer$pare : null;
computedAnchorRef.current = (_ref = explicit !== null && explicit !== void 0 ? explicit : tokensRoot) !== null && _ref !== void 0 ? _ref : inputRef.current;
}, [menuAnchorRef, inputRef]);
const {
floatingElementRef,
position
} = useAnchoredPosition({
side: 'outside-bottom',
align: 'start',
anchorElementRef: computedAnchorRef
}, [showMenu, selectedItemLength]);
const mergedScrollContainerRef = useMergedRefs(scrollContainerRef, floatingElementRef);
const closeOptionList = useCallback(() => {
setShowMenu(false);
}, [setShowMenu]);
if (typeof window === 'undefined') {
return null;
}
return showMenu ? /*#__PURE__*/jsx(Overlay, {
returnFocusRef: inputRef,
preventFocusOnOpen: true,
onClickOutside: closeOptionList,
onEscape: closeOptionList,
ref: mergedScrollContainerRef,
top: position === null || position === void 0 ? void 0 : position.top,
left: position === null || position === void 0 ? void 0 : position.left,
className: clsx(classes.Overlay, className),
...overlayProps,
children: children
}) :
/*#__PURE__*/
// HACK: This ensures AutocompleteMenu is still mounted when closing the menu and all of the hooks inside of it are still called.
// A better way to do this would be to move the hooks to AutocompleteOverlay or somewhere that won't get unmounted.
jsx(VisuallyHidden, {
"aria-hidden": "true",
children: children
});
}
AutocompleteOverlay.displayName = 'AutocompleteOverlay';
AutocompleteOverlay.__SLOT__ = Symbol('Autocomplete.Overlay');
export { AutocompleteOverlay as default };