UNPKG

@wordpress/components

Version:
336 lines (327 loc) 12.3 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _clsx = _interopRequireDefault(require("clsx")); var _i18n = require("@wordpress/i18n"); var _element = require("@wordpress/element"); var _compose = require("@wordpress/compose"); var _a11y = require("@wordpress/a11y"); var _icons = require("@wordpress/icons"); var _styles = require("./styles"); var _tokenInput = _interopRequireDefault(require("../form-token-field/token-input")); var _suggestionsList = _interopRequireDefault(require("../form-token-field/suggestions-list")); var _baseControl = _interopRequireDefault(require("../base-control")); var _button = _interopRequireDefault(require("../button")); var _flex = require("../flex"); var _withFocusOutside = _interopRequireDefault(require("../higher-order/with-focus-outside")); var _hooks = require("../utils/hooks"); var _strings = require("../utils/strings"); var _useDeprecatedProps = require("../utils/use-deprecated-props"); var _withIgnoreImeEvents = require("../utils/with-ignore-ime-events"); var _deprecated36pxSize = require("../utils/deprecated-36px-size"); var _spinner = _interopRequireDefault(require("../spinner")); var _jsxRuntime = require("react/jsx-runtime"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ const noop = () => {}; const DetectOutside = (0, _withFocusOutside.default)(class extends _element.Component { handleFocusOutside(event) { this.props.onFocusOutside(event); } render() { return this.props.children; } }); const getIndexOfMatchingSuggestion = (selectedSuggestion, matchingSuggestions) => selectedSuggestion === null ? -1 : matchingSuggestions.indexOf(selectedSuggestion); /** * `ComboboxControl` is an enhanced version of a [`SelectControl`](../select-control/README.md) with the addition of * being able to search for options using a search input. * * ```jsx * import { ComboboxControl } from '@wordpress/components'; * import { useState } from '@wordpress/element'; * * const options = [ * { * value: 'small', * label: 'Small', * }, * { * value: 'normal', * label: 'Normal', * disabled: true, * }, * { * value: 'large', * label: 'Large', * disabled: false, * }, * ]; * * function MyComboboxControl() { * const [ fontSize, setFontSize ] = useState(); * const [ filteredOptions, setFilteredOptions ] = useState( options ); * return ( * <ComboboxControl * __next40pxDefaultSize * __nextHasNoMarginBottom * label="Font Size" * value={ fontSize } * onChange={ setFontSize } * options={ filteredOptions } * onFilterValueChange={ ( inputValue ) => * setFilteredOptions( * options.filter( ( option ) => * option.label * .toLowerCase() * .startsWith( inputValue.toLowerCase() ) * ) * ) * } * /> * ); * } * ``` */ function ComboboxControl(props) { var _currentOption$label; const { __nextHasNoMarginBottom = false, __next40pxDefaultSize = false, value: valueProp, label, options, onChange: onChangeProp, onFilterValueChange = noop, hideLabelFromVision, help, allowReset = true, className, isLoading = false, messages = { selected: (0, _i18n.__)('Item selected.') }, __experimentalRenderItem, expandOnFocus = true, placeholder } = (0, _useDeprecatedProps.useDeprecated36pxDefaultSizeProp)(props); const [value, setValue] = (0, _hooks.useControlledValue)({ value: valueProp, onChange: onChangeProp }); const currentOption = options.find(option => option.value === value); const currentLabel = (_currentOption$label = currentOption?.label) !== null && _currentOption$label !== void 0 ? _currentOption$label : ''; // Use a custom prefix when generating the `instanceId` to avoid having // duplicate input IDs when rendering this component and `FormTokenField` // in the same page (see https://github.com/WordPress/gutenberg/issues/42112). const instanceId = (0, _compose.useInstanceId)(ComboboxControl, 'combobox-control'); const [selectedSuggestion, setSelectedSuggestion] = (0, _element.useState)(currentOption || null); const [isExpanded, setIsExpanded] = (0, _element.useState)(false); const [inputHasFocus, setInputHasFocus] = (0, _element.useState)(false); const [inputValue, setInputValue] = (0, _element.useState)(''); const inputContainer = (0, _element.useRef)(null); const matchingSuggestions = (0, _element.useMemo)(() => { const startsWithMatch = []; const containsMatch = []; const match = (0, _strings.normalizeTextString)(inputValue); options.forEach(option => { const index = (0, _strings.normalizeTextString)(option.label).indexOf(match); if (index === 0) { startsWithMatch.push(option); } else if (index > 0) { containsMatch.push(option); } }); return startsWithMatch.concat(containsMatch); }, [inputValue, options]); const onSuggestionSelected = newSelectedSuggestion => { if (newSelectedSuggestion.disabled) { return; } setValue(newSelectedSuggestion.value); (0, _a11y.speak)(messages.selected, 'assertive'); setSelectedSuggestion(newSelectedSuggestion); setInputValue(''); setIsExpanded(false); }; const handleArrowNavigation = (offset = 1) => { const index = getIndexOfMatchingSuggestion(selectedSuggestion, matchingSuggestions); let nextIndex = index + offset; if (nextIndex < 0) { nextIndex = matchingSuggestions.length - 1; } else if (nextIndex >= matchingSuggestions.length) { nextIndex = 0; } setSelectedSuggestion(matchingSuggestions[nextIndex]); setIsExpanded(true); }; const onKeyDown = (0, _withIgnoreImeEvents.withIgnoreIMEEvents)(event => { let preventDefault = false; if (event.defaultPrevented) { return; } switch (event.code) { case 'Enter': if (selectedSuggestion) { onSuggestionSelected(selectedSuggestion); preventDefault = true; } break; case 'ArrowUp': handleArrowNavigation(-1); preventDefault = true; break; case 'ArrowDown': handleArrowNavigation(1); preventDefault = true; break; case 'Escape': setIsExpanded(false); setSelectedSuggestion(null); preventDefault = true; break; default: break; } if (preventDefault) { event.preventDefault(); } }); const onBlur = () => { setInputHasFocus(false); }; const onFocus = () => { setInputHasFocus(true); if (expandOnFocus) { setIsExpanded(true); } onFilterValueChange(''); setInputValue(''); }; const onClick = () => { setIsExpanded(true); }; const onFocusOutside = () => { setIsExpanded(false); }; const onInputChange = event => { const text = event.value; setInputValue(text); onFilterValueChange(text); if (inputHasFocus) { setIsExpanded(true); } }; const handleOnReset = () => { setValue(null); inputContainer.current?.focus(); }; // Stop propagation of the keydown event when pressing Enter on the Reset // button to prevent calling the onKeydown callback on the container div // element which actually sets the selected suggestion. const handleResetStopPropagation = event => { event.stopPropagation(); }; // Update current selections when the filter input changes. (0, _element.useEffect)(() => { const hasMatchingSuggestions = matchingSuggestions.length > 0; const hasSelectedMatchingSuggestions = getIndexOfMatchingSuggestion(selectedSuggestion, matchingSuggestions) > 0; if (hasMatchingSuggestions && !hasSelectedMatchingSuggestions) { // If the current selection isn't present in the list of suggestions, then automatically select the first item from the list of suggestions. setSelectedSuggestion(matchingSuggestions[0]); } }, [matchingSuggestions, selectedSuggestion]); // Announcements. (0, _element.useEffect)(() => { const hasMatchingSuggestions = matchingSuggestions.length > 0; if (isExpanded) { const message = hasMatchingSuggestions ? (0, _i18n.sprintf)(/* translators: %d: number of results. */ (0, _i18n._n)('%d result found, use up and down arrow keys to navigate.', '%d results found, use up and down arrow keys to navigate.', matchingSuggestions.length), matchingSuggestions.length) : (0, _i18n.__)('No results.'); (0, _a11y.speak)(message, 'polite'); } }, [matchingSuggestions, isExpanded]); (0, _deprecated36pxSize.maybeWarnDeprecated36pxSize)({ componentName: 'ComboboxControl', __next40pxDefaultSize, size: undefined }); // Disable reason: There is no appropriate role which describes the // input container intended accessible usability. // TODO: Refactor click detection to use blur to stop propagation. /* eslint-disable jsx-a11y/no-static-element-interactions */ return /*#__PURE__*/(0, _jsxRuntime.jsx)(DetectOutside, { onFocusOutside: onFocusOutside, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_baseControl.default, { __nextHasNoMarginBottom: __nextHasNoMarginBottom, __associatedWPComponentName: "ComboboxControl", className: (0, _clsx.default)(className, 'components-combobox-control'), label: label, id: `components-form-token-input-${instanceId}`, hideLabelFromVision: hideLabelFromVision, help: help, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: "components-combobox-control__suggestions-container", tabIndex: -1, onKeyDown: onKeyDown, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_styles.InputWrapperFlex, { __next40pxDefaultSize: __next40pxDefaultSize, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_flex.FlexBlock, { children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_tokenInput.default, { className: "components-combobox-control__input", instanceId: instanceId, ref: inputContainer, placeholder: placeholder, value: isExpanded ? inputValue : currentLabel, onFocus: onFocus, onBlur: onBlur, onClick: onClick, isExpanded: isExpanded, selectedSuggestionIndex: getIndexOfMatchingSuggestion(selectedSuggestion, matchingSuggestions), onChange: onInputChange }) }), isLoading && /*#__PURE__*/(0, _jsxRuntime.jsx)(_spinner.default, {}), allowReset && /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.default, { size: "small", icon: _icons.closeSmall // Disable reason: Focus returns to input field when reset is clicked. // eslint-disable-next-line no-restricted-syntax , disabled: !value, onClick: handleOnReset, onKeyDown: handleResetStopPropagation, label: (0, _i18n.__)('Reset') })] }), isExpanded && !isLoading && /*#__PURE__*/(0, _jsxRuntime.jsx)(_suggestionsList.default, { instanceId: instanceId // The empty string for `value` here is not actually used, but is // just a quick way to satisfy the TypeScript requirements of SuggestionsList. // See: https://github.com/WordPress/gutenberg/pull/47581/files#r1091089330 , match: { label: inputValue, value: '' }, displayTransform: suggestion => suggestion.label, suggestions: matchingSuggestions, selectedIndex: getIndexOfMatchingSuggestion(selectedSuggestion, matchingSuggestions), onHover: setSelectedSuggestion, onSelect: onSuggestionSelected, scrollIntoView: true, __experimentalRenderItem: __experimentalRenderItem })] }) }) }); /* eslint-enable jsx-a11y/no-static-element-interactions */ } var _default = exports.default = ComboboxControl; //# sourceMappingURL=index.js.map