UNPKG

4.78 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 {useEffectEvent, useLayoutEffect} from '@react-aria/utils';
15
16export class SyntheticFocusEvent<Target = Element> implements ReactFocusEvent<Target> {
17 nativeEvent: FocusEvent;
18 target: EventTarget & Target;
19 currentTarget: EventTarget & Target;
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 EventTarget & Target;
32 this.currentTarget = nativeEvent.currentTarget as EventTarget & Target;
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<Target = Element>(onBlur: (e: ReactFocusEvent<Target>) => void) {
65 let stateRef = useRef({
66 isFocused: false,
67 observer: null as MutationObserver | null
68 });
69
70 // Clean up MutationObserver on unmount. See below.
71 // eslint-disable-next-line arrow-body-style
72 useLayoutEffect(() => {
73 const state = stateRef.current;
74 return () => {
75 if (state.observer) {
76 state.observer.disconnect();
77 state.observer = null;
78 }
79 };
80 }, []);
81
82 let dispatchBlur = useEffectEvent((e: SyntheticFocusEvent<Target>) => {
83 onBlur?.(e);
84 });
85
86 // This function is called during a React onFocus event.
87 return useCallback((e: ReactFocusEvent<Target>) => {
88 // React does not fire onBlur when an element is disabled. https://github.com/facebook/react/issues/9142
89 // Most browsers fire a native focusout event in this case, except for Firefox. In that case, we use a
90 // MutationObserver to watch for the disabled attribute, and dispatch these events ourselves.
91 // For browsers that do, focusout fires before the MutationObserver, so onBlur should not fire twice.
92 if (
93 e.target instanceof HTMLButtonElement ||
94 e.target instanceof HTMLInputElement ||
95 e.target instanceof HTMLTextAreaElement ||
96 e.target instanceof HTMLSelectElement
97 ) {
98 stateRef.current.isFocused = true;
99
100 let target = e.target;
101 let onBlurHandler: EventListenerOrEventListenerObject | null = (e) => {
102 stateRef.current.isFocused = false;
103
104 if (target.disabled) {
105 // For backward compatibility, dispatch a (fake) React synthetic event.
106 dispatchBlur(new SyntheticFocusEvent('blur', e as FocusEvent));
107 }
108
109 // We no longer need the MutationObserver once the target is blurred.
110 if (stateRef.current.observer) {
111 stateRef.current.observer.disconnect();
112 stateRef.current.observer = null;
113 }
114 };
115
116 target.addEventListener('focusout', onBlurHandler, {once: true});
117
118 stateRef.current.observer = new MutationObserver(() => {
119 if (stateRef.current.isFocused && target.disabled) {
120 stateRef.current.observer?.disconnect();
121 let relatedTargetEl = target === document.activeElement ? null : document.activeElement;
122 target.dispatchEvent(new FocusEvent('blur', {relatedTarget: relatedTargetEl}));
123 target.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget: relatedTargetEl}));
124 }
125 });
126
127 stateRef.current.observer.observe(target, {attributes: true, attributeFilter: ['disabled']});
128 }
129 }, [dispatchBlur]);
130}