import { useState, useMemo, useDeferredValue, useCallback } from 'react'; import SubstringHighlight from './substring-highlight'; import Accordion from './accordion'; import Loader from './loader'; import Message from './message'; import SearchInput from './search-input'; import '../styles/components/accordion-search.scss'; export const getLeafKeys = ( item: AccordionItem[] | AccordionItem ): string[] | string => { if (Array.isArray(item)) { return item.flatMap((i) => getLeafKeys(i)); } if (item.items) { return getLeafKeys(item.items); } return item.id; }; export const filterAccordionData = ( accordionData: AccordionItem[], query: string ): AccordionItem[] => { if (!query) { return accordionData; } const result = []; for (const item of accordionData) { if (item.label.toLowerCase().includes(query)) { result.push(item); } else if (item.items?.length) { const items = filterAccordionData(item.items, query); if (items.length) { result.push({ ...item, items }); } } } return result; }; export type AccordionItem = { label: string; id: string; items?: AccordionItem[]; addAsterisk?: boolean; }; type AccordionSearchItemProps = { /** * An array of objects which populates the list items */ items: AccordionItem[]; /** * String used to fill in the search input when empty */ id: string; /** * Callback that is fired when an accordion's item is selected */ onSelect: (id: string) => unknown; /** * Array of the selected items' IDs */ selected: string[]; /** * Indicates if the accordion should always be open */ alwaysOpen?: boolean; /** * Indicates if the item should initially be open ie the user can * still collapse after intial load. */ initialOpen?: boolean; /** * A boolean indicating whether the component should span multiple * columns: 2 columns for medium to 3 columns for large+ screens. */ columns?: boolean; /** * The title, works as a trigger to open/close */ label: string; /** * The user's query, passed to highlight in the item's label */ query: string; /** * Appends an asterisk to selected items (in the case of xrefs this indicates full xrefs) */ addAsterisk?: boolean; }; const AccordionSearchCheckbox = ({ onSelect, selected, id, label, query, addAsterisk, }: Omit) => (
  • ); const AccordionSearchItem = ({ label, alwaysOpen, items, selected, columns, onSelect, id, query, addAsterisk, initialOpen = false, }: AccordionSearchItemProps) => { const itemKeys = useMemo(() => new Set(getLeafKeys(items)), [items]); const count = selected.filter((s) => itemKeys.has(s)).length; const areChildrenCheckboxes = items.every((item) => !item.items); return ( {label} } count={count} alwaysOpen={alwaysOpen} key={id} initialOpen={initialOpen} > {areChildrenCheckboxes ? ( ) : ( )} ); }; type AccordionSearchProps = { /** * An array of objects each of which is used to populate an accordion. */ accordionData: AccordionItem[]; /** * String used to fill in the search input when empty */ placeholder?: string; /** * Callback that is fired when an accordion's item is selected */ onSelect: (itemId: string) => unknown; /** * Array of the selected items' IDs */ selected: string[]; /** * A boolean indicating whether the component should span multiple * columns: 2 columns for medium to 3 columns for large+ screens. */ columns?: boolean; }; const AccordionSearch = ({ accordionData, placeholder = '', onSelect, selected, columns, }: AccordionSearchProps) => { const [inputValue, setInputValue] = useState(''); const deferredInputValue = useDeferredValue(inputValue); const filteredAccordionData = useMemo( () => filterAccordionData( accordionData, deferredInputValue.trim().toLowerCase() ), [accordionData, deferredInputValue] ); const handleSearchInputChange = useCallback( (event: React.ChangeEvent) => { setInputValue(event.target.value); }, [] ); if (!accordionData || !accordionData.length) { return ; } const accordionGroupNode = filteredAccordionData.length ? ( filteredAccordionData.map( ({ label, id, items, addAsterisk }, index) => items?.length && ( ) ) ) : ( No matches found ); return ( <> {accordionGroupNode} ); }; export default AccordionSearch;