diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 59a535447bf0d..80f18b473d7ed 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -5,6 +5,7 @@ ### Enhancements - `InputControl`/`SelectControl`: update `height`/`min-height` to `32px` instead of `30px` to align with modern sizing scale ([#55490](https://github.com/WordPress/gutenberg/pull/55490)). +- `ComboboxControl`: Add `shouldFilter` prop to allow the options to be controlled and filtered externally ([#55574](https://github.com/WordPress/gutenberg/pull/55574)). ### Bug Fix diff --git a/packages/components/src/combobox-control/README.md b/packages/components/src/combobox-control/README.md index 30f1f47e653e8..121b7df9049f3 100644 --- a/packages/components/src/combobox-control/README.md +++ b/packages/components/src/combobox-control/README.md @@ -95,6 +95,14 @@ Function called when the control's search input value changes. The argument cont - Type: `( value: string ) => void` - Required: No +#### shouldFilter + +Function called to determine whether the control should filter the list of displayed options based on the user input or whether it's something that is handled externally by updating the `options` prop when the `onFilterValueChange` callback is called. + +- Type: `boolean` +- Required: No +- Default: `true` + #### onChange Function called with the selected value changes. diff --git a/packages/components/src/combobox-control/index.tsx b/packages/components/src/combobox-control/index.tsx index f4292d9980ee4..dbacea29908e5 100644 --- a/packages/components/src/combobox-control/index.tsx +++ b/packages/components/src/combobox-control/index.tsx @@ -120,6 +120,7 @@ function ComboboxControl( props: ComboboxControlProps ) { help, allowReset = true, className, + shouldFilter = true, messages = { selected: __( 'Item selected.' ), }, @@ -151,6 +152,7 @@ function ComboboxControl( props: ComboboxControlProps ) { const matchingSuggestions = useMemo( () => { const startsWithMatch: ComboboxControlOption[] = []; const containsMatch: ComboboxControlOption[] = []; + const others: ComboboxControlOption[] = []; const match = normalizeTextString( inputValue ); options.forEach( ( option ) => { const index = normalizeTextString( option.label ).indexOf( match ); @@ -158,11 +160,13 @@ function ComboboxControl( props: ComboboxControlProps ) { startsWithMatch.push( option ); } else if ( index > 0 ) { containsMatch.push( option ); + } else if ( ! shouldFilter ) { + others.push( option ); } } ); - return startsWithMatch.concat( containsMatch ); - }, [ inputValue, options ] ); + return startsWithMatch.concat( containsMatch ).concat( others ); + }, [ shouldFilter, inputValue, options ] ); const onSuggestionSelected = ( newSelectedSuggestion: ComboboxControlOption @@ -266,18 +270,26 @@ function ComboboxControl( props: ComboboxControlProps ) { // Update current selections when the filter input changes. useEffect( () => { - const hasMatchingSuggestions = matchingSuggestions.length > 0; - const hasSelectedMatchingSuggestions = - getIndexOfMatchingSuggestion( - selectedSuggestion, - matchingSuggestions - ) > 0; - - if ( hasMatchingSuggestions && ! hasSelectedMatchingSuggestions ) { - // If the current selection isn't present in the list of suggestions, then automatically select the first item from the list of suggestions. - setSelectedSuggestion( matchingSuggestions[ 0 ] ); + if ( ! shouldFilter ) { + setSelectedSuggestion( null ); + return; } - }, [ matchingSuggestions, selectedSuggestion ] ); + + // If the current selection isn't present in the list of suggestions, then automatically select the first item from the list of suggestions. + setSelectedSuggestion( ( previousSuggetion ) => { + const hasMatchingSuggestions = matchingSuggestions.length > 0; + const hasSelectedMatchingSuggestions = + getIndexOfMatchingSuggestion( + previousSuggetion, + matchingSuggestions + ) > 0; + + if ( hasMatchingSuggestions && ! hasSelectedMatchingSuggestions ) { + return matchingSuggestions[ 0 ]; + } + return previousSuggetion; + } ); + }, [ shouldFilter, matchingSuggestions ] ); // Announcements. useEffect( () => { diff --git a/packages/components/src/combobox-control/test/index.tsx b/packages/components/src/combobox-control/test/index.tsx index c26d8ab5afeb9..f0c803f622967 100644 --- a/packages/components/src/combobox-control/test/index.tsx +++ b/packages/components/src/combobox-control/test/index.tsx @@ -212,6 +212,48 @@ describe.each( [ expect( input ).toHaveValue( targetOption.label ); } ); + it( 'should not filter the list of options if shouldFilter is false', async () => { + const user = await userEvent.setup(); + const targetOption = timezones[ 0 ]; + const japanTargetOption = timezones[ 12 ]; + const onChangeSpy = jest.fn(); + render( + + ); + const input = getInput( defaultLabelText ); + + // Pressing tab selects the input and shows the options + await user.tab(); + + // Type enough characters to ensure a predictable search result + await user.keyboard( 'Japan' ); + + // Select first item (which should be Japan because the items are ordered) + await user.keyboard( '{ArrowDown}' ); + + // Pressing Enter/Return selects the currently focused option + await user.keyboard( '{Enter}' ); + + expect( onChangeSpy ).toHaveBeenCalledTimes( 1 ); + expect( onChangeSpy ).toHaveBeenCalledWith( japanTargetOption.value ); + expect( input ).toHaveValue( japanTargetOption.label ); + + // Select first item (which should be Japan because the items are ordered) + await user.keyboard( '{ArrowDown}' ); + + // Pressing Enter/Return selects the currently focused option + await user.keyboard( '{Enter}' ); + + expect( onChangeSpy ).toHaveBeenCalledTimes( 2 ); + expect( onChangeSpy ).toHaveBeenCalledWith( targetOption.value ); + expect( input ).toHaveValue( targetOption.label ); + } ); + it( 'should render aria-live announcement upon selection', async () => { const user = await userEvent.setup(); const targetOption = timezones[ 9 ]; diff --git a/packages/components/src/combobox-control/types.ts b/packages/components/src/combobox-control/types.ts index 6a32fb17e16c0..03aaafd0a47cb 100644 --- a/packages/components/src/combobox-control/types.ts +++ b/packages/components/src/combobox-control/types.ts @@ -73,4 +73,10 @@ export type ComboboxControlProps = Pick< * The current value of the control. */ value?: string | null; + /** + * By default the control will filter the options based on the input value. + * but if you're providing filtered options using REST API or something else, + * you may consider disabling the filtering by marking this prop as false. + */ + shouldFilter?: boolean; }; diff --git a/packages/editor/src/components/page-attributes/parent.js b/packages/editor/src/components/page-attributes/parent.js index b1f9e15ff45e8..80037cbf7feb6 100644 --- a/packages/editor/src/components/page-attributes/parent.js +++ b/packages/editor/src/components/page-attributes/parent.js @@ -167,6 +167,7 @@ export function PageAttributesParent() { options={ parentOptions } onFilterValueChange={ debounce( handleKeydown, 300 ) } onChange={ handleChange } + shouldFilter={ false } /> ); }