@wordpress/components 30.2.1-next.0f6f9d12c.0 → 30.2.1-next.f34ab90e9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/build/menu-item/index.js +1 -0
  3. package/build/menu-item/index.js.map +1 -1
  4. package/build/validated-form-controls/components/checkbox-control.js +5 -3
  5. package/build/validated-form-controls/components/checkbox-control.js.map +1 -1
  6. package/build/validated-form-controls/components/combobox-control.js +5 -3
  7. package/build/validated-form-controls/components/combobox-control.js.map +1 -1
  8. package/build/validated-form-controls/components/custom-select-control.js +5 -3
  9. package/build/validated-form-controls/components/custom-select-control.js.map +1 -1
  10. package/build/validated-form-controls/components/input-control.js +5 -3
  11. package/build/validated-form-controls/components/input-control.js.map +1 -1
  12. package/build/validated-form-controls/components/number-control.js +5 -3
  13. package/build/validated-form-controls/components/number-control.js.map +1 -1
  14. package/build/validated-form-controls/components/radio-control.js +5 -3
  15. package/build/validated-form-controls/components/radio-control.js.map +1 -1
  16. package/build/validated-form-controls/components/range-control.js +5 -3
  17. package/build/validated-form-controls/components/range-control.js.map +1 -1
  18. package/build/validated-form-controls/components/select-control.js +5 -3
  19. package/build/validated-form-controls/components/select-control.js.map +1 -1
  20. package/build/validated-form-controls/components/text-control.js +5 -3
  21. package/build/validated-form-controls/components/text-control.js.map +1 -1
  22. package/build/validated-form-controls/components/textarea-control.js +5 -3
  23. package/build/validated-form-controls/components/textarea-control.js.map +1 -1
  24. package/build/validated-form-controls/components/toggle-control.js +5 -3
  25. package/build/validated-form-controls/components/toggle-control.js.map +1 -1
  26. package/build/validated-form-controls/components/toggle-group-control.js +5 -3
  27. package/build/validated-form-controls/components/toggle-group-control.js.map +1 -1
  28. package/build/validated-form-controls/components/types.js.map +1 -1
  29. package/build/validated-form-controls/control-with-error.js +57 -22
  30. package/build/validated-form-controls/control-with-error.js.map +1 -1
  31. package/build/validated-form-controls/validity-indicator.js +45 -0
  32. package/build/validated-form-controls/validity-indicator.js.map +1 -0
  33. package/build-module/menu-item/index.js +1 -0
  34. package/build-module/menu-item/index.js.map +1 -1
  35. package/build-module/validated-form-controls/components/checkbox-control.js +5 -3
  36. package/build-module/validated-form-controls/components/checkbox-control.js.map +1 -1
  37. package/build-module/validated-form-controls/components/combobox-control.js +5 -3
  38. package/build-module/validated-form-controls/components/combobox-control.js.map +1 -1
  39. package/build-module/validated-form-controls/components/custom-select-control.js +5 -3
  40. package/build-module/validated-form-controls/components/custom-select-control.js.map +1 -1
  41. package/build-module/validated-form-controls/components/input-control.js +5 -3
  42. package/build-module/validated-form-controls/components/input-control.js.map +1 -1
  43. package/build-module/validated-form-controls/components/number-control.js +5 -3
  44. package/build-module/validated-form-controls/components/number-control.js.map +1 -1
  45. package/build-module/validated-form-controls/components/radio-control.js +5 -3
  46. package/build-module/validated-form-controls/components/radio-control.js.map +1 -1
  47. package/build-module/validated-form-controls/components/range-control.js +5 -3
  48. package/build-module/validated-form-controls/components/range-control.js.map +1 -1
  49. package/build-module/validated-form-controls/components/select-control.js +5 -3
  50. package/build-module/validated-form-controls/components/select-control.js.map +1 -1
  51. package/build-module/validated-form-controls/components/text-control.js +5 -3
  52. package/build-module/validated-form-controls/components/text-control.js.map +1 -1
  53. package/build-module/validated-form-controls/components/textarea-control.js +5 -3
  54. package/build-module/validated-form-controls/components/textarea-control.js.map +1 -1
  55. package/build-module/validated-form-controls/components/toggle-control.js +5 -3
  56. package/build-module/validated-form-controls/components/toggle-control.js.map +1 -1
  57. package/build-module/validated-form-controls/components/toggle-group-control.js +5 -3
  58. package/build-module/validated-form-controls/components/toggle-group-control.js.map +1 -1
  59. package/build-module/validated-form-controls/components/types.js.map +1 -1
  60. package/build-module/validated-form-controls/control-with-error.js +57 -21
  61. package/build-module/validated-form-controls/control-with-error.js.map +1 -1
  62. package/build-module/validated-form-controls/validity-indicator.js +37 -0
  63. package/build-module/validated-form-controls/validity-indicator.js.map +1 -0
  64. package/build-style/style-rtl.css +34 -22
  65. package/build-style/style.css +34 -22
  66. package/build-types/menu-item/index.d.ts.map +1 -1
  67. package/build-types/validated-form-controls/components/checkbox-control.d.ts.map +1 -1
  68. package/build-types/validated-form-controls/components/combobox-control.d.ts.map +1 -1
  69. package/build-types/validated-form-controls/components/custom-select-control.d.ts.map +1 -1
  70. package/build-types/validated-form-controls/components/input-control.d.ts.map +1 -1
  71. package/build-types/validated-form-controls/components/number-control.d.ts.map +1 -1
  72. package/build-types/validated-form-controls/components/radio-control.d.ts.map +1 -1
  73. package/build-types/validated-form-controls/components/range-control.d.ts.map +1 -1
  74. package/build-types/validated-form-controls/components/select-control.d.ts.map +1 -1
  75. package/build-types/validated-form-controls/components/stories/checkbox-control.story.d.ts.map +1 -1
  76. package/build-types/validated-form-controls/components/stories/combobox-control.story.d.ts.map +1 -1
  77. package/build-types/validated-form-controls/components/stories/custom-select-control.story.d.ts.map +1 -1
  78. package/build-types/validated-form-controls/components/stories/input-control.story.d.ts.map +1 -1
  79. package/build-types/validated-form-controls/components/stories/number-control.story.d.ts.map +1 -1
  80. package/build-types/validated-form-controls/components/stories/overview.story.d.ts +13 -0
  81. package/build-types/validated-form-controls/components/stories/overview.story.d.ts.map +1 -1
  82. package/build-types/validated-form-controls/components/stories/radio-control.story.d.ts.map +1 -1
  83. package/build-types/validated-form-controls/components/stories/range-control.story.d.ts.map +1 -1
  84. package/build-types/validated-form-controls/components/stories/select-control.story.d.ts.map +1 -1
  85. package/build-types/validated-form-controls/components/stories/text-control.story.d.ts.map +1 -1
  86. package/build-types/validated-form-controls/components/stories/textarea-control.story.d.ts.map +1 -1
  87. package/build-types/validated-form-controls/components/stories/toggle-control.story.d.ts.map +1 -1
  88. package/build-types/validated-form-controls/components/stories/toggle-group-control.story.d.ts.map +1 -1
  89. package/build-types/validated-form-controls/components/text-control.d.ts.map +1 -1
  90. package/build-types/validated-form-controls/components/textarea-control.d.ts.map +1 -1
  91. package/build-types/validated-form-controls/components/toggle-control.d.ts.map +1 -1
  92. package/build-types/validated-form-controls/components/toggle-group-control.d.ts.map +1 -1
  93. package/build-types/validated-form-controls/components/types.d.ts +21 -10
  94. package/build-types/validated-form-controls/components/types.d.ts.map +1 -1
  95. package/build-types/validated-form-controls/control-with-error.d.ts +4 -5
  96. package/build-types/validated-form-controls/control-with-error.d.ts.map +1 -1
  97. package/build-types/validated-form-controls/validity-indicator.d.ts +5 -0
  98. package/build-types/validated-form-controls/validity-indicator.d.ts.map +1 -0
  99. package/package.json +19 -19
  100. package/src/calendar/style.scss +22 -22
  101. package/src/menu-item/index.tsx +1 -0
  102. package/src/utils/theme-variables.scss +1 -0
  103. package/src/validated-form-controls/components/checkbox-control.tsx +5 -3
  104. package/src/validated-form-controls/components/combobox-control.tsx +5 -3
  105. package/src/validated-form-controls/components/custom-select-control.tsx +5 -3
  106. package/src/validated-form-controls/components/input-control.tsx +5 -3
  107. package/src/validated-form-controls/components/number-control.tsx +5 -3
  108. package/src/validated-form-controls/components/radio-control.tsx +5 -3
  109. package/src/validated-form-controls/components/range-control.tsx +5 -3
  110. package/src/validated-form-controls/components/select-control.tsx +5 -3
  111. package/src/validated-form-controls/components/stories/checkbox-control.story.tsx +17 -6
  112. package/src/validated-form-controls/components/stories/combobox-control.story.tsx +17 -6
  113. package/src/validated-form-controls/components/stories/custom-select-control.story.tsx +17 -6
  114. package/src/validated-form-controls/components/stories/input-control.story.tsx +51 -18
  115. package/src/validated-form-controls/components/stories/number-control.story.tsx +17 -6
  116. package/src/validated-form-controls/components/stories/overview.mdx +1 -1
  117. package/src/validated-form-controls/components/stories/overview.story.tsx +122 -14
  118. package/src/validated-form-controls/components/stories/radio-control.story.tsx +17 -6
  119. package/src/validated-form-controls/components/stories/range-control.story.tsx +17 -6
  120. package/src/validated-form-controls/components/stories/select-control.story.tsx +17 -6
  121. package/src/validated-form-controls/components/stories/text-control.story.tsx +17 -6
  122. package/src/validated-form-controls/components/stories/textarea-control.story.tsx +17 -6
  123. package/src/validated-form-controls/components/stories/toggle-control.story.tsx +17 -6
  124. package/src/validated-form-controls/components/stories/toggle-group-control.story.tsx +17 -6
  125. package/src/validated-form-controls/components/text-control.tsx +5 -3
  126. package/src/validated-form-controls/components/textarea-control.tsx +5 -3
  127. package/src/validated-form-controls/components/toggle-control.tsx +5 -3
  128. package/src/validated-form-controls/components/toggle-group-control.tsx +5 -3
  129. package/src/validated-form-controls/components/types.ts +21 -12
  130. package/src/validated-form-controls/control-with-error.tsx +77 -26
  131. package/src/validated-form-controls/style.scss +19 -5
  132. package/src/validated-form-controls/validity-indicator.tsx +48 -0
  133. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { useState } from '@wordpress/element';
