@wordpress/components 30.1.0 → 30.2.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 (211) hide show
  1. package/CHANGELOG.md +19 -3
  2. package/build/custom-gradient-picker/serializer.js +14 -0
  3. package/build/custom-gradient-picker/serializer.js.map +1 -1
  4. package/build/custom-gradient-picker/utils.js +12 -0
  5. package/build/custom-gradient-picker/utils.js.map +1 -1
  6. package/build/date-time/date/index.js +2 -4
  7. package/build/date-time/date/index.js.map +1 -1
  8. package/build/date-time/date/styles.js +41 -20
  9. package/build/date-time/date/styles.js.map +1 -1
  10. package/build/menu-item/index.js +1 -0
  11. package/build/menu-item/index.js.map +1 -1
  12. package/build/validated-form-controls/components/checkbox-control.js +5 -3
  13. package/build/validated-form-controls/components/checkbox-control.js.map +1 -1
  14. package/build/validated-form-controls/components/combobox-control.js +5 -3
  15. package/build/validated-form-controls/components/combobox-control.js.map +1 -1
  16. package/build/validated-form-controls/components/custom-select-control.js +5 -3
  17. package/build/validated-form-controls/components/custom-select-control.js.map +1 -1
  18. package/build/validated-form-controls/components/input-control.js +5 -3
  19. package/build/validated-form-controls/components/input-control.js.map +1 -1
  20. package/build/validated-form-controls/components/number-control.js +5 -3
  21. package/build/validated-form-controls/components/number-control.js.map +1 -1
  22. package/build/validated-form-controls/components/radio-control.js +5 -3
  23. package/build/validated-form-controls/components/radio-control.js.map +1 -1
  24. package/build/validated-form-controls/components/range-control.js +5 -3
  25. package/build/validated-form-controls/components/range-control.js.map +1 -1
  26. package/build/validated-form-controls/components/select-control.js +5 -3
  27. package/build/validated-form-controls/components/select-control.js.map +1 -1
  28. package/build/validated-form-controls/components/text-control.js +5 -3
  29. package/build/validated-form-controls/components/text-control.js.map +1 -1
  30. package/build/validated-form-controls/components/textarea-control.js +5 -3
  31. package/build/validated-form-controls/components/textarea-control.js.map +1 -1
  32. package/build/validated-form-controls/components/toggle-control.js +5 -3
  33. package/build/validated-form-controls/components/toggle-control.js.map +1 -1
  34. package/build/validated-form-controls/components/toggle-group-control.js +5 -3
  35. package/build/validated-form-controls/components/toggle-group-control.js.map +1 -1
  36. package/build/validated-form-controls/components/types.js.map +1 -1
  37. package/build/validated-form-controls/control-with-error.js +57 -22
  38. package/build/validated-form-controls/control-with-error.js.map +1 -1
  39. package/build/validated-form-controls/validity-indicator.js +45 -0
  40. package/build/validated-form-controls/validity-indicator.js.map +1 -0
  41. package/build-module/custom-gradient-picker/serializer.js +14 -0
  42. package/build-module/custom-gradient-picker/serializer.js.map +1 -1
  43. package/build-module/custom-gradient-picker/utils.js +12 -0
  44. package/build-module/custom-gradient-picker/utils.js.map +1 -1
  45. package/build-module/date-time/date/index.js +3 -4
  46. package/build-module/date-time/date/index.js.map +1 -1
  47. package/build-module/date-time/date/styles.js +39 -14
  48. package/build-module/date-time/date/styles.js.map +1 -1
  49. package/build-module/menu-item/index.js +1 -0
  50. package/build-module/menu-item/index.js.map +1 -1
  51. package/build-module/validated-form-controls/components/checkbox-control.js +5 -3
  52. package/build-module/validated-form-controls/components/checkbox-control.js.map +1 -1
  53. package/build-module/validated-form-controls/components/combobox-control.js +5 -3
  54. package/build-module/validated-form-controls/components/combobox-control.js.map +1 -1
  55. package/build-module/validated-form-controls/components/custom-select-control.js +5 -3
  56. package/build-module/validated-form-controls/components/custom-select-control.js.map +1 -1
  57. package/build-module/validated-form-controls/components/input-control.js +5 -3
  58. package/build-module/validated-form-controls/components/input-control.js.map +1 -1
  59. package/build-module/validated-form-controls/components/number-control.js +5 -3
  60. package/build-module/validated-form-controls/components/number-control.js.map +1 -1
  61. package/build-module/validated-form-controls/components/radio-control.js +5 -3
  62. package/build-module/validated-form-controls/components/radio-control.js.map +1 -1
  63. package/build-module/validated-form-controls/components/range-control.js +5 -3
  64. package/build-module/validated-form-controls/components/range-control.js.map +1 -1
  65. package/build-module/validated-form-controls/components/select-control.js +5 -3
  66. package/build-module/validated-form-controls/components/select-control.js.map +1 -1
  67. package/build-module/validated-form-controls/components/text-control.js +5 -3
  68. package/build-module/validated-form-controls/components/text-control.js.map +1 -1
  69. package/build-module/validated-form-controls/components/textarea-control.js +5 -3
  70. package/build-module/validated-form-controls/components/textarea-control.js.map +1 -1
  71. package/build-module/validated-form-controls/components/toggle-control.js +5 -3
  72. package/build-module/validated-form-controls/components/toggle-control.js.map +1 -1
  73. package/build-module/validated-form-controls/components/toggle-group-control.js +5 -3
  74. package/build-module/validated-form-controls/components/toggle-group-control.js.map +1 -1
  75. package/build-module/validated-form-controls/components/types.js.map +1 -1
  76. package/build-module/validated-form-controls/control-with-error.js +57 -21
  77. package/build-module/validated-form-controls/control-with-error.js.map +1 -1
  78. package/build-module/validated-form-controls/validity-indicator.js +37 -0
  79. package/build-module/validated-form-controls/validity-indicator.js.map +1 -0
  80. package/build-style/style-rtl.css +37 -25
  81. package/build-style/style.css +37 -25
  82. package/build-types/confirm-dialog/stories/index.story.d.ts.map +1 -1
  83. package/build-types/custom-gradient-picker/serializer.d.ts.map +1 -1
  84. package/build-types/custom-gradient-picker/utils.d.ts.map +1 -1
  85. package/build-types/date-time/date/index.d.ts.map +1 -1
  86. package/build-types/date-time/date/styles.d.ts +6 -0
  87. package/build-types/date-time/date/styles.d.ts.map +1 -1
  88. package/build-types/divider/stories/index.story.d.ts.map +1 -1
  89. package/build-types/elevation/stories/index.story.d.ts.map +1 -1
  90. package/build-types/form-token-field/stories/index.story.d.ts +10 -5
  91. package/build-types/form-token-field/stories/index.story.d.ts.map +1 -1
  92. package/build-types/gradient-picker/stories/index.story.d.ts +2 -1
  93. package/build-types/gradient-picker/stories/index.story.d.ts.map +1 -1
  94. package/build-types/grid/stories/index.story.d.ts.map +1 -1
  95. package/build-types/h-stack/stories/index.story.d.ts.map +1 -1
  96. package/build-types/heading/stories/index.story.d.ts.map +1 -1
  97. package/build-types/input-control/stories/index.story.d.ts.map +1 -1
  98. package/build-types/item-group/stories/index.story.d.ts.map +1 -1
  99. package/build-types/menu-item/index.d.ts.map +1 -1
  100. package/build-types/number-control/stories/index.story.d.ts.map +1 -1
  101. package/build-types/scrollable/stories/index.story.d.ts.map +1 -1
  102. package/build-types/spacer/stories/index.story.d.ts.map +1 -1
  103. package/build-types/surface/stories/index.story.d.ts.map +1 -1
  104. package/build-types/text/stories/index.story.d.ts.map +1 -1
  105. package/build-types/toggle-group-control/stories/index.story.d.ts.map +1 -1
  106. package/build-types/tools-panel/stories/index.story.d.ts.map +1 -1
  107. package/build-types/tree-grid/stories/index.story.d.ts.map +1 -1
  108. package/build-types/truncate/stories/index.story.d.ts.map +1 -1
  109. package/build-types/unit-control/stories/index.story.d.ts.map +1 -1
  110. package/build-types/v-stack/stories/index.story.d.ts.map +1 -1
  111. package/build-types/validated-form-controls/components/checkbox-control.d.ts.map +1 -1
  112. package/build-types/validated-form-controls/components/combobox-control.d.ts.map +1 -1
  113. package/build-types/validated-form-controls/components/custom-select-control.d.ts.map +1 -1
  114. package/build-types/validated-form-controls/components/input-control.d.ts.map +1 -1
  115. package/build-types/validated-form-controls/components/number-control.d.ts.map +1 -1
  116. package/build-types/validated-form-controls/components/radio-control.d.ts.map +1 -1
  117. package/build-types/validated-form-controls/components/range-control.d.ts.map +1 -1
  118. package/build-types/validated-form-controls/components/select-control.d.ts.map +1 -1
  119. package/build-types/validated-form-controls/components/stories/checkbox-control.story.d.ts.map +1 -1
  120. package/build-types/validated-form-controls/components/stories/combobox-control.story.d.ts.map +1 -1
  121. package/build-types/validated-form-controls/components/stories/custom-select-control.story.d.ts.map +1 -1
  122. package/build-types/validated-form-controls/components/stories/input-control.story.d.ts.map +1 -1
  123. package/build-types/validated-form-controls/components/stories/number-control.story.d.ts.map +1 -1
  124. package/build-types/validated-form-controls/components/stories/overview.story.d.ts +13 -0
  125. package/build-types/validated-form-controls/components/stories/overview.story.d.ts.map +1 -1
  126. package/build-types/validated-form-controls/components/stories/radio-control.story.d.ts.map +1 -1
  127. package/build-types/validated-form-controls/components/stories/range-control.story.d.ts.map +1 -1
  128. package/build-types/validated-form-controls/components/stories/select-control.story.d.ts.map +1 -1
  129. package/build-types/validated-form-controls/components/stories/text-control.story.d.ts.map +1 -1
  130. package/build-types/validated-form-controls/components/stories/textarea-control.story.d.ts.map +1 -1
  131. package/build-types/validated-form-controls/components/stories/toggle-control.story.d.ts.map +1 -1
  132. package/build-types/validated-form-controls/components/stories/toggle-group-control.story.d.ts.map +1 -1
  133. package/build-types/validated-form-controls/components/text-control.d.ts.map +1 -1
  134. package/build-types/validated-form-controls/components/textarea-control.d.ts.map +1 -1
  135. package/build-types/validated-form-controls/components/toggle-control.d.ts.map +1 -1
  136. package/build-types/validated-form-controls/components/toggle-group-control.d.ts.map +1 -1
  137. package/build-types/validated-form-controls/components/types.d.ts +21 -10
  138. package/build-types/validated-form-controls/components/types.d.ts.map +1 -1
  139. package/build-types/validated-form-controls/control-with-error.d.ts +4 -5
  140. package/build-types/validated-form-controls/control-with-error.d.ts.map +1 -1
  141. package/build-types/validated-form-controls/validity-indicator.d.ts +5 -0
  142. package/build-types/validated-form-controls/validity-indicator.d.ts.map +1 -0
  143. package/build-types/view/stories/index.story.d.ts.map +1 -1
  144. package/build-types/z-stack/stories/index.story.d.ts.map +1 -1
  145. package/package.json +21 -21
  146. package/src/button/style.scss +3 -3
  147. package/src/calendar/style.scss +22 -22
  148. package/src/confirm-dialog/stories/index.story.tsx +3 -2
  149. package/src/custom-gradient-picker/serializer.ts +14 -0
  150. package/src/custom-gradient-picker/test/serializer.ts +25 -0
  151. package/src/custom-gradient-picker/utils.ts +10 -0
  152. package/src/date-time/date/index.tsx +4 -3
  153. package/src/date-time/date/styles.ts +13 -20
  154. package/src/divider/stories/index.story.tsx +2 -1
  155. package/src/elevation/stories/index.story.tsx +2 -1
  156. package/src/form-token-field/stories/index.story.tsx +15 -7
  157. package/src/gradient-picker/stories/index.story.tsx +48 -0
  158. package/src/grid/stories/index.story.tsx +2 -1
  159. package/src/h-stack/stories/e2e/index.story.tsx +1 -1
  160. package/src/h-stack/stories/index.story.tsx +3 -2
  161. package/src/heading/stories/index.story.tsx +3 -2
  162. package/src/input-control/stories/index.story.tsx +3 -2
  163. package/src/item-group/stories/index.story.tsx +2 -1
  164. package/src/menu/stories/index.story.tsx +1 -1
  165. package/src/menu-item/index.tsx +1 -0
  166. package/src/number-control/stories/index.story.tsx +3 -2
  167. package/src/scrollable/stories/index.story.tsx +2 -1
  168. package/src/spacer/stories/index.story.tsx +2 -1
  169. package/src/surface/stories/index.story.tsx +2 -1
  170. package/src/text/stories/index.story.tsx +3 -2
  171. package/src/toggle-group-control/stories/index.story.tsx +3 -2
  172. package/src/tools-panel/stories/index.story.tsx +2 -1
  173. package/src/tree-grid/stories/index.story.tsx +3 -2
  174. package/src/truncate/stories/index.story.tsx +3 -2
  175. package/src/unit-control/stories/index.story.tsx +3 -2
  176. package/src/utils/theme-variables.scss +1 -0
  177. package/src/v-stack/stories/e2e/index.story.tsx +1 -1
  178. package/src/v-stack/stories/index.story.tsx +3 -2
  179. package/src/validated-form-controls/components/checkbox-control.tsx +5 -3
  180. package/src/validated-form-controls/components/combobox-control.tsx +5 -3
  181. package/src/validated-form-controls/components/custom-select-control.tsx +5 -3
  182. package/src/validated-form-controls/components/input-control.tsx +5 -3
  183. package/src/validated-form-controls/components/number-control.tsx +5 -3
  184. package/src/validated-form-controls/components/radio-control.tsx +5 -3
  185. package/src/validated-form-controls/components/range-control.tsx +5 -3
  186. package/src/validated-form-controls/components/select-control.tsx +5 -3
  187. package/src/validated-form-controls/components/stories/checkbox-control.story.tsx +19 -7
  188. package/src/validated-form-controls/components/stories/combobox-control.story.tsx +19 -7
  189. package/src/validated-form-controls/components/stories/custom-select-control.story.tsx +19 -7
  190. package/src/validated-form-controls/components/stories/input-control.story.tsx +53 -19
  191. package/src/validated-form-controls/components/stories/number-control.story.tsx +19 -7
  192. package/src/validated-form-controls/components/stories/overview.mdx +2 -2
  193. package/src/validated-form-controls/components/stories/overview.story.tsx +124 -16
  194. package/src/validated-form-controls/components/stories/radio-control.story.tsx +19 -7
  195. package/src/validated-form-controls/components/stories/range-control.story.tsx +19 -7
  196. package/src/validated-form-controls/components/stories/select-control.story.tsx +19 -7
  197. package/src/validated-form-controls/components/stories/text-control.story.tsx +19 -7
  198. package/src/validated-form-controls/components/stories/textarea-control.story.tsx +19 -7
  199. package/src/validated-form-controls/components/stories/toggle-control.story.tsx +19 -7
  200. package/src/validated-form-controls/components/stories/toggle-group-control.story.tsx +19 -7
  201. package/src/validated-form-controls/components/text-control.tsx +5 -3
  202. package/src/validated-form-controls/components/textarea-control.tsx +5 -3
  203. package/src/validated-form-controls/components/toggle-control.tsx +5 -3
  204. package/src/validated-form-controls/components/toggle-group-control.tsx +5 -3
  205. package/src/validated-form-controls/components/types.ts +21 -12
  206. package/src/validated-form-controls/control-with-error.tsx +77 -26
  207. package/src/validated-form-controls/style.scss +19 -5
  208. package/src/validated-form-controls/validity-indicator.tsx +48 -0
  209. package/src/view/stories/index.story.tsx +2 -1
  210. package/src/z-stack/stories/index.story.tsx +2 -1
  211. package/tsconfig.tsbuildinfo +1 -1
