diff --git a/packages/components/src/form-token-field/index.tsx b/packages/components/src/form-token-field/index.tsx index d89080b4410c41..57043f1cbdc02a 100644 --- a/packages/components/src/form-token-field/index.tsx +++ b/packages/components/src/form-token-field/index.tsx @@ -74,6 +74,9 @@ export function FormTokenField( props: FormTokenFieldProps ) { __next40pxDefaultSize = false, __experimentalAutoSelectFirstMatch = false, __nextHasNoMarginBottom = false, + __filterSuggestions, + __onSuggestionClick, + __forceSuggestionFocus = false, tokenizeOnBlur = false, } = useDeprecated36pxDefaultSizeProp< FormTokenFieldProps >( props ); @@ -449,17 +452,35 @@ export function FormTokenField( props: FormTokenFieldProps ) { speak( messages.__experimentalInvalid, 'assertive' ); return; } + addNewTokens( [ token ] ); speak( messages.added, 'assertive' ); setIncompleteTokenValue( '' ); - setSelectedSuggestionIndex( -1 ); - setSelectedSuggestionScroll( false ); setIsExpanded( ! __experimentalExpandOnFocus ); if ( isActive && ! tokenizeOnBlur ) { focus(); } + + /* + * If a __filterSuggestions is provided, + * it's possible that the token has been already added. + */ + const isTokenTaken = value.some( ( item ) => { + return getTokenValue( item ) === token; + } ); + + __onSuggestionClick?.( token, isTokenTaken ); + + if ( __forceSuggestionFocus ) { + const index = getMatchingSuggestions().indexOf( token ); + setSelectedSuggestionIndex( index ); + setSelectedSuggestionScroll( true ); + } else { + setSelectedSuggestionScroll( false ); + setSelectedSuggestionIndex( -1 ); + } } function deleteToken( token: string | TokenItem ) { @@ -516,6 +537,11 @@ export function FormTokenField( props: FormTokenFieldProps ) { _suggestions = startsWithMatch.concat( containsMatch ); } + // Apply custom filter if provided. + if ( __filterSuggestions ) { + _suggestions = __filterSuggestions( _suggestions, match ); + } + return _suggestions.slice( 0, _maxSuggestions ); } diff --git a/packages/components/src/form-token-field/stories/index.story.tsx b/packages/components/src/form-token-field/stories/index.story.tsx index da61a0b313bfe9..af051a6d108d0e 100644 --- a/packages/components/src/form-token-field/stories/index.story.tsx +++ b/packages/components/src/form-token-field/stories/index.story.tsx @@ -3,6 +3,8 @@ */ import type { Meta, StoryFn } from '@storybook/react'; import type { ComponentProps } from 'react'; +import styled from '@emotion/styled'; + /** * WordPress dependencies */ @@ -12,6 +14,8 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import FormTokenField from '../'; +import CheckboxControl from '../../checkbox-control'; +import Label from '../../input-control/label'; const meta: Meta< typeof FormTokenField > = { component: FormTokenField, @@ -135,3 +139,92 @@ WithValidatedInput.args = { __experimentalValidateInput: ( input: string ) => continents.includes( input ), }; + +const fruits = [ + 'Apple', + 'Apricot', + 'Avocado', + 'Banana', + 'Blackberry', + 'Blueberry', +]; + +const CheckboxLabelWrapper = styled.div` + .suggestion-checkbox-label { + display: flex !important; + align-items: center; + color: inherit; + } +`; + +export const WithSuggestionCheckbox: StoryFn< typeof FormTokenField > = ( { + suggestions, + ...args +} ) => { + const [ selectedFruits, setSelectedFruits ] = useState< + ComponentProps< typeof FormTokenField >[ 'value' ] + >( [] ); + + /* + * Combine the suggested fruits with + */ + let allFruits = suggestions ? [ ...suggestions ] : []; + + if ( selectedFruits ) { + allFruits = allFruits.concat( + selectedFruits.map( ( fruit ) => String( fruit ) ) + ); + + // Remove duplicates + allFruits = [ ...new Set( allFruits ) ]; + + // Sort the values + allFruits.sort(); + } + + return ( + { + return allFruits.filter( ( suggestion ) => + suggestion.toLowerCase().includes( value.toLowerCase() ) + ); + } } + __onSuggestionClick={ ( suggestion, isSuggestionTaken ) => { + // If the suggestion is taken, filter the selected continents + if ( isSuggestionTaken ) { + const filteredContinents = selectedFruits?.filter( + ( continent ) => continent !== suggestion + ); + setSelectedFruits( filteredContinents ); + } + } } + __experimentalRenderItem={ ( { item } ) => { + const itemTaken = selectedFruits?.includes( item ); + return ( + + + + ); + } } + /> + ); +}; + +WithSuggestionCheckbox.args = { + label: 'Pick or create a fruit', + suggestions: fruits, + __nextHasNoMarginBottom: true, + __experimentalExpandOnFocus: true, + __experimentalAutoSelectFirstMatch: true, + __forceSuggestionFocus: true, +}; diff --git a/packages/components/src/form-token-field/types.ts b/packages/components/src/form-token-field/types.ts index db4549a7f0779c..5e038388057745 100644 --- a/packages/components/src/form-token-field/types.ts +++ b/packages/components/src/form-token-field/types.ts @@ -183,6 +183,27 @@ export interface FormTokenFieldProps * @default false */ __nextHasNoMarginBottom?: boolean; + + /** + * Use this prop to filter the suggestions list. + * It receives the list of suggestions and the current input value. + * It should return a filtered list of suggestions. + */ + __filterSuggestions?: ( suggestions: string[], value: string ) => string[]; + + /** + * Callback to be called when a suggestion is clicked. + */ + __onSuggestionClick?: ( + suggestion: string, + isSuggestionTaken: boolean + ) => void; + + /** + * If true, the suggestion item will be focused when the suggestions list is opened. + */ + __forceSuggestionFocus?: boolean; + /** * If true, add any incompleteTokenValue as a new token when the field loses focus. *