4
+ import { useRef, useCallback, useState } from '@wordpress/element';
5
5
 
6
6
  /**
7
7
  * External dependencies
@@ -14,6 +14,7 @@ import type { Meta, StoryObj } from '@storybook/react';
14
14
  import { ValidatedInputControl } from '..';
15
15
  import { formDecorator } from './story-utils';
16
16
  import type { ControlWithError } from '../../control-with-error';
17
+ import { debounce } from '@wordpress/compose';
17
18
 
18
19
  const meta: Meta< typeof ControlWithError > = {
19
20
  title: 'Components/Selection & Input/Validated Form Controls/Overview',
@@ -32,6 +33,18 @@ export const WithMultipleControls: Story = {
32
33
  render: function Template() {
33
34
  const [ text, setText ] = useState( '' );
34
35
  const [ text2, setText2 ] = useState( '' );
36
+ const [ customValidity, setCustomValidity ] =
37
+ useState<
38
+ React.ComponentProps<
39
+ typeof ValidatedInputControl
40
+ >[ 'customValidity' ]
41
+ >( undefined );
42
+ const [ customValidity2, setCustomValidity2 ] =
43
+ useState<
44
+ React.ComponentProps<
45
+ typeof ValidatedInputControl
46
+ >[ 'customValidity' ]
47
+ >( undefined );
35
48
 
36
49
  return (
37
50
  <>
@@ -40,12 +53,17 @@ export const WithMultipleControls: Story = {
40
53
  required
41
54
  value={ text }
42
55
  help="The word 'error' will trigger an error."
43
- customValidator={ ( value ) => {
56
+ onValidate={ ( value ) => {
44
57
  if ( value?.toLowerCase() === 'error' ) {
45
- return 'The word "error" is not allowed.';
58
+ setCustomValidity( {
59
+ type: 'invalid',
60
+ message: 'The word "error" is not allowed.',
61
+ } );
62
+ } else {
63
+ setCustomValidity( undefined );
46
64
  }
47
- return undefined;
48
65
  } }
66
+ customValidity={ customValidity }
49
67
  onChange={ ( value ) => setText( value ?? '' ) }
50
68
  />
51
69
  <ValidatedInputControl
@@ -53,13 +71,18 @@ export const WithMultipleControls: Story = {
53
71
  required
54
72
  value={ text2 }
55
73
  help="The word 'error' will trigger an error."
56
- customValidator={ ( value ) => {
74
+ onValidate={ ( value ) => {
57
75
  if ( value?.toLowerCase() === 'error' ) {
58
- return 'The word "error" is not allowed.';
76
+ setCustomValidity2( {
77
+ type: 'invalid',
78
+ message: 'The word "error" is not allowed.',
79
+ } );
80
+ } else {
81
+ setCustomValidity2( undefined );
59
82
  }
60
- return undefined;
61
83
  } }
62
84
  onChange={ ( value ) => setText2( value ?? '' ) }
85
+ customValidity={ customValidity2 }
63
86
  />
64
87
  </>
65
88
  );
@@ -73,7 +96,12 @@ export const WithMultipleControls: Story = {
73
96
  export const WithHelpTextReplacement: Story = {
74
97
  render: function Template() {
75
98
  const [ text, setText ] = useState( '' );
76
- const [ hasCustomError, setHasCustomError ] = useState( false );
99
+ const [ customValidity, setCustomValidity ] =
100
+ useState<
101
+ React.ComponentProps<
102
+ typeof ValidatedInputControl
103
+ >[ 'customValidity' ]
104
+ >( undefined );
77
105
 
78
106
  return (
79
107
  <ValidatedInputControl
@@ -81,20 +109,100 @@ export const WithHelpTextReplacement: Story = {
81
109
  required
82
110
  value={ text }
83
111
  help={
84
- hasCustomError
112
+ customValidity
85
113
  ? undefined
86
114
  : 'The word "error" is not allowed.'
87
115
  }
88
- customValidator={ ( value ) => {
116
+ onValidate={ ( value ) => {
89
117
  if ( value?.toLowerCase() === 'error' ) {
90
- setHasCustomError( true );
91
- return 'The word "error" is not allowed.';
118
+ setCustomValidity( {
119
+ type: 'invalid',
120
+ message: 'The word "error" is not allowed.',
121
+ } );
122
+ } else {
123
+ setCustomValidity( undefined );
92
124
  }
93
- setHasCustomError( false );
94
- return undefined;
95
125
  } }
96
126
  onChange={ ( value ) => setText( value ?? '' ) }
127
+ customValidity={ customValidity }
97
128
  />
98
129
  );
99
130
  },
100
131
  };
132
+
133
+ /**
134
+ * To provide feedback from server-side validation, the `customValidity` prop can be used
135
+ * to show additional status indicators while waiting for the server response,
136
+ * and after the response is received.
137
+ *
138
+ * These indicators are intended for asynchronous validation calls that may take more than 1 second to complete.
139
+ * They may be unnecessary when responses are generally quick.
140
+ */
141
+ export const AsyncValidation: StoryObj< typeof ValidatedInputControl > = {
142
+ render: function Template( { ...args } ) {
143
+ const [ text, setText ] = useState( '' );
144
+ const [ customValidity, setCustomValidity ] =
145
+ useState<
146
+ React.ComponentProps<
147
+ typeof ValidatedInputControl
148
+ >[ 'customValidity' ]
149
+ >( undefined );
150
+
151
+ const timeoutRef = useRef< ReturnType< typeof setTimeout > >();
152
+ const previousValidationValueRef = useRef< unknown >( '' );
153
+
154
+ // eslint-disable-next-line react-hooks/exhaustive-deps
155
+ const debouncedValidate = useCallback(
156
+ debounce( ( v ) => {
157
+ if ( v === previousValidationValueRef.current ) {
158
+ return;
159
+ }
160
+
161
+ previousValidationValueRef.current = v;
162
+
163
+ setCustomValidity( {
164
+ type: 'validating',
165
+ message: 'Validating...',
166
+ } );
167
+
168
+ clearTimeout( timeoutRef.current );
169
+ timeoutRef.current = setTimeout(
170
+ () => {
171
+ if ( v?.toString().toLowerCase() === 'error' ) {
172
+ setCustomValidity( {
173
+ type: 'invalid',
174
+ message: 'The word "error" is not allowed.',
175
+ } );
176
+ } else {
177
+ setCustomValidity( {
178
+ type: 'valid',
179
+ message: 'Validated',
180
+ } );
181
+ }
182
+ },
183
+ // Mimics a random server response time.
184
+ // eslint-disable-next-line no-restricted-syntax
185
+ Math.random() < 0.5 ? 1500 : 300
186
+ );
187
+ }, 500 ),
188
+ []
189
+ );
190
+
191
+ return (
192
+ <ValidatedInputControl
193
+ { ...args }
194
+ value={ text }
195
+ onChange={ ( newValue ) => {
196
+ setText( newValue ?? '' );
197
+ } }
198
+ onValidate={ debouncedValidate }
199
+ customValidity={ customValidity }
200
+ />
201
+ );
202
+ },
203
+ };
204
+ AsyncValidation.args = {
205
+ label: 'Text',
206
+ help: 'The word "error" will trigger an error asynchronously.',
207
+ required: true,
208
+ };
@@ -35,6 +35,12 @@ export const Default: StoryObj< typeof ValidatedRadioControl > = {
35
35
  typeof ValidatedRadioControl
36
36
  >[ 'selected' ]
37
37
  >();
38
+ const [ customValidity, setCustomValidity ] =
39
+ useState<
40
+ React.ComponentProps<
41
+ typeof ValidatedRadioControl
42
+ >[ 'customValidity' ]
43
+ >( undefined );
38
44
 
39
45
  return (
40
46
  <ValidatedRadioControl
@@ -44,6 +50,17 @@ export const Default: StoryObj< typeof ValidatedRadioControl > = {
44
50
  setSelected( value );
45
51
  onChange?.( value );
46
52
  } }
53
+ onValidate={ ( v ) => {
54
+ if ( v === 'b' ) {
55
+ setCustomValidity( {
56
+ type: 'invalid',
57
+ message: 'Option B is not allowed.',
58
+ } );
59
+ } else {
60
+ setCustomValidity( undefined );
61
+ }
62
+ } }
63
+ customValidity={ customValidity }
47
64
  />
48
65
  );
