-
-
Notifications
You must be signed in to change notification settings - Fork 199
/
Copy pathuseIsFocusVisible.ts
147 lines (129 loc) · 4.55 KB
/
useIsFocusVisible.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// Straight out copied from https://github.com/mui-org/material-ui 😂
// based on https://github.com/WICG/focus-visible/blob/v4.1.5/src/focus-visible.js
import { useCallback } from 'react';
let hadKeyboardEvent = true;
let hadFocusVisibleRecently = false;
let hadFocusVisibleRecentlyTimeout: number;
const inputTypesWhitelist: Record<string, boolean> = {
text: true,
search: true,
url: true,
tel: true,
email: true,
password: true,
number: true,
date: true,
month: true,
week: true,
time: true,
datetime: true,
'datetime-local': true
};
/**
* Computes whether the given element should automatically trigger the
* `focus-visible` class being added, i.e. whether it should always match
* `:focus-visible` when focused.
* @param {Element} node
* @return {boolean}
*/
function focusTriggersKeyboardModality(
node: Element | HTMLElement | HTMLInputElement
) {
if ('type' in node) {
const { type, tagName } = node;
if (tagName === 'INPUT' && inputTypesWhitelist[type] && !node.readOnly) {
return true;
}
if (tagName === 'TEXTAREA' && !node.readOnly) {
return true;
}
}
if ('isContentEditable' in node && node.isContentEditable) {
return true;
}
return false;
}
/**
* Keep track of our keyboard modality state with `hadKeyboardEvent`.
* If the most recent user interaction was via the keyboard;
* and the key press did not include a meta, alt/option, or control key;
* then the modality is keyboard. Otherwise, the modality is not keyboard.
* @param {KeyboardEvent} event
*/
function handleKeyDown(event: KeyboardEvent) {
if (event.metaKey || event.altKey || event.ctrlKey) {
return;
}
hadKeyboardEvent = true;
}
/**
* If at any point a user clicks with a pointing device, ensure that we change
* the modality away from keyboard.
* This avoids the situation where a user presses a key on an already focused
* element, and then clicks on a different element, focusing it with a
* pointing device, while we still think we're in keyboard modality.
*/
function handlePointerDown() {
hadKeyboardEvent = false;
}
function handleVisibilityChange(this: Document) {
if (this.visibilityState === 'hidden') {
// If the tab becomes active again, the browser will handle calling focus
// on the element (Safari actually calls it twice).
// If this tab change caused a blur on an element with focus-visible,
// re-apply the class when the user switches back to the tab.
if (hadFocusVisibleRecently) {
hadKeyboardEvent = true;
}
}
}
function prepare(doc: Document) {
doc.addEventListener('keydown', handleKeyDown, true);
doc.addEventListener('mousedown', handlePointerDown, true);
doc.addEventListener('pointerdown', handlePointerDown, true);
doc.addEventListener('touchstart', handlePointerDown, true);
doc.addEventListener('visibilitychange', handleVisibilityChange, true);
}
export function teardown(doc: Document) {
doc.removeEventListener('keydown', handleKeyDown, true);
doc.removeEventListener('mousedown', handlePointerDown, true);
doc.removeEventListener('pointerdown', handlePointerDown, true);
doc.removeEventListener('touchstart', handlePointerDown, true);
doc.removeEventListener('visibilitychange', handleVisibilityChange, true);
}
function isFocusVisible(event: React.FocusEvent) {
const { target } = event;
try {
return target.matches(':focus-visible');
} catch (error) {
// browsers not implementing :focus-visible will throw a SyntaxError
// we use our own heuristic for those browsers
// rethrow might be better if it's not the expected error but do we really
// want to crash if focus-visible malfunctioned?
}
// no need for validFocusTarget check. the user does that by attaching it to
// focusable events only
return hadKeyboardEvent || focusTriggersKeyboardModality(target);
}
/**
* Should be called if a blur event is fired on a focus-visible element
*/
function handleBlurVisible() {
// To detect a tab/window switch, we look for a blur event followed
// rapidly by a visibility change.
// If we don't see a visibility change within 100ms, it's probably a
// regular focus change.
hadFocusVisibleRecently = true;
window.clearTimeout(hadFocusVisibleRecentlyTimeout);
hadFocusVisibleRecentlyTimeout = window.setTimeout(() => {
hadFocusVisibleRecently = false;
}, 100);
}
export function useIsFocusVisible<T extends Element = HTMLElement>() {
const ref = useCallback((node: T) => {
if (node != null) {
prepare(node.ownerDocument);
}
}, []);
return { isFocusVisible, onBlurVisible: handleBlurVisible, ref };
}