Skip to content

Commit 5f56cd1

Browse files
authored
Form components: Support async validation (#71184)
* Form components: Support async validation * Move demo to Overview doc * Improve support for multiple errors * Update "Overview" stories * Rename `customValidator` prop * Switch to spinner * Update usage in DataForm * Add changelog * Always prioritize error message over status * Only show `validating` state after 1s elapses * Extract validation indicator to separate file * Simplify * Add guidance for status indicators * Rename CSS classes * Rename indicator * Try spinning icon * Rename `customValidityMessage` * Make `message` required An icon is not enough to communicate the status * Remove unnecessary optional * Revert "Try spinning icon" This reverts commit 88ab560.
1 parent 5b9c8dd commit 5f56cd1

35 files changed

+689
-223
lines changed

packages/components/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
- Upgrade `gradient-parser` to version `1.1.1` to support HSL/HSLA color, CSS variables, and `calc()` expressions ([#71186](https://github.com/WordPress/gutenberg/pull/71186)).
88

9+
### Internal
10+
11+
- Validated form controls: Add support for async validation. This is a breaking API change that splits the `customValidator` prop into an `onValidate` callback and a `customValidity` object. ([#71184](https://github.com/WordPress/gutenberg/pull/71184)).
12+
913
## 30.1.0 (2025-08-07)
1014

1115
### Enhancement

packages/components/src/validated-form-controls/components/checkbox-control.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ type Value = CheckboxControlProps[ 'checked' ];
1717
const UnforwardedValidatedCheckboxControl = (
1818
{
1919
required,
20-
customValidator,
20+
onValidate,
21+
customValidity,
2122
onChange,
2223
markWhenOptional,
2324
...restProps
@@ -37,9 +38,10 @@ const UnforwardedValidatedCheckboxControl = (
3738
required={ required }
3839
markWhenOptional={ markWhenOptional }
3940
ref={ mergedRefs }
40-
customValidator={ () => {
41-
return customValidator?.( valueRef.current );
41+
onValidate={ () => {
42+
return onValidate?.( valueRef.current );
4243
} }
44+
customValidity={ customValidity }
4345
getValidityTarget={ () =>
4446
validityTargetRef.current?.querySelector< HTMLInputElement >(
4547
'input[type="checkbox"]'

packages/components/src/validated-form-controls/components/combobox-control.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ type Value = ComboboxControlProps[ 'value' ];
1717
const UnforwardedValidatedComboboxControl = (
1818
{
1919
required,
20-
customValidator,
20+
onValidate,
21+
customValidity,
2122
onChange,
2223
markWhenOptional,
2324
...restProps
@@ -50,9 +51,10 @@ const UnforwardedValidatedComboboxControl = (
5051
required={ required }
5152
markWhenOptional={ markWhenOptional }
5253
ref={ mergedRefs }
53-
customValidator={ () => {
54-
return customValidator?.( valueRef.current );
54+
onValidate={ () => {
55+
return onValidate?.( valueRef.current );
5556
} }
57+
customValidity={ customValidity }
5658
getValidityTarget={ () =>
5759
validityTargetRef.current?.querySelector< HTMLInputElement >(
5860
'input[role="combobox"]'

packages/components/src/validated-form-controls/components/custom-select-control.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ type Value = CustomSelectControlProps[ 'value' ];
2121
const UnforwardedValidatedCustomSelectControl = (
2222
{
2323
required,
24-
customValidator,
24+
onValidate,
25+
customValidity,
2526
onChange,
2627
markWhenOptional,
2728
...restProps
@@ -43,9 +44,10 @@ const UnforwardedValidatedCustomSelectControl = (
4344
<ControlWithError
4445
required={ required }
4546
markWhenOptional={ markWhenOptional }
46-
customValidator={ () => {
47-
return customValidator?.( valueRef.current );
47+
onValidate={ () => {
48+
return onValidate?.( valueRef.current );
4849
} }
50+
customValidity={ customValidity }
4951
getValidityTarget={ () => validityTargetRef.current }
5052
>
5153
<CustomSelectControl

packages/components/src/validated-form-controls/components/input-control.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ type Value = InputControlProps[ 'value' ];
1717
const UnforwardedValidatedInputControl = (
1818
{
1919
required,
20-
customValidator,
20+
onValidate,
21+
customValidity,
2122
onChange,
2223
markWhenOptional,
2324
...restProps
@@ -36,9 +37,10 @@ const UnforwardedValidatedInputControl = (
3637
<ControlWithError
3738
required={ required }
3839
markWhenOptional={ markWhenOptional }
39-
customValidator={ () => {
40-
return customValidator?.( valueRef.current );
40+
onValidate={ () => {
41+
return onValidate?.( valueRef.current );
4142
} }
43+
customValidity={ customValidity }
4244
getValidityTarget={ () => validityTargetRef.current }
4345
>
4446
<InputControl

packages/components/src/validated-form-controls/components/number-control.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ type Value = NumberControlProps[ 'value' ];
1717
const UnforwardedValidatedNumberControl = (
1818
{
1919
required,
20-
customValidator,
20+
onValidate,
21+
customValidity,
2122
onChange,
2223
markWhenOptional,
2324
...restProps
@@ -36,9 +37,10 @@ const UnforwardedValidatedNumberControl = (
3637
<ControlWithError
3738
required={ required }
3839
markWhenOptional={ markWhenOptional }
39-
customValidator={ () => {
40-
return customValidator?.( valueRef.current );
40+
onValidate={ () => {
41+
return onValidate?.( valueRef.current );
4142
} }
43+
customValidity={ customValidity }
4244
getValidityTarget={ () => validityTargetRef.current }
4345
>
4446
<NumberControl

packages/components/src/validated-form-controls/components/radio-control.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ type Value = RadioControlProps[ 'selected' ];
1717
const UnforwardedValidatedRadioControl = (
1818
{
1919
required,
20-
customValidator,
20+
onValidate,
21+
customValidity,
2122
onChange,
2223
markWhenOptional,
2324
...restProps
@@ -35,9 +36,10 @@ const UnforwardedValidatedRadioControl = (
3536
markWhenOptional={ markWhenOptional }
3637
// TODO: Upstream limitation - RadioControl does not accept a ref.
3738
ref={ mergedRefs }
38-
customValidator={ () => {
39-
return customValidator?.( valueRef.current );
39+
onValidate={ () => {
40+
return onValidate?.( valueRef.current );
4041
} }
42+
customValidity={ customValidity }
4143
getValidityTarget={ () =>
4244
validityTargetRef.current?.querySelector< HTMLInputElement >(
4345
'input[type="radio"]'

packages/components/src/validated-form-controls/components/range-control.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ type Value = RangeControlProps[ 'value' ];
1717
const UnforwardedValidatedRangeControl = (
1818
{
1919
required,
20-
customValidator,
20+
onValidate,
21+
customValidity,
2122
onChange,
2223
markWhenOptional,
2324
...restProps
@@ -36,9 +37,10 @@ const UnforwardedValidatedRangeControl = (
3637
<ControlWithError
3738
required={ required }
3839
markWhenOptional={ markWhenOptional }
39-
customValidator={ () => {
40-
return customValidator?.( valueRef.current );
40+
onValidate={ () => {
41+
return onValidate?.( valueRef.current );
4142
} }
43+
customValidity={ customValidity }
4244
getValidityTarget={ () => validityTargetRef.current }
4345
>
4446
<RangeControl

packages/components/src/validated-form-controls/components/select-control.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ type Value = SelectControlProps[ 'value' ];
2626
const UnforwardedValidatedSelectControl = (
2727
{
2828
required,
29-
customValidator,
29+
onValidate,
30+
customValidity,
3031
onChange,
3132
markWhenOptional,
3233
...restProps
@@ -51,9 +52,10 @@ const UnforwardedValidatedSelectControl = (
5152
<ControlWithError
5253
required={ required }
5354
markWhenOptional={ markWhenOptional }
54-
customValidator={ () => {
55-
return customValidator?.( valueRef.current );
55+
onValidate={ () => {
56+
return onValidate?.( valueRef.current );
5657
} }
58+
customValidity={ customValidity }
5759
getValidityTarget={ () => validityTargetRef.current }
5860
>
5961
<SelectControl

packages/components/src/validated-form-controls/components/stories/checkbox-control.story.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export default meta;
3232
export const Default: StoryObj< typeof ValidatedCheckboxControl > = {
3333
render: function Template( { onChange, ...args } ) {
3434
const [ checked, setChecked ] = useState( false );
35+
const [ customValidity, setCustomValidity ] =
36+
useState<
37+
React.ComponentProps<
38+
typeof ValidatedCheckboxControl
39+
>[ 'customValidity' ]
40+
>( undefined );
3541

3642
return (
3743
<ValidatedCheckboxControl
@@ -41,6 +47,17 @@ export const Default: StoryObj< typeof ValidatedCheckboxControl > = {
4147
setChecked( value );
4248
onChange?.( value );
4349
} }
50+
onValidate={ ( value ) => {
51+
if ( value ) {
52+
setCustomValidity( {
53+
type: 'invalid',
54+
message: 'This checkbox may not be checked.',
55+
} );
56+
} else {
57+
setCustomValidity( undefined );
58+
}
59+
} }
60+
customValidity={ customValidity }
4461
/>
4562
);
4663
},
@@ -49,10 +66,4 @@ Default.args = {
4966
required: true,
5067
label: 'Checkbox',
5168
help: 'This checkbox may neither be checked nor unchecked.',
52-
customValidator: ( value ) => {
53-
if ( value ) {
54-
return 'This checkbox may not be checked.';
55-
}
56-
return undefined;
57-
},
5869
};

0 commit comments

Comments
 (0)