49
66
  },
@@ -56,10 +73,4 @@ Default.args = {
56
73
  { label: 'Option A', value: 'a' },
57
74
  { label: 'Option B (not allowed)', value: 'b' },
58
75
  ],
59
- customValidator: ( value ) => {
60
- if ( value === 'b' ) {
61
- return 'Option B is not allowed.';
62
- }
63
- return undefined;
64
- },
65
76
  };
@@ -33,6 +33,12 @@ export const Default: StoryObj< typeof ValidatedRangeControl > = {
33
33
  useState<
34
34
  React.ComponentProps< typeof ValidatedRangeControl >[ 'value' ]
35
35
  >();
36
+ const [ customValidity, setCustomValidity ] =
37
+ useState<
38
+ React.ComponentProps<
39
+ typeof ValidatedRangeControl
40
+ >[ 'customValidity' ]
41
+ >( undefined );
36
42
 
37
43
  return (
38
44
  <ValidatedRangeControl
@@ -42,6 +48,17 @@ export const Default: StoryObj< typeof ValidatedRangeControl > = {
42
48
  setValue( newValue );
43
49
  onChange?.( newValue );
44
50
  } }
51
+ onValidate={ ( v ) => {
52
+ if ( v && v % 2 !== 0 ) {
53
+ setCustomValidity( {
54
+ type: 'invalid',
55
+ message: 'Choose an even number.',
56
+ } );
57
+ } else {
58
+ setCustomValidity( undefined );
59
+ }
60
+ } }
61
+ customValidity={ customValidity }
45
62
  />
46
63
  );
