10
10
* governing permissions and limitations under the License.
11
11
*/
12
12
13
+ import { announce } from '@react-aria/live-announcer' ;
13
14
import { FormValidationState } from '@react-stately/form' ;
15
+ import { getActiveElement , getOwnerDocument , useEffectEvent , useLayoutEffect } from '@react-aria/utils' ;
14
16
import { RefObject , Validation , ValidationResult } from '@react-types/shared' ;
15
17
import { setInteractionModality } from '@react-aria/interactions' ;
16
- import { useEffect } from 'react' ;
17
- import { useEffectEvent , useLayoutEffect } from '@react-aria/utils' ;
18
+ import { useEffect , useRef } from 'react' ;
18
19
19
20
type ValidatableElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ;
20
21
@@ -25,12 +26,24 @@ interface FormValidationProps<T> extends Validation<T> {
25
26
export function useFormValidation < T > ( props : FormValidationProps < T > , state : FormValidationState , ref : RefObject < ValidatableElement | null > | undefined ) : void {
26
27
let { validationBehavior, focus} = props ;
27
28
29
+ let timeoutId = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
30
+ function announceErrorMessage ( errorMessage : string = '' ) : void {
31
+ clearTimeout ( timeoutId . current ! ) ;
32
+ if ( ref ?. current &&
33
+ errorMessage !== '' &&
34
+ ref . current . contains ( getActiveElement ( getOwnerDocument ( ref . current ) ) ) ) {
35
+ timeoutId . current = setTimeout ( ( ) => announce ( errorMessage , 'polite' ) , 250 ) ;
36
+ }
37
+ }
38
+
28
39
// This is a useLayoutEffect so that it runs before the useEffect in useFormValidationState, which commits the validation change.
29
40
useLayoutEffect ( ( ) => {
30
41
if ( validationBehavior === 'native' && ref ?. current && ! ref . current . disabled ) {
31
42
let errorMessage = state . realtimeValidation . isInvalid ? state . realtimeValidation . validationErrors . join ( ' ' ) || 'Invalid value.' : '' ;
32
43
ref . current . setCustomValidity ( errorMessage ) ;
33
44
45
+ announceErrorMessage ( errorMessage ) ;
46
+
34
47
// Prevent default tooltip for validation message.
35
48
// https://bugzilla.mozilla.org/show_bug.cgi?id=605277
36
49
if ( ! ref . current . hasAttribute ( 'title' ) ) {
@@ -56,11 +69,14 @@ export function useFormValidation<T>(props: FormValidationProps<T>, state: FormV
56
69
57
70
// Auto focus the first invalid input in a form, unless the error already had its default prevented.
58
71
let form = ref ?. current ?. form ;
59
- if ( ! e . defaultPrevented && ref && form && getFirstInvalidInput ( form ) === ref . current ) {
60
- if ( focus ) {
61
- focus ( ) ;
62
- } else {
63
- ref . current ?. focus ( ) ;
72
+ if ( ! e . defaultPrevented && ref && form ) {
73
+ announceErrorMessage ( ref ?. current ?. validationMessage || '' ) ;
74
+ if ( getFirstInvalidInput ( form ) === ref . current ) {
75
+ if ( focus ) {
76
+ focus ( ) ;
77
+ } else {
78
+ ref . current ?. focus ( ) ;
79
+ }
64
80
}
65
81
66
82
// Always show focus ring.
@@ -86,6 +102,7 @@ export function useFormValidation<T>(props: FormValidationProps<T>, state: FormV
86
102
input . addEventListener ( 'change' , onChange ) ;
87
103
form ?. addEventListener ( 'reset' , onReset ) ;
88
104
return ( ) => {
105
+ clearTimeout ( timeoutId . current ! ) ;
89
106
input ! . removeEventListener ( 'invalid' , onInvalid ) ;
90
107
input ! . removeEventListener ( 'change' , onChange ) ;
91
108
form ?. removeEventListener ( 'reset' , onReset ) ;
0 commit comments