@wordpress/components 30.2.1-next.0f6f9d12c.0 → 30.2.2-next.e256d081a.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 (158) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/build/context/context-connect.js.map +1 -1
  3. package/build/menu-item/index.js +1 -0
  4. package/build/menu-item/index.js.map +1 -1
  5. package/build/tabs/styles.js +5 -5
  6. package/build/tabs/styles.js.map +1 -1
  7. package/build/utils/font-size.js.map +1 -1
  8. package/build/utils/get-valid-children.js.map +1 -1
  9. package/build/validated-form-controls/components/checkbox-control.js +5 -3
  10. package/build/validated-form-controls/components/checkbox-control.js.map +1 -1
  11. package/build/validated-form-controls/components/combobox-control.js +5 -3
  12. package/build/validated-form-controls/components/combobox-control.js.map +1 -1
  13. package/build/validated-form-controls/components/custom-select-control.js +5 -3
  14. package/build/validated-form-controls/components/custom-select-control.js.map +1 -1
  15. package/build/validated-form-controls/components/input-control.js +5 -3
  16. package/build/validated-form-controls/components/input-control.js.map +1 -1
  17. package/build/validated-form-controls/components/number-control.js +5 -3
  18. package/build/validated-form-controls/components/number-control.js.map +1 -1
  19. package/build/validated-form-controls/components/radio-control.js +5 -3
  20. package/build/validated-form-controls/components/radio-control.js.map +1 -1
  21. package/build/validated-form-controls/components/range-control.js +5 -3
  22. package/build/validated-form-controls/components/range-control.js.map +1 -1
  23. package/build/validated-form-controls/components/select-control.js +5 -3
  24. package/build/validated-form-controls/components/select-control.js.map +1 -1
  25. package/build/validated-form-controls/components/text-control.js +5 -3
  26. package/build/validated-form-controls/components/text-control.js.map +1 -1
  27. package/build/validated-form-controls/components/textarea-control.js +5 -3
  28. package/build/validated-form-controls/components/textarea-control.js.map +1 -1
  29. package/build/validated-form-controls/components/toggle-control.js +5 -3
  30. package/build/validated-form-controls/components/toggle-control.js.map +1 -1
  31. package/build/validated-form-controls/components/toggle-group-control.js +5 -3
  32. package/build/validated-form-controls/components/toggle-group-control.js.map +1 -1
  33. package/build/validated-form-controls/components/types.js.map +1 -1
  34. package/build/validated-form-controls/control-with-error.js +70 -35
  35. package/build/validated-form-controls/control-with-error.js.map +1 -1
  36. package/build/validated-form-controls/validity-indicator.js +45 -0
  37. package/build/validated-form-controls/validity-indicator.js.map +1 -0
  38. package/build-module/context/context-connect.js.map +1 -1
  39. package/build-module/menu-item/index.js +1 -0
  40. package/build-module/menu-item/index.js.map +1 -1
  41. package/build-module/tabs/styles.js +6 -6
  42. package/build-module/tabs/styles.js.map +1 -1
  43. package/build-module/utils/font-size.js.map +1 -1
  44. package/build-module/utils/get-valid-children.js.map +1 -1
  45. package/build-module/validated-form-controls/components/checkbox-control.js +5 -3
  46. package/build-module/validated-form-controls/components/checkbox-control.js.map +1 -1
  47. package/build-module/validated-form-controls/components/combobox-control.js +5 -3
  48. package/build-module/validated-form-controls/components/combobox-control.js.map +1 -1
  49. package/build-module/validated-form-controls/components/custom-select-control.js +5 -3
  50. package/build-module/validated-form-controls/components/custom-select-control.js.map +1 -1
  51. package/build-module/validated-form-controls/components/input-control.js +5 -3
  52. package/build-module/validated-form-controls/components/input-control.js.map +1 -1
  53. package/build-module/validated-form-controls/components/number-control.js +5 -3
  54. package/build-module/validated-form-controls/components/number-control.js.map +1 -1
  55. package/build-module/validated-form-controls/components/radio-control.js +5 -3
  56. package/build-module/validated-form-controls/components/radio-control.js.map +1 -1
  57. package/build-module/validated-form-controls/components/range-control.js +5 -3
  58. package/build-module/validated-form-controls/components/range-control.js.map +1 -1
  59. package/build-module/validated-form-controls/components/select-control.js +5 -3
  60. package/build-module/validated-form-controls/components/select-control.js.map +1 -1
  61. package/build-module/validated-form-controls/components/text-control.js +5 -3
  62. package/build-module/validated-form-controls/components/text-control.js.map +1 -1
  63. package/build-module/validated-form-controls/components/textarea-control.js +5 -3
  64. package/build-module/validated-form-controls/components/textarea-control.js.map +1 -1
  65. package/build-module/validated-form-controls/components/toggle-control.js +5 -3
  66. package/build-module/validated-form-controls/components/toggle-control.js.map +1 -1
  67. package/build-module/validated-form-controls/components/toggle-group-control.js +5 -3
  68. package/build-module/validated-form-controls/components/toggle-group-control.js.map +1 -1
  69. package/build-module/validated-form-controls/components/types.js.map +1 -1
  70. package/build-module/validated-form-controls/control-with-error.js +70 -34
  71. package/build-module/validated-form-controls/control-with-error.js.map +1 -1
  72. package/build-module/validated-form-controls/validity-indicator.js +37 -0
  73. package/build-module/validated-form-controls/validity-indicator.js.map +1 -0
  74. package/build-style/style-rtl.css +34 -22
  75. package/build-style/style.css +34 -22
  76. package/build-types/context/context-connect.d.ts +2 -2
  77. package/build-types/context/context-connect.d.ts.map +1 -1
  78. package/build-types/menu-item/index.d.ts.map +1 -1
  79. package/build-types/tabs/styles.d.ts.map +1 -1
  80. package/build-types/utils/font-size.d.ts +2 -2
  81. package/build-types/utils/font-size.d.ts.map +1 -1
  82. package/build-types/utils/get-valid-children.d.ts +2 -2
  83. package/build-types/utils/get-valid-children.d.ts.map +1 -1
  84. package/build-types/validated-form-controls/components/checkbox-control.d.ts.map +1 -1
  85. package/build-types/validated-form-controls/components/combobox-control.d.ts.map +1 -1
  86. package/build-types/validated-form-controls/components/custom-select-control.d.ts.map +1 -1
  87. package/build-types/validated-form-controls/components/input-control.d.ts.map +1 -1
  88. package/build-types/validated-form-controls/components/number-control.d.ts.map +1 -1
  89. package/build-types/validated-form-controls/components/radio-control.d.ts.map +1 -1
  90. package/build-types/validated-form-controls/components/range-control.d.ts.map +1 -1
  91. package/build-types/validated-form-controls/components/select-control.d.ts.map +1 -1
  92. package/build-types/validated-form-controls/components/stories/checkbox-control.story.d.ts.map +1 -1
  93. package/build-types/validated-form-controls/components/stories/combobox-control.story.d.ts.map +1 -1
  94. package/build-types/validated-form-controls/components/stories/custom-select-control.story.d.ts.map +1 -1
  95. package/build-types/validated-form-controls/components/stories/input-control.story.d.ts.map +1 -1
  96. package/build-types/validated-form-controls/components/stories/number-control.story.d.ts.map +1 -1
  97. package/build-types/validated-form-controls/components/stories/overview.story.d.ts +13 -0
  98. package/build-types/validated-form-controls/components/stories/overview.story.d.ts.map +1 -1
  99. package/build-types/validated-form-controls/components/stories/radio-control.story.d.ts.map +1 -1
  100. package/build-types/validated-form-controls/components/stories/range-control.story.d.ts.map +1 -1
  101. package/build-types/validated-form-controls/components/stories/select-control.story.d.ts.map +1 -1
  102. package/build-types/validated-form-controls/components/stories/text-control.story.d.ts.map +1 -1
  103. package/build-types/validated-form-controls/components/stories/textarea-control.story.d.ts.map +1 -1
  104. package/build-types/validated-form-controls/components/stories/toggle-control.story.d.ts.map +1 -1
  105. package/build-types/validated-form-controls/components/stories/toggle-group-control.story.d.ts.map +1 -1
  106. package/build-types/validated-form-controls/components/text-control.d.ts.map +1 -1
  107. package/build-types/validated-form-controls/components/textarea-control.d.ts.map +1 -1
  108. package/build-types/validated-form-controls/components/toggle-control.d.ts.map +1 -1
  109. package/build-types/validated-form-controls/components/toggle-group-control.d.ts.map +1 -1
  110. package/build-types/validated-form-controls/components/types.d.ts +21 -10
  111. package/build-types/validated-form-controls/components/types.d.ts.map +1 -1
  112. package/build-types/validated-form-controls/control-with-error.d.ts +4 -5
  113. package/build-types/validated-form-controls/control-with-error.d.ts.map +1 -1
  114. package/build-types/validated-form-controls/test/control-with-error.d.ts +2 -0
  115. package/build-types/validated-form-controls/test/control-with-error.d.ts.map +1 -0
  116. package/build-types/validated-form-controls/validity-indicator.d.ts +5 -0
  117. package/build-types/validated-form-controls/validity-indicator.d.ts.map +1 -0
  118. package/package.json +19 -19
  119. package/src/calendar/style.scss +22 -22
  120. package/src/context/context-connect.ts +2 -2
  121. package/src/menu-item/index.tsx +1 -0
  122. package/src/tabs/styles.ts +2 -1
  123. package/src/tools-panel/stories/index.story.tsx +3 -3
  124. package/src/utils/font-size.ts +2 -2
  125. package/src/utils/get-valid-children.ts +4 -2
  126. package/src/utils/theme-variables.scss +1 -0
  127. package/src/validated-form-controls/components/checkbox-control.tsx +5 -3
  128. package/src/validated-form-controls/components/combobox-control.tsx +5 -3
  129. package/src/validated-form-controls/components/custom-select-control.tsx +5 -3
  130. package/src/validated-form-controls/components/input-control.tsx +5 -3
  131. package/src/validated-form-controls/components/number-control.tsx +5 -3
  132. package/src/validated-form-controls/components/radio-control.tsx +5 -3
  133. package/src/validated-form-controls/components/range-control.tsx +5 -3
  134. package/src/validated-form-controls/components/select-control.tsx +5 -3
  135. package/src/validated-form-controls/components/stories/checkbox-control.story.tsx +17 -6
  136. package/src/validated-form-controls/components/stories/combobox-control.story.tsx +17 -6
  137. package/src/validated-form-controls/components/stories/custom-select-control.story.tsx +17 -6
  138. package/src/validated-form-controls/components/stories/input-control.story.tsx +51 -18
  139. package/src/validated-form-controls/components/stories/number-control.story.tsx +17 -6
  140. package/src/validated-form-controls/components/stories/overview.mdx +1 -1
  141. package/src/validated-form-controls/components/stories/overview.story.tsx +207 -17
  142. package/src/validated-form-controls/components/stories/radio-control.story.tsx +17 -6
  143. package/src/validated-form-controls/components/stories/range-control.story.tsx +17 -6
  144. package/src/validated-form-controls/components/stories/select-control.story.tsx +17 -6
  145. package/src/validated-form-controls/components/stories/text-control.story.tsx +17 -6
  146. package/src/validated-form-controls/components/stories/textarea-control.story.tsx +17 -6
  147. package/src/validated-form-controls/components/stories/toggle-control.story.tsx +17 -6
  148. package/src/validated-form-controls/components/stories/toggle-group-control.story.tsx +17 -6
  149. package/src/validated-form-controls/components/text-control.tsx +5 -3
  150. package/src/validated-form-controls/components/textarea-control.tsx +5 -3
  151. package/src/validated-form-controls/components/toggle-control.tsx +5 -3
  152. package/src/validated-form-controls/components/toggle-group-control.tsx +5 -3
  153. package/src/validated-form-controls/components/types.ts +21 -12
  154. package/src/validated-form-controls/control-with-error.tsx +93 -41
  155. package/src/validated-form-controls/style.scss +19 -5
  156. package/src/validated-form-controls/test/control-with-error.tsx +224 -0
  157. package/src/validated-form-controls/validity-indicator.tsx +48 -0
  158. package/tsconfig.tsbuildinfo +1 -1