47
64
  },
@@ -52,10 +69,4 @@ Default.args = {
52
69
  help: 'Odd numbers are not allowed.',
53
70
  min: 0,
54
71
  max: 20,
55
- customValidator: ( value ) => {
56
- if ( value && value % 2 !== 0 ) {
57
- return 'Choose an even number.';
58
- }
59
- return undefined;
60
- },
61
72
  };
@@ -30,6 +30,12 @@ export default meta;
30
30
  export const Default: StoryObj< typeof ValidatedSelectControl > = {
31
31
  render: function Template( { onChange, ...args } ) {
32
32
  const [ value, setValue ] = useState( '' );
33
+ const [ customValidity, setCustomValidity ] =
34
+ useState<
35
+ React.ComponentProps<
36
+ typeof ValidatedSelectControl
37
+ >[ 'customValidity' ]
38
+ >( undefined );
33
39
 
34
40
  return (
35
41
  <ValidatedSelectControl
@@ -39,6 +45,17 @@ export const Default: StoryObj< typeof ValidatedSelectControl > = {
39
45
  setValue( newValue );
40
46
  onChange?.( newValue );
41
47
  } }
48
+ onValidate={ ( v ) => {
49
+ if ( v === '1' ) {
50
+ setCustomValidity( {
51
+ type: 'invalid',
52
+ message: 'Option 1 is not allowed.',
53
+ } );
54
+ } else {
55
+ setCustomValidity( undefined );
56
+ }
57
+ } }
58
+ customValidity={ customValidity }
42
59
  />
43
60
  );
44
61
  },