@@ -15,7 +15,8 @@ import { ValidatedCheckboxControl } from '../checkbox-control';
15
15
  import { formDecorator } from './story-utils';
16
16
 
17
17
  const meta: Meta< typeof ValidatedCheckboxControl > = {
18
- title: 'Components (Experimental)/Validated Form Controls/ValidatedCheckboxControl',
18
+ title: 'Components/Selection & Input/Validated Form Controls/ValidatedCheckboxControl',
19
+ id: 'components-validatedcheckboxcontrol',
19
20
  component: ValidatedCheckboxControl,
20
21
  tags: [ 'status-private' ],
21
22
  decorators: formDecorator,
@@ -31,6 +32,12 @@ export default meta;
31
32
  export const Default: StoryObj< typeof ValidatedCheckboxControl > = {
32
33
  render: function Template( { onChange, ...args } ) {
33
34
  const [ checked, setChecked ] = useState( false );
35
+ const [ customValidity, setCustomValidity ] =
36
+ useState<
37
+ React.ComponentProps<
38
+ typeof ValidatedCheckboxControl
39
+ >[ 'customValidity' ]
40
+ >( undefined );
34
41
 
35
42
  return (
36
43
  <ValidatedCheckboxControl
@@ -40,6 +47,17 @@ export const Default: StoryObj< typeof ValidatedCheckboxControl > = {
40
47
  setChecked( value );
41
48
  onChange?.( value );
42
49
  } }
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 }
43
61
  />
44
62
  );
45
63
  },