@@ -35,6 +35,12 @@ export const Default: StoryObj< typeof ValidatedComboboxControl > = {
35
35
  typeof ValidatedComboboxControl
36
36
  >[ 'value' ]
37
37
  >();
38
+ const [ customValidity, setCustomValidity ] =
39
+ useState<
40
+ React.ComponentProps<
41
+ typeof ValidatedComboboxControl
42
+ >[ 'customValidity' ]
43
+ >( undefined );
38
44
 
39
45
  return (
40
46
  <ValidatedComboboxControl
@@ -44,6 +50,17 @@ export const Default: StoryObj< typeof ValidatedComboboxControl > = {
44
50
  setValue( newValue );
45
51
  onChange?.( newValue );
46
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 }
47
64
  />
48
65
  );
49
66
  },
@@ -56,10 +73,4 @@ Default.args = {
56
73
  { value: 'a', label: 'Option A (not allowed)' },
57
74
  { value: 'b', label: 'Option B' },
58
75
  ],
59
- customValidator: ( value ) => {
60
- if ( value === 'a' ) {
61
- return 'Option A is not allowed.';
62
- }
63
- return undefined;
64
- },
65
76
  };
@@ -35,6 +35,12 @@ export const Default: StoryObj< typeof ValidatedCustomSelectControl > = {
35
35
  typeof ValidatedCustomSelectControl
36
36
  >[ 'value' ]
37
37
  >();
38
+ const [ customValidity, setCustomValidity ] =
39
+ useState<
40
+ React.ComponentProps<
41
+ typeof ValidatedCustomSelectControl
42
+ >[ 'customValidity' ]
43
+ >( undefined );
38
44
 
39
45
  return (
40
46
  <ValidatedCustomSelectControl
@@ -44,6 +50,17 @@ export const Default: StoryObj< typeof ValidatedCustomSelectControl > = {
44
50
  setValue( newValue.selectedItem );
45
51
  onChange?.( newValue );
46
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 }
47
64
  />
48
65
  );
49
66
  },
