From 000595e020e6f60270abf91325e04b571ea14bec Mon Sep 17 00:00:00 2001 From: Ahmed Date: Wed, 13 Aug 2025 15:13:12 +0100 Subject: [PATCH 01/34] Add support for validating elements --- packages/dataviews/src/types.ts | 1 + packages/dataviews/src/validation.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index f7dd6f44dd27d4..3bfbe02ecf7074 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -148,6 +148,7 @@ export type FieldTypeDefinition< Item > = { export type Rules< Item > = { required?: boolean; + elements?: boolean; custom?: ( item: Item, field: NormalizedField< Item > ) => null | string; }; diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts index 3b3263c3ad7a0f..bf4a3669ee22f2 100644 --- a/packages/dataviews/src/validation.ts +++ b/packages/dataviews/src/validation.ts @@ -44,6 +44,16 @@ export function isItemValid< Item >( } } + if ( field.isValid.elements ) { + if ( field.type === 'array' ) { + const elementValue = field.elements?.find( + ( element ) => element.label === item + ); + + return !! elementValue; + } + } + if ( typeof field.isValid.custom === 'function' && field.isValid.custom( item, field ) !== null From cf015c55271de5c5e0f2eb9d8f3f38db255f1d8c Mon Sep 17 00:00:00 2001 From: Ahmed Date: Wed, 13 Aug 2025 15:16:11 +0100 Subject: [PATCH 02/34] Add validation input and how to --- packages/dataviews/src/dataform-controls/array.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index 8b900b86cf105f..04fbbc90df90cb 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -7,7 +7,8 @@ import { useCallback, useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import type { DataFormControlProps } from '../types'; +import type { DataFormControlProps, Field } from '../types'; +import { isItemValid } from '../validation'; export default function ArrayControl< Item >( { data, @@ -77,9 +78,19 @@ export default function ArrayControl< Item >( { suggestions={ elements?.map( ( suggestion ) => suggestion.label ) ?? [] } + __experimentalValidateInput={ ( token ) => { + if ( ! field.isValid.elements ) { + return true; + } + + return isItemValid( token as Item, [ field as Field< Item > ], { + fields: [ id ], + } ); + } } __experimentalExpandOnFocus={ elements && elements.length > 0 } __next40pxDefaultSize __nextHasNoMarginBottom + __experimentalShowHowTo={ ! field.isValid.elements } /> ); } From 14e0308e22bb134e0305cf3bf1957a7549a43b46 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 15 Aug 2025 12:06:25 +0100 Subject: [PATCH 03/34] Add array validation with required param --- packages/dataviews/src/validation.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts index bf4a3669ee22f2..a51ad95b60cffd 100644 --- a/packages/dataviews/src/validation.ts +++ b/packages/dataviews/src/validation.ts @@ -25,6 +25,14 @@ export function isItemValid< Item >( const isEmptyNullOrUndefined = ( value: any ) => [ undefined, '', null ].includes( value ); + const isNotArrayEmptyOrContainsInvalidElements = ( value: any ) => { + return ( + ! Array.isArray( value ) || + value.length === 0 || + value.every( ( element: any ) => isEmptyNullOrUndefined( element ) ) + ); + }; + return _fields.every( ( field ) => { const value = field.getValue( { item } ); @@ -34,6 +42,8 @@ export function isItemValid< Item >( ( field.type === 'email' && isEmptyNullOrUndefined( value ) ) || ( field.type === 'integer' && isEmptyNullOrUndefined( value ) ) || + ( field.type === 'array' && + isNotArrayEmptyOrContainsInvalidElements( value ) ) || ( field.type === undefined && isEmptyNullOrUndefined( value ) ) ) { return false; From 59ade4421085b1b9a26f408787408cc35aa2aa0f Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 15 Aug 2025 12:12:28 +0100 Subject: [PATCH 04/34] Update validation logic to check if the token matches any available elements using `findElementByLabel` instead of `isItemValid`. --- packages/dataviews/src/dataform-controls/array.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index 04fbbc90df90cb..61993124b47b15 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -7,8 +7,7 @@ import { useCallback, useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import type { DataFormControlProps, Field } from '../types'; -import { isItemValid } from '../validation'; +import type { DataFormControlProps } from '../types'; export default function ArrayControl< Item >( { data, @@ -83,9 +82,9 @@ export default function ArrayControl< Item >( { return true; } - return isItemValid( token as Item, [ field as Field< Item > ], { - fields: [ id ], - } ); + // Check if the token matches any of the available elements + const tokenByLabel = findElementByLabel( token ); + return !! tokenByLabel; } } __experimentalExpandOnFocus={ elements && elements.length > 0 } __next40pxDefaultSize From 1db0a20a4c3ca93855c10af21c07f0780cc5debb Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 15 Aug 2025 12:50:44 +0100 Subject: [PATCH 05/34] Enhance validation logic to support array types by ensuring all values are valid elements and streamline single-value checks for element validity. --- packages/dataviews/src/validation.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts index a51ad95b60cffd..04cfe891874846 100644 --- a/packages/dataviews/src/validation.ts +++ b/packages/dataviews/src/validation.ts @@ -55,12 +55,22 @@ export function isItemValid< Item >( } if ( field.isValid.elements ) { - if ( field.type === 'array' ) { - const elementValue = field.elements?.find( - ( element ) => element.label === item + if ( field.elements ) { + const validValues = field.elements.map( + ( element ) => element.value ); - return !! elementValue; + if ( field.type === 'array' ) { + // For arrays, check if all values are valid elements + if ( Array.isArray( value ) ) { + return value.every( ( arrayItem ) => + validValues.includes( arrayItem ) + ); + } + return false; + } + // For single-value fields, check if the value is a valid element + return validValues.includes( value ); } } From b7b321ab41f81e41bf46c99e035265b04f8f9785 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 15 Aug 2025 13:00:21 +0100 Subject: [PATCH 06/34] Add tests for array and text field validations and validating against specified elements. --- packages/dataviews/src/test/validation.ts | 192 ++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/packages/dataviews/src/test/validation.ts b/packages/dataviews/src/test/validation.ts index d94430aada9ec6..824e9ecd78da98 100644 --- a/packages/dataviews/src/test/validation.ts +++ b/packages/dataviews/src/test/validation.ts @@ -117,4 +117,196 @@ describe( 'validation', () => { const result = isItemValid( item, fields, form ); expect( result ).toBe( true ); } ); + + it( 'array field is invalid when required but empty', () => { + const item = { id: 1, tags: [] }; + const fields: Field< {} >[] = [ + { + id: 'tags', + type: 'array', + isValid: { + required: true, + }, + }, + ]; + const form = { fields: [ 'tags' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( false ); + } ); + + it( 'array field is invalid when required but not an array', () => { + const item = { id: 1, tags: null }; + const fields: Field< {} >[] = [ + { + id: 'tags', + type: 'array', + isValid: { + required: true, + }, + }, + ]; + const form = { fields: [ 'tags' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( false ); + } ); + + it( 'array field is valid when required and has values', () => { + const item = { id: 1, tags: [ 'tag1', 'tag2' ] }; + const fields: Field< {} >[] = [ + { + id: 'tags', + type: 'array', + isValid: { + required: true, + }, + }, + ]; + const form = { fields: [ 'tags' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( true ); + } ); + + it( 'text field with isValid.elements validates against elements', () => { + const item = { id: 1, status: 'published' }; + const fields: Field< {} >[] = [ + { + id: 'status', + type: 'text', + elements: [ + { value: 'draft', label: 'Draft' }, + { value: 'published', label: 'Published' }, + ], + isValid: { + elements: true, + }, + }, + ]; + const form = { fields: [ 'status' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( true ); + } ); + + it( 'text field with isValid.elements rejects invalid values', () => { + const item = { id: 1, status: 'invalid-status' }; + const fields: Field< {} >[] = [ + { + id: 'status', + type: 'text', + elements: [ + { value: 'draft', label: 'Draft' }, + { value: 'published', label: 'Published' }, + ], + isValid: { + elements: true, + }, + }, + ]; + const form = { fields: [ 'status' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( false ); + } ); + + it( 'integer field with isValid.elements validates against elements', () => { + const item = { id: 1, priority: 2 }; + const fields: Field< {} >[] = [ + { + id: 'priority', + type: 'integer', + elements: [ + { value: 1, label: 'Low' }, + { value: 2, label: 'Medium' }, + { value: 3, label: 'High' }, + ], + isValid: { + elements: true, + }, + }, + ]; + const form = { fields: [ 'priority' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( true ); + } ); + + it( 'integer field with isValid.elements rejects invalid values', () => { + const item = { id: 1, priority: 5 }; + const fields: Field< {} >[] = [ + { + id: 'priority', + type: 'integer', + elements: [ + { value: 1, label: 'Low' }, + { value: 2, label: 'Medium' }, + { value: 3, label: 'High' }, + ], + isValid: { + elements: true, + }, + }, + ]; + const form = { fields: [ 'priority' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( false ); + } ); + + it( 'array field with isValid.elements validates all items against elements', () => { + const item = { id: 1, tags: [ 'red', 'blue' ] }; + const fields: Field< {} >[] = [ + { + id: 'tags', + type: 'array', + elements: [ + { value: 'red', label: 'Red' }, + { value: 'blue', label: 'Blue' }, + { value: 'green', label: 'Green' }, + ], + isValid: { + elements: true, + }, + }, + ]; + const form = { fields: [ 'tags' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( true ); + } ); + + it( 'array field with isValid.elements rejects arrays with invalid items', () => { + const item = { id: 1, tags: [ 'red', 'yellow' ] }; + const fields: Field< {} >[] = [ + { + id: 'tags', + type: 'array', + elements: [ + { value: 'red', label: 'Red' }, + { value: 'blue', label: 'Blue' }, + { value: 'green', label: 'Green' }, + ], + isValid: { + elements: true, + }, + }, + ]; + const form = { fields: [ 'tags' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( false ); + } ); + + it( 'array field with isValid.elements handles non-array values', () => { + const item = { id: 1, tags: 'not-an-array' }; + const fields: Field< {} >[] = [ + { + id: 'tags', + type: 'array', + elements: [ + { value: 'red', label: 'Red' }, + { value: 'blue', label: 'Blue' }, + ], + isValid: { + elements: true, + }, + }, + ]; + const form = { fields: [ 'tags' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( false ); + } ); } ); From 81d355f3dd33d09affe85b333718525222343a3b Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 18 Aug 2025 15:19:50 +0100 Subject: [PATCH 07/34] Add categories field to DataFormValidationComponent with validation rules and predefined options --- .../dataform/stories/index.story.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 3ae8b81e04e3b3..b5a420422944dd 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -329,6 +329,7 @@ const DataFormValidationComponent = ( { required }: { required: boolean } ) => { boolean: boolean; customEdit: string; customValidation: string; + categories: string[]; }; const [ post, setPost ] = useState< ValidatedItem >( { @@ -336,6 +337,7 @@ const DataFormValidationComponent = ( { required }: { required: boolean } ) => { email: 'hi@example.com', integer: 2, boolean: true, + categories: [ 'astronomy' ], customEdit: 'custom control', customValidation: 'potato', } ); @@ -373,6 +375,21 @@ const DataFormValidationComponent = ( { required }: { required: boolean } ) => { required, }, }, + { + id: 'categories', + type: 'array' as const, + label: 'Categories', + isValid: { + required, + }, + elements: [ + { value: 'astronomy', label: 'Astronomy' }, + { value: 'book-review', label: 'Book review' }, + { value: 'event', label: 'Event' }, + { value: 'photography', label: 'Photography' }, + { value: 'travel', label: 'Travel' }, + ], + }, { id: 'customEdit', label: 'Custom Control', @@ -408,6 +425,7 @@ const DataFormValidationComponent = ( { required }: { required: boolean } ) => { 'email', 'integer', 'boolean', + 'categories', 'customEdit', 'customValidation', ], From b3b0c281795816a7b54a736fc033c9f35fa1aca1 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 22 Aug 2025 10:50:23 +0100 Subject: [PATCH 08/34] Add ValidatedFormTokenControl component to enhance array input validation with custom and required checks --- .../dataviews/src/dataform-controls/array.tsx | 104 +++++++++++++++++- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index 61993124b47b15..55594589a501e3 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -1,14 +1,93 @@ /** * WordPress dependencies */ +import { useCallback, useMemo, useRef, useState } from '@wordpress/element'; import { FormTokenField } from '@wordpress/components'; -import { useCallback, useMemo } from '@wordpress/element'; /** * Internal dependencies */ import type { DataFormControlProps } from '../types'; +// Create a validated FormTokenField wrapper +const ValidatedFormTokenControl = ( { + required, + customValidator, + onChange, + markWhenOptional, + label, + ...restProps +}: React.ComponentProps< typeof FormTokenField > & { + required?: boolean; + customValidator?: ( value: any ) => string | void; + markWhenOptional?: boolean; +} ) => { + const [ errorMessage, setErrorMessage ] = useState< string | undefined >(); + const [ isTouched, setIsTouched ] = useState( false ); + const valueRef = useRef( restProps.value ); + + const validate = useCallback( () => { + if ( + required && + ( ! valueRef.current || valueRef.current.length === 0 ) + ) { + setErrorMessage( 'This field is required.' ); + return; + } + + const customError = customValidator?.( valueRef.current ); + setErrorMessage( customError || undefined ); + }, [ required, customValidator ] ); + + const onBlur = useCallback( () => { + setIsTouched( true ); + validate(); + }, [ validate ] ); + + const onChangeControl = useCallback( + ( value: any ) => { + valueRef.current = value; + onChange?.( value ); + + // Only validate incrementally if the field has been touched or currently has an error + if ( isTouched || errorMessage ) { + validate(); + } + }, + [ onChange, isTouched, errorMessage, validate ] + ); + + // Append required indicator to label + const labelWithIndicator = useMemo( () => { + if ( required && ! markWhenOptional ) { + return `${ label } (Required)`; + } + if ( ! required && markWhenOptional ) { + return `${ label } (Optional)`; + } + return label; + }, [ label, required, markWhenOptional ] ); + + return ( +
+ +
+ { errorMessage && ( +

+ { errorMessage } +

+ ) } +
+
+ ); +}; + export default function ArrayControl< Item >( { data, field, @@ -69,7 +148,22 @@ export default function ArrayControl< Item >( { ); return ( - { + if ( field.isValid?.custom ) { + const result = field.isValid.custom( + { + ...data, + [ id ]: newValue, + }, + field + ); + return result || undefined; + } + + return undefined; + } } label={ hideLabelFromVision ? undefined : label } value={ arrayValue } onChange={ onChangeControl } @@ -78,7 +172,7 @@ export default function ArrayControl< Item >( { elements?.map( ( suggestion ) => suggestion.label ) ?? [] } __experimentalValidateInput={ ( token ) => { - if ( ! field.isValid.elements ) { + if ( ! field.isValid?.elements ) { return true; } @@ -87,9 +181,7 @@ export default function ArrayControl< Item >( { return !! tokenByLabel; } } __experimentalExpandOnFocus={ elements && elements.length > 0 } - __next40pxDefaultSize - __nextHasNoMarginBottom - __experimentalShowHowTo={ ! field.isValid.elements } + __experimentalShowHowTo={ ! field.isValid?.elements } /> ); } From 382cb4c09ec20e9e9552d004224d32ed80ca5e03 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 22 Aug 2025 11:11:06 +0100 Subject: [PATCH 09/34] Enhance array validation logic to ensure elements are only checked if isValid elements is passed --- packages/dataviews/src/field-types/array.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/src/field-types/array.tsx b/packages/dataviews/src/field-types/array.tsx index 5afca72b20af21..5a756d7105170b 100644 --- a/packages/dataviews/src/field-types/array.tsx +++ b/packages/dataviews/src/field-types/array.tsx @@ -59,7 +59,7 @@ const arrayFieldType: FieldTypeDefinition< any > = { return __( 'Every value must be a string.' ); } - if ( field?.elements ) { + if ( field?.elements && field.isValid?.elements ) { const validValues = field.elements.map( ( f ) => f.value ); if ( ! value.every( ( v: any ) => validValues.includes( v ) ) From 203af536e6d5d69e96e8fd526d24ad276d1b601a Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 22 Aug 2025 11:12:03 +0100 Subject: [PATCH 10/34] Refactor array control to support internationalization and improve value handling. --- .../dataviews/src/dataform-controls/array.tsx | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index 55594589a501e3..8368c8e238da89 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -3,6 +3,7 @@ */ import { useCallback, useMemo, useRef, useState } from '@wordpress/element'; import { FormTokenField } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -31,7 +32,7 @@ const ValidatedFormTokenControl = ( { required && ( ! valueRef.current || valueRef.current.length === 0 ) ) { - setErrorMessage( 'This field is required.' ); + setErrorMessage( __( 'This field is required.' ) ); return; } @@ -60,10 +61,10 @@ const ValidatedFormTokenControl = ( { // Append required indicator to label const labelWithIndicator = useMemo( () => { if ( required && ! markWhenOptional ) { - return `${ label } (Required)`; + return `${ label } (${ __( 'Required' ) })`; } if ( ! required && markWhenOptional ) { - return `${ label } (Optional)`; + return `${ label } (${ __( 'Optional' ) })`; } return label; }, [ label, required, markWhenOptional ] ); @@ -115,8 +116,8 @@ export default function ArrayControl< Item >( { [ elements ] ); - // Ensure value is an array - const arrayValue = useMemo( + // Convert values to labels for display purposes only + const arrayValueForDisplay = useMemo( () => Array.isArray( value ) ? value.map( ( token ) => { @@ -129,19 +130,25 @@ export default function ArrayControl< Item >( { const onChangeControl = useCallback( ( tokens: ( string | { value: string } )[] ) => { - // Convert TokenItem objects to strings - const stringTokens = tokens.map( ( token ) => { + // Convert display labels back to values for storage + const valueTokens = tokens.map( ( token ) => { if ( typeof token !== 'string' ) { return token.value; } - const tokenByLabel = findElementByLabel( token ); + // If user entered a label, convert it to its corresponding value + const elementByLabel = findElementByLabel( token ); + if ( elementByLabel ) { + return elementByLabel.value; + } - return tokenByLabel?.value || token; + // If no matching element found, treat it as a direct value + // This handles cases where user types values directly or when elements aren't defined + return token; } ); onChange( { - [ id ]: stringTokens, + [ id ]: valueTokens, } ); }, [ id, onChange, findElementByLabel ] @@ -150,12 +157,21 @@ export default function ArrayControl< Item >( { return ( { + customValidator={ ( displayLabels: any ) => { if ( field.isValid?.custom ) { + // Convert display labels back to values for validation + const actualValues = Array.isArray( displayLabels ) + ? displayLabels.map( ( displayLabel ) => { + const elementByLabel = + findElementByLabel( displayLabel ); + return elementByLabel?.value || displayLabel; + } ) + : displayLabels; + const result = field.isValid.custom( { ...data, - [ id ]: newValue, + [ id ]: actualValues, }, field ); @@ -165,7 +181,7 @@ export default function ArrayControl< Item >( { return undefined; } } label={ hideLabelFromVision ? undefined : label } - value={ arrayValue } + value={ arrayValueForDisplay } onChange={ onChangeControl } placeholder={ placeholder } suggestions={ From a65e3b0ac146d8538d854499088c09f5d3b9754d Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 22 Aug 2025 11:12:38 +0100 Subject: [PATCH 11/34] Add countries field to DataFormValidationComponent with validation rules and predefined options --- .../dataform/stories/index.story.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index b5a420422944dd..e36cbe4894d6dc 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -330,6 +330,7 @@ const DataFormValidationComponent = ( { required }: { required: boolean } ) => { customEdit: string; customValidation: string; categories: string[]; + countries: string[]; }; const [ post, setPost ] = useState< ValidatedItem >( { @@ -338,6 +339,7 @@ const DataFormValidationComponent = ( { required }: { required: boolean } ) => { integer: 2, boolean: true, categories: [ 'astronomy' ], + countries: [ 'us' ], customEdit: 'custom control', customValidation: 'potato', } ); @@ -390,6 +392,26 @@ const DataFormValidationComponent = ( { required }: { required: boolean } ) => { { value: 'travel', label: 'Travel' }, ], }, + { + id: 'countries', + label: 'Countries Visited', + type: 'array' as const, + placeholder: 'Select countries', + description: 'Countries you have visited', + isValid: { + required, + elements: true, + }, + elements: [ + { value: 'us', label: 'United States' }, + { value: 'ca', label: 'Canada' }, + { value: 'uk', label: 'United Kingdom' }, + { value: 'fr', label: 'France' }, + { value: 'de', label: 'Germany' }, + { value: 'jp', label: 'Japan' }, + { value: 'au', label: 'Australia' }, + ], + }, { id: 'customEdit', label: 'Custom Control', @@ -426,6 +448,7 @@ const DataFormValidationComponent = ( { required }: { required: boolean } ) => { 'integer', 'boolean', 'categories', + 'countries', 'customEdit', 'customValidation', ], From 8d5d5514a10c88880a1513863be23640f1682112 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Wed, 10 Sep 2025 14:40:33 +0100 Subject: [PATCH 12/34] Expose ValidatedFormTokenField from private API --- packages/components/src/private-apis.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index e99b3ae037ae05..d01f02a3ff9694 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -20,6 +20,7 @@ import { ValidatedTextareaControl, ValidatedToggleControl, } from './validated-form-controls'; +import { ValidatedFormTokenField } from './validated-form-controls/components/form-token-field'; import { Picker } from './color-picker/picker'; export const privateApis = {}; @@ -43,4 +44,5 @@ lock( privateApis, { ValidatedTextControl, ValidatedTextareaControl, ValidatedToggleControl, + ValidatedFormTokenField, } ); From 0559f4c967ea934629484dc59e34e6b456e1e0be Mon Sep 17 00:00:00 2001 From: Ahmed Date: Wed, 10 Sep 2025 14:40:55 +0100 Subject: [PATCH 13/34] Remove customValidation --- .../dataviews/src/components/dataform/stories/index.story.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 5965144fe92c2d..e6c3f87380670b 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -404,7 +404,6 @@ const ValidationComponent = ( { integer: number; boolean: boolean; customEdit: string; - customValidation: string; categories: string[]; countries: string[]; password: string; From b3b00240fe6b651672e8a71bf0e8f9a142ce6daf Mon Sep 17 00:00:00 2001 From: Ahmed Date: Wed, 10 Sep 2025 14:41:27 +0100 Subject: [PATCH 14/34] Import ValidatedFormTokenField and use it in Array --- .../dataviews/src/dataform-controls/array.tsx | 89 ++----------------- 1 file changed, 6 insertions(+), 83 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index 8368c8e238da89..93f408d8da18fd 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -1,93 +1,16 @@ /** * WordPress dependencies */ -import { useCallback, useMemo, useRef, useState } from '@wordpress/element'; -import { FormTokenField } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { privateApis } from '@wordpress/components'; +import { useCallback, useMemo } from '@wordpress/element'; /** * Internal dependencies */ import type { DataFormControlProps } from '../types'; +import { unlock } from '../lock-unlock'; -// Create a validated FormTokenField wrapper -const ValidatedFormTokenControl = ( { - required, - customValidator, - onChange, - markWhenOptional, - label, - ...restProps -}: React.ComponentProps< typeof FormTokenField > & { - required?: boolean; - customValidator?: ( value: any ) => string | void; - markWhenOptional?: boolean; -} ) => { - const [ errorMessage, setErrorMessage ] = useState< string | undefined >(); - const [ isTouched, setIsTouched ] = useState( false ); - const valueRef = useRef( restProps.value ); - - const validate = useCallback( () => { - if ( - required && - ( ! valueRef.current || valueRef.current.length === 0 ) - ) { - setErrorMessage( __( 'This field is required.' ) ); - return; - } - - const customError = customValidator?.( valueRef.current ); - setErrorMessage( customError || undefined ); - }, [ required, customValidator ] ); - - const onBlur = useCallback( () => { - setIsTouched( true ); - validate(); - }, [ validate ] ); - - const onChangeControl = useCallback( - ( value: any ) => { - valueRef.current = value; - onChange?.( value ); - - // Only validate incrementally if the field has been touched or currently has an error - if ( isTouched || errorMessage ) { - validate(); - } - }, - [ onChange, isTouched, errorMessage, validate ] - ); - - // Append required indicator to label - const labelWithIndicator = useMemo( () => { - if ( required && ! markWhenOptional ) { - return `${ label } (${ __( 'Required' ) })`; - } - if ( ! required && markWhenOptional ) { - return `${ label } (${ __( 'Optional' ) })`; - } - return label; - }, [ label, required, markWhenOptional ] ); - - return ( -
- -
- { errorMessage && ( -

- { errorMessage } -

- ) } -
-
- ); -}; +const { ValidatedFormTokenField } = unlock( privateApis ); export default function ArrayControl< Item >( { data, @@ -155,7 +78,7 @@ export default function ArrayControl< Item >( { ); return ( - { if ( field.isValid?.custom ) { @@ -187,7 +110,7 @@ export default function ArrayControl< Item >( { suggestions={ elements?.map( ( suggestion ) => suggestion.label ) ?? [] } - __experimentalValidateInput={ ( token ) => { + __experimentalValidateInput={ ( token: string ) => { if ( ! field.isValid?.elements ) { return true; } From fc42406b7ade8ab2a3e71e42fe738bc9e77f40ad Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 08:39:33 +0100 Subject: [PATCH 15/34] Add displayTransform and __experimentalRenderItem --- .../dataviews/src/dataform-controls/array.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index 93f408d8da18fd..a738da20653299 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -121,6 +121,30 @@ export default function ArrayControl< Item >( { } } __experimentalExpandOnFocus={ elements && elements.length > 0 } __experimentalShowHowTo={ ! field.isValid?.elements } + displayTransform={ ( token: any ) => { + // For existing tokens (element objects), display their label + if ( typeof token === 'object' && 'label' in token ) { + return token.label; + } + // For suggestions (value strings), find the corresponding element and show its label + if ( typeof token === 'string' && elements ) { + const element = elements.find( + ( el ) => el.value === token + ); + return element?.label || token; + } + return token; + } } + __experimentalRenderItem={ ( { item }: { item: any } ) => { + // Custom rendering for suggestion items (item is a value string) + if ( typeof item === 'string' && elements ) { + const element = elements.find( + ( el ) => el.value === item + ); + return { element?.label || item }; + } + return { item }; + } } /> ); } From 6d527cdf6ab6d78e615a1014f472bb4250ff6d03 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 08:42:38 +0100 Subject: [PATCH 16/34] Replace arrays for display const and remove unused findElementByValue --- .../dataviews/src/dataform-controls/array.tsx | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index a738da20653299..af100fe29f2aaa 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -21,15 +21,6 @@ export default function ArrayControl< Item >( { const { id, label, placeholder, elements } = field; const value = field.getValue( { item: data } ); - const findElementByValue = useCallback( - ( suggestionValue: string ) => { - return elements?.find( - ( suggestion ) => suggestion.value === suggestionValue - ); - }, - [ elements ] - ); - const findElementByLabel = useCallback( ( suggestionLabel: string ) => { return elements?.find( @@ -39,16 +30,18 @@ export default function ArrayControl< Item >( { [ elements ] ); - // Convert values to labels for display purposes only - const arrayValueForDisplay = useMemo( + // Convert stored values to element objects for the token field + const arrayValueAsElements = useMemo( () => Array.isArray( value ) ? value.map( ( token ) => { - const tokenLabel = findElementByValue( token )?.label; - return tokenLabel || token; + const element = elements?.find( + ( suggestion ) => suggestion.value === token + ); + return element || { value: token, label: token }; } ) : [], - [ value, findElementByValue ] + [ value, elements ] ); const onChangeControl = useCallback( @@ -104,7 +97,7 @@ export default function ArrayControl< Item >( { return undefined; } } label={ hideLabelFromVision ? undefined : label } - value={ arrayValueForDisplay } + value={ arrayValueAsElements } onChange={ onChangeControl } placeholder={ placeholder } suggestions={ From 3f625d84e4e7fb9626063d2e5820de256419e7ec Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 09:24:31 +0100 Subject: [PATCH 17/34] Adapt suggestions to pass values --- packages/dataviews/src/dataform-controls/array.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index af100fe29f2aaa..03f8ea2432d86a 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -100,9 +100,7 @@ export default function ArrayControl< Item >( { value={ arrayValueAsElements } onChange={ onChangeControl } placeholder={ placeholder } - suggestions={ - elements?.map( ( suggestion ) => suggestion.label ) ?? [] - } + suggestions={ elements?.map( ( element ) => element.value ) } __experimentalValidateInput={ ( token: string ) => { if ( ! field.isValid?.elements ) { return true; From 53c83fdf6553e2be2769afc3b0a5ce74273d5f42 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 09:25:47 +0100 Subject: [PATCH 18/34] Add customValidity and onFocus --- .../dataviews/src/dataform-controls/array.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index 03f8ea2432d86a..89eab62cdfb7ef 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -2,7 +2,7 @@ * WordPress dependencies */ import { privateApis } from '@wordpress/components'; -import { useCallback, useMemo } from '@wordpress/element'; +import { useCallback, useMemo, useState } from '@wordpress/element'; /** * Internal dependencies @@ -29,6 +29,13 @@ export default function ArrayControl< Item >( { }, [ elements ] ); + const [ customValidity, setCustomValidity ] = useState< + | { + type: 'validating' | 'valid' | 'invalid'; + message: string; + } + | undefined + >( undefined ); // Convert stored values to element objects for the token field const arrayValueAsElements = useMemo( @@ -70,6 +77,10 @@ export default function ArrayControl< Item >( { [ id, onChange, findElementByLabel ] ); + const onFocus = useCallback( () => { + setCustomValidity( undefined ); + }, [] ); + return ( ( { return undefined; } } + customValidity={ customValidity } label={ hideLabelFromVision ? undefined : label } value={ arrayValueAsElements } onChange={ onChangeControl } + onFocus={ onFocus } placeholder={ placeholder } suggestions={ elements?.map( ( element ) => element.value ) } __experimentalValidateInput={ ( token: string ) => { From 822e31cf75cfe96eafbe71b62a796fde9b4d25f7 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 09:28:10 +0100 Subject: [PATCH 19/34] Add validation with validateTokens --- .../dataviews/src/dataform-controls/array.tsx | 143 ++++++++++++------ 1 file changed, 95 insertions(+), 48 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index 89eab62cdfb7ef..f1af02a56cd18b 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -3,6 +3,7 @@ */ import { privateApis } from '@wordpress/components'; import { useCallback, useMemo, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -21,14 +22,6 @@ export default function ArrayControl< Item >( { const { id, label, placeholder, elements } = field; const value = field.getValue( { item: data } ); - const findElementByLabel = useCallback( - ( suggestionLabel: string ) => { - return elements?.find( - ( suggestion ) => suggestion.label === suggestionLabel - ); - }, - [ elements ] - ); const [ customValidity, setCustomValidity ] = useState< | { type: 'validating' | 'valid' | 'invalid'; @@ -51,30 +44,85 @@ export default function ArrayControl< Item >( { [ value, elements ] ); - const onChangeControl = useCallback( - ( tokens: ( string | { value: string } )[] ) => { - // Convert display labels back to values for storage - const valueTokens = tokens.map( ( token ) => { - if ( typeof token !== 'string' ) { + const validateTokens = useCallback( + ( tokens: ( string | { value: string; label?: string } )[] ) => { + // Extract actual values from tokens for validation + const tokenValues = tokens.map( ( token ) => { + if ( typeof token === 'object' && 'value' in token ) { return token.value; } + return token; + } ); + + // First, check if elements validation is required and any tokens are invalid + if ( field.isValid?.elements && elements ) { + const invalidTokens = tokenValues.filter( ( tokenValue ) => { + return ! elements.some( + ( element ) => element.value === tokenValue + ); + } ); + + if ( invalidTokens.length > 0 ) { + setCustomValidity( { + type: 'invalid', + message: __( + 'Please select from the available options.' + ), + } ); + return; + } + } + + // Then check custom validation if provided + if ( field.isValid?.custom ) { + const result = field.isValid.custom( + { + ...data, + [ id ]: tokenValues, + }, + field + ); - // If user entered a label, convert it to its corresponding value - const elementByLabel = findElementByLabel( token ); - if ( elementByLabel ) { - return elementByLabel.value; + if ( result ) { + setCustomValidity( { + type: 'invalid', + message: result, + } ); + return; } + } + + // If no validation errors, clear custom validity + setCustomValidity( undefined ); + }, + [ elements, data, id, field ] + ); - // If no matching element found, treat it as a direct value - // This handles cases where user types values directly or when elements aren't defined + // Generate custom invalid message for elements validation + const customInvalidMessage = useMemo( () => { + if ( field.isValid?.elements && elements ) { + return __( 'Please select from the available options.' ); + } + return __( 'Invalid item' ); + }, [ field.isValid?.elements, elements ] ); + + const onChangeControl = useCallback( + ( tokens: ( string | { value: string; label?: string } )[] ) => { + const valueTokens = tokens.map( ( token ) => { + if ( typeof token === 'object' && 'value' in token ) { + return token.value; + } + // If it's a string, it's either a new suggestion value or user input return token; } ); onChange( { [ id ]: valueTokens, } ); + + validateTokens( tokens ); }, - [ id, onChange, findElementByLabel ] + [ id, onChange, validateTokens ] ); const onFocus = useCallback( () => { @@ -84,29 +132,7 @@ export default function ArrayControl< Item >( { return ( { - if ( field.isValid?.custom ) { - // Convert display labels back to values for validation - const actualValues = Array.isArray( displayLabels ) - ? displayLabels.map( ( displayLabel ) => { - const elementByLabel = - findElementByLabel( displayLabel ); - return elementByLabel?.value || displayLabel; - } ) - : displayLabels; - - const result = field.isValid.custom( - { - ...data, - [ id ]: actualValues, - }, - field - ); - return result || undefined; - } - - return undefined; - } } + onValidate={ validateTokens } customValidity={ customValidity } label={ hideLabelFromVision ? undefined : label } value={ arrayValueAsElements } @@ -114,14 +140,35 @@ export default function ArrayControl< Item >( { onFocus={ onFocus } placeholder={ placeholder } suggestions={ elements?.map( ( element ) => element.value ) } + messages={ { + added: __( 'Item added.' ), + removed: __( 'Item removed.' ), + remove: __( 'Remove item' ), + __experimentalInvalid: customInvalidMessage, + } } __experimentalValidateInput={ ( token: string ) => { - if ( ! field.isValid?.elements ) { - return true; + // If elements validation is required, check if token is valid + if ( field.isValid?.elements && elements ) { + const isValidToken = elements.some( + ( element ) => + element.value === token || element.label === token + ); + + // If invalid, trigger error message display using the main validation function + if ( ! isValidToken ) { + // Create a temporary token object to trigger validation + const tempTokens = [ + ...arrayValueAsElements, + { value: token, label: token }, + ]; + validateTokens( tempTokens ); + } + + return isValidToken; } - // Check if the token matches any of the available elements - const tokenByLabel = findElementByLabel( token ); - return !! tokenByLabel; + // For non-elements validation, allow all tokens + return true; } } __experimentalExpandOnFocus={ elements && elements.length > 0 } __experimentalShowHowTo={ ! field.isValid?.elements } From d24449b542c1beb5eb90b409f7c94750acc794c9 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 09:32:05 +0100 Subject: [PATCH 20/34] Rename function --- packages/dataviews/src/validation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts index 6799c72fd1fccd..5c15043d8acdb5 100644 --- a/packages/dataviews/src/validation.ts +++ b/packages/dataviews/src/validation.ts @@ -25,7 +25,7 @@ export function isItemValid< Item >( const isEmptyNullOrUndefined = ( value: any ) => [ undefined, '', null ].includes( value ); - const isNotArrayEmptyOrContainsInvalidElements = ( value: any ) => { + const isArrayOrElementsEmptyNullOrUndefined = ( value: any ) => { return ( ! Array.isArray( value ) || value.length === 0 || @@ -48,7 +48,7 @@ export function isItemValid< Item >( ( field.type === 'integer' && isEmptyNullOrUndefined( value ) ) || ( field.type === 'array' && - isNotArrayEmptyOrContainsInvalidElements( value ) ) || + isArrayOrElementsEmptyNullOrUndefined( value ) ) || ( field.type === undefined && isEmptyNullOrUndefined( value ) ) ) { return false; From 661758485a0dc5c2d9dedae50dc92124a7a4720e Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 09:34:15 +0100 Subject: [PATCH 21/34] Add changelogs --- packages/components/CHANGELOG.md | 2 ++ packages/dataviews/CHANGELOG.md | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6bd03332c751f0..b7147abfc4507e 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,8 @@ - `ValidatedCheckboxControl`: Expose the component under private API's ([#71505](https://github.com/WordPress/gutenberg/pull/71505/)). - Expose `ValidatedTextareaControl` via Private APIs ([#71495](https://github.com/WordPress/gutenberg/pull/71495)) - Add support for ValidatedFormTokenField [#71350](https://github.com/WordPress/gutenberg/pull/71350). +- Add support for elements validation in DataForm's array fields [#71194](https://github.com/WordPress/gutenberg/pull/71194) + ## 30.3.0 (2025-09-03) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index ea318c6807af37..f51308dbd718fd 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -16,6 +16,7 @@ - Dataform: Add new `password` field type and field control. [#71545](https://github.com/WordPress/gutenberg/pull/71545) - DataForm: Add a textarea control for use with the `text` field type ([#71495](https://github.com/WordPress/gutenberg/pull/71495)) - DataViews: support groupBy in the list layout. [#71548](https://github.com/WordPress/gutenberg/pull/71548) +- DataForm: Add support for elements validation in array fields [#71194](https://github.com/WordPress/gutenberg/pull/71194) ### Bug Fixes From 75a0a5f4ba1f1aa0a20ad4ac7a716645dc13dd00 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 10:19:46 +0100 Subject: [PATCH 22/34] Change changelog for components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André <583546+oandregal@users.noreply.github.com> --- packages/components/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index b7147abfc4507e..09e4914fa0ec1a 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,7 +11,7 @@ - `ValidatedCheckboxControl`: Expose the component under private API's ([#71505](https://github.com/WordPress/gutenberg/pull/71505/)). - Expose `ValidatedTextareaControl` via Private APIs ([#71495](https://github.com/WordPress/gutenberg/pull/71495)) - Add support for ValidatedFormTokenField [#71350](https://github.com/WordPress/gutenberg/pull/71350). -- Add support for elements validation in DataForm's array fields [#71194](https://github.com/WordPress/gutenberg/pull/71194) +- Expose `ValidatedFormTokenField` via private APIs [#71194](https://github.com/WordPress/gutenberg/pull/71194) ## 30.3.0 (2025-09-03) From fe03b3d23646ee59854e312b99a449e1cb7067f0 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 10:29:48 +0100 Subject: [PATCH 23/34] Call onInputChange after reseting the input --- packages/components/src/form-token-field/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/form-token-field/index.tsx b/packages/components/src/form-token-field/index.tsx index b16a5366884291..2e04ca6448fe5d 100644 --- a/packages/components/src/form-token-field/index.tsx +++ b/packages/components/src/form-token-field/index.tsx @@ -185,6 +185,7 @@ export function FormTokenField( props: FormTokenFieldProps ) { } else { // Reset to initial state setIncompleteTokenValue( '' ); + onInputChange( '' ); setInputOffsetFromEnd( 0 ); setIsActive( false ); From 6ef2a570073fc8a11ff4ebdfa32c66c78f17188e Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 10:30:06 +0100 Subject: [PATCH 24/34] Pass an onInputChange instead of onFocus --- packages/dataviews/src/dataform-controls/array.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index f1af02a56cd18b..0c3422d91a20e6 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -125,8 +125,11 @@ export default function ArrayControl< Item >( { [ id, onChange, validateTokens ] ); - const onFocus = useCallback( () => { - setCustomValidity( undefined ); + const onInputChange = useCallback( ( input: string ) => { + if ( input === '' ) { + // Reset custom validity when input is cleared + setCustomValidity( undefined ); + } }, [] ); return ( @@ -137,7 +140,7 @@ export default function ArrayControl< Item >( { label={ hideLabelFromVision ? undefined : label } value={ arrayValueAsElements } onChange={ onChangeControl } - onFocus={ onFocus } + onInputChange={ onInputChange } placeholder={ placeholder } suggestions={ elements?.map( ( element ) => element.value ) } messages={ { From f2e301d40ff34bd97cacdef411de311eb4dddaf6 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 10:47:42 +0100 Subject: [PATCH 25/34] Remove validation for elements as it is replaced by control-level validation --- packages/dataviews/src/field-types/array.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/dataviews/src/field-types/array.tsx b/packages/dataviews/src/field-types/array.tsx index 5a756d7105170b..77da3c14e8ead4 100644 --- a/packages/dataviews/src/field-types/array.tsx +++ b/packages/dataviews/src/field-types/array.tsx @@ -59,14 +59,6 @@ const arrayFieldType: FieldTypeDefinition< any > = { return __( 'Every value must be a string.' ); } - if ( field?.elements && field.isValid?.elements ) { - const validValues = field.elements.map( ( f ) => f.value ); - if ( - ! value.every( ( v: any ) => validValues.includes( v ) ) - ) { - return __( 'Value must be one of the elements.' ); - } - } return null; }, }, From 27536bcb94958dfe860d2c3a16451dedeadbe39f Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 11 Sep 2025 16:10:32 +0100 Subject: [PATCH 26/34] Add documentation for elements --- packages/dataviews/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index f371d5d60b6ab9..414803a9a1cc52 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -1140,6 +1140,7 @@ Example: Object that contains the validation rules for the field. If a rule is not met, the control will be marked as invalid and a message will be displayed. - `required`: boolean indicating whether the field is required or not. +- `elements`: boolean restricting selection to the provided list of elements only. Used with the `array` field type. - `custom`: a function that validates a field's value. If the value is invalid, the function should return a string explaining why the value is invalid. Otherwise, the function must return null. Example: From 300841d384ec9fc11e485037c26130b5ed7ca0c0 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 18 Sep 2025 11:52:20 +0100 Subject: [PATCH 27/34] Add changes to use the new getValue/setValue API --- .../dataform/stories/index.story.tsx | 1 - .../dataviews/src/dataform-controls/array.tsx | 30 ++++++++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index e81379abb98dde..299324ec3abaf0 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -853,7 +853,6 @@ const ValidationComponent = ( { 'boolean', 'categories', 'countries', - 'customEdit', 'toggle', 'toggleGroup', 'password', diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index 3e511475e2f23c..e33a001d033fdc 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import deepMerge from 'deepmerge'; + /** * WordPress dependencies */ @@ -73,13 +78,16 @@ export default function ArrayControl< Item >( { } } - // Then check custom validation if provided + // Then check custom validation if provided. if ( field.isValid?.custom ) { - const result = field.isValid.custom( - { - ...data, - [ id ]: tokenValues, - }, + const result = field.isValid?.custom?.( + deepMerge( + data, + setValue( { + item: data, + value: tokenValues, + } ) as Partial< Item > + ), field ); @@ -95,7 +103,7 @@ export default function ArrayControl< Item >( { // If no validation errors, clear custom validity setCustomValidity( undefined ); }, - [ elements, data, id, field ] + [ elements, data, field, setValue ] ); // Generate custom invalid message for elements validation @@ -116,11 +124,11 @@ export default function ArrayControl< Item >( { return token; } ); - onChange( setValue( { item: data, value: stringTokens } ) ); - - validateTokens( tokens ); + onChange( setValue( { item: data, value: valueTokens } ) ); + + validateTokens( tokens ); }, - [ onChange, setValue, data, findElementByLabel ] + [ onChange, setValue, data, validateTokens ] ); const onInputChange = useCallback( ( input: string ) => { From 08f9b87d26c7d795b4603602724d17a2dcf8839b Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 18 Sep 2025 13:02:33 +0100 Subject: [PATCH 28/34] Remove validateToken from onChange --- packages/dataviews/src/dataform-controls/array.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index e33a001d033fdc..bdb6d9d75c2960 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -125,10 +125,8 @@ export default function ArrayControl< Item >( { } ); onChange( setValue( { item: data, value: valueTokens } ) ); - - validateTokens( tokens ); }, - [ onChange, setValue, data, validateTokens ] + [ onChange, setValue, data ] ); const onInputChange = useCallback( ( input: string ) => { From a9872b29ef123979bc1e6db13f0bd06bf97343c1 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 18 Sep 2025 13:11:31 +0100 Subject: [PATCH 29/34] Remove customInvalidMessage and messages --- packages/dataviews/src/dataform-controls/array.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index bdb6d9d75c2960..26d79b1dc6f7a8 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -106,14 +106,6 @@ export default function ArrayControl< Item >( { [ elements, data, field, setValue ] ); - // Generate custom invalid message for elements validation - const customInvalidMessage = useMemo( () => { - if ( field.isValid?.elements && elements ) { - return __( 'Please select from the available options.' ); - } - return __( 'Invalid item' ); - }, [ field.isValid?.elements, elements ] ); - const onChangeControl = useCallback( ( tokens: ( string | { value: string; label?: string } )[] ) => { const valueTokens = tokens.map( ( token ) => { @@ -147,12 +139,6 @@ export default function ArrayControl< Item >( { onInputChange={ onInputChange } placeholder={ placeholder } suggestions={ elements?.map( ( element ) => element.value ) } - messages={ { - added: __( 'Item added.' ), - removed: __( 'Item removed.' ), - remove: __( 'Remove item' ), - __experimentalInvalid: customInvalidMessage, - } } __experimentalValidateInput={ ( token: string ) => { // If elements validation is required, check if token is valid if ( field.isValid?.elements && elements ) { From 32b0d52de18fc6df819c1058c674e6f89f0499c3 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 18 Sep 2025 13:12:26 +0100 Subject: [PATCH 30/34] Fix linting --- packages/components/src/private-apis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index 5590e5dd478619..a6e3235ccd79f4 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -50,5 +50,5 @@ lock( privateApis, { ValidatedTextareaControl, ValidatedToggleControl, ValidatedToggleGroupControl, - ValidatedFormTokenField, + ValidatedFormTokenField, } ); From 5ed466c3135572bb7151d17398a95c3c4a6c3b5e Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 18 Sep 2025 13:14:53 +0100 Subject: [PATCH 31/34] Moves changelog entries to Unreleased --- packages/components/CHANGELOG.md | 5 ++++- packages/dataviews/CHANGELOG.md | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index db0aacfb602816..bf6a63b043ec19 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Expose `ValidatedFormTokenField` via private APIs [#71194](https://github.com/WordPress/gutenberg/pull/71194) + ## 30.4.0 (2025-09-17) ### Bug Fixes @@ -16,7 +20,6 @@ - Add support for ValidatedFormTokenField [#71350](https://github.com/WordPress/gutenberg/pull/71350). - Expose `ValidatedSelectControl` via Private APIs ([#71665](https://github.com/WordPress/gutenberg/pull/71665)) - Expose `ValidatedToggleGroupControl` via private APIs. Also properly detects `undefined` values for `required` validation ([#71666](https://github.com/WordPress/gutenberg/pull/71666)). -- Expose `ValidatedFormTokenField` via private APIs [#71194](https://github.com/WordPress/gutenberg/pull/71194) ## 30.3.0 (2025-09-03) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 8fffbd5d266e39..401ed3c974b43d 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- DataForm: Add support for elements validation in array fields [#71194](https://github.com/WordPress/gutenberg/pull/71194) + ## 9.0.0 (2025-09-17) ### Breaking changes @@ -24,7 +28,6 @@ - DataForm: Add object configuration support for Edit property with some options. ([#71582](https://github.com/WordPress/gutenberg/pull/71582)) - DataForm: Add summary field support for composed fields. ([#71614](https://github.com/WordPress/gutenberg/pull/71614)) - DataForm: update radio control to support `required` and `custom` validation. [#71664](https://github.com/WordPress/gutenberg/pull/71664) -- DataForm: Add support for elements validation in array fields [#71194](https://github.com/WordPress/gutenberg/pull/71194) ### Bug Fixes From 20410e48e6e9995bf14cd1982f6361599b984c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:34:07 +0200 Subject: [PATCH 32/34] Remove input logic --- .../components/src/form-token-field/index.tsx | 1 - .../dataviews/src/dataform-controls/array.tsx | 22 +------------------ 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/packages/components/src/form-token-field/index.tsx b/packages/components/src/form-token-field/index.tsx index 2e04ca6448fe5d..b16a5366884291 100644 --- a/packages/components/src/form-token-field/index.tsx +++ b/packages/components/src/form-token-field/index.tsx @@ -185,7 +185,6 @@ export function FormTokenField( props: FormTokenFieldProps ) { } else { // Reset to initial state setIncompleteTokenValue( '' ); - onInputChange( '' ); setInputOffsetFromEnd( 0 ); setIsActive( false ); diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index 26d79b1dc6f7a8..b803dbbd701eee 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -121,13 +121,6 @@ export default function ArrayControl< Item >( { [ onChange, setValue, data ] ); - const onInputChange = useCallback( ( input: string ) => { - if ( input === '' ) { - // Reset custom validity when input is cleared - setCustomValidity( undefined ); - } - }, [] ); - return ( ( { label={ hideLabelFromVision ? undefined : label } value={ arrayValueAsElements } onChange={ onChangeControl } - onInputChange={ onInputChange } placeholder={ placeholder } suggestions={ elements?.map( ( element ) => element.value ) } __experimentalValidateInput={ ( token: string ) => { // If elements validation is required, check if token is valid if ( field.isValid?.elements && elements ) { - const isValidToken = elements.some( + return elements.some( ( element ) => element.value === token || element.label === token ); - - // If invalid, trigger error message display using the main validation function - if ( ! isValidToken ) { - // Create a temporary token object to trigger validation - const tempTokens = [ - ...arrayValueAsElements, - { value: token, label: token }, - ]; - validateTokens( tempTokens ); - } - - return isValidToken; } // For non-elements validation, allow all tokens From 87c94f6062ed0dcfd264b7a05fa0750820057231 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 19 Sep 2025 17:52:24 +0100 Subject: [PATCH 33/34] Update message by including tokens --- packages/dataviews/src/dataform-controls/array.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/array.tsx b/packages/dataviews/src/dataform-controls/array.tsx index b803dbbd701eee..15e0fbc6da1b51 100644 --- a/packages/dataviews/src/dataform-controls/array.tsx +++ b/packages/dataviews/src/dataform-controls/array.tsx @@ -8,7 +8,7 @@ import deepMerge from 'deepmerge'; */ import { privateApis } from '@wordpress/components'; import { useCallback, useMemo, useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { _n, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -70,8 +70,14 @@ export default function ArrayControl< Item >( { if ( invalidTokens.length > 0 ) { setCustomValidity( { type: 'invalid', - message: __( - 'Please select from the available options.' + message: sprintf( + /* translators: %s: list of invalid tokens */ + _n( + 'Please select from the available options: %s is invalid.', + 'Please select from the available options: %s are invalid.', + invalidTokens.length + ), + invalidTokens.join( ', ' ) ), } ); return; From ce7465f1569e90816466cd0b503530c8ebea9541 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 23 Sep 2025 08:46:15 +0100 Subject: [PATCH 34/34] Change order of changelog to reflect the timeline --- packages/dataviews/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 543aa305e55b02..71b05d2b150a8a 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -4,8 +4,8 @@ ### Features -- DataForm: Add support for elements validation in array fields [#71194](https://github.com/WordPress/gutenberg/pull/71194) - Introduce a new `DataViewsPicker` component. [#70971](https://github.com/WordPress/gutenberg/pull/70971) and [#71836](https://github.com/WordPress/gutenberg/pull/71836). +- DataForm: Add support for elements validation in array fields [#71194](https://github.com/WordPress/gutenberg/pull/71194) ### Bug Fixes