UNPKG

5.27 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, useRef} from 'react';
14import {useLayoutEffect} from '@react-aria/utils';
15
16// Original licensing for the following method can be found in the
17// NOTICE file in the root directory of this source tree.
18// See https://github.com/facebook/react/blob/3c713d513195a53788b3f8bb4b70279d68b15bcc/packages/react-interactions/events/src/dom/shared/index.js#L74-L87
19
20// Keyboards, Assistive Technologies, and element.click() all produce a "virtual"
21// click event. This is a method of inferring such clicks. Every browser except
22// IE 11 only sets a zero value of "detail" for click events that are "virtual".
23// However, IE 11 uses a zero value for all click events. For IE 11 we rely on
24// the quirk that it produces click events that are of type PointerEvent, and
25// where only the "virtual" click lacks a pointerType field.
26
27export function isVirtualClick(event: MouseEvent | PointerEvent): boolean {
28 // JAWS/NVDA with Firefox.
29 if ((event as any).mozInputSource === 0 && event.isTrusted) {
30 return true;
31 }
32
33 return event.detail === 0 && !(event as PointerEvent).pointerType;
34}
35
36export class SyntheticFocusEvent implements ReactFocusEvent {
37 nativeEvent: FocusEvent;
38 target: Element;
39 currentTarget: Element;
40 relatedTarget: Element;
41 bubbles: boolean;
42 cancelable: boolean;
43 defaultPrevented: boolean;
44 eventPhase: number;
45 isTrusted: boolean;
46 timeStamp: number;
47 type: string;
48
49 constructor(type: string, nativeEvent: FocusEvent) {
50 this.nativeEvent = nativeEvent;
51 this.target = nativeEvent.target as Element;
52 this.currentTarget = nativeEvent.currentTarget as Element;
53 this.relatedTarget = nativeEvent.relatedTarget as Element;
54 this.bubbles = nativeEvent.bubbles;
55 this.cancelable = nativeEvent.cancelable;
56 this.defaultPrevented = nativeEvent.defaultPrevented;
57 this.eventPhase = nativeEvent.eventPhase;
58 this.isTrusted = nativeEvent.isTrusted;
59 this.timeStamp = nativeEvent.timeStamp;
60 this.type = type;
61 }
62
63 isDefaultPrevented(): boolean {
64 return this.nativeEvent.defaultPrevented;
65 }
66
67 preventDefault(): void {
68 this.defaultPrevented = true;
69 this.nativeEvent.preventDefault();
70 }
71
72 stopPropagation(): void {
73 this.nativeEvent.stopPropagation();
74 this.isPropagationStopped = () => true;
75 }
76
77 isPropagationStopped(): boolean {
78 return false;
79 }
80
81 persist() {}
82}
83
84export function useSyntheticBlurEvent(onBlur: (e: ReactFocusEvent) => void) {
85 let stateRef = useRef({
86 isFocused: false,
87 onBlur,
88 observer: null as MutationObserver
89 });
90 let state = stateRef.current;
91 state.onBlur = onBlur;
92
93 // Clean up MutationObserver on unmount. See below.
94 // eslint-disable-next-line arrow-body-style
95 useLayoutEffect(() => {
96 return () => {
97 if (state.observer) {
98 state.observer.disconnect();
99 state.observer = null;
100 }
101 };
102 }, [state]);
103
104 // This function is called during a React onFocus event.
105 return (e: ReactFocusEvent) => {
106 // React does not fire onBlur when an element is disabled. https://github.com/facebook/react/issues/9142
107 // Most browsers fire a native focusout event in this case, except for Firefox. In that case, we use a
108 // MutationObserver to watch for the disabled attribute, and dispatch these events ourselves.
109 // For browsers that do, focusout fires before the MutationObserver, so onBlur should not fire twice.
110 if (
111 e.target instanceof HTMLButtonElement ||
112 e.target instanceof HTMLInputElement ||
113 e.target instanceof HTMLTextAreaElement ||
114 e.target instanceof HTMLSelectElement
115 ) {
116 state.isFocused = true;
117
118 let target = e.target;
119 let onBlurHandler = (e: FocusEvent) => {
120 let state = stateRef.current;
121 state.isFocused = false;
122
123 if (target.disabled) {
124 // For backward compatibility, dispatch a (fake) React synthetic event.
125 state.onBlur?.(new SyntheticFocusEvent('blur', e));
126 }
127
128 // We no longer need the MutationObserver once the target is blurred.
129 if (state.observer) {
130 state.observer.disconnect();
131 state.observer = null;
132 }
133 };
134
135 target.addEventListener('focusout', onBlurHandler, {once: true});
136
137 state.observer = new MutationObserver(() => {
138 if (state.isFocused && target.disabled) {
139 state.observer.disconnect();
140 target.dispatchEvent(new FocusEvent('blur'));
141 target.dispatchEvent(new FocusEvent('focusout', {bubbles: true}));
142 }
143 });
144
145 state.observer.observe(target, {attributes: true, attributeFilter: ['disabled']});
146 }
147 };
148}