@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
523 lines (522 loc) • 23.6 kB
JavaScript
"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