UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

523 lines (522 loc) 23.6 kB
"use strict"; "use client"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = require("react"); var z = _interopRequireWildcard(require("zod")); var _clsx = _interopRequireDefault(require("clsx")); var _index = require("../../../../components/index.js"); var _index2 = _interopRequireDefault(require("../../FieldBlock/index.js")); var _index3 = require("../../hooks/index.js"); var _utils = require("../../../../components/flex/utils.js"); var _Context = _interopRequireDefault(require("../../DataContext/Context.js")); var _useDataValue = _interopRequireDefault(require("../../hooks/useDataValue.js")); var _useTranslation = _interopRequireDefault(require("../../hooks/useTranslation.js")); var _componentHelper = require("../../../../shared/component-helper.js"); var _whatInput = _interopRequireDefault(require("../../../../shared/helpers/whatInput.js")); var _useIsomorphicLayoutEffect = _interopRequireDefault(require("../../../../shared/helpers/useIsomorphicLayoutEffect.js")); var _withComponentMarkers = _interopRequireDefault(require("../../../../shared/helpers/withComponentMarkers.js")); var _useSharedState = require("../../../../shared/helpers/useSharedState.js"); var _MultiSelectionTrigger = require("./MultiSelectionTrigger.js"); var _MultiSelectionSearch = require("./MultiSelectionSearch.js"); var _MultiSelectionSelectedTags = require("./MultiSelectionSelectedTags.js"); var _MultiSelectionItemList = require("./MultiSelectionItemList.js"); var _MultiSelectionActions = require("./MultiSelectionActions.js"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } function MultiSelection(props) { const { id, path, dataPath, data, className, variant = 'popover', width, showSearchField = false, showSelectedTags = false, showConfirmButton = false, showSelectAll = false, selectedItemsCollapsibleThreshold = 10, value, disabled, emptyValue, htmlAttributes, handleChange, setDisplayValue } = (0, _index3.useFieldProps)({ ...props, schema: (() => { if (typeof props.minItems === 'number' || typeof props.maxItems === 'number') { return p => { let s = z.array(z.union([z.string(), z.number()])); if (typeof p.minItems === 'number') { var _p$errorMessages$minI, _p$errorMessages; s = s.min(p.minItems, { message: (_p$errorMessages$minI = (_p$errorMessages = p.errorMessages) === null || _p$errorMessages === void 0 ? void 0 : _p$errorMessages.minItems) !== null && _p$errorMessages$minI !== void 0 ? _p$errorMessages$minI : 'MultiSelectionField.errorMinItems' }); } if (typeof p.maxItems === 'number') { var _p$errorMessages$maxI, _p$errorMessages2; s = s.max(p.maxItems, { message: (_p$errorMessages$maxI = (_p$errorMessages2 = p.errorMessages) === null || _p$errorMessages2 === void 0 ? void 0 : _p$errorMessages2.maxItems) !== null && _p$errorMessages$maxI !== void 0 ? _p$errorMessages$maxI : 'MultiSelectionField.errorMaxItems' }); } return s; }; } return props.schema; })() }); const { MultiSelectionField: translation, formatMessage } = (0, _useTranslation.default)(); const formatSelectionCount = (0, _react.useCallback)((count, total) => formatMessage(translation.selectionCount, { count, total }), [formatMessage, translation.selectionCount]); const { getValueByPath } = (0, _useDataValue.default)(); const { setFieldInternals } = (0, _react.useContext)(_Context.default); const dataList = dataPath ? getValueByPath(dataPath) : data; const [isOpen, setIsOpen] = (0, _react.useState)(false); const [searchValue, setSearchValue] = (0, _react.useState)(''); const [tempValue, setTempValue] = (0, _react.useState)(value || []); const [ariaLiveCheckedCount, setAriaLiveCheckedCount] = (0, _react.useState)(''); const confirmedRef = (0, _react.useRef)(false); const isInline = variant === 'inline'; (0, _react.useEffect)(() => { if (!isOpen || isInline) { setTempValue(value || []); } }, [value, isOpen, isInline]); const popoverContentRef = (0, _react.useRef)(null); const triggerRef = (0, _react.useRef)(null); const handlePopoverContentRef = (0, _react.useCallback)(el => { popoverContentRef.current = el; if (!el || !isOpen) { return; } requestAnimationFrame(() => { var _triggerRef$current; const trigger = (_triggerRef$current = triggerRef.current) !== null && _triggerRef$current !== void 0 ? _triggerRef$current : document.getElementById(id); triggerRef.current = trigger; const contentRect = el.getBoundingClientRect(); const triggerRect = trigger === null || trigger === void 0 ? void 0 : trigger.getBoundingClientRect(); const margin = 16; const isBottomPlacement = !triggerRect || contentRect.top >= triggerRect.bottom; const maxHeight = isBottomPlacement ? window.innerHeight - contentRect.top - margin : contentRect.bottom - margin; if (maxHeight > 100) { el.style.setProperty('--popover-max-height', `${Math.max(0, maxHeight)}px`); } }); }, [id, isOpen]); const pendingTriggerNavigationRef = (0, _react.useRef)(null); const pendingCheckedCountAnnouncementRef = (0, _react.useRef)(false); const previousTempValueRef = (0, _react.useRef)(value || []); const hasFeature = showSearchField || showSelectedTags || showConfirmButton; const toSearchText = (0, _react.useCallback)(content => { return (0, _componentHelper.convertJsxToString)(content || '').toLowerCase(); }, []); (0, _useIsomorphicLayoutEffect.default)(() => { if (isOpen) { _whatInput.default.specificKeys(['Tab', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'PageUp', 'PageDown', 'End', 'Home']); } return () => { _whatInput.default.specificKeys(['Tab']); }; }, [isOpen]); const flattenItems = (0, _react.useCallback)(items => { if (!items) { return []; } return items.flatMap(item => [item, ...(item.children ? flattenItems(item.children) : [])]); }, []); const allFlatItems = (0, _react.useMemo)(() => flattenItems(dataList), [dataList, flattenItems]); const filteredItems = (0, _react.useMemo)(() => { if (!dataList) { return []; } if (!searchValue) { return dataList; } const searchLower = searchValue.toLowerCase(); const filterRecursive = items => { return items.map(item => { const title = (0, _componentHelper.convertJsxToString)(item.title).toLowerCase(); const text = toSearchText(item.text); const description = toSearchText(item.description); const matches = title.includes(searchLower) || text.includes(searchLower) || description.includes(searchLower); const children = item.children ? filterRecursive(item.children) : []; if (matches || children.length > 0) { return { ...item, children: children.length > 0 ? children : item.children }; } return null; }).filter(Boolean); }; return filterRecursive(dataList); }, [dataList, searchValue, toSearchText]); const selectedItems = (0, _react.useMemo)(() => { if (!tempValue) { return []; } return allFlatItems.filter(item => tempValue.includes(item.value)); }, [allFlatItems, tempValue]); const totalCount = allFlatItems.length; const selectedCount = selectedItems.length; const isCollapsible = totalCount > selectedItemsCollapsibleThreshold; const confirmedItems = (0, _react.useMemo)(() => { if (!value) { return []; } return allFlatItems.filter(item => value.includes(item.value)); }, [allFlatItems, value]); const displayCount = showConfirmButton ? confirmedItems.length : selectedCount; (0, _react.useEffect)(() => { const previousValue = previousTempValueRef.current; const hasChanged = previousValue.length !== tempValue.length || previousValue.some((item, index) => item !== tempValue[index]); if (!hasChanged) { previousTempValueRef.current = tempValue; return; } previousTempValueRef.current = tempValue; if (!pendingCheckedCountAnnouncementRef.current) { return; } pendingCheckedCountAnnouncementRef.current = false; setAriaLiveCheckedCount(formatSelectionCount(tempValue.length, totalCount)); }, [tempValue, totalCount, formatSelectionCount]); const getParentState = (0, _react.useCallback)(item => { if (!item.children || item.children.length === 0) { return { checked: tempValue.includes(item.value), indeterminate: false }; } const children = flattenItems(item.children); const checkedChildren = children.filter(child => tempValue.includes(child.value)).length; return { checked: checkedChildren === children.length, indeterminate: checkedChildren > 0 && checkedChildren < children.length }; }, [tempValue, flattenItems]); const normalizeValue = (0, _react.useCallback)(nextValue => { const normalized = new Set(nextValue); normalized.forEach(itemValue => { const item = allFlatItems.find(i => i.value === itemValue); if (item !== null && item !== void 0 && item.children) { const childValues = flattenItems(item.children).map(c => c.value); const allChildrenInValue = childValues.every(childVal => normalized.has(childVal)); if (!allChildrenInValue) { normalized.delete(itemValue); } } }); const parentItems = allFlatItems.filter(item => item.children); parentItems.forEach(item => { const childValues = flattenItems(item.children).map(c => c.value); const allChildrenInValue = childValues.every(childVal => normalized.has(childVal)); if (allChildrenInValue && childValues.length > 0) { normalized.add(item.value); } }); return Array.from(normalized); }, [allFlatItems, flattenItems]); const applyChange = (0, _react.useCallback)(nextValue => { const normalizedValue = normalizeValue(nextValue); const finalValue = normalizedValue.length === 0 ? emptyValue : normalizedValue; handleChange === null || handleChange === void 0 || handleChange(finalValue); const nextSelectedItems = allFlatItems.filter(item => normalizedValue.includes(item.value)); setDisplayValue(nextSelectedItems.map(item => item.title)); if (path) { setFieldInternals === null || setFieldInternals === void 0 || setFieldInternals(path + '/multiSelectionData', { props: nextSelectedItems }); } }, [allFlatItems, emptyValue, handleChange, setDisplayValue, path, setFieldInternals, normalizeValue]); const handleToggleItem = (0, _react.useCallback)(itemValue => { const next = tempValue.includes(itemValue) ? tempValue.filter(v => v !== itemValue) : [...tempValue, itemValue]; pendingCheckedCountAnnouncementRef.current = true; setTempValue(next); if (!showConfirmButton) { applyChange(next); } }, [tempValue, showConfirmButton, applyChange]); const handleToggleParent = (0, _react.useCallback)(item => { const children = item.children ? flattenItems(item.children) : []; const allChildValues = children.map(child => child.value); const allChildrenChecked = allChildValues.every(childVal => tempValue.includes(childVal)); let next = [...tempValue]; if (allChildrenChecked) { next = next.filter(v => ![item.value, ...allChildValues].includes(v)); } else { next = Array.from(new Set([...next, ...allChildValues])); } pendingCheckedCountAnnouncementRef.current = true; setTempValue(next); if (!showConfirmButton) { applyChange(next); } }, [tempValue, showConfirmButton, applyChange, flattenItems]); const handleSelectAll = (0, _react.useCallback)(() => { const allFilteredFlat = flattenItems(filteredItems); const selectableItems = allFilteredFlat.filter(item => !item.disabled); const allSelectableChecked = selectableItems.every(item => tempValue.includes(item.value)); const next = allSelectableChecked ? tempValue.filter(v => !selectableItems.some(item => item.value === v)) : Array.from(new Set([...tempValue, ...selectableItems.map(item => item.value)])); pendingCheckedCountAnnouncementRef.current = true; setTempValue(next); if (!showConfirmButton) { applyChange(next); } }, [filteredItems, tempValue, showConfirmButton, applyChange, flattenItems]); const handleRemoveTag = (0, _react.useCallback)(itemValue => { const item = allFlatItems.find(i => i.value === itemValue); if (item !== null && item !== void 0 && item.disabled) { return; } const next = tempValue.filter(v => v !== itemValue); pendingCheckedCountAnnouncementRef.current = true; setTempValue(next); if (!showConfirmButton) { applyChange(next); } }, [allFlatItems, tempValue, showConfirmButton, applyChange]); const handleConfirm = (0, _react.useCallback)(() => { applyChange(tempValue); confirmedRef.current = true; setIsOpen(false); }, [tempValue, applyChange]); const handleCancel = (0, _react.useCallback)(() => { setTempValue(value || []); setSearchValue(''); setIsOpen(false); }, [value]); const allFilteredFlat = (0, _react.useMemo)(() => flattenItems(filteredItems), [filteredItems, flattenItems]); const selectableFilteredFlat = (0, _react.useMemo)(() => allFilteredFlat.filter(item => !item.disabled), [allFilteredFlat]); const allFilteredSelected = selectableFilteredFlat.length > 0 && selectableFilteredFlat.every(item => tempValue.includes(item.value)); const someFilteredSelected = !allFilteredSelected && selectableFilteredFlat.some(item => tempValue.includes(item.value)); const getCheckboxes = (0, _react.useCallback)(() => { var _popoverContentRef$cu; return Array.from(((_popoverContentRef$cu = popoverContentRef.current) === null || _popoverContentRef$cu === void 0 ? void 0 : _popoverContentRef$cu.querySelectorAll('.dnb-checkbox__input:not(:disabled)')) || []); }, []); const getSearchInput = (0, _react.useCallback)(() => { var _popoverContentRef$cu2, _popoverContentRef$cu3; return (_popoverContentRef$cu2 = (_popoverContentRef$cu3 = popoverContentRef.current) === null || _popoverContentRef$cu3 === void 0 ? void 0 : _popoverContentRef$cu3.querySelector('.dnb-forms-field-multi-selection__search input:not(:disabled)')) !== null && _popoverContentRef$cu2 !== void 0 ? _popoverContentRef$cu2 : null; }, []); const handlePopoverKeyDown = (0, _react.useCallback)(event => { var _closest; if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') { return; } event.preventDefault(); if (disabled) { return; } const checkboxes = getCheckboxes(); const searchInput = getSearchInput(); const navigable = [...(searchInput ? [searchInput] : []), ...checkboxes]; if (!navigable.length) { return; } const active = document.activeElement; const rowCheckbox = active === null || active === void 0 || (_closest = active.closest('.dnb-forms-field-multi-selection__item')) === null || _closest === void 0 ? void 0 : _closest.querySelector('.dnb-checkbox__input'); const current = navigable.includes(active) ? active : rowCheckbox && navigable.includes(rowCheckbox) ? rowCheckbox : null; const index = current ? navigable.indexOf(current) : -1; const next = index === -1 ? event.key === 'ArrowDown' ? navigable[0] : navigable[navigable.length - 1] : event.key === 'ArrowDown' ? navigable[(index + 1) % navigable.length] : navigable[(index - 1 + navigable.length) % navigable.length]; next === null || next === void 0 || next.focus(); if (next !== searchInput) { var _next$closest; next === null || next === void 0 || (_next$closest = next.closest('.dnb-forms-field-multi-selection__item')) === null || _next$closest === void 0 || _next$closest.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }, [disabled, getCheckboxes, getSearchInput]); const resolveFocusOnOpenElement = (0, _react.useCallback)(() => { const dir = pendingTriggerNavigationRef.current; const checkboxes = getCheckboxes(); const searchInput = getSearchInput(); if (dir === null) { return popoverContentRef.current; } if (dir === 1) { return searchInput !== null && searchInput !== void 0 ? searchInput : checkboxes[0]; } return checkboxes[checkboxes.length - 1]; }, [getCheckboxes, getSearchInput]); const handleFocusComplete = (0, _react.useCallback)(() => { pendingTriggerNavigationRef.current = null; }, []); const fieldBlockProps = { forId: id, className: (0, _clsx.default)('dnb-forms-field-multi-selection', className, isInline && 'dnb-forms-field-multi-selection--inline'), contentClassName: 'dnb-forms-field-multi-selection__field-content', disableStatusSummary: true, asFieldset: isInline, ...(0, _utils.pickSpacingProps)(props) }; if (!isInline) { fieldBlockProps.contentWidth = width !== null && width !== void 0 ? width : 'large'; } const handleTriggerKeyDown = (0, _react.useCallback)(event => { if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') { return; } event.preventDefault(); if (disabled) { return; } if (isOpen) { const checkboxes = getCheckboxes(); const searchInput = getSearchInput(); const target = event.key === 'ArrowDown' ? searchInput !== null && searchInput !== void 0 ? searchInput : checkboxes[0] : checkboxes[checkboxes.length - 1]; target === null || target === void 0 || target.focus(); return; } pendingTriggerNavigationRef.current = event.key === 'ArrowDown' ? 1 : -1; setIsOpen(true); }, [disabled, getCheckboxes, getSearchInput, isOpen]); const searchContent = (0, _jsxRuntime.jsx)(_MultiSelectionSearch.MultiSelectionSearch, { show: showSearchField, placeholder: translation.searchPlaceholder, value: searchValue, disabled: disabled, onSearchChange: setSearchValue }); const itemListContent = (0, _jsxRuntime.jsx)(_MultiSelectionItemList.MultiSelectionItemList, { disabled: disabled, filteredItems: filteredItems, tempValue: tempValue, searchValue: searchValue, showSelectAll: showSelectAll, htmlAttributes: htmlAttributes, translation: { selectAll: translation.selectAll, noOptions: translation.noOptions }, getParentState: getParentState, onToggleItem: handleToggleItem, onToggleParent: handleToggleParent, onToggleSelectAll: handleSelectAll, selectableFilteredFlat: selectableFilteredFlat, allFilteredSelected: allFilteredSelected, someFilteredSelected: someFilteredSelected }); const selectedTagsContent = (0, _jsxRuntime.jsx)(_MultiSelectionSelectedTags.MultiSelectionSelectedTags, { id: id, show: showSelectedTags, disabled: disabled, isCollapsible: isCollapsible, selectedItems: selectedItems, totalCount: totalCount, formatSelectionCount: formatSelectionCount, translation: { clearAll: translation.clearAll, placeholder: translation.placeholder }, onRemoveTag: handleRemoveTag, onClearAll: () => { const disabledValues = allFlatItems.filter(item => item.disabled && tempValue.includes(item.value)).map(item => item.value); setTempValue(disabledValues); (0, _useSharedState.createSharedState)(`${id}-selected-accordion`).set({ expanded: true }); if (!showConfirmButton) { applyChange(disabledValues); } } }); if (isInline) { return (0, _jsxRuntime.jsx)(_index2.default, { ...fieldBlockProps, children: (0, _jsxRuntime.jsxs)("div", { className: "dnb-forms-field-multi-selection__container", children: [(0, _jsxRuntime.jsx)(_index.AriaLive, { priority: "high", children: ariaLiveCheckedCount }), (0, _jsxRuntime.jsxs)("div", { className: "dnb-forms-field-multi-selection__inline-content", children: [searchContent, selectedTagsContent, itemListContent] })] }) }); } return (0, _jsxRuntime.jsx)(_index2.default, { ...fieldBlockProps, children: (0, _jsxRuntime.jsxs)("div", { className: "dnb-forms-field-multi-selection__container", children: [(0, _jsxRuntime.jsx)(_index.AriaLive, { priority: "high", children: ariaLiveCheckedCount }), (0, _jsxRuntime.jsx)(_index.Popover, { open: isOpen, focusOnOpen: true, focusOnOpenElement: resolveFocusOnOpenElement, onFocusComplete: handleFocusComplete, onOpenChange: open => { setIsOpen(open); if (!open && !confirmedRef.current) { setTempValue(value || []); setSearchValue(''); } if (!open) { confirmedRef.current = false; } }, placement: "bottom", autoAlignViewportThreshold: 0.75, horizontalOffset: width === 'medium' ? 40 : 0, hideCloseButton: true, noInnerSpace: !hasFeature, hideArrow: true, className: "dnb-forms-field-multi-selection__popover", trigger: ({ active, ...triggerProps }) => (0, _jsxRuntime.jsx)(_MultiSelectionTrigger.MultiSelectionTrigger, { id: id, active: active, disabled: disabled, displayCount: displayCount, totalCount: totalCount, formatSelectionCount: formatSelectionCount, onKeyDown: handleTriggerKeyDown, triggerProps: triggerProps }), children: (0, _jsxRuntime.jsxs)("div", { className: "dnb-forms-field-multi-selection__popover-content", ref: handlePopoverContentRef, tabIndex: -1, onKeyDownCapture: handlePopoverKeyDown, children: [searchContent, selectedTagsContent, itemListContent, (0, _jsxRuntime.jsx)(_MultiSelectionActions.MultiSelectionActions, { show: showConfirmButton, disabled: disabled, tempValueLength: tempValue.length, formatMessage: formatMessage, translation: { confirmButton: translation.confirmButton, cancelButton: translation.cancelButton }, onConfirm: handleConfirm, onCancel: handleCancel })] }) })] }) }); } (0, _withComponentMarkers.default)(MultiSelection, { _supportsSpacingProps: true }); var _default = exports.default = MultiSelection; //# sourceMappingURL=MultiSelection.js.map