@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
@@ -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
  };
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
+ import { usePrevious } from '@wordpress/compose';
4
5
  import { __ } from '@wordpress/i18n';
5
- import { error } from '@wordpress/icons';
6
-
7
- /**
8
- * External dependencies
9
- */
10
6
  import {
11
7
  cloneElement,
12
8
  forwardRef,
@@ -18,8 +14,8 @@ import {
18
14
  * Internal dependencies
19
15
  */
20
16
  import { withIgnoreIMEEvents } from '../utils/with-ignore-ime-events';
21
-
22
- import Icon from '../icon';
17
+ import type { ValidatedControlProps } from './components/types';
18
+ import { ValidityIndicator } from './validity-indicator';
23
19
 
24
20
  function appendRequiredIndicator(
25
21
  label: React.ReactNode,
@@ -61,7 +57,8 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
61
57
  {
62
58
  required,
63
59
  markWhenOptional,
64
- customValidator,
60
+ onValidate,
61
+ customValidity,
65
62
  getValidityTarget,
66
63
  children,
67
64
  }: {
@@ -74,12 +71,10 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
74
71
  */
75
72
  markWhenOptional?: boolean;
76
73
  /**
77
- * A function that returns a custom validity message when applicable.
78
- *
79
- * This message will be applied to the element returned by `getValidityTarget`.
80
- * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity
74
+ * The callback to run when the input should be validated.
81
75
  */
82
- customValidator?: () => string | void;
76
+ onValidate?: () => void;
77
+ customValidity?: ValidatedControlProps< unknown >[ 'customValidity' ];
83
78
  /**
84
79
  * A function that returns the actual element on which the validity data should be applied.
85
80
  */
@@ -92,7 +87,15 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
92
87
  forwardedRef: React.ForwardedRef< HTMLDivElement >
93
88
  ) {
94
89
  const [ errorMessage, setErrorMessage ] = useState< string | undefined >();
90
+ const [ statusMessage, setStatusMessage ] = useState<
91
+ | {
92
+ type: 'validating' | 'valid';
93
+ message?: string;
94
+ }
95
+ | undefined
96
+ >();
95
97
  const [ isTouched, setIsTouched ] = useState( false );
98
+ const previousCustomValidityType = usePrevious( customValidity?.type );
96
99
 
97
100
  // Ensure that error messages are visible after user attemps to submit a form
98
101
  // with multiple invalid fields.
@@ -111,15 +114,74 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
111
114
  };
112
115
  } );
113
116
 
114
- const validate = () => {
115
- const message = customValidator?.();
117
+ useEffect( (): ReturnType< React.EffectCallback > => {
118
+ if ( ! isTouched ) {
119
+ return;
120
+ }
121
+
116
122
  const validityTarget = getValidityTarget();
117
123
 
118
- validityTarget?.setCustomValidity( message ?? '' );
119
- setErrorMessage( validityTarget?.validationMessage );
120
- };
124
+ if ( ! customValidity?.type ) {
125
+ validityTarget?.setCustomValidity( '' );
126
+ setErrorMessage( validityTarget?.validationMessage );
127
+ setStatusMessage( undefined );
128
+ return;
129
+ }
130
+
131
+ switch ( customValidity.type ) {
132
+ case 'validating': {
133
+ // Wait before showing a validating state.
134
+ const timer = setTimeout( () => {
135
+ validityTarget?.setCustomValidity( '' );
136
+ setErrorMessage( undefined );
137
+
138
+ setStatusMessage( {
139
+ type: 'validating',
140
+ message: customValidity.message,
141
+ } );
142
+ }, 1000 );
143
+
144
+ return () => clearTimeout( timer );
145
+ }
146
+ case 'valid': {
147
+ // Ensures that we wait for any async responses before showing
148
+ // a synchronously valid state.
149
+ if ( previousCustomValidityType === 'valid' ) {
150
+ break;
151
+ }
152
+
153
+ validityTarget?.setCustomValidity( '' );
154
+ setErrorMessage( validityTarget?.validationMessage );
155
+
156
+ setStatusMessage( {
157
+ type: 'valid',
158
+ message: customValidity.message,
159
+ } );
160
+ break;
161
+ }
162
+ case 'invalid': {
163
+ validityTarget?.setCustomValidity(
164
+ customValidity.message ?? ''
165
+ );
166
+ setErrorMessage( validityTarget?.validationMessage );
167
+
168
+ setStatusMessage( undefined );
169
+ break;
170
+ }
171
+ }
172
+ }, [
173
+ isTouched,
174
+ customValidity?.type,
175
+ customValidity?.message,
176
+ getValidityTarget,
177
+ previousCustomValidityType,
178
+ ] );
121
179
 
122
180
  const onBlur = ( event: React.FocusEvent< HTMLDivElement > ) => {
181
+ if ( isTouched ) {
182
+ return;
183
+ }
184
+
123
185
  // Only consider "blurred from the component" if focus has fully left the wrapping div.
124
186
  // This prevents unnecessary blurs from components with multiple focusable elements.
125
187
  if (
@@ -127,18 +189,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
127
189
  ! event.currentTarget.contains( event.relatedTarget )
128
190
  ) {
129
191
  setIsTouched( true );
130
-
131
- const validityTarget = getValidityTarget();
132
-
133
- // Prevents a double flash of the native error tooltip when the control is already showing one.
134
- if ( ! validityTarget?.validity.valid ) {
135
- if ( ! errorMessage ) {
136
- setErrorMessage( validityTarget?.validationMessage );
137
- }
138
- return;
139
- }
140
-
141
- validate();
192
+ onValidate?.();
142
193
  }
143
194
  };
144
195
 
@@ -148,7 +199,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
148
199
  // Only validate incrementally if the field has blurred at least once,
149
200
  // or currently has an error message.
150
201
  if ( isTouched || errorMessage ) {
151
- validate();
202
+ onValidate?.();
152
203
  }
153
204
  };