@@ -52,10 +69,4 @@ Default.args = {
52
69
  { value: '1', label: 'Option 1 (not allowed)' },
53
70
  { value: '2', label: 'Option 2' },
54
71
  ],
55
- customValidator: ( value ) => {
56
- if ( value === '1' ) {
57
- return 'Option 1 is not allowed.';
58
- }
59
- return undefined;
60
- },
61
72
  };
@@ -30,6 +30,12 @@ export default meta;
30
30
  export const Default: StoryObj< typeof ValidatedTextControl > = {
31
31
  render: function Template( { onChange, ...args } ) {
32
32
  const [ value, setValue ] = useState( '' );
33
+ const [ customValidity, setCustomValidity ] =
34
+ useState<
35
+ React.ComponentProps<
36
+ typeof ValidatedTextControl
37
+ >[ 'customValidity' ]
38
+ >( undefined );
33
39
 
34
40
  return (
35
41
  <ValidatedTextControl
@@ -39,6 +45,17 @@ export const Default: StoryObj< typeof ValidatedTextControl > = {
39
45
  setValue( newValue );
40
46
  onChange?.( newValue );
41
47
  } }
48
+ onValidate={ ( v ) => {
49
+ if ( v?.toString().toLowerCase() === 'error' ) {
50
+ setCustomValidity( {
51
+ type: 'invalid',
52
+ message: 'The word "error" is not allowed.',
53
+ } );
54
+ } else {
55
+ setCustomValidity( undefined );
56
+ }
57
+ } }
58
+ customValidity={ customValidity }
42
59
  />
43
60
  );
44
61
  },