@@ -56,10 +73,4 @@ Default.args = {
56
73
  { key: 'a', name: 'Option A (not allowed)' },
57
74
  { key: 'b', name: 'Option B' },
58
75
  ],
59
- customValidator: ( value ) => {
60
- if ( value?.key === 'a' ) {
61
- return 'Option A is not allowed.';
62
- }
63
- return undefined;
64
- },
65
76
  };
@@ -46,6 +46,12 @@ export const Default: StoryObj< typeof ValidatedInputControl > = {
46
46
  useState<
47
47
  React.ComponentProps< typeof ValidatedInputControl >[ 'value' ]
48
48
  >( '' );
49
+ const [ customValidity, setCustomValidity ] =
50
+ useState<
51
+ React.ComponentProps<
52
+ typeof ValidatedInputControl
53
+ >[ 'customValidity' ]
54
+ >( undefined );
49
55
 
50
56
  return (
51
57
  <ValidatedInputControl
@@ -55,6 +61,17 @@ export const Default: StoryObj< typeof ValidatedInputControl > = {
55
61
  setValue( newValue );
56
62
  onChange?.( newValue, ...rest );
57
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 }
58
75
  />
59
76
  );
60
77
  },
@@ -63,12 +80,6 @@ Default.args = {
63
80
  required: true,
64
81
  label: 'Input',
65
82
  help: 'The word "error" will trigger an error.',
66
- customValidator: ( value ) => {
67
- if ( value?.toLowerCase() === 'error' ) {
68
- return 'The word "error" is not allowed.';
69
- }
70
- return undefined;
71
- },
72
83
  };