@@ -48,10 +66,4 @@ Default.args = {
48
66
  required: true,
49
67
  label: 'Checkbox',
50
68
  help: 'This checkbox may neither be checked nor unchecked.',
51
- customValidator: ( value ) => {
52
- if ( value ) {
53
- return 'This checkbox may not be checked.';
54
- }
55
- return undefined;
56
- },
57
69
  };
@@ -15,7 +15,8 @@ import { ValidatedComboboxControl } from '../combobox-control';
15
15
  import { formDecorator } from './story-utils';
16
16
 
17
17
  const meta: Meta< typeof ValidatedComboboxControl > = {
18
- title: 'Components (Experimental)/Validated Form Controls/ValidatedComboboxControl',
18
+ title: 'Components/Selection & Input/Validated Form Controls/ValidatedComboboxControl',
19
+ id: 'components-validatedcomboboxcontrol',
19
20
  component: ValidatedComboboxControl,
20
21
  tags: [ 'status-private' ],
21
22
  decorators: formDecorator,
@@ -34,6 +35,12 @@ export const Default: StoryObj< typeof ValidatedComboboxControl > = {
34
35
  typeof ValidatedComboboxControl
35
36
  >[ 'value' ]
36
37
  >();
38
+ const [ customValidity, setCustomValidity ] =
39
+ useState<
40
+ React.ComponentProps<
41
+ typeof ValidatedComboboxControl
42
+ >[ 'customValidity' ]
43
+ >( undefined );
37
44
 
38
45
  return (
39
46
  <ValidatedComboboxControl
@@ -43,6 +50,17 @@ export const Default: StoryObj< typeof ValidatedComboboxControl > = {
43
50
  setValue( newValue );
44
51
  onChange?.( newValue );
45
52
  } }
53
+ onValidate={ ( v ) => {
54
+ if ( v === 'a' ) {
55
+ setCustomValidity( {
56
+ type: 'invalid',
57
+ message: 'Option A is not allowed.',
58
+ } );
59
+ } else {
60
+ setCustomValidity( undefined );
61
+ }
62
+ } }
63
+ customValidity={ customValidity }
46
64
  />
47
65
  );
48
66
  },