@@ -47,10 +64,4 @@ Default.args = {
47
64
  required: true,
48
65
  label: 'Text',
49
66
  help: "The word 'error' will trigger an error.",
50
- customValidator: ( value ) => {
51
- if ( value?.toString().toLowerCase() === 'error' ) {
52
- return 'The word "error" is not allowed.';
53
- }
54
- return undefined;
55
- },
56
67
  };
@@ -27,6 +27,12 @@ export default meta;
27
27
  export const Default: StoryObj< typeof ValidatedTextareaControl > = {
28
28
  render: function Template( { onChange, ...args } ) {
29
29
  const [ value, setValue ] = useState( '' );
30
+ const [ customValidity, setCustomValidity ] =
31
+ useState<
32
+ React.ComponentProps<
33
+ typeof ValidatedTextareaControl
34
+ >[ 'customValidity' ]
35
+ >( undefined );
30
36
 
31
37
  return (
32
38
  <ValidatedTextareaControl
@@ -36,6 +42,17 @@ export const Default: StoryObj< typeof ValidatedTextareaControl > = {
36
42
  onChange?.( newValue );
37
43
  } }
38
44
  value={ value }
45
+ onValidate={ ( v ) => {
46
+ if ( v?.toLowerCase() === 'error' ) {
47
+ setCustomValidity( {
48
+ type: 'invalid',
49
+ message: 'The word "error" is not allowed.',
50
+ } );
51
+ } else {
52
+ setCustomValidity( undefined );
53
+ }
54
+ } }
55
+ customValidity={ customValidity }
39
56
  />
40
57
  );
41
58
  },
@@ -44,10 +61,4 @@ Default.args = {
44
61
  required: true,
45
62
  label: 'Textarea',
46
63
  help: 'The word "error" will trigger an error.',
47
- customValidator: ( value ) => {
48
- if ( value?.toLowerCase() === 'error' ) {
49
- return 'The word "error" is not allowed.';
50
- }
51
- return undefined;
52
- },
53
64
  };