73
84
 
74
85
  /**
@@ -83,6 +94,12 @@ export const Password: StoryObj< typeof ValidatedInputControl > = {
83
94
  React.ComponentProps< typeof ValidatedInputControl >[ 'value' ]
84
95
  >( '' );
85
96
  const [ visible, setVisible ] = useState( false );
97
+ const [ customValidity, setCustomValidity ] =
98
+ useState<
99
+ React.ComponentProps<
100
+ typeof ValidatedInputControl
101
+ >[ 'customValidity' ]
102
+ >( undefined );
86
103
 
87
104
  return (
88
105
  <ValidatedInputControl
@@ -105,6 +122,34 @@ export const Password: StoryObj< typeof ValidatedInputControl > = {
105
122
  setValue( newValue );
106
123
  onChange?.( newValue, ...rest );
107
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 }
108
153
  />
109
154
  );
110
155
  },
@@ -114,18 +159,6 @@ Password.args = {
114
159
  label: 'Password',
115
160
  help: 'Minimum 8 characters, include a number, capital letter, and symbol (!@£$%^&*#).',
116
161
  minLength: 8,
117
- customValidator: ( value ) => {
118
- if ( ! /\d/.test( value ?? '' ) ) {
119
- return 'Password must include at least one number.';
120
- }
121
- if ( ! /[A-Z]/.test( value ?? '' ) ) {
122
- return 'Password must include at least one capital letter.';
123
- }
124
- if ( ! /[!@£$%^&*#]/.test( value ?? '' ) ) {
125
- return 'Password must include at least one symbol.';
126
- }
127
- return undefined;
128
- },
129
162
  };
130
163
  Password.argTypes = {
131
164
  suffix: { control: false },
@@ -37,6 +37,12 @@ export const Default: StoryObj< typeof ValidatedNumberControl > = {
37
37
  useState<
38
38
  React.ComponentProps< typeof ValidatedNumberControl >[ 'value' ]
39
39
  >();
40
+ const [ customValidity, setCustomValidity ] =
41
+ useState<
42
+ React.ComponentProps<
43
+ typeof ValidatedNumberControl
44
+ >[ 'customValidity' ]
45
+ >( undefined );
40
46
 
41
47
  return (
42
48
  <ValidatedNumberControl
@@ -46,6 +52,17 @@ export const Default: StoryObj< typeof ValidatedNumberControl > = {
46
52
  setValue( newValue );
47
53
  onChange?.( newValue, ...rest );
48
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 }
49
66
  />
50
67
  );
51
68
  },
@@ -54,10 +71,4 @@ Default.args = {
54
71
  required: true,
55
72
  label: 'Number',
56
73
  help: 'Odd numbers are not allowed.',
57
- customValidator: ( value ) => {
58
- if ( value && parseInt( value.toString(), 10 ) % 2 !== 0 ) {
59
- return 'Choose an even number.';
60
- }
61
- return undefined;
62
- },
63
74
  };
@@ -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,12 +1,14 @@
1
1
  /**
2
- * WordPress dependencies
2
+ * External dependencies
3
3
  */
