'use client'; import * as Ariakit from '@ariakit/react'; import React, { useMemo, useState, useId, useTransition, useEffect, useCallback } from 'react'; import MdHelpButton from '../help/MdHelpButton'; import MdHelpText from '../help/MdHelpText'; import MdIconClose from '../icons-material/MdIconClose'; import MdIconKeyboardArrowDown from '../icons-material/MdIconKeyboardArrowDown'; import MdIconSearch from '../icons-material/MdIconSearch'; import MdLoadingSpinner from '../loadingSpinner/MdLoadingSpinner'; import MdCheckbox from './MdCheckbox'; import type { MdComboBoxBaseProps, MdComboBoxOption } from './MdComboBox'; interface Labels { helpTextFor?: string; reset?: string; openClose?: string; } export interface MdComboBoxGroupedOption { label: string; labels?: Labels; icon?: React.ReactNode; values: MdComboBoxOption[]; } export interface MdComboBoxGroupedProps extends React.InputHTMLAttributes, MdComboBoxBaseProps { options: MdComboBoxGroupedOption[]; defaultOptions?: MdComboBoxGroupedOption[]; value: string | string[]; hideSeparatorLine?: boolean; onSelectOption(_value: string[] | string): void; } const MdComboBoxGrouped = React.forwardRef( ( { id, label, labels = {}, options, defaultOptions, value, disabled = false, placeholder = 'Søk', numberOfElementsShown, mode = 'medium', helpText, error = false, errorText, noResultsText = 'Ingen treff', dropdownHeight, prefixIcon, isSearching = false, hidePrefixIcon = false, allowReset = false, flip = false, hideSeparatorLine = false, onSelectOption, unmountOnHide, ...otherProps }, ref, ) => { const uuid = `combobox_${useId()}`; const comboBoxId = id || uuid; const isMultiSelect = Array.isArray(value); const [isPending, startTransition] = useTransition(); const [searchValue, setSearchValue] = useState(''); const [selectedValues, setSelectedValues] = useState(value); const [helpOpen, setHelpOpen] = useState(false); const [popoverOpen, setPopoverOpen] = useState(false); const [pendingSearchClear, setPendingSearchClear] = useState(false); const store = Ariakit.useComboboxStore(); const defaultLabels: Required = { helpTextFor: 'Hjelpetekst for', reset: 'Nullstill', openClose: 'Åpne/lukke liste', }; const mergedLabels: Required = { ...defaultLabels, ...labels }; useEffect(() => { setSelectedValues(value); if (!value) { setSearchValue(''); } }, [value]); useEffect(() => { if (!pendingSearchClear) return; const checkAnimationEnd = () => { const state = store.getState(); if (!state.animating && !state.open) { setSearchValue(''); setPendingSearchClear(false); } else { requestAnimationFrame(checkAnimationEnd); } }; requestAnimationFrame(checkAnimationEnd); }, [store, pendingSearchClear]); const normalizeString = (str: string) => { return str .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .trim(); }; const matches = useMemo(() => { if (!searchValue && defaultOptions && defaultOptions.length > 0) { return defaultOptions; } const normalizedSearch = normalizeString(searchValue || ''); const results = options .map(group => { // Filter the values within each group based on the searchValue const matchingValues = group.values.filter(value => { const normalizedValue = normalizeString(value.value || ''); const normalizedText = normalizeString(value.text || ''); return normalizedValue.includes(normalizedSearch) || normalizedText.includes(normalizedSearch); }); // Return the group only if it has matching values if (matchingValues.length > 0) { return { ...group, values: matchingValues, // Include only the matching values }; } return null; // Exclude groups with no matching values }) .filter(group => { return group !== null; }); // Remove null groups return numberOfElementsShown ? results.slice(0, numberOfElementsShown) : results; }, [searchValue, defaultOptions, options, numberOfElementsShown]); const getValueById = useMemo(() => { return (value: string) => { let val = placeholder; options.forEach(option => { option.values.forEach(v => { if (v.value === value) { val = v.text; } }); }); return val; }; }, [options, placeholder]); const onReset = () => { const newValue = isMultiSelect ? [] : ''; setSearchValue(''); if (value) { setSelectedValues(newValue); onSelectOption(newValue); } }; let displayValue: string | string[] = placeholder; if (isMultiSelect) { displayValue = selectedValues.length > 0 ? getValueById(selectedValues[0]) : placeholder; } else if (selectedValues !== '') { displayValue = getValueById(selectedValues as string); } let ariaDescribedBy = helpText && helpText !== '' ? `md-combobox_help-text_${comboBoxId}` : undefined; ariaDescribedBy = error && errorText && errorText !== '' ? `md-combobox_error_${comboBoxId}` : ariaDescribedBy; const showLabel = (label && label !== '') || (helpText && helpText !== ''); const setItemCallback = useCallback(() => { if (!isMultiSelect) { setSearchValue(''); } return false; }, [isMultiSelect]); const getOpenState = () => { return store.getState().open; }; return (
0 : true) && 'md-combobox--has-value'}`} > { const mutableValues = Array.isArray(values) ? (Array.from(values) as string[]) : (values as string); setSelectedValues(mutableValues); onSelectOption(mutableValues); }} setValue={val => { startTransition(() => { setSearchValue(val); }); }} setOpen={() => { setPopoverOpen(getOpenState()); }} > {showLabel && (
{label && label !== '' && {label}} {helpText && helpText !== '' && (
{ return setHelpOpen(!helpOpen); }} expanded={helpOpen} />
)}
{helpText && helpText !== '' && (
{helpText}
)}
)}
{!hidePrefixIcon && (
{isSearching ? : prefixIcon ? prefixIcon : }
)}
{isMultiSelect && selectedValues.length > 0 && `+${selectedValues.length}`}
{allowReset && (selectedValues.length > 0 || searchValue !== '') && ( )}
{ setPendingSearchClear(true); }} > {matches && matches.map((group, index: number) => { return ( {!hideSeparatorLine && index !== 0 && (

)}
{group.icon &&
{group.icon}
} {group.label}
{group.values && group.values.map((option, i) => { const isChecked = isMultiSelect ? selectedValues.toString().includes(option.value) : selectedValues === option.value; return ( {isMultiSelect ? ( ) : ( option.text )} ); })}
); })} {!matches.length && (
{noResultsText}
)}
{error && errorText && errorText !== '' && (
{errorText}
)}
); }, ); MdComboBoxGrouped.displayName = 'MdComboBoxGrouped'; export default MdComboBoxGrouped;