@@ -30,6 +30,12 @@ export default meta;
30
30
  export const Default: StoryObj< typeof ValidatedToggleControl > = {
31
31
  render: function Template( { onChange, ...args } ) {
32
32
  const [ checked, setChecked ] = useState( false );
33
+ const [ customValidity, setCustomValidity ] =
34
+ useState<
35
+ React.ComponentProps<
36
+ typeof ValidatedToggleControl
37
+ >[ 'customValidity' ]
38
+ >( undefined );
33
39
 
34
40
  return (
35
41
  <ValidatedToggleControl
@@ -39,6 +45,17 @@ export const Default: StoryObj< typeof ValidatedToggleControl > = {
39
45
  setChecked( value );
40
46
  onChange?.( value );
41
47
  } }
48
+ onValidate={ ( v ) => {
49
+ if ( v ) {
50
+ setCustomValidity( {
51
+ type: 'invalid',
52
+ message: 'This toggle may not be enabled.',
53
+ } );
54
+ } else {
55
+ setCustomValidity( undefined );
56
+ }
57
+ } }
58
+ customValidity={ customValidity }
42
59
  />
43
60
  );
44
61
  },
@@ -47,10 +64,4 @@ Default.args = {
47
64
  required: true,
48
65
  label: 'Toggle',
49
66
  help: 'This toggle may neither be enabled nor disabled.',
50
- customValidator: ( value ) => {
51
- if ( value ) {
52
- return 'This toggle may not be enabled.';
53
- }
54
- return undefined;
55
- },
56
67
  };
@@ -36,6 +36,12 @@ export const Default: StoryObj< typeof ValidatedToggleGroupControl > = {
36
36
  typeof ValidatedToggleGroupControl
37
37
  >[ 'value' ]
38
38
  >( '1' );
39
+ const [ customValidity, setCustomValidity ] =
40
+ useState<
41
+ React.ComponentProps<
42
+ typeof ValidatedToggleGroupControl
43
+ >[ 'customValidity' ]
44
+ >( undefined );
39
45
 
40
46
  return (
41
47
  <ValidatedToggleGroupControl
@@ -45,6 +51,17 @@ export const Default: StoryObj< typeof ValidatedToggleGroupControl > = {
45
51
  setValue( newValue );
46
52
  onChange?.( newValue );
47
53
  } }
54
+ onValidate={ ( v ) => {
55
+ if ( v === '2' ) {
56
+ setCustomValidity( {
57
+ type: 'invalid',
58
+ message: 'Option 2 is not allowed.',
59
+ } );
60
+ } else {
61
+ setCustomValidity( undefined );
62
+ }
63
+ } }
64
+ customValidity={ customValidity }
48
65
  />
49
66
  );
50
67
  },
@@ -58,10 +75,4 @@ Default.args = {
58
75
  <ToggleGroupControlOption value="2" key="2" label="Option 2" />,
59
76
  ],
60
77
  help: 'Selecting option 2 will trigger an error.',
61
- customValidator: ( value ) => {
62
- if ( value === '2' ) {
63
- return 'Option 2 is not allowed.';
64
- }
65
- return undefined;
66
- },
67
78
  };