@@ -55,10 +73,4 @@ Default.args = {
55
73
  { value: 'a', label: 'Option A (not allowed)' },
56
74
  { value: 'b', label: 'Option B' },
57
75
  ],
58
- customValidator: ( value ) => {
59
- if ( value === 'a' ) {
60
- return 'Option A is not allowed.';
61
- }
62
- return undefined;
63
- },
64
76
  };
@@ -15,7 +15,8 @@ import { ValidatedCustomSelectControl } from '../custom-select-control';
15
15
  import { formDecorator } from './story-utils';
16
16
 
17
17
  const meta: Meta< typeof ValidatedCustomSelectControl > = {
18
- title: 'Components (Experimental)/Validated Form Controls/ValidatedCustomSelectControl',
18
+ title: 'Components/Selection & Input/Validated Form Controls/ValidatedCustomSelectControl',
19
+ id: 'components-validatedcustomselectcontrol',
19
20
  component: ValidatedCustomSelectControl,
20
21
  tags: [ 'status-private' ],
21
22
  decorators: formDecorator,
@@ -34,6 +35,12 @@ export const Default: StoryObj< typeof ValidatedCustomSelectControl > = {
34
35
  typeof ValidatedCustomSelectControl
35
36
  >[ 'value' ]
36
37
  >();
38
+ const [ customValidity, setCustomValidity ] =
39
+ useState<
40
+ React.ComponentProps<
41
+ typeof ValidatedCustomSelectControl
42
+ >[ 'customValidity' ]
43
+ >( undefined );
37
44
 
38
45
  return (
39
46
  <ValidatedCustomSelectControl
@@ -43,6 +50,17 @@ export const Default: StoryObj< typeof ValidatedCustomSelectControl > = {
43
50
  setValue( newValue.selectedItem );
44
51
  onChange?.( newValue );
45
52
  } }
53
+ onValidate={ ( v ) => {
54
+ if ( v?.key === 'a' ) {
55
+ setCustomValidity( {
56
+ type: 'invalid',
57
+ message: 'Option A is not allowed.',
58
+ } );
59
+ } else {
60
+ setCustomValidity( undefined );
61
+ }
62
+ } }
63
+ customValidity={ customValidity }
46
64
  />
47
65
  );
48
66
  },
