diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index 3b9682c4adf4c6..e200416e29e0ba 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Enhancement
+
+- `FormTokenField`: Add `tokenizeOnBlur` prop to add any incompleteTokenValue as a new token when field loses focus ([#53976](https://github.com/WordPress/gutenberg/pull/53976)).
+
### Bug Fix
- `SandBox`: Fix the cleanup method in useEffect ([#53796](https://github.com/WordPress/gutenberg/pull/53796)).
diff --git a/packages/components/src/form-token-field/README.md b/packages/components/src/form-token-field/README.md
index 90fe84e56f2ee6..3a4ce2a143624a 100644
--- a/packages/components/src/form-token-field/README.md
+++ b/packages/components/src/form-token-field/README.md
@@ -62,6 +62,7 @@ The `value` property is handled in a manner similar to controlled form component
- `__experimentalValidateInput` - If passed, all introduced values will be validated before being added as tokens.
- `__experimentalAutoSelectFirstMatch` - If true, the select the first matching suggestion when the user presses the Enter key (or space when tokenizeOnSpace is true).
- `__nextHasNoMarginBottom` - Start opting into the new margin-free styles that will become the default in a future version, currently scheduled to be WordPress 6.5. (The prop can be safely removed once this happens.)
+- `tokenizeOnBlur` - If true, add any incompleteTokenValue as a new token when the field loses focus.
## Usage
diff --git a/packages/components/src/form-token-field/index.tsx b/packages/components/src/form-token-field/index.tsx
index e3781880370628..b3a5c5a53d4c0a 100644
--- a/packages/components/src/form-token-field/index.tsx
+++ b/packages/components/src/form-token-field/index.tsx
@@ -73,6 +73,7 @@ export function FormTokenField( props: FormTokenFieldProps ) {
__next40pxDefaultSize = false,
__experimentalAutoSelectFirstMatch = false,
__nextHasNoMarginBottom = false,
+ tokenizeOnBlur = false,
} = useDeprecated36pxDefaultSizeProp< FormTokenFieldProps >(
props,
'wp.components.FormTokenField'
@@ -167,6 +168,9 @@ export function FormTokenField( props: FormTokenFieldProps ) {
__experimentalValidateInput( incompleteTokenValue )
) {
setIsActive( false );
+ if ( tokenizeOnBlur && inputHasValidValue() ) {
+ addNewToken( incompleteTokenValue );
+ }
} else {
// Reset to initial state
setIncompleteTokenValue( '' );
@@ -451,7 +455,7 @@ export function FormTokenField( props: FormTokenFieldProps ) {
setSelectedSuggestionScroll( false );
setIsExpanded( ! __experimentalExpandOnFocus );
- if ( isActive ) {
+ if ( isActive && ! tokenizeOnBlur ) {
focus();
}
}
diff --git a/packages/components/src/form-token-field/test/index.tsx b/packages/components/src/form-token-field/test/index.tsx
index e62e851f1cf226..76e308d5993beb 100644
--- a/packages/components/src/form-token-field/test/index.tsx
+++ b/packages/components/src/form-token-field/test/index.tsx
@@ -205,7 +205,42 @@ describe( 'FormTokenField', () => {
] );
} );
- it( "should not add a token with the input's value when pressing the tab key", async () => {
+ it( 'should add a token with the input value with onBlur when `tokenizeOnBlur` prop is `true`', async () => {
+ const user = userEvent.setup();
+
+ const onChangeSpy = jest.fn();
+
+ const { rerender } = render(
+
+ );
+
+ const input = screen.getByRole( 'combobox' );
+
+ // Add 'grapefruit' token by typing it and check blur of field does not tokenize it.
+ await user.type( input, 'grapefruit' );
+ await user.click( document.body );
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 0 );
+ expectTokensNotToBeInTheDocument( [ 'grapefruit' ] );
+
+ rerender(
+
+ );
+ await user.clear( input );
+
+ // Add 'grapefruit' token by typing it and check blur of field tokenizes it.
+ await user.type( input, 'grapefruit' );
+
+ await user.click( document.body );
+ expect( onChangeSpy ).toHaveBeenNthCalledWith( 1, [
+ 'grapefruit',
+ ] );
+ expectTokensToBeInTheDocument( [ 'grapefruit' ] );
+ } );
+
+ it( "should not add a token with the input's value when tokenizeOnBlur is not set and pressing the tab key", async () => {
const user = userEvent.setup();
const onChangeSpy = jest.fn();
diff --git a/packages/components/src/form-token-field/types.ts b/packages/components/src/form-token-field/types.ts
index fe466ffbe59f84..e343601106f411 100644
--- a/packages/components/src/form-token-field/types.ts
+++ b/packages/components/src/form-token-field/types.ts
@@ -182,6 +182,12 @@ export interface FormTokenFieldProps
* @default false
*/
__nextHasNoMarginBottom?: boolean;
+ /**
+ * If true, add any incompleteTokenValue as a new token when the field loses focus.
+ *
+ * @default false
+ */
+ tokenizeOnBlur?: boolean;
}
/**
diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js
index f8295af2a79dc9..c9305806c7e13f 100644
--- a/packages/patterns/src/components/category-selector.js
+++ b/packages/patterns/src/components/category-selector.js
@@ -111,6 +111,7 @@ export default function CategorySelector( { onCategorySelection } ) {
onInputChange={ debouncedSearch }
maxSuggestions={ MAX_TERMS_SUGGESTIONS }
label={ __( 'Categories' ) }
+ tokenizeOnBlur={ true }
/>
>
);