UNPKG

@wordpress/components

Version:
221 lines (191 loc) 6.65 kB
import { createElement } from "@wordpress/element"; /** * External dependencies */ import classnames from 'classnames'; import { noop, deburr } from 'lodash'; /** * WordPress dependencies */ import { __, _n, sprintf } from '@wordpress/i18n'; import { Component, useState, useMemo, useRef, useEffect } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; import { ENTER, UP, DOWN, ESCAPE } from '@wordpress/keycodes'; import { speak } from '@wordpress/a11y'; import { closeSmall } from '@wordpress/icons'; /** * Internal dependencies */ import TokenInput from '../form-token-field/token-input'; import SuggestionsList from '../form-token-field/suggestions-list'; import BaseControl from '../base-control'; import Button from '../button'; import { Flex, FlexBlock, FlexItem } from '../flex'; import withFocusOutside from '../higher-order/with-focus-outside'; const DetectOutside = withFocusOutside(class extends Component { handleFocusOutside(event) { this.props.onFocusOutside(event); } render() { return this.props.children; } }); function ComboboxControl({ value, label, options, onChange, onFilterValueChange = noop, hideLabelFromVision, help, allowReset = true, className, messages = { selected: __('Item selected.') } }) { var _currentOption$label; const instanceId = useInstanceId(ComboboxControl); const [selectedSuggestion, setSelectedSuggestion] = useState(null); const [isExpanded, setIsExpanded] = useState(false); const [inputValue, setInputValue] = useState(''); const inputContainer = useRef(); const currentOption = options.find(option => option.value === value); const currentLabel = (_currentOption$label = currentOption === null || currentOption === void 0 ? void 0 : currentOption.label) !== null && _currentOption$label !== void 0 ? _currentOption$label : ''; const matchingSuggestions = useMemo(() => { const startsWithMatch = []; const containsMatch = []; const match = deburr(inputValue.toLocaleLowerCase()); options.forEach(option => { const index = deburr(option.label).toLocaleLowerCase().indexOf(match); if (index === 0) { startsWithMatch.push(option); } else if (index > 0) { containsMatch.push(option); } }); return startsWithMatch.concat(containsMatch); }, [inputValue, options, value]); const onSuggestionSelected = newSelectedSuggestion => { onChange(newSelectedSuggestion.value); speak(messages.selected, 'assertive'); setSelectedSuggestion(newSelectedSuggestion); setInputValue(''); setIsExpanded(false); }; const handleArrowNavigation = (offset = 1) => { const index = matchingSuggestions.indexOf(selectedSuggestion); 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 = event => { let preventDefault = false; switch (event.keyCode) { case ENTER: if (selectedSuggestion) { onSuggestionSelected(selectedSuggestion); preventDefault = true; } break; case UP: handleArrowNavigation(-1); preventDefault = true; break; case DOWN: handleArrowNavigation(1); preventDefault = true; break; case ESCAPE: setIsExpanded(false); setSelectedSuggestion(null); preventDefault = true; event.stopPropagation(); break; default: break; } if (preventDefault) { event.preventDefault(); } }; const onFocus = () => { setIsExpanded(true); onFilterValueChange(''); setInputValue(''); }; const onFocusOutside = () => { setIsExpanded(false); }; const onInputChange = event => { const text = event.value; setInputValue(text); onFilterValueChange(text); setIsExpanded(true); }; const handleOnReset = () => { onChange(null); inputContainer.current.input.focus(); }; // Announcements useEffect(() => { const hasMatchingSuggestions = matchingSuggestions.length > 0; if (isExpanded) { const message = hasMatchingSuggestions ? sprintf( /* translators: %d: number of results. */ _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) : __('No results.'); speak(message, 'polite'); } }, [matchingSuggestions, isExpanded]); // 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 createElement(DetectOutside, { onFocusOutside: onFocusOutside }, createElement(BaseControl, { className: classnames(className, 'components-combobox-control'), tabIndex: "-1", label: label, id: `components-form-token-input-${instanceId}`, hideLabelFromVision: hideLabelFromVision, help: help }, createElement("div", { className: "components-combobox-control__suggestions-container", tabIndex: "-1", onKeyDown: onKeyDown }, createElement(Flex, null, createElement(FlexBlock, null, createElement(TokenInput, { className: "components-combobox-control__input", instanceId: instanceId, ref: inputContainer, value: isExpanded ? inputValue : currentLabel, "aria-label": currentLabel ? `${currentLabel}, ${label}` : null, onFocus: onFocus, isExpanded: isExpanded, selectedSuggestionIndex: matchingSuggestions.indexOf(selectedSuggestion), onChange: onInputChange })), allowReset && createElement(FlexItem, null, createElement(Button, { className: "components-combobox-control__reset", icon: closeSmall, disabled: !value, onClick: handleOnReset, label: __('Reset') }))), isExpanded && createElement(SuggestionsList, { instanceId: instanceId, match: { label: inputValue }, displayTransform: suggestion => suggestion.label, suggestions: matchingSuggestions, selectedIndex: matchingSuggestions.indexOf(selectedSuggestion), onHover: setSelectedSuggestion, onSelect: onSuggestionSelected, scrollIntoView: true })))); /* eslint-enable jsx-a11y/no-static-element-interactions */ } export default ComboboxControl; //# sourceMappingURL=index.js.map