@@ -55,10 +73,4 @@ Default.args = {
55
73
  { key: 'a', name: 'Option A (not allowed)' },
56
74
  { key: 'b', name: 'Option B' },
57
75
  ],
58
- customValidator: ( value ) => {
59
- if ( value?.key === 'a' ) {
60
- return 'Option A is not allowed.';
61
- }
62
- return undefined;
63
- },
64
76
  };
@@ -22,7 +22,8 @@ import InputControlSuffixWrapper from '../../../input-control/input-suffix-wrapp
22
22
  import { Button } from '../../../button';
23
23
 
24
24
  const meta: Meta< typeof ValidatedInputControl > = {
25
- title: 'Components (Experimental)/Validated Form Controls/ValidatedInputControl',
25
+ title: 'Components/Selection & Input/Validated Form Controls/ValidatedInputControl',
26
+ id: 'components-validatedinputcontrol',
26
27
  component: ValidatedInputControl,
27
28
  tags: [ 'status-private' ],
28
29
  decorators: formDecorator,
@@ -45,6 +46,12 @@ export const Default: StoryObj< typeof ValidatedInputControl > = {
45
46
  useState<
46
47
  React.ComponentProps< typeof ValidatedInputControl >[ 'value' ]
47
48
  >( '' );
49
+ const [ customValidity, setCustomValidity ] =
50
+ useState<
51
+ React.ComponentProps<
52
+ typeof ValidatedInputControl
53
+ >[ 'customValidity' ]
54
+ >( undefined );
48
55
 
49
56
  return (
50
57
  <ValidatedInputControl
@@ -54,6 +61,17 @@ export const Default: StoryObj< typeof ValidatedInputControl > = {
54
61
  setValue( newValue );
55
62
  onChange?.( newValue, ...rest );
56
63
  } }
64
+ onValidate={ ( v ) => {
65
+ if ( v?.toLowerCase() === 'error' ) {
66
+ setCustomValidity( {
67
+ type: 'invalid',
68
+ message: 'The word "error" is not allowed.',
69
+ } );
70
+ } else {
71
+ setCustomValidity( undefined );
72
+ }
73
+ } }
74
+ customValidity={ customValidity }
57
75
  />
58
76
  );
59
77
  },