4
- import { useState } from '@wordpress/element';
4
+ import type { Meta, StoryObj } from '@storybook/react';
5
+ import { expect, userEvent, waitFor, within } from '@storybook/test';
5
6
 
6
7
  /**
7
- * External dependencies
8
+ * WordPress dependencies
8
9
  */
9
- import type { Meta, StoryObj } from '@storybook/react';
10
+ import { useRef, useCallback, useState } from '@wordpress/element';
11
+ import { debounce } from '@wordpress/compose';
10
12
 
11
13
  /**
12
14
  * Internal dependencies
@@ -32,6 +34,18 @@ export const WithMultipleControls: Story = {
32
34
  render: function Template() {
33
35
  const [ text, setText ] = useState( '' );
34
36
  const [ text2, setText2 ] = useState( '' );
37
+ const [ customValidity, setCustomValidity ] =
38
+ useState<
39
+ React.ComponentProps<
40
+ typeof ValidatedInputControl
41
+ >[ 'customValidity' ]
42
+ >( undefined );
43
+ const [ customValidity2, setCustomValidity2 ] =
44
+ useState<
45
+ React.ComponentProps<
46
+ typeof ValidatedInputControl
47
+ >[ 'customValidity' ]
48
+ >( undefined );
35
49
 
36
50
  return (
37
51
  <>
@@ -40,12 +54,17 @@ export const WithMultipleControls: Story = {
40
54
  required
41
55
  value={ text }
42
56
  help="The word 'error' will trigger an error."
43
- customValidator={ ( value ) => {
57
+ onValidate={ ( value ) => {
44
58
  if ( value?.toLowerCase() === 'error' ) {
45
- return 'The word "error" is not allowed.';
59
+ setCustomValidity( {
60
+ type: 'invalid',
61
+ message: 'The word "error" is not allowed.',
62
+ } );
63
+ } else {
64
+ setCustomValidity( undefined );
46
65
  }
47
- return undefined;
48
66
  } }
67
+ customValidity={ customValidity }
49
68
  onChange={ ( value ) => setText( value ?? '' ) }
50
69
  />
51
70
  <ValidatedInputControl
@@ -53,13 +72,18 @@ export const WithMultipleControls: Story = {
53
72
  required
54
73
  value={ text2 }
55
74
  help="The word 'error' will trigger an error."
56
- customValidator={ ( value ) => {
75
+ onValidate={ ( value ) => {
57
76
  if ( value?.toLowerCase() === 'error' ) {
58
- return 'The word "error" is not allowed.';
77
+ setCustomValidity2( {
78
+ type: 'invalid',
79
+ message: 'The word "error" is not allowed.',
80
+ } );
81
+ } else {
82
+ setCustomValidity2( undefined );
59
83
  }
60
- return undefined;
61
84
  } }
62
85
  onChange={ ( value ) => setText2( value ?? '' ) }
86
+ customValidity={ customValidity2 }
63
87
  />
64
88
  </>
65
89
  );
@@ -73,7 +97,12 @@ export const WithMultipleControls: Story = {
73
97
  export const WithHelpTextReplacement: Story = {
74
98
  render: function Template() {
75
99
  const [ text, setText ] = useState( '' );
76
- const [ hasCustomError, setHasCustomError ] = useState( false );
100
+ const [ customValidity, setCustomValidity ] =
101
+ useState<
102
+ React.ComponentProps<
103
+ typeof ValidatedInputControl
104
+ >[ 'customValidity' ]
105
+ >( undefined );
77
106
 
78
107
  return (
79
108
  <ValidatedInputControl
@@ -81,20 +110,181 @@ export const WithHelpTextReplacement: Story = {
81
110
  required
82
111
  value={ text }
83
112
  help={
84
- hasCustomError
113
+ customValidity
85
114
  ? undefined
86
115
  : 'The word "error" is not allowed.'
87
116
  }
88
- customValidator={ ( value ) => {
117
+ onValidate={ ( value ) => {
89
118
  if ( value?.toLowerCase() === 'error' ) {
90
- setHasCustomError( true );
91
- return 'The word "error" is not allowed.';
119
+ setCustomValidity( {
120
+ type: 'invalid',
121
+ message: 'The word "error" is not allowed.',
122
+ } );
123
+ } else {
124
+ setCustomValidity( undefined );
92
125
  }
93
- setHasCustomError( false );
94
- return undefined;
95
126
  } }
96
127
  onChange={ ( value ) => setText( value ?? '' ) }
128
+ customValidity={ customValidity }
129
+ />
130
+ );
131
+ },
132
+ };
133
+
134
+ /**
135
+ * To provide feedback from server-side validation, the `customValidity` prop can be used
136
+ * to show additional status indicators while waiting for the server response,
137
+ * and after the response is received.
138
+ *
139
+ * These indicators are intended for asynchronous validation calls that may take more than 1 second to complete.
140
+ * They may be unnecessary when responses are generally quick.
141
+ */
142
+ export const AsyncValidation: StoryObj< typeof ValidatedInputControl > = {
143
+ render: function Template( { ...args } ) {
144
+ const [ text, setText ] = useState( '' );
145
+ const [ customValidity, setCustomValidity ] =
146
+ useState<
147
+ React.ComponentProps<
148
+ typeof ValidatedInputControl
149
+ >[ 'customValidity' ]
150
+ >( undefined );
151
+
152
+ const timeoutRef = useRef< ReturnType< typeof setTimeout > >();
153
+ const previousValidationValueRef = useRef< unknown >( '' );
154
+
155
+ // eslint-disable-next-line react-hooks/exhaustive-deps
156
+ const debouncedValidate = useCallback(
157
+ debounce( ( v ) => {
158
+ if ( v === previousValidationValueRef.current ) {
159
+ return;
160
+ }
161
+
162
+ previousValidationValueRef.current = v;
163
+
164
+ setCustomValidity( {
165
+ type: 'validating',
166
+ message: 'Validating...',
167
+ } );
168
+
169
+ clearTimeout( timeoutRef.current );
170
+ timeoutRef.current = setTimeout( () => {
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
+ }, 1500 );
183
+ }, 500 ),
184
+ []
185
+ );
186
+
187
+ return (
188
+ <ValidatedInputControl
189
+ { ...args }
190
+ value={ text }
191
+ onChange={ ( newValue ) => {
192
+ setText( newValue ?? '' );
193
+ } }
194
+ onValidate={ debouncedValidate }
195
+ customValidity={ customValidity }
97
196
  />
98
197
  );
99
198
  },
199
+ args: {
200
+ label: 'Text',
201
+ help: 'The word "error" will trigger an error asynchronously.',
202
+ required: true,
203
+ },
204
+ };
205
+
206
+ // Not exported - Only for testing purposes.
207
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
208
+ const AsyncValidationWithTest: StoryObj< typeof ValidatedInputControl > = {
209
+ ...AsyncValidation,
210
+ play: async ( { canvasElement } ) => {
211
+ const canvas = within( canvasElement );
212
+ await userEvent.click( canvas.getByRole( 'textbox' ) );
213
+ await userEvent.type( canvas.getByRole( 'textbox' ), 'valid text', {
214
+ delay: 10,
215
+ } );
216
+ await userEvent.tab();
217
+
218
+ await waitFor(
219
+ () => {
220
+ expect( canvas.getByText( 'Validated' ) ).toBeVisible();
221
+ },
222
+ { timeout: 2500 }
223
+ );
224
+
225
+ await new Promise( ( resolve ) => setTimeout( resolve, 500 ) );
226
+ await userEvent.clear( canvas.getByRole( 'textbox' ) );
227
+
228
+ // Should show validating state when transitioning from valid to invalid.
229
+ await waitFor(
230
+ () => {
231
+ expect( canvas.getByText( 'Validating...' ) ).toBeVisible();
232
+ },
233
+ { timeout: 2500 }
234
+ );
235
+
236
+ await waitFor(
237
+ () => {
238
+ expect(
239
+ canvas.getByText( 'Please fill out this field.' )
240
+ ).toBeVisible();
241
+ },
242
+ { timeout: 2500 }
243
+ );
244
+
245
+ // Should not show validating state if there were no changes
246
+ // after a valid/invalid state was already shown.
247
+ await new Promise( ( resolve ) => setTimeout( resolve, 1500 ) );
248
+ await expect(
249
+ canvas.queryByText( 'Validating...' )
250
+ ).not.toBeInTheDocument();
251
+
252
+ await userEvent.type( canvas.getByRole( 'textbox' ), 'e', {
253
+ delay: 10,
254
+ } );
255
+
256
+ // Should not show valid state if server has not yet responded.
257
+ await expect(
258
+ canvas.queryByText( 'Validated' )
259
+ ).not.toBeInTheDocument();
260
+
261
+ // Should show validating state when transitioning from invalid to valid.
262
+ await waitFor(
263
+ () => {
264
+ expect( canvas.getByText( 'Validating...' ) ).toBeVisible();
265
+ },
266
+ { timeout: 2500 }
267
+ );
268
+
269
+ await waitFor(
270
+ () => {
271
+ expect( canvas.getByText( 'Validated' ) ).toBeVisible();
272
+ },
273
+ { timeout: 2500 }
274
+ );
275
+
276
+ await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) );
277
+ await userEvent.type( canvas.getByRole( 'textbox' ), 'rror', {
278
+ delay: 10,
279
+ } );
280
+
281
+ await waitFor(
282
+ () => {
283
+ expect(
284
+ canvas.getByText( 'The word "error" is not allowed.' )
285
+ ).toBeVisible();
286
+ },
287
+ { timeout: 2500 }
288
+ );
289
+ },
100
290
  };
@@ -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
  };