From 305bc1094576de50e467e5cf6ef6d6852b200c6d Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 11 Nov 2025 06:15:56 +0000 Subject: [PATCH 01/11] initial pattern validation version --- .../src/dataform-controls/textarea.tsx | 8 ++ .../utils/validated-input.tsx | 8 ++ .../dataviews/src/stories/dataform.story.tsx | 86 +++++++++++++++++++ packages/dataviews/src/types/field-api.ts | 1 + 4 files changed, 103 insertions(+) diff --git a/packages/dataviews/src/dataform-controls/textarea.tsx b/packages/dataviews/src/dataform-controls/textarea.tsx index d74e7c837c38f1..bd959f826b935b 100644 --- a/packages/dataviews/src/dataform-controls/textarea.tsx +++ b/packages/dataviews/src/dataform-controls/textarea.tsx @@ -31,6 +31,13 @@ export default function Textarea< Item >( { [ data, onChange, setValue ] ); + // Convert pattern to string if it's a RegExp + const pattern = isValid?.pattern + ? isValid.pattern instanceof RegExp + ? isValid.pattern.source + : isValid.pattern + : undefined; + return ( ( { help={ description } onChange={ onChangeControl } rows={ rows } + pattern={ pattern } __next40pxDefaultSize __nextHasNoMarginBottom hideLabelFromVision={ hideLabelFromVision } diff --git a/packages/dataviews/src/dataform-controls/utils/validated-input.tsx b/packages/dataviews/src/dataform-controls/utils/validated-input.tsx index b1b87238b3b858..aaf571b0f13248 100644 --- a/packages/dataviews/src/dataform-controls/utils/validated-input.tsx +++ b/packages/dataviews/src/dataform-controls/utils/validated-input.tsx @@ -54,6 +54,13 @@ export default function ValidatedText< Item >( { [ data, setValue, onChange ] ); + // Convert pattern to string if it's a RegExp + const pattern = isValid?.pattern + ? isValid.pattern instanceof RegExp + ? isValid.pattern.source + : isValid.pattern + : undefined; + return ( ( { type={ type } prefix={ prefix } suffix={ suffix } + pattern={ pattern } __next40pxDefaultSize /> ); diff --git a/packages/dataviews/src/stories/dataform.story.tsx b/packages/dataviews/src/stories/dataform.story.tsx index 58b7226abcdece..1f7fdf34bfc472 100644 --- a/packages/dataviews/src/stories/dataform.story.tsx +++ b/packages/dataviews/src/stories/dataform.story.tsx @@ -579,6 +579,12 @@ const ValidationComponent = ( { date?: string; dateRange?: string; datetime?: string; + username?: string; + zipcode?: string; + phonePattern?: string; + emailPattern?: string; + urlPattern?: string; + textareaPattern?: string; }; const [ post, setPost ] = useState< ValidatedItem >( { @@ -602,6 +608,12 @@ const ValidationComponent = ( { date: undefined, dateRange: undefined, datetime: undefined, + username: 'john_doe_123', + zipcode: '12345', + phonePattern: '+1-555-123-4567', + emailPattern: 'user@company.com', + urlPattern: 'https://github.com/wordpress/gutenberg', + textareaPattern: '#wordpress #gutenberg #react', } ); // Cache for getElements functions - ensures promises are only created once @@ -918,6 +930,29 @@ const ValidationComponent = ( { custom: maybeCustomRule( customTextRule ), }, }, + { + id: 'username', + type: 'text', + label: 'Username (pattern: alphanumeric + underscore)', + placeholder: 'user_name123', + description: + 'Must contain only letters, numbers, and underscores', + isValid: { + required, + pattern: /^[a-zA-Z0-9_]+$/, + }, + }, + { + id: 'zipcode', + type: 'text', + label: 'Zip Code (pattern: 5 digits)', + placeholder: '12345', + description: 'Must be exactly 5 digits', + isValid: { + required, + pattern: '[0-9]{5}', + }, + }, { id: 'select', type: 'text', @@ -970,6 +1005,18 @@ const ValidationComponent = ( { custom: maybeCustomRule( customTextareaRule ), }, }, + { + id: 'textareaPattern', + type: 'text', + Edit: 'textarea', + label: 'Textarea (pattern: hashtags only)', + placeholder: '#tag1 #tag2 #tag3', + description: 'Must contain only hashtags separated by spaces', + isValid: { + required, + pattern: /^(#\w+\s*)+$/, + }, + }, { id: 'email', type: 'email', @@ -980,6 +1027,17 @@ const ValidationComponent = ( { custom: maybeCustomRule( customEmailRule ), }, }, + { + id: 'emailPattern', + type: 'email', + label: 'Email (pattern: must end with @company.com)', + placeholder: 'user@company.com', + description: 'Email must be from @company.com domain', + isValid: { + required, + pattern: /^[a-zA-Z0-9._%+-]+@company\.com$/, + }, + }, { id: 'telephone', type: 'telephone', @@ -990,6 +1048,17 @@ const ValidationComponent = ( { custom: maybeCustomRule( customTelephoneRule ), }, }, + { + id: 'phonePattern', + type: 'telephone', + label: 'Phone (pattern: +1-XXX-XXX-XXXX)', + placeholder: '+1-555-123-4567', + description: 'US phone format with country code', + isValid: { + required, + pattern: /^\+1-\d{3}-\d{3}-\d{4}$/, + }, + }, { id: 'url', type: 'url', @@ -1000,6 +1069,17 @@ const ValidationComponent = ( { custom: maybeCustomRule( customUrlRule ), }, }, + { + id: 'urlPattern', + type: 'url', + label: 'URL (pattern: must be GitHub repo)', + placeholder: 'https://github.com/user/repo', + description: 'Must be a GitHub repository URL', + isValid: { + required, + pattern: /^https:\/\/github\.com\/[\w-]+\/[\w-]+\/?$/, + }, + }, { id: 'color', type: 'color', @@ -1158,6 +1238,8 @@ const ValidationComponent = ( { () => ( { fields: [ 'text', + 'username', + 'zipcode', { id: 'customEdit' }, { id: 'level1Integer', @@ -1180,6 +1262,7 @@ const ValidationComponent = ( { }, ], }, + 'emailPattern', { id: 'level1Telephone', children: [ @@ -1199,10 +1282,13 @@ const ValidationComponent = ( { }, ], }, + 'phonePattern', 'url', + 'urlPattern', 'color', 'password', 'textarea', + 'textareaPattern', 'select', 'textWithRadio', 'boolean', diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index 31f1011eeaa454..8d62fab700950c 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -79,6 +79,7 @@ export type FieldType = export type Rules< Item > = { required?: boolean; elements?: boolean; + pattern?: string | RegExp; custom?: | ( ( item: Item, field: NormalizedField< Item > ) => null | string ) | ( ( From 7ff8fe8721b44f98c5efb27676857ff8051eeb46 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 11 Nov 2025 07:58:53 +0000 Subject: [PATCH 02/11] simplify some storybook regex --- packages/dataviews/src/stories/dataform.story.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dataviews/src/stories/dataform.story.tsx b/packages/dataviews/src/stories/dataform.story.tsx index 1f7fdf34bfc472..779294aac5cc94 100644 --- a/packages/dataviews/src/stories/dataform.story.tsx +++ b/packages/dataviews/src/stories/dataform.story.tsx @@ -1035,7 +1035,7 @@ const ValidationComponent = ( { description: 'Email must be from @company.com domain', isValid: { required, - pattern: /^[a-zA-Z0-9._%+-]+@company\.com$/, + pattern: /^[a-zA-Z0-9]+@company\.com$/, }, }, { @@ -1077,7 +1077,7 @@ const ValidationComponent = ( { description: 'Must be a GitHub repository URL', isValid: { required, - pattern: /^https:\/\/github\.com\/[\w-]+\/[\w-]+\/?$/, + pattern: /^https:\/\/github\.com\/.*/, }, }, { From 898503dbaa828c4f273c3a7fa3461d430bce0e94 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 11 Nov 2025 08:15:11 +0000 Subject: [PATCH 03/11] remove pattern from textarea it is not supported --- packages/dataviews/src/stories/dataform.story.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/dataviews/src/stories/dataform.story.tsx b/packages/dataviews/src/stories/dataform.story.tsx index 779294aac5cc94..cd4f3eca49e4f2 100644 --- a/packages/dataviews/src/stories/dataform.story.tsx +++ b/packages/dataviews/src/stories/dataform.story.tsx @@ -584,7 +584,6 @@ const ValidationComponent = ( { phonePattern?: string; emailPattern?: string; urlPattern?: string; - textareaPattern?: string; }; const [ post, setPost ] = useState< ValidatedItem >( { @@ -613,7 +612,6 @@ const ValidationComponent = ( { phonePattern: '+1-555-123-4567', emailPattern: 'user@company.com', urlPattern: 'https://github.com/wordpress/gutenberg', - textareaPattern: '#wordpress #gutenberg #react', } ); // Cache for getElements functions - ensures promises are only created once @@ -1005,18 +1003,6 @@ const ValidationComponent = ( { custom: maybeCustomRule( customTextareaRule ), }, }, - { - id: 'textareaPattern', - type: 'text', - Edit: 'textarea', - label: 'Textarea (pattern: hashtags only)', - placeholder: '#tag1 #tag2 #tag3', - description: 'Must contain only hashtags separated by spaces', - isValid: { - required, - pattern: /^(#\w+\s*)+$/, - }, - }, { id: 'email', type: 'email', @@ -1288,7 +1274,6 @@ const ValidationComponent = ( { 'color', 'password', 'textarea', - 'textareaPattern', 'select', 'textWithRadio', 'boolean', From e534d9d65f0ad561ce0f29db3a006177f2c83a94 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 11 Nov 2025 10:30:41 +0000 Subject: [PATCH 04/11] remove leftover from textarea try --- packages/dataviews/src/dataform-controls/textarea.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/textarea.tsx b/packages/dataviews/src/dataform-controls/textarea.tsx index bd959f826b935b..d74e7c837c38f1 100644 --- a/packages/dataviews/src/dataform-controls/textarea.tsx +++ b/packages/dataviews/src/dataform-controls/textarea.tsx @@ -31,13 +31,6 @@ export default function Textarea< Item >( { [ data, onChange, setValue ] ); - // Convert pattern to string if it's a RegExp - const pattern = isValid?.pattern - ? isValid.pattern instanceof RegExp - ? isValid.pattern.source - : isValid.pattern - : undefined; - return ( ( { help={ description } onChange={ onChangeControl } rows={ rows } - pattern={ pattern } __next40pxDefaultSize __nextHasNoMarginBottom hideLabelFromVision={ hideLabelFromVision } From a7967ca2b9d7eba067cd7639897b3211471d765f Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 11 Nov 2025 10:41:16 +0000 Subject: [PATCH 05/11] lint fix --- .../src/dataform-controls/utils/validated-input.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/utils/validated-input.tsx b/packages/dataviews/src/dataform-controls/utils/validated-input.tsx index aaf571b0f13248..147194f6948398 100644 --- a/packages/dataviews/src/dataform-controls/utils/validated-input.tsx +++ b/packages/dataviews/src/dataform-controls/utils/validated-input.tsx @@ -55,11 +55,13 @@ export default function ValidatedText< Item >( { ); // Convert pattern to string if it's a RegExp - const pattern = isValid?.pattern - ? isValid.pattern instanceof RegExp - ? isValid.pattern.source - : isValid.pattern - : undefined; + let pattern; + if ( isValid?.pattern ) { + pattern = + isValid.pattern instanceof RegExp + ? isValid.pattern.source + : isValid.pattern; + } return ( Date: Mon, 17 Nov 2025 21:20:52 +0000 Subject: [PATCH 06/11] Update storybook single story per field This reverts commit 149058054350f9c9005f0da430202d8d883b3ad2. --- .../dataviews/src/stories/dataform.story.tsx | 102 +++++++----------- 1 file changed, 36 insertions(+), 66 deletions(-) diff --git a/packages/dataviews/src/stories/dataform.story.tsx b/packages/dataviews/src/stories/dataform.story.tsx index cd4f3eca49e4f2..9cb01c55fd812a 100644 --- a/packages/dataviews/src/stories/dataform.story.tsx +++ b/packages/dataviews/src/stories/dataform.story.tsx @@ -553,10 +553,12 @@ const ValidationComponent = ( { required, elements, custom, + pattern, }: { required: boolean; elements: 'sync' | 'async' | 'none'; custom: 'sync' | 'async' | 'none'; + pattern: boolean; } ) => { type ValidatedItem = { text: string; @@ -607,11 +609,6 @@ const ValidationComponent = ( { date: undefined, dateRange: undefined, datetime: undefined, - username: 'john_doe_123', - zipcode: '12345', - phonePattern: '+1-555-123-4567', - emailPattern: 'user@company.com', - urlPattern: 'https://github.com/wordpress/gutenberg', } ); // Cache for getElements functions - ensures promises are only created once @@ -922,33 +919,15 @@ const ValidationComponent = ( { id: 'text', type: 'text', label: 'Text', + placeholder: pattern ? 'user_name123' : undefined, + description: pattern + ? 'Must contain only letters, numbers, and underscores' + : undefined, isValid: { required, elements: elements !== 'none' ? true : false, custom: maybeCustomRule( customTextRule ), - }, - }, - { - id: 'username', - type: 'text', - label: 'Username (pattern: alphanumeric + underscore)', - placeholder: 'user_name123', - description: - 'Must contain only letters, numbers, and underscores', - isValid: { - required, - pattern: /^[a-zA-Z0-9_]+$/, - }, - }, - { - id: 'zipcode', - type: 'text', - label: 'Zip Code (pattern: 5 digits)', - placeholder: '12345', - description: 'Must be exactly 5 digits', - isValid: { - required, - pattern: '[0-9]{5}', + pattern: pattern ? /^[a-zA-Z0-9_]+$/ : undefined, }, }, { @@ -1007,63 +986,49 @@ const ValidationComponent = ( { id: 'email', type: 'email', label: 'e-mail', + placeholder: pattern ? 'user@company.com' : undefined, + description: pattern + ? 'Email must be from @company.com domain' + : undefined, isValid: { required, elements: elements !== 'none' ? true : false, custom: maybeCustomRule( customEmailRule ), - }, - }, - { - id: 'emailPattern', - type: 'email', - label: 'Email (pattern: must end with @company.com)', - placeholder: 'user@company.com', - description: 'Email must be from @company.com domain', - isValid: { - required, - pattern: /^[a-zA-Z0-9]+@company\.com$/, + pattern: pattern + ? /^[a-zA-Z0-9]+@company\.com$/ + : undefined, }, }, { id: 'telephone', type: 'telephone', label: 'telephone', + placeholder: pattern ? '+1-555-123-4567' : undefined, + description: pattern + ? 'US phone format with country code' + : undefined, isValid: { required, elements: elements !== 'none' ? true : false, custom: maybeCustomRule( customTelephoneRule ), - }, - }, - { - id: 'phonePattern', - type: 'telephone', - label: 'Phone (pattern: +1-XXX-XXX-XXXX)', - placeholder: '+1-555-123-4567', - description: 'US phone format with country code', - isValid: { - required, - pattern: /^\+1-\d{3}-\d{3}-\d{4}$/, + pattern: pattern ? /^\+1-\d{3}-\d{3}-\d{4}$/ : undefined, }, }, { id: 'url', type: 'url', label: 'URL', + placeholder: pattern + ? 'https://github.com/user/repo' + : undefined, + description: pattern + ? 'Must be a GitHub repository URL' + : undefined, isValid: { required, elements: elements !== 'none' ? true : false, custom: maybeCustomRule( customUrlRule ), - }, - }, - { - id: 'urlPattern', - type: 'url', - label: 'URL (pattern: must be GitHub repo)', - placeholder: 'https://github.com/user/repo', - description: 'Must be a GitHub repository URL', - isValid: { - required, - pattern: /^https:\/\/github\.com\/.*/, + pattern: pattern ? /^https:\/\/github\.com\/.*/ : undefined, }, }, { @@ -1146,10 +1111,14 @@ const ValidationComponent = ( { id: 'password', type: 'password', label: 'Password', + description: pattern + ? 'Must have 8 numbers or letters' + : undefined, isValid: { required, elements: elements !== 'none' ? true : false, custom: maybeCustomRule( customPasswordRule ), + pattern: pattern ? /^[0-9a-zA-Z]{8}$/ : undefined, }, }, { @@ -1224,8 +1193,6 @@ const ValidationComponent = ( { () => ( { fields: [ 'text', - 'username', - 'zipcode', { id: 'customEdit' }, { id: 'level1Integer', @@ -1248,7 +1215,6 @@ const ValidationComponent = ( { }, ], }, - 'emailPattern', { id: 'level1Telephone', children: [ @@ -1268,9 +1234,7 @@ const ValidationComponent = ( { }, ], }, - 'phonePattern', 'url', - 'urlPattern', 'color', 'password', 'textarea', @@ -2155,11 +2119,17 @@ export const Validation = { description: 'Whether or not the custom validation rule is active.', options: [ 'sync', 'async', 'none' ], }, + pattern: { + control: { type: 'boolean' }, + description: + 'Whether or not the pattern validation rule is active.', + }, }, args: { required: true, elements: 'sync', custom: 'sync', + pattern: false, }, }; From 38f62aa500f42e6b6779d1750c953a0f812bc71c Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 19 Nov 2025 15:53:39 +0000 Subject: [PATCH 07/11] remove unrequired code --- packages/dataviews/src/stories/dataform.story.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/dataviews/src/stories/dataform.story.tsx b/packages/dataviews/src/stories/dataform.story.tsx index 9cb01c55fd812a..19a25c919b8962 100644 --- a/packages/dataviews/src/stories/dataform.story.tsx +++ b/packages/dataviews/src/stories/dataform.story.tsx @@ -581,11 +581,6 @@ const ValidationComponent = ( { date?: string; dateRange?: string; datetime?: string; - username?: string; - zipcode?: string; - phonePattern?: string; - emailPattern?: string; - urlPattern?: string; }; const [ post, setPost ] = useState< ValidatedItem >( { From 72424ede5288ff30850f655030ac4afe24b4f92a Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 19 Nov 2025 15:56:03 +0000 Subject: [PATCH 08/11] force pattern to be a string --- .../src/dataform-controls/utils/validated-input.tsx | 11 +---------- packages/dataviews/src/types/field-api.ts | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/utils/validated-input.tsx b/packages/dataviews/src/dataform-controls/utils/validated-input.tsx index 147194f6948398..3d497a643c23e4 100644 --- a/packages/dataviews/src/dataform-controls/utils/validated-input.tsx +++ b/packages/dataviews/src/dataform-controls/utils/validated-input.tsx @@ -54,15 +54,6 @@ export default function ValidatedText< Item >( { [ data, setValue, onChange ] ); - // Convert pattern to string if it's a RegExp - let pattern; - if ( isValid?.pattern ) { - pattern = - isValid.pattern instanceof RegExp - ? isValid.pattern.source - : isValid.pattern; - } - return ( ( { type={ type } prefix={ prefix } suffix={ suffix } - pattern={ pattern } + pattern={ isValid?.pattern } __next40pxDefaultSize /> ); diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index 8d62fab700950c..abbe7c37c0eede 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -79,7 +79,7 @@ export type FieldType = export type Rules< Item > = { required?: boolean; elements?: boolean; - pattern?: string | RegExp; + pattern?: string; custom?: | ( ( item: Item, field: NormalizedField< Item > ) => null | string ) | ( ( From df2481d00f75af31527822160409f5367fbfc721 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 19 Nov 2025 16:09:02 +0000 Subject: [PATCH 09/11] update storybook patterns to be strings --- .../dataviews/src/stories/dataform.story.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/dataviews/src/stories/dataform.story.tsx b/packages/dataviews/src/stories/dataform.story.tsx index 19a25c919b8962..b6a5608e5f2e63 100644 --- a/packages/dataviews/src/stories/dataform.story.tsx +++ b/packages/dataviews/src/stories/dataform.story.tsx @@ -922,7 +922,7 @@ const ValidationComponent = ( { required, elements: elements !== 'none' ? true : false, custom: maybeCustomRule( customTextRule ), - pattern: pattern ? /^[a-zA-Z0-9_]+$/ : undefined, + pattern: pattern ? '^[a-zA-Z0-9_]+$' : undefined, }, }, { @@ -989,9 +989,7 @@ const ValidationComponent = ( { required, elements: elements !== 'none' ? true : false, custom: maybeCustomRule( customEmailRule ), - pattern: pattern - ? /^[a-zA-Z0-9]+@company\.com$/ - : undefined, + pattern: pattern ? '^[a-zA-Z0-9]+@company.com$' : undefined, }, }, { @@ -1006,7 +1004,9 @@ const ValidationComponent = ( { required, elements: elements !== 'none' ? true : false, custom: maybeCustomRule( customTelephoneRule ), - pattern: pattern ? /^\+1-\d{3}-\d{3}-\d{4}$/ : undefined, + pattern: pattern + ? '^\\+1-\\d{3}-\\d{3}-\\d{4}$' + : undefined, }, }, { @@ -1023,7 +1023,9 @@ const ValidationComponent = ( { required, elements: elements !== 'none' ? true : false, custom: maybeCustomRule( customUrlRule ), - pattern: pattern ? /^https:\/\/github\.com\/.*/ : undefined, + pattern: pattern + ? '^https:\\/\\/github\\.com\\/.*' + : undefined, }, }, { @@ -1113,7 +1115,7 @@ const ValidationComponent = ( { required, elements: elements !== 'none' ? true : false, custom: maybeCustomRule( customPasswordRule ), - pattern: pattern ? /^[0-9a-zA-Z]{8}$/ : undefined, + pattern: pattern ? '^[0-9a-zA-Z]{8}$' : undefined, }, }, { From a9cef3712fd0308074db3dd4ba629eefc9867a0d Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 19 Nov 2025 17:32:17 +0000 Subject: [PATCH 10/11] add changelog --- packages/dataviews/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index b9e9f871560929..dddb83dca65b84 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -15,6 +15,7 @@ - Documentation: surface better the `type` property in the documentation. [#73349](https://github.com/WordPress/gutenberg/pull/73349) - Documentation: improve DataView's `layout` prop. [#73470](https://github.com/WordPress/gutenberg/pull/73470) - DataForm Panel Layout: Focus the first input element when the panel opens. [#72322](https://github.com/WordPress/gutenberg/pull/72322) +- DataForm: Pattern validation is now supported on all fields that browsers support it in. [#73156](https://github.com/WordPress/gutenberg/pull/73156) ### Bug fixes From 18f035e8354d487fc2863b9162a68dd0101a5a52 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 21 Nov 2025 13:20:33 +0000 Subject: [PATCH 11/11] add pattern validation to useFormValidity hook --- .../utils/get-custom-validity.ts | 2 + .../dataviews/src/hooks/use-form-validity.ts | 36 +++ .../dataviews/src/test/use-form-validity.ts | 266 ++++++++++++++++++ packages/dataviews/src/types/field-api.ts | 4 + 4 files changed, 308 insertions(+) diff --git a/packages/dataviews/src/dataform-controls/utils/get-custom-validity.ts b/packages/dataviews/src/dataform-controls/utils/get-custom-validity.ts index f98ca2ab9b6c2b..e18c09ed91fd74 100644 --- a/packages/dataviews/src/dataform-controls/utils/get-custom-validity.ts +++ b/packages/dataviews/src/dataform-controls/utils/get-custom-validity.ts @@ -14,6 +14,8 @@ export default function getCustomValidity< Item >( customValidity = validity?.required?.message ? validity.required : undefined; + } else if ( isValid?.pattern && validity?.pattern ) { + customValidity = validity.pattern; } else if ( isValid?.elements && validity?.elements ) { customValidity = validity.elements; } else if ( validity?.custom ) { diff --git a/packages/dataviews/src/hooks/use-form-validity.ts b/packages/dataviews/src/hooks/use-form-validity.ts index 3c7d2a3d6fb02a..27bf9d5f5937bf 100644 --- a/packages/dataviews/src/hooks/use-form-validity.ts +++ b/packages/dataviews/src/hooks/use-form-validity.ts @@ -446,6 +446,42 @@ function validateFormField< Item >( }; } + // Validate the field: isValid.pattern + if ( + !! formField.field && + formField.field.isValid.pattern && + ( formField.field.type === 'text' || + formField.field.type === 'email' || + formField.field.type === 'url' || + formField.field.type === 'telephone' || + formField.field.type === 'password' ) + ) { + const value = formField.field.getValue( { item } ); + // Only validate pattern if the value is not empty + if ( ! isEmptyNullOrUndefined( value ) ) { + try { + const regex = new RegExp( formField.field.isValid.pattern ); + if ( ! regex.test( String( value ) ) ) { + return { + pattern: { + type: 'invalid', + message: __( + 'Value does not match the required pattern.' + ), + }, + }; + } + } catch ( error ) { + return { + pattern: { + type: 'invalid', + message: __( 'Invalid pattern configuration.' ), + }, + }; + } + } + } + // Validate the field: isValid.elements (static) if ( !! formField.field && diff --git a/packages/dataviews/src/test/use-form-validity.ts b/packages/dataviews/src/test/use-form-validity.ts index 7b8790c670e715..4648dae8632ff6 100644 --- a/packages/dataviews/src/test/use-form-validity.ts +++ b/packages/dataviews/src/test/use-form-validity.ts @@ -533,6 +533,272 @@ describe( 'useFormValidity', () => { } ); } ); + describe( 'isValid.pattern', () => { + const PATTERN_MESSAGE = { + pattern: { + type: 'invalid', + message: 'Value does not match the required pattern.', + }, + }; + + it( 'text is valid when value matches the pattern', () => { + const item = { id: 1, username: 'user_name123' }; + const fields: Field< {} >[] = [ + { + id: 'username', + type: 'text', + isValid: { + pattern: '^[a-zA-Z0-9_]+$', + }, + }, + ]; + const form = { fields: [ 'username' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity ).toEqual( undefined ); + expect( isValid ).toBe( true ); + } ); + + it( 'text is invalid when value does not match the pattern', () => { + const item = { id: 1, username: 'user@name!' }; + const fields: Field< {} >[] = [ + { + id: 'username', + type: 'text', + isValid: { + pattern: '^[a-zA-Z0-9_]+$', + }, + }, + ]; + const form = { fields: [ 'username' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity?.username ).toEqual( PATTERN_MESSAGE ); + expect( isValid ).toBe( false ); + } ); + + it( 'text is valid when value is empty and pattern is defined', () => { + const item = { id: 1, username: '' }; + const fields: Field< {} >[] = [ + { + id: 'username', + type: 'text', + isValid: { + pattern: '^[a-zA-Z0-9_]+$', + }, + }, + ]; + const form = { fields: [ 'username' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity ).toEqual( undefined ); + expect( isValid ).toBe( true ); + } ); + + it( 'email is valid when value matches the pattern', () => { + const item = { id: 1, email: 'user@company.com' }; + const fields: Field< {} >[] = [ + { + id: 'email', + type: 'email', + isValid: { + pattern: '^[a-zA-Z0-9]+@company.com$', + }, + }, + ]; + const form = { fields: [ 'email' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity ).toEqual( undefined ); + expect( isValid ).toBe( true ); + } ); + + it( 'email is invalid when value does not match the pattern', () => { + const item = { id: 1, email: 'user@other.com' }; + const fields: Field< {} >[] = [ + { + id: 'email', + type: 'email', + isValid: { + pattern: '^[a-zA-Z0-9]+@company.com$', + }, + }, + ]; + const form = { fields: [ 'email' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity?.email ).toEqual( PATTERN_MESSAGE ); + expect( isValid ).toBe( false ); + } ); + + it( 'url is valid when value matches the pattern', () => { + const item = { id: 1, website: 'https://example.com' }; + const fields: Field< {} >[] = [ + { + id: 'website', + type: 'url', + isValid: { + pattern: '^https://.*', + }, + }, + ]; + const form = { fields: [ 'website' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity ).toEqual( undefined ); + expect( isValid ).toBe( true ); + } ); + + it( 'url is invalid when value does not match the pattern', () => { + const item = { id: 1, website: 'http://example.com' }; + const fields: Field< {} >[] = [ + { + id: 'website', + type: 'url', + isValid: { + pattern: '^https://.*', + }, + }, + ]; + const form = { fields: [ 'website' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity?.website ).toEqual( PATTERN_MESSAGE ); + expect( isValid ).toBe( false ); + } ); + + it( 'telephone is valid when value matches the pattern', () => { + const item = { id: 1, phone: '+1-555-123-4567' }; + const fields: Field< {} >[] = [ + { + id: 'phone', + type: 'telephone', + isValid: { + pattern: '^\\+1-[0-9]{3}-[0-9]{3}-[0-9]{4}$', + }, + }, + ]; + const form = { fields: [ 'phone' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity ).toEqual( undefined ); + expect( isValid ).toBe( true ); + } ); + + it( 'telephone is invalid when value does not match the pattern', () => { + const item = { id: 1, phone: '555-123-4567' }; + const fields: Field< {} >[] = [ + { + id: 'phone', + type: 'telephone', + isValid: { + pattern: '^\\+1-[0-9]{3}-[0-9]{3}-[0-9]{4}$', + }, + }, + ]; + const form = { fields: [ 'phone' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity?.phone ).toEqual( PATTERN_MESSAGE ); + expect( isValid ).toBe( false ); + } ); + + it( 'password is valid when value matches the pattern', () => { + const item = { id: 1, password: 'Abcd1234' }; + const fields: Field< {} >[] = [ + { + id: 'password', + type: 'password', + isValid: { + pattern: '^[0-9a-zA-Z]{8}$', + }, + }, + ]; + const form = { fields: [ 'password' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity ).toEqual( undefined ); + expect( isValid ).toBe( true ); + } ); + + it( 'password is invalid when value does not match the pattern', () => { + const item = { id: 1, password: 'short' }; + const fields: Field< {} >[] = [ + { + id: 'password', + type: 'password', + isValid: { + pattern: '^[0-9a-zA-Z]{8}$', + }, + }, + ]; + const form = { fields: [ 'password' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity?.password ).toEqual( PATTERN_MESSAGE ); + expect( isValid ).toBe( false ); + } ); + + it( 'invalid regex pattern returns error', () => { + const item = { id: 1, username: 'test' }; + const fields: Field< {} >[] = [ + { + id: 'username', + type: 'text', + isValid: { + pattern: '[invalid(regex', + }, + }, + ]; + const form = { fields: [ 'username' ] }; + const { + result: { + current: { validity, isValid }, + }, + } = renderHook( () => useFormValidity( item, fields, form ) ); + expect( validity?.username ).toEqual( { + pattern: { + type: 'invalid', + message: 'Invalid pattern configuration.', + }, + } ); + expect( isValid ).toBe( false ); + } ); + } ); + describe( 'isValid.custom', () => { it( 'integer is valid if value is integer', () => { const item = { id: 1, order: 2, title: 'hi' }; diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index abbe7c37c0eede..f4e48d53e1afb7 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -304,6 +304,10 @@ export type FieldValidity = { type: 'valid' | 'invalid' | 'validating'; message?: string; }; + pattern?: { + type: 'valid' | 'invalid' | 'validating'; + message: string; + }; elements?: { type: 'valid' | 'invalid' | 'validating'; message: string;