UNPKG

@shopify/polaris

Version:

Shopify’s admin product component library

349 lines (315 loc) • 13.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var React = require('react'); var debounce = require('../../utilities/debounce.js'); var useToggle = require('../../utilities/use-toggle.js'); var types = require('../../types.js'); var shared = require('../shared.js'); var Listbox$1 = require('./Listbox.scss.js'); var hooks$1 = require('../../utilities/combobox/hooks.js'); var utilities = require('../../utilities/listbox/utilities.js'); var context = require('../../utilities/listbox/context.js'); var TextOption = require('./components/TextOption/TextOption.js'); var Loading = require('./components/Loading/Loading.js'); var Section = require('./components/Section/Section.js'); var Header = require('./components/Header/Header.js'); var Action = require('./components/Action/Action.js'); var hooks = require('../../utilities/unique-id/hooks.js'); var KeypressListener = require('../KeypressListener/KeypressListener.js'); var VisuallyHidden = require('../VisuallyHidden/VisuallyHidden.js'); var Option = require('./components/Option/Option.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var React__default = /*#__PURE__*/_interopDefaultLegacy(React); exports.AutoSelection = void 0; (function (AutoSelection) { AutoSelection["FirstSelected"] = "FIRST_SELECTED"; AutoSelection["First"] = "FIRST"; })(exports.AutoSelection || (exports.AutoSelection = {})); const OPTION_SELECTOR = '[data-listbox-option]'; const OPTION_VALUE_ATTRIBUTE = 'data-listbox-option-value'; const OPTION_ACTION_ATTRIBUTE = 'data-listbox-option-action'; const OPTION_FOCUS_ATTRIBUTE = 'data-focused'; function Listbox({ children, autoSelection = exports.AutoSelection.FirstSelected, enableKeyboardControl, accessibilityLabel, customListId, onSelect, onActiveOptionChange }) { const [loading, setLoading] = React.useState(); const [activeOption, setActiveOption] = React.useState(); const [lazyLoading, setLazyLoading] = React.useState(false); const [currentOptions, setCurrentOptions] = React.useState([]); const { value: keyboardEventsEnabled, setTrue: enableKeyboardEvents, setFalse: disableKeyboardEvents } = useToggle.useToggle(Boolean(enableKeyboardControl)); const uniqueId = hooks.useUniqueId('Listbox'); const listId = customListId || uniqueId; const scrollableRef = React.useRef(null); const listboxRef = React.useRef(null); const { listboxId, textFieldLabelId, textFieldFocused, willLoadMoreOptions, setActiveOptionId, setListboxId, onOptionSelected, onKeyToBottom } = hooks$1.useComboboxListbox(); const inCombobox = Boolean(setActiveOptionId); React.useEffect(() => { if (setListboxId && !listboxId) { setListboxId(listId); } }, [setListboxId, listboxId, listId]); const getNavigableOptions = React.useCallback(() => { if (!listboxRef.current) { return []; } return [...new Set(listboxRef.current.querySelectorAll(OPTION_SELECTOR))]; }, []); const getFirstNavigableOption = React.useCallback(currentOptions => { const hasSelectedOptions = currentOptions.some(option => option.getAttribute('aria-selected') === 'true'); let elementIndex = 0; const element = currentOptions.find((option, index) => { const isInteractable = option.getAttribute('aria-disabled') !== 'true'; let isFirstNavigableOption; if (hasSelectedOptions && autoSelection === exports.AutoSelection.FirstSelected) { const isSelected = option.getAttribute('aria-selected') === 'true'; isFirstNavigableOption = isSelected && isInteractable; } else { isFirstNavigableOption = isInteractable; } if (isFirstNavigableOption) elementIndex = index; return isFirstNavigableOption; }); if (!element) return; return { element, index: elementIndex }; }, [autoSelection]); const handleScrollIntoView = React.useCallback(option => { const { current: scrollable } = scrollableRef; if (scrollable) { utilities.scrollOptionIntoView(option.element, scrollable); } }, []); const handleScrollIntoViewDebounced = debounce.debounce(handleScrollIntoView, 50); const handleKeyToBottom = React.useCallback(() => { if (onKeyToBottom) { setLazyLoading(true); return Promise.resolve(onKeyToBottom()); } }, [onKeyToBottom]); const handleChangeActiveOption = React.useCallback(nextOption => { if (!nextOption) return setActiveOption(undefined); activeOption === null || activeOption === void 0 ? void 0 : activeOption.element.removeAttribute(OPTION_FOCUS_ATTRIBUTE); nextOption.element.setAttribute(OPTION_FOCUS_ATTRIBUTE, 'true'); handleScrollIntoViewDebounced(nextOption); setActiveOption(nextOption); setActiveOptionId === null || setActiveOptionId === void 0 ? void 0 : setActiveOptionId(nextOption.domId); onActiveOptionChange === null || onActiveOptionChange === void 0 ? void 0 : onActiveOptionChange(nextOption.value, nextOption.domId); }, [activeOption, setActiveOptionId, onActiveOptionChange, handleScrollIntoViewDebounced]); const getFormattedOption = React.useCallback((element, index) => { return { element, index, domId: element.id, value: element.getAttribute(OPTION_VALUE_ATTRIBUTE) || '', disabled: element.getAttribute('aria-disabled') === 'true', isAction: element.getAttribute(OPTION_ACTION_ATTRIBUTE) === 'true' }; }, []); const resetActiveOption = React.useCallback(() => { var _nextOption, _nextOption2, _nextOption3; let nextOption; const nextOptions = getNavigableOptions(); const nextActiveOption = getFirstNavigableOption(nextOptions); if (nextOptions.length === 0 && currentOptions.length > 0) { setCurrentOptions(nextOptions); handleChangeActiveOption(); return; } if (nextActiveOption) { const { element, index } = nextActiveOption; nextOption = getFormattedOption(element, index); } const optionIsAlreadyActive = activeOption !== undefined && ((_nextOption = nextOption) === null || _nextOption === void 0 ? void 0 : _nextOption.domId) === (activeOption === null || activeOption === void 0 ? void 0 : activeOption.domId); const actionContentHasUpdated = (activeOption === null || activeOption === void 0 ? void 0 : activeOption.isAction) && ((_nextOption2 = nextOption) === null || _nextOption2 === void 0 ? void 0 : _nextOption2.isAction) && ((_nextOption3 = nextOption) === null || _nextOption3 === void 0 ? void 0 : _nextOption3.value) !== (activeOption === null || activeOption === void 0 ? void 0 : activeOption.value); const currentValues = currentOptions.map(option => option.getAttribute(OPTION_VALUE_ATTRIBUTE)); const nextValues = nextOptions.map(option => option.getAttribute(OPTION_VALUE_ATTRIBUTE)); const listIsUnchanged = nextValues.length === currentValues.length && nextValues.every((value, index) => { return currentValues[index] === value; }); if (listIsUnchanged) { if (optionIsAlreadyActive && actionContentHasUpdated) { setCurrentOptions(nextOptions); handleChangeActiveOption(nextOption); } return; } setCurrentOptions(nextOptions); if (lazyLoading) { setLazyLoading(false); return; } handleChangeActiveOption(nextOption); }, [lazyLoading, currentOptions, activeOption, getFirstNavigableOption, getNavigableOptions, getFormattedOption, handleChangeActiveOption]); React.useEffect(() => { if (!loading && children && React.Children.count(children) > 0) { resetActiveOption(); } }, [children, activeOption, loading, resetActiveOption]); React.useEffect(() => { if (listboxRef.current) { scrollableRef.current = listboxRef.current.closest(shared.scrollable.selector); } }, []); React.useEffect(() => { if (enableKeyboardControl && !keyboardEventsEnabled) { enableKeyboardEvents(); } }, [enableKeyboardControl, keyboardEventsEnabled, enableKeyboardEvents]); const onOptionSelect = React.useCallback(option => { handleChangeActiveOption(option); if (onOptionSelected) onOptionSelected(); if (onSelect) onSelect(option.value); }, [handleChangeActiveOption, onSelect, onOptionSelected]); const getNextIndex = React.useCallback((currentIndex, lastIndex, direction) => { let nextIndex; if (direction === 'down') { if (currentIndex === lastIndex) { nextIndex = willLoadMoreOptions ? currentIndex + 1 : 0; } else { nextIndex = currentIndex + 1; } } else { nextIndex = currentIndex === 0 ? lastIndex : currentIndex - 1; } return nextIndex; }, [willLoadMoreOptions]); const getNextValidOption = React.useCallback(async key => { const lastIndex = currentOptions.length - 1; let currentIndex = (activeOption === null || activeOption === void 0 ? void 0 : activeOption.index) || 0; let nextIndex = 0; let element = activeOption === null || activeOption === void 0 ? void 0 : activeOption.element; let totalOptions = -1; while (totalOptions++ < lastIndex) { var _element; nextIndex = getNextIndex(currentIndex, lastIndex, key); element = currentOptions[nextIndex]; const triggerLazyLoad = nextIndex >= lastIndex; const isDisabled = ((_element = element) === null || _element === void 0 ? void 0 : _element.getAttribute('aria-disabled')) === 'true'; if (triggerLazyLoad && willLoadMoreOptions) { await handleKeyToBottom(); } if (isDisabled) { currentIndex = nextIndex; element = undefined; continue; } break; } return { element, nextIndex }; }, [currentOptions, activeOption, willLoadMoreOptions, getNextIndex, handleKeyToBottom]); const handleArrow = React.useCallback(async (type, event) => { event.preventDefault(); const { element, nextIndex } = await getNextValidOption(type); if (!element) return; const nextOption = getFormattedOption(element, nextIndex); handleChangeActiveOption(nextOption); }, [getFormattedOption, getNextValidOption, handleChangeActiveOption]); const handleDownArrow = React.useCallback(event => { handleArrow('down', event); }, [handleArrow]); const handleUpArrow = React.useCallback(event => { handleArrow('up', event); }, [handleArrow]); const handleEnter = React.useCallback(event => { event.preventDefault(); event.stopPropagation(); if (activeOption) { onOptionSelect(activeOption); } }, [activeOption, onOptionSelect]); const handleFocus = React.useCallback(() => { if (enableKeyboardControl) return; enableKeyboardEvents(); }, [enableKeyboardControl, enableKeyboardEvents]); const handleBlur = React.useCallback(event => { event.stopPropagation(); if (keyboardEventsEnabled) { const nextActiveOption = getFirstNavigableOption(currentOptions); if (nextActiveOption) { const { element, index } = nextActiveOption; const nextOption = getFormattedOption(element, index); handleChangeActiveOption(nextOption); } } if (enableKeyboardControl) return; disableKeyboardEvents(); }, [enableKeyboardControl, currentOptions, keyboardEventsEnabled, disableKeyboardEvents, getFirstNavigableOption, getFormattedOption, handleChangeActiveOption]); const listeners = keyboardEventsEnabled || textFieldFocused ? /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement(KeypressListener.KeypressListener, { keyEvent: "keydown", keyCode: types.Key.DownArrow, handler: handleDownArrow }), /*#__PURE__*/React__default["default"].createElement(KeypressListener.KeypressListener, { keyEvent: "keydown", keyCode: types.Key.UpArrow, handler: handleUpArrow }), /*#__PURE__*/React__default["default"].createElement(KeypressListener.KeypressListener, { keyEvent: "keydown", keyCode: types.Key.Enter, handler: handleEnter })) : null; const listboxContext = React.useMemo(() => ({ onOptionSelect, setLoading }), [onOptionSelect]); return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, listeners, /*#__PURE__*/React__default["default"].createElement(VisuallyHidden.VisuallyHidden, null, /*#__PURE__*/React__default["default"].createElement("div", { "aria-live": "polite" }, loading ? loading : null)), /*#__PURE__*/React__default["default"].createElement(context.ListboxContext.Provider, { value: listboxContext }, /*#__PURE__*/React__default["default"].createElement(context.WithinListboxContext.Provider, { value: true }, children ? /*#__PURE__*/React__default["default"].createElement("ul", { tabIndex: 0, role: "listbox", className: Listbox$1["default"].Listbox, "aria-label": inCombobox ? undefined : accessibilityLabel, "aria-labelledby": textFieldLabelId, "aria-busy": Boolean(loading), "aria-activedescendant": activeOption && activeOption.domId, id: listId, onFocus: inCombobox ? undefined : handleFocus, onBlur: inCombobox ? undefined : handleBlur, ref: listboxRef }, children) : null))); } Listbox.Option = Option.Option; Listbox.TextOption = TextOption.TextOption; Listbox.Loading = Loading.Loading; Listbox.Section = Section.Section; Listbox.Header = Header.Header; Listbox.Action = Action.Action; exports.Listbox = Listbox;