UNPKG

4.38 kBPlain TextView Raw
1/*
2 * Copyright 2020 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13import {FocusEvent as ReactFocusEvent, useCallback, useRef} from 'react';
14import {useLayoutEffect} from '@react-aria/utils';
15
16export class SyntheticFocusEvent implements ReactFocusEvent {
17 nativeEvent: FocusEvent;
18 target: Element;
19 currentTarget: Element;
20 relatedTarget: Element;
21 bubbles: boolean;
22 cancelable: boolean;
23 defaultPrevented: boolean;
24 eventPhase: number;
25 isTrusted: boolean;
26 timeStamp: number;
27 type: string;
28
29 constructor(type: string, nativeEvent: FocusEvent) {
30 this.nativeEvent = nativeEvent;
31 this.target = nativeEvent.target as Element;
32 this.currentTarget = nativeEvent.currentTarget as Element;
33 this.relatedTarget = nativeEvent.relatedTarget as Element;
34 this.bubbles = nativeEvent.bubbles;
35 this.cancelable = nativeEvent.cancelable;
36 this.defaultPrevented = nativeEvent.defaultPrevented;
37 this.eventPhase = nativeEvent.eventPhase;
38 this.isTrusted = nativeEvent.isTrusted;
39 this.timeStamp = nativeEvent.timeStamp;
40 this.type = type;
41 }
42
43 isDefaultPrevented(): boolean {
44 return this.nativeEvent.defaultPrevented;
45 }
46
47 preventDefault(): void {
48 this.defaultPrevented = true;
49 this.nativeEvent.preventDefault();
50 }
51
52 stopPropagation(): void {
53 this.nativeEvent.stopPropagation();
54 this.isPropagationStopped = () => true;
55 }
56
57 isPropagationStopped(): boolean {
58 return false;
59 }
60
61 persist() {}
62}
63
64export function useSyntheticBlurEvent(onBlur: (e: ReactFocusEvent) => void) {
65 let stateRef = useRef({
66 isFocused: false,
67 onBlur,
68 observer: null as MutationObserver
69 });
70 stateRef.current.onBlur = onBlur;
71
72 // Clean up MutationObserver on unmount. See below.
73 // eslint-disable-next-line arrow-body-style
74 useLayoutEffect(() => {
75 const state = stateRef.current;
76 return () => {
77 if (state.observer) {
78 state.observer.disconnect();
79 state.observer = null;
80 }
81 };
82 }, []);
83
84 // This function is called during a React onFocus event.
85 return useCallback((e: ReactFocusEvent) => {
86 // React does not fire onBlur when an element is disabled. https://github.com/facebook/react/issues/9142
87 // Most browsers fire a native focusout event in this case, except for Firefox. In that case, we use a
88 // MutationObserver to watch for the disabled attribute, and dispatch these events ourselves.
89 // For browsers that do, focusout fires before the MutationObserver, so onBlur should not fire twice.
90 if (
91 e.target instanceof HTMLButtonElement ||
92 e.target instanceof HTMLInputElement ||
93 e.target instanceof HTMLTextAreaElement ||
94 e.target instanceof HTMLSelectElement
95 ) {
96 stateRef.current.isFocused = true;
97
98 let target = e.target;
99 let onBlurHandler = (e: FocusEvent) => {
100 stateRef.current.isFocused = false;
101
102 if (target.disabled) {
103 // For backward compatibility, dispatch a (fake) React synthetic event.
104 stateRef.current.onBlur?.(new SyntheticFocusEvent('blur', e));
105 }
106
107 // We no longer need the MutationObserver once the target is blurred.
108 if (stateRef.current.observer) {
109 stateRef.current.observer.disconnect();
110 stateRef.current.observer = null;
111 }
112 };
113
114 target.addEventListener('focusout', onBlurHandler, {once: true});
115
116 stateRef.current.observer = new MutationObserver(() => {
117 if (stateRef.current.isFocused && target.disabled) {
118 stateRef.current.observer.disconnect();
119 target.dispatchEvent(new FocusEvent('blur'));
120 target.dispatchEvent(new FocusEvent('focusout', {bubbles: true}));
121 }
122 });
123
124 stateRef.current.observer.observe(target, {attributes: true, attributeFilter: ['disabled']});
125 }
126 }, []);
127}