/** * External dependencies */ // eslint-disable-next-line no-restricted-imports import * as Ariakit from '@ariakit/react'; import removeAccents from 'remove-accents'; import clsx from 'clsx'; /** * WordPress dependencies */ import { useInstanceId } from '@wordpress/compose'; import { __, sprintf } from '@wordpress/i18n'; import { useState, useMemo, useDeferredValue } from '@wordpress/element'; import { VisuallyHidden, Icon, Composite, Spinner, } from '@wordpress/components'; import { search, check } from '@wordpress/icons'; /** * Internal dependencies */ import { getCurrentValue } from './utils'; import type { Filter, NormalizedFilter, View, Option } from '../../types'; import useElements from '../../hooks/use-elements'; interface SearchWidgetProps { view: View; filter: NormalizedFilter & { elements: Option[]; }; onChangeView: ( view: View ) => void; } function normalizeSearchInput( input = '' ) { return removeAccents( input.trim().toLowerCase() ); } const getNewValue = ( filterDefinition: NormalizedFilter, currentFilter: Filter | undefined, value: any ) => { if ( filterDefinition.singleSelection ) { return value; } if ( Array.isArray( currentFilter?.value ) ) { return currentFilter.value.includes( value ) ? currentFilter.value.filter( ( v ) => v !== value ) : [ ...currentFilter.value, value ]; } return [ value ]; }; function generateFilterElementCompositeItemId( prefix: string, filterElementValue: string ) { return `${ prefix }-${ filterElementValue }`; } const MultiSelectionOption = ( { selected }: { selected: boolean } ) => { return ( { selected && } ); }; const SingleSelectionOption = ( { selected }: { selected: boolean } ) => { return ( ); }; function ListBox( { view, filter, onChangeView }: SearchWidgetProps ) { const baseId = useInstanceId( ListBox, 'dataviews-filter-list-box' ); const [ activeCompositeId, setActiveCompositeId ] = useState< string | null | undefined >( // When there are one or less operators, the first item is set as active // (by setting the initial `activeId` to `undefined`). // With 2 or more operators, the focus is moved on the operators control // (by setting the initial `activeId` to `null`), meaning that there won't // be an active item initially. Focus is then managed via the // `onFocusVisible` callback. filter.operators?.length === 1 ? undefined : null ); const currentFilter = view.filters?.find( ( f ) => f.field === filter.field ); const currentValue = getCurrentValue( filter, currentFilter ); return ( { // `onFocusVisible` needs the `Composite` component to be focusable, // which is implicitly achieved via the `virtualFocus` prop. if ( ! activeCompositeId && filter.elements.length ) { setActiveCompositeId( generateFilterElementCompositeItemId( baseId, filter.elements[ 0 ].value ) ); } } } render={ } > { filter.elements.map( ( element ) => ( } onClick={ () => { const newFilters = currentFilter ? [ ...( view.filters ?? [] ).map( ( _filter ) => { if ( _filter.field === filter.field ) { return { ..._filter, operator: currentFilter.operator || filter .operators[ 0 ], value: getNewValue( filter, currentFilter, element.value ), }; } return _filter; } ), ] : [ ...( view.filters ?? [] ), { field: filter.field, operator: filter.operators[ 0 ], value: getNewValue( filter, currentFilter, element.value ), }, ]; onChangeView( { ...view, page: 1, filters: newFilters, } ); } } /> } > { filter.singleSelection && ( ) } { ! filter.singleSelection && ( ) } { element.label } ) ) } ); } function ComboboxList( { view, filter, onChangeView }: SearchWidgetProps ) { const [ searchValue, setSearchValue ] = useState( '' ); const deferredSearchValue = useDeferredValue( searchValue ); const currentFilter = view.filters?.find( ( _filter ) => _filter.field === filter.field ); const currentValue = getCurrentValue( filter, currentFilter ); const matches = useMemo( () => { const normalizedSearch = normalizeSearchInput( deferredSearchValue ); return filter.elements.filter( ( item ) => normalizeSearchInput( item.label ).includes( normalizedSearch ) ); }, [ filter.elements, deferredSearchValue ] ); return ( { const newFilters = currentFilter ? [ ...( view.filters ?? [] ).map( ( _filter ) => { if ( _filter.field === filter.field ) { return { ..._filter, operator: currentFilter.operator || filter.operators[ 0 ], value, }; } return _filter; } ), ] : [ ...( view.filters ?? [] ), { field: filter.field, operator: filter.operators[ 0 ], value, }, ]; onChangeView( { ...view, page: 1, filters: newFilters, } ); } } setValue={ setSearchValue } >
{ __( 'Search items' ) } } > { __( 'Search items' ) }
{ matches.map( ( element ) => { return ( { filter.singleSelection && ( ) } { ! filter.singleSelection && ( ) } { !! element.description && ( { element.description } ) } ); } ) } { ! matches.length &&

{ __( 'No results found' ) }

}
); } export default function SearchWidget( props: SearchWidgetProps ) { const { elements, isLoading } = useElements( { elements: props.filter.elements, getElements: props.filter.getElements, } ); if ( isLoading ) { return (
); } if ( elements.length === 0 ) { return (
{ __( 'No elements found' ) }
); } const Widget = elements.length > 10 ? ComboboxList : ListBox; return ; }