154
205
 
@@ -156,7 +207,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
156
207
  // Ensures that custom validators are triggered when the user submits by pressing Enter,
157
208
  // without ever blurring the control.
158
209
  if ( event.key === 'Enter' ) {
159
- validate();
210
+ onValidate?.();
160
211
  }
161
212
  };
162
213
 
@@ -180,15 +231,16 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
180
231
  } ) }
181
232
  <div aria-live="polite">
182
233
  { errorMessage && (
183
- <p className="components-validated-control__error">
184
- <Icon
185
- className="components-validated-control__error-icon"
186
- icon={ error }
187
- size={ 16 }
188
- fill="currentColor"
189
- />
190
- { errorMessage }
191
- </p>
234
+ <ValidityIndicator
235
+ type="invalid"
236
+ message={ errorMessage }
237
+ />
238
+ ) }
239
+ { ! errorMessage && statusMessage && (
240
+ <ValidityIndicator
241
+ type={ statusMessage.type }
242
+ message={ statusMessage.message }
243
+ />
192
244
  ) }
193
245
  </div>
194
246
  </div>
@@ -45,7 +45,7 @@
45
45
  pointer-events: none;
46
46
  }
47
47
 
48
- .components-validated-control__error {
48
+ .components-validated-control__indicator {
49
49
  display: flex;
50
50
  align-items: flex-start;
51
51
  gap: 4px;
@@ -53,17 +53,31 @@
53
53
  font-family: $font-family-body;
54
54
  font-size: 0.75rem;
55
55
  line-height: 16px; // matches the icon size
56
- color: $alert-red;
56
+ color: $components-color-gray-700;
57
57
  animation:
58
- components-validated-control__error-jump 0.2s
58
+ components-validated-control__indicator-jump 0.2s
59
59
  cubic-bezier(0.68, -0.55, 0.27, 1.55);
60
+
61
+ &.is-invalid {
62
+ color: $alert-red;
63
+ }
64
+
65
+ &.is-valid {
66
+ color: color-mix(in srgb, $black 30%, $alert-green);
67
+ }
60
68
  }
61
69
 
62
- .components-validated-control__error-icon {
70
+ .components-validated-control__indicator-icon {
63
71
  flex-shrink: 0;
64
72
  }
65
73
 
66
- @keyframes components-validated-control__error-jump {
74
+ .components-validated-control__indicator-spinner {
75
+ margin: 2px;
76
+ width: $grid-unit-15;
77
+ height: $grid-unit-15;
78
+ }
79
+
80
+ @keyframes components-validated-control__indicator-jump {
67
81
  0% {
68
82
  transform: translateY(-4px);
69
83
  opacity: 0;