@@ -62,12 +80,6 @@ Default.args = {
62
80
  required: true,
63
81
  label: 'Input',
64
82
  help: 'The word "error" will trigger an error.',
65
- customValidator: ( value ) => {
66
- if ( value?.toLowerCase() === 'error' ) {
67
- return 'The word "error" is not allowed.';
68
- }
69
- return undefined;
70
- },
71
83
  };
72
84
 
73
85
  /**
@@ -82,6 +94,12 @@ export const Password: StoryObj< typeof ValidatedInputControl > = {
82
94
  React.ComponentProps< typeof ValidatedInputControl >[ 'value' ]
83
95
  >( '' );
84
96
  const [ visible, setVisible ] = useState( false );
97
+ const [ customValidity, setCustomValidity ] =
98
+ useState<
99
+ React.ComponentProps<
100
+ typeof ValidatedInputControl
101
+ >[ 'customValidity' ]
102
+ >( undefined );
85
103
 
86
104
  return (
87
105
  <ValidatedInputControl
@@ -104,6 +122,34 @@ export const Password: StoryObj< typeof ValidatedInputControl > = {
104
122
  setValue( newValue );
105
123
  onChange?.( newValue, ...rest );
106
124
  } }
125
+ onValidate={ ( v ) => {
126
+ if ( ! /\d/.test( v ?? '' ) ) {
127
+ setCustomValidity( {
128
+ type: 'invalid',
129
+ message:
130
+ 'Password must include at least one number.',
131
+ } );
132
+ return;
133
+ }
134
+ if ( ! /[A-Z]/.test( v ?? '' ) ) {
135
+ setCustomValidity( {
136
+ type: 'invalid',
137
+ message:
138
+ 'Password must include at least one capital letter.',
139
+ } );
140
+ return;
141
+ }
142
+ if ( ! /[!@£$%^&*#]/.test( v ?? '' ) ) {
143
+ setCustomValidity( {
144
+ type: 'invalid',
145
+ message:
146
+ 'Password must include at least one symbol.',
147
+ } );
148
+ return;
149
+ }
150
+ setCustomValidity( undefined );
151
+ } }
152
+ customValidity={ customValidity }
107
153
  />
108
154
  );
109
155
  },
@@ -113,18 +159,6 @@ Password.args = {
113
159
  label: 'Password',
114
160
  help: 'Minimum 8 characters, include a number, capital letter, and symbol (!@£$%^&*#).',
115
161
  minLength: 8,
116
- customValidator: ( value ) => {
117
- if ( ! /\d/.test( value ?? '' ) ) {
118
- return 'Password must include at least one number.';
119
- }
120
- if ( ! /[A-Z]/.test( value ?? '' ) ) {
121
- return 'Password must include at least one capital letter.';
122
- }
123
- if ( ! /[!@£$%^&*#]/.test( value ?? '' ) ) {
124
- return 'Password must include at least one symbol.';
125
- }
126
- return undefined;
127
- },
128
162
  };
129
163
  Password.argTypes = {
130
164
  suffix: { control: false },
@@ -15,7 +15,8 @@ import { ValidatedNumberControl } from '../number-control';
15
15
  import { formDecorator } from './story-utils';
16
16
 
17
17
  const meta: Meta< typeof ValidatedNumberControl > = {
18
- title: 'Components (Experimental)/Validated Form Controls/ValidatedNumberControl',
18
+ title: 'Components/Selection & Input/Validated Form Controls/ValidatedNumberControl',
19
+ id: 'components-validatednumbercontrol',
19
20
  component: ValidatedNumberControl,
20
21
  tags: [ 'status-private' ],
21
22
  decorators: formDecorator,
@@ -36,6 +37,12 @@ export const Default: StoryObj< typeof ValidatedNumberControl > = {
36
37
  useState<
37
38
  React.ComponentProps< typeof ValidatedNumberControl >[ 'value' ]
38
39
  >();
40
+ const [ customValidity, setCustomValidity ] =
41
+ useState<
42
+ React.ComponentProps<
43
+ typeof ValidatedNumberControl
44
+ >[ 'customValidity' ]
45
+ >( undefined );
39
46
 
40
47
  return (
41
48
  <ValidatedNumberControl
@@ -45,6 +52,17 @@ export const Default: StoryObj< typeof ValidatedNumberControl > = {
45
52
  setValue( newValue );
46
53
  onChange?.( newValue, ...rest );
47
54
  } }
55
+ onValidate={ ( v ) => {
56
+ if ( v && parseInt( v.toString(), 10 ) % 2 !== 0 ) {
57
+ setCustomValidity( {
58
+ type: 'invalid',
59
+ message: 'Choose an even number.',
60
+ } );
61
+ } else {
62
+ setCustomValidity( undefined );
63
+ }
64
+ } }
65
+ customValidity={ customValidity }
48
66
  />
49
67
  );
50
68
  },
@@ -53,10 +71,4 @@ Default.args = {
53
71
  required: true,
54
72
  label: 'Number',
55
73
  help: 'Odd numbers are not allowed.',
56
- customValidator: ( value ) => {
57
- if ( value && parseInt( value.toString(), 10 ) % 2 !== 0 ) {
58
- return 'Choose an even number.';
59
- }
60
- return undefined;
61
- },
62
74
  };
@@ -2,7 +2,7 @@ import { ArgTypes, Meta, Stories } from '@storybook/blocks';
2
2
  import * as OverviewStories from './overview.story';
3
3
  import { ValidatedInputControl } from '..';
4
4
 
5
- <Meta of={ OverviewStories } title="Components (Private)/Validated Form Controls/Overview" />
5
+ <Meta of={ OverviewStories } title="Components/Selection & Input/Validated Form Controls/Overview" />
6
6
 
7
7
  # Validated Form Controls
8
8
 
@@ -16,7 +16,7 @@ We are still gathering feedback and iterating. Please get in touch with `@WordPr
16
16
 
17
17
  Component APIs are the same as the underlying WordPress components, with the addition of some optional props:
18
18
 
19
- <ArgTypes of={ ValidatedInputControl } include={ [ 'required', 'markWhenOptional', 'customValidator' ] } />
19
+ <ArgTypes of={ ValidatedInputControl } include={ [ 'required', 'markWhenOptional', 'onValidate', 'customValidity' ] } />
20
20
 
21
21
  ## Implementation
22
22
 
@@ -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,10 +14,11 @@ 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
- title: 'Components (Experimental)/Validated Form Controls/Overview',
20
- tags: [ 'status-private' ],
20
+ title: 'Components/Selection & Input/Validated Form Controls/Overview',
21
+ id: 'components-validated-form-controls-overview',
21
22
  decorators: formDecorator,
22
23
  };
23
24
  export default meta;
@@ -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
+ };
@@ -15,7 +15,8 @@ import { ValidatedRadioControl } from '../radio-control';
15
15
  import { formDecorator } from './story-utils';
16
16
 
17
17
  const meta: Meta< typeof ValidatedRadioControl > = {
18
- title: 'Components (Experimental)/Validated Form Controls/ValidatedRadioControl',
18
+ title: 'Components/Selection & Input/Validated Form Controls/ValidatedRadioControl',
19
+ id: 'components-validatedradiocontrol',
19
20
  component: ValidatedRadioControl,
20
21
  tags: [ 'status-private' ],
21
22
  decorators: formDecorator,
@@ -34,6 +35,12 @@ export const Default: StoryObj< typeof ValidatedRadioControl > = {
34
35
  typeof ValidatedRadioControl
35
36
  >[ 'selected' ]
36
37
  >();
38
+ const [ customValidity, setCustomValidity ] =
39
+ useState<
40
+ React.ComponentProps<
41
+ typeof ValidatedRadioControl
42
+ >[ 'customValidity' ]
43
+ >( undefined );
37
44
 
38
45
  return (
39
46
  <ValidatedRadioControl
@@ -43,6 +50,17 @@ export const Default: StoryObj< typeof ValidatedRadioControl > = {
43
50
  setSelected( value );
44
51
  onChange?.( value );
45
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 }
46
64
  />
47
65
  );
48
66
  },
@@ -55,10 +73,4 @@ Default.args = {
55
73
  { label: 'Option A', value: 'a' },
56
74
  { label: 'Option B (not allowed)', value: 'b' },
57
75
  ],
58
- customValidator: ( value ) => {
59
- if ( value === 'b' ) {
60
- return 'Option B is not allowed.';
61
- }
62
- return undefined;
63
- },
64
76
  };
@@ -15,7 +15,8 @@ import { formDecorator } from './story-utils';
15
15
  import { ValidatedRangeControl } from '../range-control';
16
16
 
17
17
  const meta: Meta< typeof ValidatedRangeControl > = {
18
- title: 'Components (Experimental)/Validated Form Controls/ValidatedRangeControl',
18
+ title: 'Components/Selection & Input/Validated Form Controls/ValidatedRangeControl',
19
+ id: 'components-validatedrangecontrol',
19
20
  component: ValidatedRangeControl,
20
21
  tags: [ 'status-private' ],
21
22
  decorators: formDecorator,
@@ -32,6 +33,12 @@ export const Default: StoryObj< typeof ValidatedRangeControl > = {
32
33
  useState<
33
34
  React.ComponentProps< typeof ValidatedRangeControl >[ 'value' ]
34
35
  >();
36
+ const [ customValidity, setCustomValidity ] =
37
+ useState<
38
+ React.ComponentProps<
39
+ typeof ValidatedRangeControl
40
+ >[ 'customValidity' ]
41
+ >( undefined );
35
42
 
36
43
  return (
37
44
  <ValidatedRangeControl
@@ -41,6 +48,17 @@ export const Default: StoryObj< typeof ValidatedRangeControl > = {
41
48
  setValue( newValue );
42
49
  onChange?.( newValue );
43
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 }
44
62
  />
45
63
  );
46
64
  },
@@ -51,10 +69,4 @@ Default.args = {
51
69
  help: 'Odd numbers are not allowed.',
52
70
  min: 0,
53
71
  max: 20,
54
- customValidator: ( value ) => {
55
- if ( value && value % 2 !== 0 ) {
56
- return 'Choose an even number.';
57
- }
58
- return undefined;
59
- },
60
72
  };