UNPKG

@woocommerce/components

Version:
233 lines (232 loc) 10 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SearchListControl = void 0; /** * External dependencies */ const i18n_1 = require("@wordpress/i18n"); const components_1 = require("@wordpress/components"); const element_1 = require("@wordpress/element"); const compose_1 = require("@wordpress/compose"); const lodash_1 = require("lodash"); const notice_outline_1 = __importDefault(require("gridicons/dist/notice-outline")); const prop_types_1 = __importDefault(require("prop-types")); const clsx_1 = __importDefault(require("clsx")); /** * Internal dependencies */ const hierarchy_1 = require("./hierarchy"); const item_1 = __importDefault(require("./item")); const tag_1 = __importDefault(require("../tag")); const defaultMessages = { clear: (0, i18n_1.__)('Clear all selected items', 'woocommerce'), noItems: (0, i18n_1.__)('No items found.', 'woocommerce'), /* translators: %s: search term */ noResults: (0, i18n_1.__)('No results for %s', 'woocommerce'), search: (0, i18n_1.__)('Search for items', 'woocommerce'), selected: (n) => (0, i18n_1.sprintf)( /* translators: Number of items selected from list. */ (0, i18n_1._n)('%d item selected', '%d items selected', n, 'woocommerce'), n), updated: (0, i18n_1.__)('Search results updated.', 'woocommerce'), }; /** * Component to display a searchable, selectable list of items. * * @param {Object} props */ const SearchListControl = (props) => { const [searchValue, setSearchValue] = (0, element_1.useState)(props.search || ''); const { isSingle, isLoading, onChange, selected, instanceId, messages: propsMessages, isCompact, debouncedSpeak, onSearch, className = '', } = props; const messages = { ...defaultMessages, ...propsMessages }; (0, element_1.useEffect)(() => { if (typeof onSearch === 'function') { onSearch(searchValue); } }, [onSearch, searchValue]); const onRemove = (id) => { return () => { if (isSingle) { onChange([]); } const i = (0, lodash_1.findIndex)(selected, { id }); onChange([ ...selected.slice(0, i), ...selected.slice(i + 1), ]); }; }; const isSelected = (item) => (0, lodash_1.findIndex)(selected, { id: item.id }) !== -1; const getFilteredList = (list, search) => { const { isHierarchical } = props; if (!search) { return isHierarchical ? (0, hierarchy_1.buildTermsTree)(list) : list; } const re = new RegExp((0, lodash_1.escapeRegExp)(search), 'i'); debouncedSpeak(messages.updated); const filteredList = list .map((item) => (re.test(item.name) ? item : false)) .filter(Boolean); return isHierarchical ? (0, hierarchy_1.buildTermsTree)(filteredList, list) : filteredList; }; const onSelect = (item) => { return () => { if (isSelected(item)) { onRemove(item.id)(); return; } if (isSingle) { onChange([item]); } else { onChange([...selected, item]); } }; }; const defaultRenderItem = (args) => { return (0, element_1.createElement)(item_1.default, { ...args }); }; const renderList = (list, depth = 0) => { const renderItem = props.renderItem || defaultRenderItem; if (!list) { return null; } return list.map((item) => ((0, element_1.createElement)(element_1.Fragment, { key: item.id }, (0, element_1.createElement)("li", null, renderItem({ item, isSelected: isSelected(item), onSelect, isSingle, search: searchValue, depth, controlId: instanceId, })), renderList(item.children, depth + 1)))); }; const renderListSection = () => { if (isLoading) { return ((0, element_1.createElement)("div", { className: "woocommerce-search-list__list is-loading" }, (0, element_1.createElement)(components_1.Spinner, null))); } const list = getFilteredList(props.list, searchValue); if (!list.length) { return ((0, element_1.createElement)("div", { className: "woocommerce-search-list__list is-not-found" }, (0, element_1.createElement)("span", { className: "woocommerce-search-list__not-found-icon" }, (0, element_1.createElement)(notice_outline_1.default, { role: "img", "aria-hidden": "true", focusable: "false" })), (0, element_1.createElement)("span", { className: "woocommerce-search-list__not-found-text" }, searchValue ? // eslint-disable-next-line @wordpress/valid-sprintf (0, i18n_1.sprintf)(messages.noResults || '', searchValue) : messages.noItems))); } return ((0, element_1.createElement)("ul", { className: "woocommerce-search-list__list" }, renderList(list))); }; const renderSelectedSection = () => { if (isLoading || isSingle || !selected) { return null; } const selectedCount = selected.length; return ((0, element_1.createElement)("div", { className: "woocommerce-search-list__selected" }, (0, element_1.createElement)("div", { className: "woocommerce-search-list__selected-header" }, (0, element_1.createElement)("strong", null, messages.selected(selectedCount)), selectedCount > 0 ? ((0, element_1.createElement)(components_1.Button, { isLink: true, isDestructive: true, onClick: onChange([]), "aria-label": messages.clear }, (0, i18n_1.__)('Clear all', 'woocommerce'))) : null), selectedCount > 0 ? ((0, element_1.createElement)("ul", null, selected.map((item, i) => ((0, element_1.createElement)("li", { key: i }, (0, element_1.createElement)(tag_1.default, { label: item.name, id: item.id, remove: onRemove })))))) : null)); }; return ((0, element_1.createElement)("div", { className: (0, clsx_1.default)('woocommerce-search-list', className, { 'is-compact': isCompact, }) }, renderSelectedSection(), (0, element_1.createElement)("div", { className: "woocommerce-search-list__search" }, (0, element_1.createElement)(components_1.TextControl, { label: messages.search, type: "search", value: searchValue, onChange: (value) => setSearchValue(value) })), renderListSection())); }; exports.SearchListControl = SearchListControl; exports.SearchListControl.propTypes = { /** * Additional CSS classes. */ className: prop_types_1.default.string, /** * Whether it should be displayed in a compact way, so it occupies less space. */ isCompact: prop_types_1.default.bool, /** * Whether the list of items is hierarchical or not. If true, each list item is expected to * have a parent property. */ isHierarchical: prop_types_1.default.bool, /** * Whether the list of items is still loading. */ isLoading: prop_types_1.default.bool, /** * Restrict selections to one item. */ isSingle: prop_types_1.default.bool, /** * A complete list of item objects, each with id, name properties. This is displayed as a * clickable/keyboard-able list, and possibly filtered by the search term (searches name). */ list: prop_types_1.default.arrayOf(prop_types_1.default.shape({ id: prop_types_1.default.number, name: prop_types_1.default.string, })), /** * Messages displayed or read to the user. Configure these to reflect your object type. * See `defaultMessages` above for examples. */ messages: prop_types_1.default.shape({ /** * A more detailed label for the "Clear all" button, read to screen reader users. */ clear: prop_types_1.default.string, /** * Message to display when the list is empty (implies nothing loaded from the server * or parent component). */ noItems: prop_types_1.default.string, /** * Message to display when no matching results are found. %s is the search term. */ noResults: prop_types_1.default.string, /** * Label for the search input */ search: prop_types_1.default.string, /** * Label for the selected items. This is actually a function, so that we can pass * through the count of currently selected items. */ selected: prop_types_1.default.func, /** * Label indicating that search results have changed, read to screen reader users. */ updated: prop_types_1.default.string, }), /** * Callback fired when selected items change, whether added, cleared, or removed. * Passed an array of item objects (as passed in via props.list). */ onChange: prop_types_1.default.func.isRequired, /** * Callback fired when the search field is used. */ onSearch: prop_types_1.default.func, /** * Callback to render each item in the selection list, allows any custom object-type rendering. */ renderItem: prop_types_1.default.func, /** * The list of currently selected items. */ selected: prop_types_1.default.array.isRequired, // from withSpokenMessages debouncedSpeak: prop_types_1.default.func, // from withInstanceId instanceId: prop_types_1.default.number, }; exports.default = (0, compose_1.compose)([components_1.withSpokenMessages, compose_1.withInstanceId])(exports.SearchListControl);