@@ -17,7 +17,8 @@ type Value = TextControlProps[ 'value' ];
17
17
  const UnforwardedValidatedTextControl = (
18
18
  {
19
19
  required,
20
- customValidator,
20
+ onValidate,
21
+ customValidity,
21
22
  onChange,
22
23
  markWhenOptional,
23
24
  ...restProps
@@ -36,9 +37,10 @@ const UnforwardedValidatedTextControl = (
36
37
  <ControlWithError
37
38
  required={ required }
38
39
  markWhenOptional={ markWhenOptional }
39
- customValidator={ () => {
40
- return customValidator?.( valueRef.current );
40
+ onValidate={ () => {
41
+ return onValidate?.( valueRef.current );
41
42
  } }
43
+ customValidity={ customValidity }
42
44
  getValidityTarget={ () => validityTargetRef.current }
43
45
  >
44
46
  <TextControl
@@ -17,7 +17,8 @@ type Value = TextareaControlProps[ 'value' ];
17
17
  const UnforwardedValidatedTextareaControl = (
18
18
  {
19
19
  required,
20
- customValidator,
20
+ onValidate,
21
+ customValidity,
21
22
  onChange,
22
23
  markWhenOptional,
23
24
  ...restProps
@@ -36,9 +37,10 @@ const UnforwardedValidatedTextareaControl = (
36
37
  <ControlWithError
37
38
  required={ required }
38
39
  markWhenOptional={ markWhenOptional }
39
- customValidator={ () => {
40
- return customValidator?.( valueRef.current );
40
+ onValidate={ () => {
41
+ return onValidate?.( valueRef.current );
41
42
  } }
43
+ customValidity={ customValidity }
42
44
  getValidityTarget={ () => validityTargetRef.current }
43
45
  >
44
46
  <TextareaControl
@@ -19,7 +19,8 @@ type Value = ToggleControlProps[ 'checked' ];
19
19
  const UnforwardedValidatedToggleControl = (
20
20
  {
21
21
  required,
22
- customValidator,
22
+ onValidate,
23
+ customValidity,
23
24
  onChange,
24
25
  markWhenOptional,
25
26
  ...restProps
@@ -46,9 +47,10 @@ const UnforwardedValidatedToggleControl = (
46
47
  <ControlWithError
47
48
  required={ required }
48
49
  markWhenOptional={ markWhenOptional }
49
- customValidator={ () => {
50
- return customValidator?.( valueRef.current );
50
+ onValidate={ () => {
51
+ return onValidate?.( valueRef.current );
51
52
  } }
53
+ customValidity={ customValidity }
52
54
  getValidityTarget={ () => validityTargetRef.current }
53
55
  >
54
56
  <ToggleControl
@@ -16,7 +16,8 @@ type Value = ToggleGroupControlProps[ 'value' ];
16
16
  const UnforwardedValidatedToggleGroupControl = (
17
17
  {
18
18
  required,
19
- customValidator,
19
+ onValidate,
20
+ customValidity,
20
21
  onChange,
21
22
  markWhenOptional,
22
23
  ...restProps
@@ -37,9 +38,10 @@ const UnforwardedValidatedToggleGroupControl = (
37
38
  <ControlWithError
38
39
  required={ required }
39
40
  markWhenOptional={ markWhenOptional }
40
- customValidator={ () => {
41
- return customValidator?.( valueRef.current );
41
+ onValidate={ () => {
42
+ return onValidate?.( valueRef.current );
42
43
  } }
44
+ customValidity={ customValidity }
43
45
  getValidityTarget={ () => validityTargetRef.current }
44
46
  >
45
47
  <ToggleGroupControl
@@ -10,19 +10,28 @@ export type ValidatedControlProps< V > = {
10
10
  */
11
11
  markWhenOptional?: boolean;
12
12
  /**
13
- * A function that returns a custom validity message when applicable. This error message will be applied to the
14
- * underlying element using the native [`setCustomValidity()` method](https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity).
15
- * This means the custom validator will be run _in addition_ to any other HTML attribute-based validation, and
16
- * will be prioritized over any existing validity messages dictated by the HTML attributes.
17
- * An empty string or `undefined` return value will clear any existing custom validity message.
13
+ * Optional callback to run when the input should be validated. Use this to set
14
+ * a `customValidity` as necessary.
18
15
  *
19
- * Make sure you don't programatically pass a value (such as an initial value) to the control component
20
- * that fails this validator, because the validator will only run for user-initiated changes.
16
+ * Always prefer using standard HTML attributes like `required` and `min`/`max` over
17
+ * custom validators when possible, as they are simpler and have localized error messages built in.
18
+ */
19
+ onValidate?: ( currentValue: V ) => void;
20
+ /**
21
+ * Show a custom message based on the validation status.
21
22
  *
22
- * Always prefer using standard HTML attributes like `required` and `min`/`max` over custom validators
23
- * when possible, as they are simpler and have localized error messages built in.
23
+ * - When `type` is `invalid`, the message will be applied to the underlying element using the
24
+ * native [`setCustomValidity()` method](https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity).
25
+ * This means the custom message will be prioritized over any existing validity messages
26
+ * triggered by HTML attribute-based validation.
27
+ * - When `type` is `validating` or `valid`, the custom validity message of the underlying element
28
+ * will be cleared. If there are no remaining validity messages triggered by HTML attribute-based validation,
29
+ * the message will be presented as a status indicator rather than an error. These indicators are intended
30
+ * for asynchronous validation calls that may take more than 1 second to complete.
31
+ * Otherwise, custom errors can simply be cleared by setting the `customValidity` prop to `undefined`.
24
32
  */
25
- // TODO: Technically, we could add an optional `customValidity` string prop so the consumer can set
26
- // an error message at any point in time. We should wait until we have a use case though.
27
- customValidator?: ( currentValue: V ) => string | void;
33
+ customValidity?: {
34
+ type: 'validating' | 'valid' | 'invalid';
35
+ message: string;
36
+ };
28
37
  };