UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

127 lines (118 loc) 5.13 kB
import React, { useRef, useState, useCallback } from 'react'; import StyledSpinner from '../../Spinner/Spinner.js'; import { ActionList } from '../../ActionList/index.js'; import Box from '../../Box/Box.js'; import { useCombobox } from '../hooks/useCombobox.js'; import Overlay from '../../Overlay/Overlay.js'; import { getSuggestionValue, getSuggestionKey } from './utils.js'; import useIsomorphicLayoutEffect from '../../utils/useIsomorphicLayoutEffect.js'; const LoadingIndicator = () => /*#__PURE__*/React.createElement(Box, { sx: { display: 'flex', justifyContent: 'center', py: 2 } }, /*#__PURE__*/React.createElement(StyledSpinner, { size: "small" })); LoadingIndicator.displayName = "LoadingIndicator"; const SuggestionListItem = ({ suggestion }) => { const value = getSuggestionValue(suggestion); const sharedProps = { id: value, children: value, role: 'option', sx: { '&[aria-selected]': { backgroundColor: 'actionListItem.default.activeBg' }, '&[data-combobox-option-default]:not([aria-selected])': { backgroundColor: 'actionListItem.default.selectedBg' } } }; return typeof suggestion === 'string' ? /*#__PURE__*/React.createElement(ActionList.Item, sharedProps) : suggestion.render(sharedProps); }; /** * Renders an overlayed list at the given relative coordinates. Handles keyboard navigation * and accessibility concerns. */ const AutocompleteSuggestions = ({ suggestions, portalName, triggerCharCoords, onClose, onCommit: externalOnCommit, inputRef, visible, tabInsertsSuggestions, defaultPlacement }) => { const overlayRef = useRef(null); // It seems wierd to use state instead of a ref here, but because the list is inside an // AnchoredOverlay it is not always mounted - so we want to reinitialize the Combobox when it mounts const [list, setList] = useState(null); const onCommit = useCallback(({ option }) => { externalOnCommit(getSuggestionValue(option)); }, [externalOnCommit]); // Setup keyboard navigation useCombobox({ // Even though the list is visible when loading, we don't want to do keyboard binding in that case isOpen: visible && suggestions !== 'loading', listElement: list, inputElement: inputRef.current, onCommit, options: Array.isArray(suggestions) ? suggestions : [], tabInsertsSuggestions, defaultFirstOption: true }); const [top, setTop] = useState(0); useIsomorphicLayoutEffect(function recalculateTop() { var _overlayRef$current$o, _overlayRef$current; const overlayHeight = (_overlayRef$current$o = (_overlayRef$current = overlayRef.current) === null || _overlayRef$current === void 0 ? void 0 : _overlayRef$current.offsetHeight) !== null && _overlayRef$current$o !== void 0 ? _overlayRef$current$o : 0; const belowOffset = triggerCharCoords.top + triggerCharCoords.height; const wouldOverflowBelow = belowOffset + overlayHeight > window.innerHeight; const aboveOffset = triggerCharCoords.top - overlayHeight; const wouldOverflowAbove = aboveOffset < 0; // Only override the default if it would overflow in the default direction and it would not overflow in the override direction const result = { below: wouldOverflowBelow && !wouldOverflowAbove ? aboveOffset : belowOffset, above: wouldOverflowAbove && !wouldOverflowBelow ? belowOffset : aboveOffset }[defaultPlacement]; // Sometimes the value can be NaN if layout is not available (ie, SSR or JSDOM) const resultNotNaN = Number.isNaN(result) ? 0 : result; setTop(resultNotNaN); }, // this is a cheap effect and we want it to run when pretty much anything that could affect position changes [triggerCharCoords.top, triggerCharCoords.height, suggestions, visible, defaultPlacement]); // Conditional rendering appears wrong at first - it means that we are reconstructing the // Combobox instance every time the suggestions appear. But this is what we want - otherwise // the textarea would always have the `combobox` role, which is incorrect (a textarea should // not technically ever be a combobox). We compromise by dynamically applying the combobox // role only when suggestions are available. return visible ? /*#__PURE__*/React.createElement(Overlay, { onEscape: onClose, onClickOutside: onClose, returnFocusRef: inputRef, preventFocusOnOpen: true, portalContainerName: portalName, sx: { position: 'fixed' }, top: top, left: triggerCharCoords.left, ref: overlayRef }, /*#__PURE__*/React.createElement(ActionList, { ref: setList, role: "listbox" }, suggestions === 'loading' ? /*#__PURE__*/React.createElement(LoadingIndicator, null) : suggestions === null || suggestions === void 0 ? void 0 : suggestions.map(suggestion => /*#__PURE__*/React.createElement(SuggestionListItem, { suggestion: suggestion, key: getSuggestionKey(suggestion) })))) : /*#__PURE__*/React.createElement(React.Fragment, null); }; AutocompleteSuggestions.displayName = 'SuggestionList'; export { AutocompleteSuggestions as default };