@zag-js/focus-visible 0.68.1 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +36 -5
- package/dist/index.d.ts +36 -5
- package/dist/index.js +128 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +127 -79
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +222 -99
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
type Modality = "keyboard" | "pointer" | "virtual";
|
|
2
|
-
type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
type RootNode = Document | ShadowRoot | Node;
|
|
3
|
+
interface GlobalListenerData {
|
|
4
|
+
focus: VoidFunction;
|
|
5
|
+
}
|
|
6
|
+
declare let listenerMap: Map<Window, GlobalListenerData>;
|
|
6
7
|
declare function getInteractionModality(): Modality | null;
|
|
8
|
+
declare function setInteractionModality(modality: Modality): void;
|
|
9
|
+
interface InteractionModalityChangeDetails {
|
|
10
|
+
/** The modality of the interaction that caused the focus to be visible. */
|
|
11
|
+
modality: Modality | null;
|
|
12
|
+
}
|
|
13
|
+
interface InteractionModalityProps {
|
|
14
|
+
/** The root element to track focus visibility for. */
|
|
15
|
+
root?: RootNode;
|
|
16
|
+
/** Callback to be called when the interaction modality changes. */
|
|
17
|
+
onChange: (details: InteractionModalityChangeDetails) => void;
|
|
18
|
+
}
|
|
19
|
+
declare function trackInteractionModality(props: InteractionModalityProps): VoidFunction;
|
|
20
|
+
declare function isFocusVisible(): boolean;
|
|
21
|
+
interface FocusVisibleChangeDetails {
|
|
22
|
+
/** Whether keyboard focus is visible globally. */
|
|
23
|
+
isFocusVisible: boolean;
|
|
24
|
+
/** The modality of the interaction that caused the focus to be visible. */
|
|
25
|
+
modality: Modality | null;
|
|
26
|
+
}
|
|
27
|
+
interface FocusVisibleProps {
|
|
28
|
+
/** The root element to track focus visibility for. */
|
|
29
|
+
root?: RootNode;
|
|
30
|
+
/** Whether the element is a text input. */
|
|
31
|
+
isTextInput?: boolean;
|
|
32
|
+
/** Whether the element will be auto focused. */
|
|
33
|
+
autoFocus?: boolean;
|
|
34
|
+
/** Callback to be called when the focus visibility changes. */
|
|
35
|
+
onChange?: (details: FocusVisibleChangeDetails) => void;
|
|
36
|
+
}
|
|
37
|
+
declare function trackFocusVisible(props?: FocusVisibleProps): VoidFunction;
|
|
7
38
|
|
|
8
|
-
export { getInteractionModality, setInteractionModality, trackFocusVisible, trackInteractionModality };
|
|
39
|
+
export { type FocusVisibleChangeDetails, type FocusVisibleProps, type InteractionModalityChangeDetails, type InteractionModalityProps, type Modality, getInteractionModality, isFocusVisible, listenerMap, setInteractionModality, trackFocusVisible, trackInteractionModality };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
type Modality = "keyboard" | "pointer" | "virtual";
|
|
2
|
-
type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
type RootNode = Document | ShadowRoot | Node;
|
|
3
|
+
interface GlobalListenerData {
|
|
4
|
+
focus: VoidFunction;
|
|
5
|
+
}
|
|
6
|
+
declare let listenerMap: Map<Window, GlobalListenerData>;
|
|
6
7
|
declare function getInteractionModality(): Modality | null;
|
|
8
|
+
declare function setInteractionModality(modality: Modality): void;
|
|
9
|
+
interface InteractionModalityChangeDetails {
|
|
10
|
+
/** The modality of the interaction that caused the focus to be visible. */
|
|
11
|
+
modality: Modality | null;
|
|
12
|
+
}
|
|
13
|
+
interface InteractionModalityProps {
|
|
14
|
+
/** The root element to track focus visibility for. */
|
|
15
|
+
root?: RootNode;
|
|
16
|
+
/** Callback to be called when the interaction modality changes. */
|
|
17
|
+
onChange: (details: InteractionModalityChangeDetails) => void;
|
|
18
|
+
}
|
|
19
|
+
declare function trackInteractionModality(props: InteractionModalityProps): VoidFunction;
|
|
20
|
+
declare function isFocusVisible(): boolean;
|
|
21
|
+
interface FocusVisibleChangeDetails {
|
|
22
|
+
/** Whether keyboard focus is visible globally. */
|
|
23
|
+
isFocusVisible: boolean;
|
|
24
|
+
/** The modality of the interaction that caused the focus to be visible. */
|
|
25
|
+
modality: Modality | null;
|
|
26
|
+
}
|
|
27
|
+
interface FocusVisibleProps {
|
|
28
|
+
/** The root element to track focus visibility for. */
|
|
29
|
+
root?: RootNode;
|
|
30
|
+
/** Whether the element is a text input. */
|
|
31
|
+
isTextInput?: boolean;
|
|
32
|
+
/** Whether the element will be auto focused. */
|
|
33
|
+
autoFocus?: boolean;
|
|
34
|
+
/** Callback to be called when the focus visibility changes. */
|
|
35
|
+
onChange?: (details: FocusVisibleChangeDetails) => void;
|
|
36
|
+
}
|
|
37
|
+
declare function trackFocusVisible(props?: FocusVisibleProps): VoidFunction;
|
|
7
38
|
|
|
8
|
-
export { getInteractionModality, setInteractionModality, trackFocusVisible, trackInteractionModality };
|
|
39
|
+
export { type FocusVisibleChangeDetails, type FocusVisibleProps, type InteractionModalityChangeDetails, type InteractionModalityProps, type Modality, getInteractionModality, isFocusVisible, listenerMap, setInteractionModality, trackFocusVisible, trackInteractionModality };
|
package/dist/index.js
CHANGED
|
@@ -21,129 +21,179 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
getInteractionModality: () => getInteractionModality,
|
|
24
|
+
isFocusVisible: () => isFocusVisible,
|
|
25
|
+
listenerMap: () => listenerMap,
|
|
24
26
|
setInteractionModality: () => setInteractionModality,
|
|
25
27
|
trackFocusVisible: () => trackFocusVisible,
|
|
26
28
|
trackInteractionModality: () => trackInteractionModality
|
|
27
29
|
});
|
|
28
30
|
module.exports = __toCommonJS(src_exports);
|
|
29
31
|
var import_dom_query = require("@zag-js/dom-query");
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
var hasBlurredWindowRecently = false;
|
|
34
|
-
var handlers = /* @__PURE__ */ new Set();
|
|
35
|
-
function trigger(modality2, event) {
|
|
36
|
-
handlers.forEach((handler) => handler(modality2, event));
|
|
32
|
+
function isVirtualClick(event) {
|
|
33
|
+
if (event.mozInputSource === 0 && event.isTrusted) return true;
|
|
34
|
+
return event.detail === 0 && !event.pointerType;
|
|
37
35
|
}
|
|
38
|
-
var isMac = typeof window !== "undefined" && window.navigator != null ? /^Mac/.test(window.navigator.platform) : false;
|
|
39
36
|
function isValidKey(e) {
|
|
40
|
-
return !(e.metaKey || !isMac && e.altKey || e.ctrlKey || e.key === "Control" || e.key === "Shift" || e.key === "Meta");
|
|
37
|
+
return !(e.metaKey || !(0, import_dom_query.isMac)() && e.altKey || e.ctrlKey || e.key === "Control" || e.key === "Shift" || e.key === "Meta");
|
|
41
38
|
}
|
|
42
|
-
|
|
39
|
+
var nonTextInputTypes = /* @__PURE__ */ new Set(["checkbox", "radio", "range", "color", "file", "image", "button", "submit", "reset"]);
|
|
40
|
+
function isKeyboardFocusEvent(isTextInput, modality, e) {
|
|
41
|
+
const IHTMLInputElement = typeof window !== "undefined" ? (0, import_dom_query.getWindow)(e?.target).HTMLInputElement : HTMLInputElement;
|
|
42
|
+
const IHTMLTextAreaElement = typeof window !== "undefined" ? (0, import_dom_query.getWindow)(e?.target).HTMLTextAreaElement : HTMLTextAreaElement;
|
|
43
|
+
const IHTMLElement = typeof window !== "undefined" ? (0, import_dom_query.getWindow)(e?.target).HTMLElement : HTMLElement;
|
|
44
|
+
const IKeyboardEvent = typeof window !== "undefined" ? (0, import_dom_query.getWindow)(e?.target).KeyboardEvent : KeyboardEvent;
|
|
45
|
+
isTextInput = isTextInput || e?.target instanceof IHTMLInputElement && !nonTextInputTypes.has(e?.target?.type) || e?.target instanceof IHTMLTextAreaElement || e?.target instanceof IHTMLElement && e?.target.isContentEditable;
|
|
46
|
+
return !(isTextInput && modality === "keyboard" && e instanceof IKeyboardEvent && !Reflect.has(FOCUS_VISIBLE_INPUT_KEYS, e.key));
|
|
47
|
+
}
|
|
48
|
+
var currentModality = null;
|
|
49
|
+
var changeHandlers = /* @__PURE__ */ new Set();
|
|
50
|
+
var listenerMap = /* @__PURE__ */ new Map();
|
|
51
|
+
var hasEventBeforeFocus = false;
|
|
52
|
+
var hasBlurredWindowRecently = false;
|
|
53
|
+
var FOCUS_VISIBLE_INPUT_KEYS = {
|
|
54
|
+
Tab: true,
|
|
55
|
+
Escape: true
|
|
56
|
+
};
|
|
57
|
+
function triggerChangeHandlers(modality, e) {
|
|
58
|
+
for (let handler of changeHandlers) {
|
|
59
|
+
handler(modality, e);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function handleKeyboardEvent(e) {
|
|
43
63
|
hasEventBeforeFocus = true;
|
|
44
|
-
if (isValidKey(
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
if (isValidKey(e)) {
|
|
65
|
+
currentModality = "keyboard";
|
|
66
|
+
triggerChangeHandlers("keyboard", e);
|
|
47
67
|
}
|
|
48
68
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
if (
|
|
69
|
+
function handlePointerEvent(e) {
|
|
70
|
+
currentModality = "pointer";
|
|
71
|
+
if (e.type === "mousedown" || e.type === "pointerdown") {
|
|
52
72
|
hasEventBeforeFocus = true;
|
|
53
|
-
|
|
54
|
-
let matches = false;
|
|
55
|
-
try {
|
|
56
|
-
matches = target.matches(":focus-visible");
|
|
57
|
-
} catch {
|
|
58
|
-
}
|
|
59
|
-
if (matches) return;
|
|
60
|
-
trigger("pointer", event);
|
|
73
|
+
triggerChangeHandlers("pointer", e);
|
|
61
74
|
}
|
|
62
75
|
}
|
|
63
|
-
function
|
|
64
|
-
if (event.mozInputSource === 0 && event.isTrusted) return true;
|
|
65
|
-
return event.detail === 0 && !event.pointerType;
|
|
66
|
-
}
|
|
67
|
-
function onClickEvent(e) {
|
|
76
|
+
function handleClickEvent(e) {
|
|
68
77
|
if (isVirtualClick(e)) {
|
|
69
78
|
hasEventBeforeFocus = true;
|
|
70
|
-
|
|
79
|
+
currentModality = "virtual";
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
|
-
function
|
|
74
|
-
if (
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if (event.target instanceof Element && event.target.hasAttribute("tabindex")) {
|
|
82
|
+
function handleFocusEvent(e) {
|
|
83
|
+
if (e.target === (0, import_dom_query.getWindow)(e.target) || e.target === (0, import_dom_query.getDocument)(e.target)) {
|
|
78
84
|
return;
|
|
79
85
|
}
|
|
80
86
|
if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
currentModality = "virtual";
|
|
88
|
+
triggerChangeHandlers("virtual", e);
|
|
83
89
|
}
|
|
84
90
|
hasEventBeforeFocus = false;
|
|
85
91
|
hasBlurredWindowRecently = false;
|
|
86
92
|
}
|
|
87
|
-
function
|
|
93
|
+
function handleWindowBlur() {
|
|
88
94
|
hasEventBeforeFocus = false;
|
|
89
95
|
hasBlurredWindowRecently = true;
|
|
90
96
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
function setupGlobalFocusEvents() {
|
|
95
|
-
if (!(0, import_dom_query.isDom)() || hasSetup) {
|
|
97
|
+
function setupGlobalFocusEvents(root) {
|
|
98
|
+
if (typeof window === "undefined" || listenerMap.get((0, import_dom_query.getWindow)(root))) {
|
|
96
99
|
return;
|
|
97
100
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
101
|
+
const win = (0, import_dom_query.getWindow)(root);
|
|
102
|
+
const doc = (0, import_dom_query.getDocument)(root);
|
|
103
|
+
let focus = win.HTMLElement.prototype.focus;
|
|
104
|
+
win.HTMLElement.prototype.focus = function() {
|
|
105
|
+
currentModality = "virtual";
|
|
106
|
+
triggerChangeHandlers("virtual", null);
|
|
100
107
|
hasEventBeforeFocus = true;
|
|
101
|
-
focus.apply(this,
|
|
108
|
+
focus.apply(this, arguments);
|
|
102
109
|
};
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (typeof PointerEvent !== "undefined") {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
doc.addEventListener("keydown", handleKeyboardEvent, true);
|
|
111
|
+
doc.addEventListener("keyup", handleKeyboardEvent, true);
|
|
112
|
+
doc.addEventListener("click", handleClickEvent, true);
|
|
113
|
+
win.addEventListener("focus", handleFocusEvent, true);
|
|
114
|
+
win.addEventListener("blur", handleWindowBlur, false);
|
|
115
|
+
if (typeof win.PointerEvent !== "undefined") {
|
|
116
|
+
doc.addEventListener("pointerdown", handlePointerEvent, true);
|
|
117
|
+
doc.addEventListener("pointermove", handlePointerEvent, true);
|
|
118
|
+
doc.addEventListener("pointerup", handlePointerEvent, true);
|
|
112
119
|
} else {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
doc.addEventListener("mousedown", handlePointerEvent, true);
|
|
121
|
+
doc.addEventListener("mousemove", handlePointerEvent, true);
|
|
122
|
+
doc.addEventListener("mouseup", handlePointerEvent, true);
|
|
116
123
|
}
|
|
117
|
-
|
|
124
|
+
win.addEventListener(
|
|
125
|
+
"beforeunload",
|
|
126
|
+
() => {
|
|
127
|
+
tearDownWindowFocusTracking(root);
|
|
128
|
+
},
|
|
129
|
+
{ once: true }
|
|
130
|
+
);
|
|
131
|
+
listenerMap.set(win, { focus });
|
|
118
132
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
var tearDownWindowFocusTracking = (root, loadListener) => {
|
|
134
|
+
const win = (0, import_dom_query.getWindow)(root);
|
|
135
|
+
const doc = (0, import_dom_query.getDocument)(root);
|
|
136
|
+
if (loadListener) {
|
|
137
|
+
doc.removeEventListener("DOMContentLoaded", loadListener);
|
|
138
|
+
}
|
|
139
|
+
if (!listenerMap.has(win)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
win.HTMLElement.prototype.focus = listenerMap.get(win).focus;
|
|
143
|
+
doc.removeEventListener("keydown", handleKeyboardEvent, true);
|
|
144
|
+
doc.removeEventListener("keyup", handleKeyboardEvent, true);
|
|
145
|
+
doc.removeEventListener("click", handleClickEvent, true);
|
|
146
|
+
win.removeEventListener("focus", handleFocusEvent, true);
|
|
147
|
+
win.removeEventListener("blur", handleWindowBlur, false);
|
|
148
|
+
if (typeof win.PointerEvent !== "undefined") {
|
|
149
|
+
doc.removeEventListener("pointerdown", handlePointerEvent, true);
|
|
150
|
+
doc.removeEventListener("pointermove", handlePointerEvent, true);
|
|
151
|
+
doc.removeEventListener("pointerup", handlePointerEvent, true);
|
|
152
|
+
} else {
|
|
153
|
+
doc.removeEventListener("mousedown", handlePointerEvent, true);
|
|
154
|
+
doc.removeEventListener("mousemove", handlePointerEvent, true);
|
|
155
|
+
doc.removeEventListener("mouseup", handlePointerEvent, true);
|
|
156
|
+
}
|
|
157
|
+
listenerMap.delete(win);
|
|
158
|
+
};
|
|
159
|
+
function getInteractionModality() {
|
|
160
|
+
return currentModality;
|
|
127
161
|
}
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
162
|
+
function setInteractionModality(modality) {
|
|
163
|
+
currentModality = modality;
|
|
164
|
+
triggerChangeHandlers(modality, null);
|
|
165
|
+
}
|
|
166
|
+
function trackInteractionModality(props) {
|
|
167
|
+
const { onChange, root } = props;
|
|
168
|
+
setupGlobalFocusEvents(root);
|
|
169
|
+
onChange({ modality: currentModality });
|
|
170
|
+
const handler = () => onChange({ modality: currentModality });
|
|
171
|
+
changeHandlers.add(handler);
|
|
133
172
|
return () => {
|
|
134
|
-
|
|
173
|
+
changeHandlers.delete(handler);
|
|
135
174
|
};
|
|
136
175
|
}
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
trigger(value, null);
|
|
176
|
+
function isFocusVisible() {
|
|
177
|
+
return currentModality === "keyboard";
|
|
140
178
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
179
|
+
function trackFocusVisible(props = {}) {
|
|
180
|
+
const { isTextInput, autoFocus, onChange, root } = props;
|
|
181
|
+
setupGlobalFocusEvents(root);
|
|
182
|
+
onChange?.({ isFocusVisible: autoFocus || isFocusVisible(), modality: currentModality });
|
|
183
|
+
const handler = (modality, e) => {
|
|
184
|
+
if (!isKeyboardFocusEvent(!!isTextInput, modality, e)) return;
|
|
185
|
+
onChange?.({ isFocusVisible: isFocusVisible(), modality });
|
|
186
|
+
};
|
|
187
|
+
changeHandlers.add(handler);
|
|
188
|
+
return () => {
|
|
189
|
+
changeHandlers.delete(handler);
|
|
190
|
+
};
|
|
143
191
|
}
|
|
144
192
|
// Annotate the CommonJS export names for ESM import in node:
|
|
145
193
|
0 && (module.exports = {
|
|
146
194
|
getInteractionModality,
|
|
195
|
+
isFocusVisible,
|
|
196
|
+
listenerMap,
|
|
147
197
|
setInteractionModality,
|
|
148
198
|
trackFocusVisible,
|
|
149
199
|
trackInteractionModality
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { isDom } from \"@zag-js/dom-query\"\n\ntype Modality = \"keyboard\" | \"pointer\" | \"virtual\"\ntype HandlerEvent = PointerEvent | MouseEvent | KeyboardEvent | FocusEvent\ntype Handler = (modality: Modality, e: HandlerEvent | null) => void\ntype FocusVisibleCallback = (isFocusVisible: boolean) => void\n\nlet hasSetup = false\nlet modality: Modality | null = null\nlet hasEventBeforeFocus = false\nlet hasBlurredWindowRecently = false\n\nconst handlers = new Set<Handler>()\n\nfunction trigger(modality: Modality, event: HandlerEvent | null) {\n handlers.forEach((handler) => handler(modality, event))\n}\n\nconst isMac = typeof window !== \"undefined\" && window.navigator != null ? /^Mac/.test(window.navigator.platform) : false\n\nfunction isValidKey(e: KeyboardEvent) {\n return !(\n e.metaKey ||\n (!isMac && e.altKey) ||\n e.ctrlKey ||\n e.key === \"Control\" ||\n e.key === \"Shift\" ||\n e.key === \"Meta\"\n )\n}\n\nfunction onKeyboardEvent(event: KeyboardEvent) {\n hasEventBeforeFocus = true\n if (isValidKey(event)) {\n modality = \"keyboard\"\n trigger(\"keyboard\", event)\n }\n}\n\nfunction onPointerEvent(event: PointerEvent | MouseEvent) {\n modality = \"pointer\"\n\n if (event.type === \"mousedown\" || event.type === \"pointerdown\") {\n hasEventBeforeFocus = true\n const target = event.composedPath ? event.composedPath()[0] : event.target\n\n let matches = false\n try {\n matches = (target as any).matches(\":focus-visible\")\n } catch {}\n\n if (matches) return\n trigger(\"pointer\", event)\n }\n}\n\nfunction isVirtualClick(event: MouseEvent | PointerEvent): boolean {\n // JAWS/NVDA with Firefox.\n if ((event as any).mozInputSource === 0 && event.isTrusted) return true\n return event.detail === 0 && !(event as PointerEvent).pointerType\n}\n\nfunction onClickEvent(e: MouseEvent) {\n if (isVirtualClick(e)) {\n hasEventBeforeFocus = true\n modality = \"virtual\"\n }\n}\n\nfunction onWindowFocus(event: FocusEvent) {\n // Firefox fires two extra focus events when the user first clicks into an iframe:\n // first on the window, then on the document. We ignore these events so they don't\n // cause keyboard focus rings to appear.\n if (event.target === window || event.target === document) {\n return\n }\n\n // An extra event is fired when the user first clicks inside an element with tabindex attribute.\n // We ignore these events so they don't cause keyboard focus ring to appear.\n if (event.target instanceof Element && event.target.hasAttribute(\"tabindex\")) {\n return\n }\n\n // If a focus event occurs without a preceding keyboard or pointer event, switch to keyboard modality.\n // This occurs, for example, when navigating a form with the next/previous buttons on iOS.\n if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {\n modality = \"virtual\"\n trigger(\"virtual\", event)\n }\n\n hasEventBeforeFocus = false\n hasBlurredWindowRecently = false\n}\n\nfunction onWindowBlur() {\n // When the window is blurred, reset state. This is necessary when tabbing out of the window,\n // for example, since a subsequent focus event won't be fired.\n hasEventBeforeFocus = false\n hasBlurredWindowRecently = true\n}\n\nfunction isFocusVisible() {\n return modality !== \"pointer\"\n}\n\nfunction setupGlobalFocusEvents() {\n if (!isDom() || hasSetup) {\n return\n }\n\n // Programmatic focus() calls shouldn't affect the current input modality.\n // However, we need to detect other cases when a focus event occurs without\n // a preceding user event (e.g. screen reader focus). Overriding the focus\n // method on HTMLElement.prototype is a bit hacky, but works.\n const { focus } = HTMLElement.prototype\n HTMLElement.prototype.focus = function focusElement(...args) {\n hasEventBeforeFocus = true\n focus.apply(this, args)\n }\n\n document.addEventListener(\"keydown\", onKeyboardEvent, true)\n document.addEventListener(\"keyup\", onKeyboardEvent, true)\n document.addEventListener(\"click\", onClickEvent, true)\n\n // Register focus events on the window so they are sure to happen\n // before React's event listeners (registered on the document).\n window.addEventListener(\"focus\", onWindowFocus, true)\n window.addEventListener(\"blur\", onWindowBlur, false)\n\n if (typeof PointerEvent !== \"undefined\") {\n document.addEventListener(\"pointerdown\", onPointerEvent, true)\n document.addEventListener(\"pointermove\", onPointerEvent, true)\n document.addEventListener(\"pointerup\", onPointerEvent, true)\n } else {\n document.addEventListener(\"mousedown\", onPointerEvent, true)\n document.addEventListener(\"mousemove\", onPointerEvent, true)\n document.addEventListener(\"mouseup\", onPointerEvent, true)\n }\n\n hasSetup = true\n}\n\nexport function trackFocusVisible(fn: FocusVisibleCallback) {\n setupGlobalFocusEvents()\n\n fn(isFocusVisible())\n const handler = () => fn(isFocusVisible())\n\n handlers.add(handler)\n return () => {\n handlers.delete(handler)\n }\n}\n\nexport function trackInteractionModality(fn: (value: Modality | null) => void) {\n setupGlobalFocusEvents()\n\n fn(modality)\n const handler = () => fn(modality)\n\n handlers.add(handler)\n return () => {\n handlers.delete(handler)\n }\n}\n\nexport function setInteractionModality(value: Modality) {\n modality = value\n trigger(value, null)\n}\n\nexport function getInteractionModality() {\n return modality\n}\n"],"mappings":"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","names":["modality"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Credit: Huge props to the team at Adobe for inspiring this implementation.\n * https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/interactions/src/useFocusVisible.ts\n */\nimport { getDocument, getWindow, isMac } from \"@zag-js/dom-query\"\n\nfunction isVirtualClick(event: MouseEvent | PointerEvent): boolean {\n if ((event as any).mozInputSource === 0 && event.isTrusted) return true\n return event.detail === 0 && !(event as PointerEvent).pointerType\n}\n\nfunction isValidKey(e: KeyboardEvent) {\n return !(\n e.metaKey ||\n (!isMac() && e.altKey) ||\n e.ctrlKey ||\n e.key === \"Control\" ||\n e.key === \"Shift\" ||\n e.key === \"Meta\"\n )\n}\n\nconst nonTextInputTypes = new Set([\"checkbox\", \"radio\", \"range\", \"color\", \"file\", \"image\", \"button\", \"submit\", \"reset\"])\n\nfunction isKeyboardFocusEvent(isTextInput: boolean, modality: Modality, e: HandlerEvent) {\n const IHTMLInputElement =\n typeof window !== \"undefined\" ? getWindow(e?.target as Element).HTMLInputElement : HTMLInputElement\n const IHTMLTextAreaElement =\n typeof window !== \"undefined\" ? getWindow(e?.target as Element).HTMLTextAreaElement : HTMLTextAreaElement\n const IHTMLElement = typeof window !== \"undefined\" ? getWindow(e?.target as Element).HTMLElement : HTMLElement\n const IKeyboardEvent = typeof window !== \"undefined\" ? getWindow(e?.target as Element).KeyboardEvent : KeyboardEvent\n\n isTextInput =\n isTextInput ||\n (e?.target instanceof IHTMLInputElement && !nonTextInputTypes.has(e?.target?.type)) ||\n e?.target instanceof IHTMLTextAreaElement ||\n (e?.target instanceof IHTMLElement && e?.target.isContentEditable)\n\n return !(\n isTextInput &&\n modality === \"keyboard\" &&\n e instanceof IKeyboardEvent &&\n !Reflect.has(FOCUS_VISIBLE_INPUT_KEYS, e.key)\n )\n}\n\n/////////////////////////////////////////////////////////////////////////////////////////////\n\nexport type Modality = \"keyboard\" | \"pointer\" | \"virtual\"\n\ntype RootNode = Document | ShadowRoot | Node\n\ntype HandlerEvent = PointerEvent | MouseEvent | KeyboardEvent | FocusEvent | null\n\ntype Handler = (modality: Modality, e: HandlerEvent) => void\n\n/////////////////////////////////////////////////////////////////////////////////////////////\n\nlet currentModality: Modality | null = null\n\nlet changeHandlers = new Set<Handler>()\n\ninterface GlobalListenerData {\n focus: VoidFunction\n}\n\nexport let listenerMap = new Map<Window, GlobalListenerData>()\n\nlet hasEventBeforeFocus = false\nlet hasBlurredWindowRecently = false\n\n// Only Tab or Esc keys will make focus visible on text input elements\nconst FOCUS_VISIBLE_INPUT_KEYS = {\n Tab: true,\n Escape: true,\n}\n\nfunction triggerChangeHandlers(modality: Modality, e: HandlerEvent) {\n for (let handler of changeHandlers) {\n handler(modality, e)\n }\n}\n\nfunction handleKeyboardEvent(e: KeyboardEvent) {\n hasEventBeforeFocus = true\n if (isValidKey(e)) {\n currentModality = \"keyboard\"\n triggerChangeHandlers(\"keyboard\", e)\n }\n}\n\nfunction handlePointerEvent(e: PointerEvent | MouseEvent) {\n currentModality = \"pointer\"\n if (e.type === \"mousedown\" || e.type === \"pointerdown\") {\n hasEventBeforeFocus = true\n triggerChangeHandlers(\"pointer\", e)\n }\n}\n\nfunction handleClickEvent(e: MouseEvent) {\n if (isVirtualClick(e)) {\n hasEventBeforeFocus = true\n currentModality = \"virtual\"\n }\n}\n\nfunction handleFocusEvent(e: FocusEvent) {\n // Firefox fires two extra focus events when the user first clicks into an iframe:\n // first on the window, then on the document. We ignore these events so they don't\n // cause keyboard focus rings to appear.\n if (e.target === getWindow(e.target as Element) || e.target === getDocument(e.target as Element)) {\n return\n }\n\n // If a focus event occurs without a preceding keyboard or pointer event, switch to virtual modality.\n // This occurs, for example, when navigating a form with the next/previous buttons on iOS.\n if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {\n currentModality = \"virtual\"\n triggerChangeHandlers(\"virtual\", e)\n }\n\n hasEventBeforeFocus = false\n hasBlurredWindowRecently = false\n}\n\nfunction handleWindowBlur() {\n // When the window is blurred, reset state. This is necessary when tabbing out of the window,\n // for example, since a subsequent focus event won't be fired.\n hasEventBeforeFocus = false\n hasBlurredWindowRecently = true\n}\n\n/**\n * Setup global event listeners to control when keyboard focus style should be visible.\n */\nfunction setupGlobalFocusEvents(root?: RootNode) {\n if (typeof window === \"undefined\" || listenerMap.get(getWindow(root))) {\n return\n }\n\n const win = getWindow(root)\n const doc = getDocument(root)\n\n let focus = win.HTMLElement.prototype.focus\n win.HTMLElement.prototype.focus = function () {\n // For programmatic focus, we remove the focus visible state to prevent showing focus rings\n // When `options.focusVisible` is supported in most browsers, we can remove this\n // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#focusvisible\n currentModality = \"virtual\"\n triggerChangeHandlers(\"virtual\", null)\n\n hasEventBeforeFocus = true\n focus.apply(this, arguments as unknown as [options?: FocusOptions | undefined])\n }\n\n doc.addEventListener(\"keydown\", handleKeyboardEvent, true)\n doc.addEventListener(\"keyup\", handleKeyboardEvent, true)\n doc.addEventListener(\"click\", handleClickEvent, true)\n\n win.addEventListener(\"focus\", handleFocusEvent, true)\n win.addEventListener(\"blur\", handleWindowBlur, false)\n\n if (typeof win.PointerEvent !== \"undefined\") {\n doc.addEventListener(\"pointerdown\", handlePointerEvent, true)\n doc.addEventListener(\"pointermove\", handlePointerEvent, true)\n doc.addEventListener(\"pointerup\", handlePointerEvent, true)\n } else {\n doc.addEventListener(\"mousedown\", handlePointerEvent, true)\n doc.addEventListener(\"mousemove\", handlePointerEvent, true)\n doc.addEventListener(\"mouseup\", handlePointerEvent, true)\n }\n\n // Add unmount handler\n win.addEventListener(\n \"beforeunload\",\n () => {\n tearDownWindowFocusTracking(root)\n },\n { once: true },\n )\n\n listenerMap.set(win, { focus })\n}\n\nconst tearDownWindowFocusTracking = (root?: RootNode, loadListener?: () => void) => {\n const win = getWindow(root)\n const doc = getDocument(root)\n\n if (loadListener) {\n doc.removeEventListener(\"DOMContentLoaded\", loadListener)\n }\n\n if (!listenerMap.has(win)) {\n return\n }\n\n win.HTMLElement.prototype.focus = listenerMap.get(win)!.focus\n\n doc.removeEventListener(\"keydown\", handleKeyboardEvent, true)\n doc.removeEventListener(\"keyup\", handleKeyboardEvent, true)\n doc.removeEventListener(\"click\", handleClickEvent, true)\n win.removeEventListener(\"focus\", handleFocusEvent, true)\n win.removeEventListener(\"blur\", handleWindowBlur, false)\n\n if (typeof win.PointerEvent !== \"undefined\") {\n doc.removeEventListener(\"pointerdown\", handlePointerEvent, true)\n doc.removeEventListener(\"pointermove\", handlePointerEvent, true)\n doc.removeEventListener(\"pointerup\", handlePointerEvent, true)\n } else {\n doc.removeEventListener(\"mousedown\", handlePointerEvent, true)\n doc.removeEventListener(\"mousemove\", handlePointerEvent, true)\n doc.removeEventListener(\"mouseup\", handlePointerEvent, true)\n }\n\n listenerMap.delete(win)\n}\n\n/////////////////////////////////////////////////////////////////////////////////////////////\n\nexport function getInteractionModality(): Modality | null {\n return currentModality\n}\n\nexport function setInteractionModality(modality: Modality) {\n currentModality = modality\n triggerChangeHandlers(modality, null)\n}\n\nexport interface InteractionModalityChangeDetails {\n /** The modality of the interaction that caused the focus to be visible. */\n modality: Modality | null\n}\n\nexport interface InteractionModalityProps {\n /** The root element to track focus visibility for. */\n root?: RootNode\n /** Callback to be called when the interaction modality changes. */\n onChange: (details: InteractionModalityChangeDetails) => void\n}\n\nexport function trackInteractionModality(props: InteractionModalityProps): VoidFunction {\n const { onChange, root } = props\n\n setupGlobalFocusEvents(root)\n\n onChange({ modality: currentModality })\n\n const handler = () => onChange({ modality: currentModality })\n\n changeHandlers.add(handler)\n return () => {\n changeHandlers.delete(handler)\n }\n}\n\n/////////////////////////////////////////////////////////////////////////////////////////////\n\nexport function isFocusVisible(): boolean {\n return currentModality === \"keyboard\"\n}\n\nexport interface FocusVisibleChangeDetails {\n /** Whether keyboard focus is visible globally. */\n isFocusVisible: boolean\n /** The modality of the interaction that caused the focus to be visible. */\n modality: Modality | null\n}\n\nexport interface FocusVisibleProps {\n /** The root element to track focus visibility for. */\n root?: RootNode\n /** Whether the element is a text input. */\n isTextInput?: boolean\n /** Whether the element will be auto focused. */\n autoFocus?: boolean\n /** Callback to be called when the focus visibility changes. */\n onChange?: (details: FocusVisibleChangeDetails) => void\n}\n\nexport function trackFocusVisible(props: FocusVisibleProps = {}): VoidFunction {\n const { isTextInput, autoFocus, onChange, root } = props\n\n setupGlobalFocusEvents(root)\n\n onChange?.({ isFocusVisible: autoFocus || isFocusVisible(), modality: currentModality })\n\n const handler = (modality: Modality, e: HandlerEvent) => {\n if (!isKeyboardFocusEvent(!!isTextInput, modality, e)) return\n onChange?.({ isFocusVisible: isFocusVisible(), modality })\n }\n\n changeHandlers.add(handler)\n\n return () => {\n changeHandlers.delete(handler)\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,121 +1,169 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
var hasBlurredWindowRecently = false;
|
|
7
|
-
var handlers = /* @__PURE__ */ new Set();
|
|
8
|
-
function trigger(modality2, event) {
|
|
9
|
-
handlers.forEach((handler) => handler(modality2, event));
|
|
2
|
+
import { getDocument, getWindow, isMac } from "@zag-js/dom-query";
|
|
3
|
+
function isVirtualClick(event) {
|
|
4
|
+
if (event.mozInputSource === 0 && event.isTrusted) return true;
|
|
5
|
+
return event.detail === 0 && !event.pointerType;
|
|
10
6
|
}
|
|
11
|
-
var isMac = typeof window !== "undefined" && window.navigator != null ? /^Mac/.test(window.navigator.platform) : false;
|
|
12
7
|
function isValidKey(e) {
|
|
13
|
-
return !(e.metaKey || !isMac && e.altKey || e.ctrlKey || e.key === "Control" || e.key === "Shift" || e.key === "Meta");
|
|
8
|
+
return !(e.metaKey || !isMac() && e.altKey || e.ctrlKey || e.key === "Control" || e.key === "Shift" || e.key === "Meta");
|
|
14
9
|
}
|
|
15
|
-
|
|
10
|
+
var nonTextInputTypes = /* @__PURE__ */ new Set(["checkbox", "radio", "range", "color", "file", "image", "button", "submit", "reset"]);
|
|
11
|
+
function isKeyboardFocusEvent(isTextInput, modality, e) {
|
|
12
|
+
const IHTMLInputElement = typeof window !== "undefined" ? getWindow(e?.target).HTMLInputElement : HTMLInputElement;
|
|
13
|
+
const IHTMLTextAreaElement = typeof window !== "undefined" ? getWindow(e?.target).HTMLTextAreaElement : HTMLTextAreaElement;
|
|
14
|
+
const IHTMLElement = typeof window !== "undefined" ? getWindow(e?.target).HTMLElement : HTMLElement;
|
|
15
|
+
const IKeyboardEvent = typeof window !== "undefined" ? getWindow(e?.target).KeyboardEvent : KeyboardEvent;
|
|
16
|
+
isTextInput = isTextInput || e?.target instanceof IHTMLInputElement && !nonTextInputTypes.has(e?.target?.type) || e?.target instanceof IHTMLTextAreaElement || e?.target instanceof IHTMLElement && e?.target.isContentEditable;
|
|
17
|
+
return !(isTextInput && modality === "keyboard" && e instanceof IKeyboardEvent && !Reflect.has(FOCUS_VISIBLE_INPUT_KEYS, e.key));
|
|
18
|
+
}
|
|
19
|
+
var currentModality = null;
|
|
20
|
+
var changeHandlers = /* @__PURE__ */ new Set();
|
|
21
|
+
var listenerMap = /* @__PURE__ */ new Map();
|
|
22
|
+
var hasEventBeforeFocus = false;
|
|
23
|
+
var hasBlurredWindowRecently = false;
|
|
24
|
+
var FOCUS_VISIBLE_INPUT_KEYS = {
|
|
25
|
+
Tab: true,
|
|
26
|
+
Escape: true
|
|
27
|
+
};
|
|
28
|
+
function triggerChangeHandlers(modality, e) {
|
|
29
|
+
for (let handler of changeHandlers) {
|
|
30
|
+
handler(modality, e);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function handleKeyboardEvent(e) {
|
|
16
34
|
hasEventBeforeFocus = true;
|
|
17
|
-
if (isValidKey(
|
|
18
|
-
|
|
19
|
-
|
|
35
|
+
if (isValidKey(e)) {
|
|
36
|
+
currentModality = "keyboard";
|
|
37
|
+
triggerChangeHandlers("keyboard", e);
|
|
20
38
|
}
|
|
21
39
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
if (
|
|
40
|
+
function handlePointerEvent(e) {
|
|
41
|
+
currentModality = "pointer";
|
|
42
|
+
if (e.type === "mousedown" || e.type === "pointerdown") {
|
|
25
43
|
hasEventBeforeFocus = true;
|
|
26
|
-
|
|
27
|
-
let matches = false;
|
|
28
|
-
try {
|
|
29
|
-
matches = target.matches(":focus-visible");
|
|
30
|
-
} catch {
|
|
31
|
-
}
|
|
32
|
-
if (matches) return;
|
|
33
|
-
trigger("pointer", event);
|
|
44
|
+
triggerChangeHandlers("pointer", e);
|
|
34
45
|
}
|
|
35
46
|
}
|
|
36
|
-
function
|
|
37
|
-
if (event.mozInputSource === 0 && event.isTrusted) return true;
|
|
38
|
-
return event.detail === 0 && !event.pointerType;
|
|
39
|
-
}
|
|
40
|
-
function onClickEvent(e) {
|
|
47
|
+
function handleClickEvent(e) {
|
|
41
48
|
if (isVirtualClick(e)) {
|
|
42
49
|
hasEventBeforeFocus = true;
|
|
43
|
-
|
|
50
|
+
currentModality = "virtual";
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
|
-
function
|
|
47
|
-
if (
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (event.target instanceof Element && event.target.hasAttribute("tabindex")) {
|
|
53
|
+
function handleFocusEvent(e) {
|
|
54
|
+
if (e.target === getWindow(e.target) || e.target === getDocument(e.target)) {
|
|
51
55
|
return;
|
|
52
56
|
}
|
|
53
57
|
if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
currentModality = "virtual";
|
|
59
|
+
triggerChangeHandlers("virtual", e);
|
|
56
60
|
}
|
|
57
61
|
hasEventBeforeFocus = false;
|
|
58
62
|
hasBlurredWindowRecently = false;
|
|
59
63
|
}
|
|
60
|
-
function
|
|
64
|
+
function handleWindowBlur() {
|
|
61
65
|
hasEventBeforeFocus = false;
|
|
62
66
|
hasBlurredWindowRecently = true;
|
|
63
67
|
}
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
function setupGlobalFocusEvents() {
|
|
68
|
-
if (!isDom() || hasSetup) {
|
|
68
|
+
function setupGlobalFocusEvents(root) {
|
|
69
|
+
if (typeof window === "undefined" || listenerMap.get(getWindow(root))) {
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
72
|
+
const win = getWindow(root);
|
|
73
|
+
const doc = getDocument(root);
|
|
74
|
+
let focus = win.HTMLElement.prototype.focus;
|
|
75
|
+
win.HTMLElement.prototype.focus = function() {
|
|
76
|
+
currentModality = "virtual";
|
|
77
|
+
triggerChangeHandlers("virtual", null);
|
|
73
78
|
hasEventBeforeFocus = true;
|
|
74
|
-
focus.apply(this,
|
|
79
|
+
focus.apply(this, arguments);
|
|
75
80
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (typeof PointerEvent !== "undefined") {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
doc.addEventListener("keydown", handleKeyboardEvent, true);
|
|
82
|
+
doc.addEventListener("keyup", handleKeyboardEvent, true);
|
|
83
|
+
doc.addEventListener("click", handleClickEvent, true);
|
|
84
|
+
win.addEventListener("focus", handleFocusEvent, true);
|
|
85
|
+
win.addEventListener("blur", handleWindowBlur, false);
|
|
86
|
+
if (typeof win.PointerEvent !== "undefined") {
|
|
87
|
+
doc.addEventListener("pointerdown", handlePointerEvent, true);
|
|
88
|
+
doc.addEventListener("pointermove", handlePointerEvent, true);
|
|
89
|
+
doc.addEventListener("pointerup", handlePointerEvent, true);
|
|
85
90
|
} else {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
doc.addEventListener("mousedown", handlePointerEvent, true);
|
|
92
|
+
doc.addEventListener("mousemove", handlePointerEvent, true);
|
|
93
|
+
doc.addEventListener("mouseup", handlePointerEvent, true);
|
|
89
94
|
}
|
|
90
|
-
|
|
95
|
+
win.addEventListener(
|
|
96
|
+
"beforeunload",
|
|
97
|
+
() => {
|
|
98
|
+
tearDownWindowFocusTracking(root);
|
|
99
|
+
},
|
|
100
|
+
{ once: true }
|
|
101
|
+
);
|
|
102
|
+
listenerMap.set(win, { focus });
|
|
91
103
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
var tearDownWindowFocusTracking = (root, loadListener) => {
|
|
105
|
+
const win = getWindow(root);
|
|
106
|
+
const doc = getDocument(root);
|
|
107
|
+
if (loadListener) {
|
|
108
|
+
doc.removeEventListener("DOMContentLoaded", loadListener);
|
|
109
|
+
}
|
|
110
|
+
if (!listenerMap.has(win)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
win.HTMLElement.prototype.focus = listenerMap.get(win).focus;
|
|
114
|
+
doc.removeEventListener("keydown", handleKeyboardEvent, true);
|
|
115
|
+
doc.removeEventListener("keyup", handleKeyboardEvent, true);
|
|
116
|
+
doc.removeEventListener("click", handleClickEvent, true);
|
|
117
|
+
win.removeEventListener("focus", handleFocusEvent, true);
|
|
118
|
+
win.removeEventListener("blur", handleWindowBlur, false);
|
|
119
|
+
if (typeof win.PointerEvent !== "undefined") {
|
|
120
|
+
doc.removeEventListener("pointerdown", handlePointerEvent, true);
|
|
121
|
+
doc.removeEventListener("pointermove", handlePointerEvent, true);
|
|
122
|
+
doc.removeEventListener("pointerup", handlePointerEvent, true);
|
|
123
|
+
} else {
|
|
124
|
+
doc.removeEventListener("mousedown", handlePointerEvent, true);
|
|
125
|
+
doc.removeEventListener("mousemove", handlePointerEvent, true);
|
|
126
|
+
doc.removeEventListener("mouseup", handlePointerEvent, true);
|
|
127
|
+
}
|
|
128
|
+
listenerMap.delete(win);
|
|
129
|
+
};
|
|
130
|
+
function getInteractionModality() {
|
|
131
|
+
return currentModality;
|
|
100
132
|
}
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
function setInteractionModality(modality) {
|
|
134
|
+
currentModality = modality;
|
|
135
|
+
triggerChangeHandlers(modality, null);
|
|
136
|
+
}
|
|
137
|
+
function trackInteractionModality(props) {
|
|
138
|
+
const { onChange, root } = props;
|
|
139
|
+
setupGlobalFocusEvents(root);
|
|
140
|
+
onChange({ modality: currentModality });
|
|
141
|
+
const handler = () => onChange({ modality: currentModality });
|
|
142
|
+
changeHandlers.add(handler);
|
|
106
143
|
return () => {
|
|
107
|
-
|
|
144
|
+
changeHandlers.delete(handler);
|
|
108
145
|
};
|
|
109
146
|
}
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
trigger(value, null);
|
|
147
|
+
function isFocusVisible() {
|
|
148
|
+
return currentModality === "keyboard";
|
|
113
149
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
150
|
+
function trackFocusVisible(props = {}) {
|
|
151
|
+
const { isTextInput, autoFocus, onChange, root } = props;
|
|
152
|
+
setupGlobalFocusEvents(root);
|
|
153
|
+
onChange?.({ isFocusVisible: autoFocus || isFocusVisible(), modality: currentModality });
|
|
154
|
+
const handler = (modality, e) => {
|
|
155
|
+
if (!isKeyboardFocusEvent(!!isTextInput, modality, e)) return;
|
|
156
|
+
onChange?.({ isFocusVisible: isFocusVisible(), modality });
|
|
157
|
+
};
|
|
158
|
+
changeHandlers.add(handler);
|
|
159
|
+
return () => {
|
|
160
|
+
changeHandlers.delete(handler);
|
|
161
|
+
};
|
|
116
162
|
}
|
|
117
163
|
export {
|
|
118
164
|
getInteractionModality,
|
|
165
|
+
isFocusVisible,
|
|
166
|
+
listenerMap,
|
|
119
167
|
setInteractionModality,
|
|
120
168
|
trackFocusVisible,
|
|
121
169
|
trackInteractionModality
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { isDom } from \"@zag-js/dom-query\"\n\ntype Modality = \"keyboard\" | \"pointer\" | \"virtual\"\ntype HandlerEvent = PointerEvent | MouseEvent | KeyboardEvent | FocusEvent\ntype Handler = (modality: Modality, e: HandlerEvent | null) => void\ntype FocusVisibleCallback = (isFocusVisible: boolean) => void\n\nlet hasSetup = false\nlet modality: Modality | null = null\nlet hasEventBeforeFocus = false\nlet hasBlurredWindowRecently = false\n\nconst handlers = new Set<Handler>()\n\nfunction trigger(modality: Modality, event: HandlerEvent | null) {\n handlers.forEach((handler) => handler(modality, event))\n}\n\nconst isMac = typeof window !== \"undefined\" && window.navigator != null ? /^Mac/.test(window.navigator.platform) : false\n\nfunction isValidKey(e: KeyboardEvent) {\n return !(\n e.metaKey ||\n (!isMac && e.altKey) ||\n e.ctrlKey ||\n e.key === \"Control\" ||\n e.key === \"Shift\" ||\n e.key === \"Meta\"\n )\n}\n\nfunction onKeyboardEvent(event: KeyboardEvent) {\n hasEventBeforeFocus = true\n if (isValidKey(event)) {\n modality = \"keyboard\"\n trigger(\"keyboard\", event)\n }\n}\n\nfunction onPointerEvent(event: PointerEvent | MouseEvent) {\n modality = \"pointer\"\n\n if (event.type === \"mousedown\" || event.type === \"pointerdown\") {\n hasEventBeforeFocus = true\n const target = event.composedPath ? event.composedPath()[0] : event.target\n\n let matches = false\n try {\n matches = (target as any).matches(\":focus-visible\")\n } catch {}\n\n if (matches) return\n trigger(\"pointer\", event)\n }\n}\n\nfunction isVirtualClick(event: MouseEvent | PointerEvent): boolean {\n // JAWS/NVDA with Firefox.\n if ((event as any).mozInputSource === 0 && event.isTrusted) return true\n return event.detail === 0 && !(event as PointerEvent).pointerType\n}\n\nfunction onClickEvent(e: MouseEvent) {\n if (isVirtualClick(e)) {\n hasEventBeforeFocus = true\n modality = \"virtual\"\n }\n}\n\nfunction onWindowFocus(event: FocusEvent) {\n // Firefox fires two extra focus events when the user first clicks into an iframe:\n // first on the window, then on the document. We ignore these events so they don't\n // cause keyboard focus rings to appear.\n if (event.target === window || event.target === document) {\n return\n }\n\n // An extra event is fired when the user first clicks inside an element with tabindex attribute.\n // We ignore these events so they don't cause keyboard focus ring to appear.\n if (event.target instanceof Element && event.target.hasAttribute(\"tabindex\")) {\n return\n }\n\n // If a focus event occurs without a preceding keyboard or pointer event, switch to keyboard modality.\n // This occurs, for example, when navigating a form with the next/previous buttons on iOS.\n if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {\n modality = \"virtual\"\n trigger(\"virtual\", event)\n }\n\n hasEventBeforeFocus = false\n hasBlurredWindowRecently = false\n}\n\nfunction onWindowBlur() {\n // When the window is blurred, reset state. This is necessary when tabbing out of the window,\n // for example, since a subsequent focus event won't be fired.\n hasEventBeforeFocus = false\n hasBlurredWindowRecently = true\n}\n\nfunction isFocusVisible() {\n return modality !== \"pointer\"\n}\n\nfunction setupGlobalFocusEvents() {\n if (!isDom() || hasSetup) {\n return\n }\n\n // Programmatic focus() calls shouldn't affect the current input modality.\n // However, we need to detect other cases when a focus event occurs without\n // a preceding user event (e.g. screen reader focus). Overriding the focus\n // method on HTMLElement.prototype is a bit hacky, but works.\n const { focus } = HTMLElement.prototype\n HTMLElement.prototype.focus = function focusElement(...args) {\n hasEventBeforeFocus = true\n focus.apply(this, args)\n }\n\n document.addEventListener(\"keydown\", onKeyboardEvent, true)\n document.addEventListener(\"keyup\", onKeyboardEvent, true)\n document.addEventListener(\"click\", onClickEvent, true)\n\n // Register focus events on the window so they are sure to happen\n // before React's event listeners (registered on the document).\n window.addEventListener(\"focus\", onWindowFocus, true)\n window.addEventListener(\"blur\", onWindowBlur, false)\n\n if (typeof PointerEvent !== \"undefined\") {\n document.addEventListener(\"pointerdown\", onPointerEvent, true)\n document.addEventListener(\"pointermove\", onPointerEvent, true)\n document.addEventListener(\"pointerup\", onPointerEvent, true)\n } else {\n document.addEventListener(\"mousedown\", onPointerEvent, true)\n document.addEventListener(\"mousemove\", onPointerEvent, true)\n document.addEventListener(\"mouseup\", onPointerEvent, true)\n }\n\n hasSetup = true\n}\n\nexport function trackFocusVisible(fn: FocusVisibleCallback) {\n setupGlobalFocusEvents()\n\n fn(isFocusVisible())\n const handler = () => fn(isFocusVisible())\n\n handlers.add(handler)\n return () => {\n handlers.delete(handler)\n }\n}\n\nexport function trackInteractionModality(fn: (value: Modality | null) => void) {\n setupGlobalFocusEvents()\n\n fn(modality)\n const handler = () => fn(modality)\n\n handlers.add(handler)\n return () => {\n handlers.delete(handler)\n }\n}\n\nexport function setInteractionModality(value: Modality) {\n modality = value\n trigger(value, null)\n}\n\nexport function getInteractionModality() {\n return modality\n}\n"],"mappings":"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","names":["modality"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Credit: Huge props to the team at Adobe for inspiring this implementation.\n * https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/interactions/src/useFocusVisible.ts\n */\nimport { getDocument, getWindow, isMac } from \"@zag-js/dom-query\"\n\nfunction isVirtualClick(event: MouseEvent | PointerEvent): boolean {\n if ((event as any).mozInputSource === 0 && event.isTrusted) return true\n return event.detail === 0 && !(event as PointerEvent).pointerType\n}\n\nfunction isValidKey(e: KeyboardEvent) {\n return !(\n e.metaKey ||\n (!isMac() && e.altKey) ||\n e.ctrlKey ||\n e.key === \"Control\" ||\n e.key === \"Shift\" ||\n e.key === \"Meta\"\n )\n}\n\nconst nonTextInputTypes = new Set([\"checkbox\", \"radio\", \"range\", \"color\", \"file\", \"image\", \"button\", \"submit\", \"reset\"])\n\nfunction isKeyboardFocusEvent(isTextInput: boolean, modality: Modality, e: HandlerEvent) {\n const IHTMLInputElement =\n typeof window !== \"undefined\" ? getWindow(e?.target as Element).HTMLInputElement : HTMLInputElement\n const IHTMLTextAreaElement =\n typeof window !== \"undefined\" ? getWindow(e?.target as Element).HTMLTextAreaElement : HTMLTextAreaElement\n const IHTMLElement = typeof window !== \"undefined\" ? getWindow(e?.target as Element).HTMLElement : HTMLElement\n const IKeyboardEvent = typeof window !== \"undefined\" ? getWindow(e?.target as Element).KeyboardEvent : KeyboardEvent\n\n isTextInput =\n isTextInput ||\n (e?.target instanceof IHTMLInputElement && !nonTextInputTypes.has(e?.target?.type)) ||\n e?.target instanceof IHTMLTextAreaElement ||\n (e?.target instanceof IHTMLElement && e?.target.isContentEditable)\n\n return !(\n isTextInput &&\n modality === \"keyboard\" &&\n e instanceof IKeyboardEvent &&\n !Reflect.has(FOCUS_VISIBLE_INPUT_KEYS, e.key)\n )\n}\n\n/////////////////////////////////////////////////////////////////////////////////////////////\n\nexport type Modality = \"keyboard\" | \"pointer\" | \"virtual\"\n\ntype RootNode = Document | ShadowRoot | Node\n\ntype HandlerEvent = PointerEvent | MouseEvent | KeyboardEvent | FocusEvent | null\n\ntype Handler = (modality: Modality, e: HandlerEvent) => void\n\n/////////////////////////////////////////////////////////////////////////////////////////////\n\nlet currentModality: Modality | null = null\n\nlet changeHandlers = new Set<Handler>()\n\ninterface GlobalListenerData {\n focus: VoidFunction\n}\n\nexport let listenerMap = new Map<Window, GlobalListenerData>()\n\nlet hasEventBeforeFocus = false\nlet hasBlurredWindowRecently = false\n\n// Only Tab or Esc keys will make focus visible on text input elements\nconst FOCUS_VISIBLE_INPUT_KEYS = {\n Tab: true,\n Escape: true,\n}\n\nfunction triggerChangeHandlers(modality: Modality, e: HandlerEvent) {\n for (let handler of changeHandlers) {\n handler(modality, e)\n }\n}\n\nfunction handleKeyboardEvent(e: KeyboardEvent) {\n hasEventBeforeFocus = true\n if (isValidKey(e)) {\n currentModality = \"keyboard\"\n triggerChangeHandlers(\"keyboard\", e)\n }\n}\n\nfunction handlePointerEvent(e: PointerEvent | MouseEvent) {\n currentModality = \"pointer\"\n if (e.type === \"mousedown\" || e.type === \"pointerdown\") {\n hasEventBeforeFocus = true\n triggerChangeHandlers(\"pointer\", e)\n }\n}\n\nfunction handleClickEvent(e: MouseEvent) {\n if (isVirtualClick(e)) {\n hasEventBeforeFocus = true\n currentModality = \"virtual\"\n }\n}\n\nfunction handleFocusEvent(e: FocusEvent) {\n // Firefox fires two extra focus events when the user first clicks into an iframe:\n // first on the window, then on the document. We ignore these events so they don't\n // cause keyboard focus rings to appear.\n if (e.target === getWindow(e.target as Element) || e.target === getDocument(e.target as Element)) {\n return\n }\n\n // If a focus event occurs without a preceding keyboard or pointer event, switch to virtual modality.\n // This occurs, for example, when navigating a form with the next/previous buttons on iOS.\n if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {\n currentModality = \"virtual\"\n triggerChangeHandlers(\"virtual\", e)\n }\n\n hasEventBeforeFocus = false\n hasBlurredWindowRecently = false\n}\n\nfunction handleWindowBlur() {\n // When the window is blurred, reset state. This is necessary when tabbing out of the window,\n // for example, since a subsequent focus event won't be fired.\n hasEventBeforeFocus = false\n hasBlurredWindowRecently = true\n}\n\n/**\n * Setup global event listeners to control when keyboard focus style should be visible.\n */\nfunction setupGlobalFocusEvents(root?: RootNode) {\n if (typeof window === \"undefined\" || listenerMap.get(getWindow(root))) {\n return\n }\n\n const win = getWindow(root)\n const doc = getDocument(root)\n\n let focus = win.HTMLElement.prototype.focus\n win.HTMLElement.prototype.focus = function () {\n // For programmatic focus, we remove the focus visible state to prevent showing focus rings\n // When `options.focusVisible` is supported in most browsers, we can remove this\n // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#focusvisible\n currentModality = \"virtual\"\n triggerChangeHandlers(\"virtual\", null)\n\n hasEventBeforeFocus = true\n focus.apply(this, arguments as unknown as [options?: FocusOptions | undefined])\n }\n\n doc.addEventListener(\"keydown\", handleKeyboardEvent, true)\n doc.addEventListener(\"keyup\", handleKeyboardEvent, true)\n doc.addEventListener(\"click\", handleClickEvent, true)\n\n win.addEventListener(\"focus\", handleFocusEvent, true)\n win.addEventListener(\"blur\", handleWindowBlur, false)\n\n if (typeof win.PointerEvent !== \"undefined\") {\n doc.addEventListener(\"pointerdown\", handlePointerEvent, true)\n doc.addEventListener(\"pointermove\", handlePointerEvent, true)\n doc.addEventListener(\"pointerup\", handlePointerEvent, true)\n } else {\n doc.addEventListener(\"mousedown\", handlePointerEvent, true)\n doc.addEventListener(\"mousemove\", handlePointerEvent, true)\n doc.addEventListener(\"mouseup\", handlePointerEvent, true)\n }\n\n // Add unmount handler\n win.addEventListener(\n \"beforeunload\",\n () => {\n tearDownWindowFocusTracking(root)\n },\n { once: true },\n )\n\n listenerMap.set(win, { focus })\n}\n\nconst tearDownWindowFocusTracking = (root?: RootNode, loadListener?: () => void) => {\n const win = getWindow(root)\n const doc = getDocument(root)\n\n if (loadListener) {\n doc.removeEventListener(\"DOMContentLoaded\", loadListener)\n }\n\n if (!listenerMap.has(win)) {\n return\n }\n\n win.HTMLElement.prototype.focus = listenerMap.get(win)!.focus\n\n doc.removeEventListener(\"keydown\", handleKeyboardEvent, true)\n doc.removeEventListener(\"keyup\", handleKeyboardEvent, true)\n doc.removeEventListener(\"click\", handleClickEvent, true)\n win.removeEventListener(\"focus\", handleFocusEvent, true)\n win.removeEventListener(\"blur\", handleWindowBlur, false)\n\n if (typeof win.PointerEvent !== \"undefined\") {\n doc.removeEventListener(\"pointerdown\", handlePointerEvent, true)\n doc.removeEventListener(\"pointermove\", handlePointerEvent, true)\n doc.removeEventListener(\"pointerup\", handlePointerEvent, true)\n } else {\n doc.removeEventListener(\"mousedown\", handlePointerEvent, true)\n doc.removeEventListener(\"mousemove\", handlePointerEvent, true)\n doc.removeEventListener(\"mouseup\", handlePointerEvent, true)\n }\n\n listenerMap.delete(win)\n}\n\n/////////////////////////////////////////////////////////////////////////////////////////////\n\nexport function getInteractionModality(): Modality | null {\n return currentModality\n}\n\nexport function setInteractionModality(modality: Modality) {\n currentModality = modality\n triggerChangeHandlers(modality, null)\n}\n\nexport interface InteractionModalityChangeDetails {\n /** The modality of the interaction that caused the focus to be visible. */\n modality: Modality | null\n}\n\nexport interface InteractionModalityProps {\n /** The root element to track focus visibility for. */\n root?: RootNode\n /** Callback to be called when the interaction modality changes. */\n onChange: (details: InteractionModalityChangeDetails) => void\n}\n\nexport function trackInteractionModality(props: InteractionModalityProps): VoidFunction {\n const { onChange, root } = props\n\n setupGlobalFocusEvents(root)\n\n onChange({ modality: currentModality })\n\n const handler = () => onChange({ modality: currentModality })\n\n changeHandlers.add(handler)\n return () => {\n changeHandlers.delete(handler)\n }\n}\n\n/////////////////////////////////////////////////////////////////////////////////////////////\n\nexport function isFocusVisible(): boolean {\n return currentModality === \"keyboard\"\n}\n\nexport interface FocusVisibleChangeDetails {\n /** Whether keyboard focus is visible globally. */\n isFocusVisible: boolean\n /** The modality of the interaction that caused the focus to be visible. */\n modality: Modality | null\n}\n\nexport interface FocusVisibleProps {\n /** The root element to track focus visibility for. */\n root?: RootNode\n /** Whether the element is a text input. */\n isTextInput?: boolean\n /** Whether the element will be auto focused. */\n autoFocus?: boolean\n /** Callback to be called when the focus visibility changes. */\n onChange?: (details: FocusVisibleChangeDetails) => void\n}\n\nexport function trackFocusVisible(props: FocusVisibleProps = {}): VoidFunction {\n const { isTextInput, autoFocus, onChange, root } = props\n\n setupGlobalFocusEvents(root)\n\n onChange?.({ isFocusVisible: autoFocus || isFocusVisible(), modality: currentModality })\n\n const handler = (modality: Modality, e: HandlerEvent) => {\n if (!isKeyboardFocusEvent(!!isTextInput, modality, e)) return\n onChange?.({ isFocusVisible: isFocusVisible(), modality })\n }\n\n changeHandlers.add(handler)\n\n return () => {\n changeHandlers.delete(handler)\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zag-js/focus-visible",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"description": "Focus visible polyfill utility based on WICG",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clean-package": "../../../clean-package.config.json",
|
|
27
27
|
"main": "dist/index.js",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@zag-js/dom-query": "0.
|
|
29
|
+
"@zag-js/dom-query": "0.70.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"clean-package": "2.2.0"
|
package/src/index.ts
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Credit: Huge props to the team at Adobe for inspiring this implementation.
|
|
3
|
+
* https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/interactions/src/useFocusVisible.ts
|
|
4
|
+
*/
|
|
5
|
+
import { getDocument, getWindow, isMac } from "@zag-js/dom-query"
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type FocusVisibleCallback = (isFocusVisible: boolean) => void
|
|
7
|
-
|
|
8
|
-
let hasSetup = false
|
|
9
|
-
let modality: Modality | null = null
|
|
10
|
-
let hasEventBeforeFocus = false
|
|
11
|
-
let hasBlurredWindowRecently = false
|
|
12
|
-
|
|
13
|
-
const handlers = new Set<Handler>()
|
|
14
|
-
|
|
15
|
-
function trigger(modality: Modality, event: HandlerEvent | null) {
|
|
16
|
-
handlers.forEach((handler) => handler(modality, event))
|
|
7
|
+
function isVirtualClick(event: MouseEvent | PointerEvent): boolean {
|
|
8
|
+
if ((event as any).mozInputSource === 0 && event.isTrusted) return true
|
|
9
|
+
return event.detail === 0 && !(event as PointerEvent).pointerType
|
|
17
10
|
}
|
|
18
11
|
|
|
19
|
-
const isMac = typeof window !== "undefined" && window.navigator != null ? /^Mac/.test(window.navigator.platform) : false
|
|
20
|
-
|
|
21
12
|
function isValidKey(e: KeyboardEvent) {
|
|
22
13
|
return !(
|
|
23
14
|
e.metaKey ||
|
|
24
|
-
(!isMac && e.altKey) ||
|
|
15
|
+
(!isMac() && e.altKey) ||
|
|
25
16
|
e.ctrlKey ||
|
|
26
17
|
e.key === "Control" ||
|
|
27
18
|
e.key === "Shift" ||
|
|
@@ -29,146 +20,278 @@ function isValidKey(e: KeyboardEvent) {
|
|
|
29
20
|
)
|
|
30
21
|
}
|
|
31
22
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
const nonTextInputTypes = new Set(["checkbox", "radio", "range", "color", "file", "image", "button", "submit", "reset"])
|
|
24
|
+
|
|
25
|
+
function isKeyboardFocusEvent(isTextInput: boolean, modality: Modality, e: HandlerEvent) {
|
|
26
|
+
const IHTMLInputElement =
|
|
27
|
+
typeof window !== "undefined" ? getWindow(e?.target as Element).HTMLInputElement : HTMLInputElement
|
|
28
|
+
const IHTMLTextAreaElement =
|
|
29
|
+
typeof window !== "undefined" ? getWindow(e?.target as Element).HTMLTextAreaElement : HTMLTextAreaElement
|
|
30
|
+
const IHTMLElement = typeof window !== "undefined" ? getWindow(e?.target as Element).HTMLElement : HTMLElement
|
|
31
|
+
const IKeyboardEvent = typeof window !== "undefined" ? getWindow(e?.target as Element).KeyboardEvent : KeyboardEvent
|
|
32
|
+
|
|
33
|
+
isTextInput =
|
|
34
|
+
isTextInput ||
|
|
35
|
+
(e?.target instanceof IHTMLInputElement && !nonTextInputTypes.has(e?.target?.type)) ||
|
|
36
|
+
e?.target instanceof IHTMLTextAreaElement ||
|
|
37
|
+
(e?.target instanceof IHTMLElement && e?.target.isContentEditable)
|
|
38
|
+
|
|
39
|
+
return !(
|
|
40
|
+
isTextInput &&
|
|
41
|
+
modality === "keyboard" &&
|
|
42
|
+
e instanceof IKeyboardEvent &&
|
|
43
|
+
!Reflect.has(FOCUS_VISIBLE_INPUT_KEYS, e.key)
|
|
44
|
+
)
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
modality = "pointer"
|
|
47
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
export type Modality = "keyboard" | "pointer" | "virtual"
|
|
50
|
+
|
|
51
|
+
type RootNode = Document | ShadowRoot | Node
|
|
52
|
+
|
|
53
|
+
type HandlerEvent = PointerEvent | MouseEvent | KeyboardEvent | FocusEvent | null
|
|
54
|
+
|
|
55
|
+
type Handler = (modality: Modality, e: HandlerEvent) => void
|
|
56
|
+
|
|
57
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
matches = (target as any).matches(":focus-visible")
|
|
50
|
-
} catch {}
|
|
59
|
+
let currentModality: Modality | null = null
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
let changeHandlers = new Set<Handler>()
|
|
62
|
+
|
|
63
|
+
interface GlobalListenerData {
|
|
64
|
+
focus: VoidFunction
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export let listenerMap = new Map<Window, GlobalListenerData>()
|
|
68
|
+
|
|
69
|
+
let hasEventBeforeFocus = false
|
|
70
|
+
let hasBlurredWindowRecently = false
|
|
71
|
+
|
|
72
|
+
// Only Tab or Esc keys will make focus visible on text input elements
|
|
73
|
+
const FOCUS_VISIBLE_INPUT_KEYS = {
|
|
74
|
+
Tab: true,
|
|
75
|
+
Escape: true,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function triggerChangeHandlers(modality: Modality, e: HandlerEvent) {
|
|
79
|
+
for (let handler of changeHandlers) {
|
|
80
|
+
handler(modality, e)
|
|
54
81
|
}
|
|
55
82
|
}
|
|
56
83
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
if ((
|
|
60
|
-
|
|
84
|
+
function handleKeyboardEvent(e: KeyboardEvent) {
|
|
85
|
+
hasEventBeforeFocus = true
|
|
86
|
+
if (isValidKey(e)) {
|
|
87
|
+
currentModality = "keyboard"
|
|
88
|
+
triggerChangeHandlers("keyboard", e)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handlePointerEvent(e: PointerEvent | MouseEvent) {
|
|
93
|
+
currentModality = "pointer"
|
|
94
|
+
if (e.type === "mousedown" || e.type === "pointerdown") {
|
|
95
|
+
hasEventBeforeFocus = true
|
|
96
|
+
triggerChangeHandlers("pointer", e)
|
|
97
|
+
}
|
|
61
98
|
}
|
|
62
99
|
|
|
63
|
-
function
|
|
100
|
+
function handleClickEvent(e: MouseEvent) {
|
|
64
101
|
if (isVirtualClick(e)) {
|
|
65
102
|
hasEventBeforeFocus = true
|
|
66
|
-
|
|
103
|
+
currentModality = "virtual"
|
|
67
104
|
}
|
|
68
105
|
}
|
|
69
106
|
|
|
70
|
-
function
|
|
107
|
+
function handleFocusEvent(e: FocusEvent) {
|
|
71
108
|
// Firefox fires two extra focus events when the user first clicks into an iframe:
|
|
72
109
|
// first on the window, then on the document. We ignore these events so they don't
|
|
73
110
|
// cause keyboard focus rings to appear.
|
|
74
|
-
if (
|
|
111
|
+
if (e.target === getWindow(e.target as Element) || e.target === getDocument(e.target as Element)) {
|
|
75
112
|
return
|
|
76
113
|
}
|
|
77
114
|
|
|
78
|
-
//
|
|
79
|
-
// We ignore these events so they don't cause keyboard focus ring to appear.
|
|
80
|
-
if (event.target instanceof Element && event.target.hasAttribute("tabindex")) {
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// If a focus event occurs without a preceding keyboard or pointer event, switch to keyboard modality.
|
|
115
|
+
// If a focus event occurs without a preceding keyboard or pointer event, switch to virtual modality.
|
|
85
116
|
// This occurs, for example, when navigating a form with the next/previous buttons on iOS.
|
|
86
117
|
if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {
|
|
87
|
-
|
|
88
|
-
|
|
118
|
+
currentModality = "virtual"
|
|
119
|
+
triggerChangeHandlers("virtual", e)
|
|
89
120
|
}
|
|
90
121
|
|
|
91
122
|
hasEventBeforeFocus = false
|
|
92
123
|
hasBlurredWindowRecently = false
|
|
93
124
|
}
|
|
94
125
|
|
|
95
|
-
function
|
|
126
|
+
function handleWindowBlur() {
|
|
96
127
|
// When the window is blurred, reset state. This is necessary when tabbing out of the window,
|
|
97
128
|
// for example, since a subsequent focus event won't be fired.
|
|
98
129
|
hasEventBeforeFocus = false
|
|
99
130
|
hasBlurredWindowRecently = true
|
|
100
131
|
}
|
|
101
132
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!isDom() || hasSetup) {
|
|
133
|
+
/**
|
|
134
|
+
* Setup global event listeners to control when keyboard focus style should be visible.
|
|
135
|
+
*/
|
|
136
|
+
function setupGlobalFocusEvents(root?: RootNode) {
|
|
137
|
+
if (typeof window === "undefined" || listenerMap.get(getWindow(root))) {
|
|
108
138
|
return
|
|
109
139
|
}
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
const win = getWindow(root)
|
|
142
|
+
const doc = getDocument(root)
|
|
143
|
+
|
|
144
|
+
let focus = win.HTMLElement.prototype.focus
|
|
145
|
+
win.HTMLElement.prototype.focus = function () {
|
|
146
|
+
// For programmatic focus, we remove the focus visible state to prevent showing focus rings
|
|
147
|
+
// When `options.focusVisible` is supported in most browsers, we can remove this
|
|
148
|
+
// @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#focusvisible
|
|
149
|
+
currentModality = "virtual"
|
|
150
|
+
triggerChangeHandlers("virtual", null)
|
|
151
|
+
|
|
117
152
|
hasEventBeforeFocus = true
|
|
118
|
-
focus.apply(this,
|
|
153
|
+
focus.apply(this, arguments as unknown as [options?: FocusOptions | undefined])
|
|
119
154
|
}
|
|
120
155
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
156
|
+
doc.addEventListener("keydown", handleKeyboardEvent, true)
|
|
157
|
+
doc.addEventListener("keyup", handleKeyboardEvent, true)
|
|
158
|
+
doc.addEventListener("click", handleClickEvent, true)
|
|
124
159
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
window.addEventListener("focus", onWindowFocus, true)
|
|
128
|
-
window.addEventListener("blur", onWindowBlur, false)
|
|
160
|
+
win.addEventListener("focus", handleFocusEvent, true)
|
|
161
|
+
win.addEventListener("blur", handleWindowBlur, false)
|
|
129
162
|
|
|
130
|
-
if (typeof PointerEvent !== "undefined") {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
163
|
+
if (typeof win.PointerEvent !== "undefined") {
|
|
164
|
+
doc.addEventListener("pointerdown", handlePointerEvent, true)
|
|
165
|
+
doc.addEventListener("pointermove", handlePointerEvent, true)
|
|
166
|
+
doc.addEventListener("pointerup", handlePointerEvent, true)
|
|
134
167
|
} else {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
doc.addEventListener("mousedown", handlePointerEvent, true)
|
|
169
|
+
doc.addEventListener("mousemove", handlePointerEvent, true)
|
|
170
|
+
doc.addEventListener("mouseup", handlePointerEvent, true)
|
|
138
171
|
}
|
|
139
172
|
|
|
140
|
-
|
|
173
|
+
// Add unmount handler
|
|
174
|
+
win.addEventListener(
|
|
175
|
+
"beforeunload",
|
|
176
|
+
() => {
|
|
177
|
+
tearDownWindowFocusTracking(root)
|
|
178
|
+
},
|
|
179
|
+
{ once: true },
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
listenerMap.set(win, { focus })
|
|
141
183
|
}
|
|
142
184
|
|
|
143
|
-
|
|
144
|
-
|
|
185
|
+
const tearDownWindowFocusTracking = (root?: RootNode, loadListener?: () => void) => {
|
|
186
|
+
const win = getWindow(root)
|
|
187
|
+
const doc = getDocument(root)
|
|
145
188
|
|
|
146
|
-
|
|
147
|
-
|
|
189
|
+
if (loadListener) {
|
|
190
|
+
doc.removeEventListener("DOMContentLoaded", loadListener)
|
|
191
|
+
}
|
|
148
192
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
handlers.delete(handler)
|
|
193
|
+
if (!listenerMap.has(win)) {
|
|
194
|
+
return
|
|
152
195
|
}
|
|
196
|
+
|
|
197
|
+
win.HTMLElement.prototype.focus = listenerMap.get(win)!.focus
|
|
198
|
+
|
|
199
|
+
doc.removeEventListener("keydown", handleKeyboardEvent, true)
|
|
200
|
+
doc.removeEventListener("keyup", handleKeyboardEvent, true)
|
|
201
|
+
doc.removeEventListener("click", handleClickEvent, true)
|
|
202
|
+
win.removeEventListener("focus", handleFocusEvent, true)
|
|
203
|
+
win.removeEventListener("blur", handleWindowBlur, false)
|
|
204
|
+
|
|
205
|
+
if (typeof win.PointerEvent !== "undefined") {
|
|
206
|
+
doc.removeEventListener("pointerdown", handlePointerEvent, true)
|
|
207
|
+
doc.removeEventListener("pointermove", handlePointerEvent, true)
|
|
208
|
+
doc.removeEventListener("pointerup", handlePointerEvent, true)
|
|
209
|
+
} else {
|
|
210
|
+
doc.removeEventListener("mousedown", handlePointerEvent, true)
|
|
211
|
+
doc.removeEventListener("mousemove", handlePointerEvent, true)
|
|
212
|
+
doc.removeEventListener("mouseup", handlePointerEvent, true)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
listenerMap.delete(win)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
219
|
+
|
|
220
|
+
export function getInteractionModality(): Modality | null {
|
|
221
|
+
return currentModality
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function setInteractionModality(modality: Modality) {
|
|
225
|
+
currentModality = modality
|
|
226
|
+
triggerChangeHandlers(modality, null)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface InteractionModalityChangeDetails {
|
|
230
|
+
/** The modality of the interaction that caused the focus to be visible. */
|
|
231
|
+
modality: Modality | null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface InteractionModalityProps {
|
|
235
|
+
/** The root element to track focus visibility for. */
|
|
236
|
+
root?: RootNode
|
|
237
|
+
/** Callback to be called when the interaction modality changes. */
|
|
238
|
+
onChange: (details: InteractionModalityChangeDetails) => void
|
|
153
239
|
}
|
|
154
240
|
|
|
155
|
-
export function trackInteractionModality(
|
|
156
|
-
|
|
241
|
+
export function trackInteractionModality(props: InteractionModalityProps): VoidFunction {
|
|
242
|
+
const { onChange, root } = props
|
|
157
243
|
|
|
158
|
-
|
|
159
|
-
const handler = () => fn(modality)
|
|
244
|
+
setupGlobalFocusEvents(root)
|
|
160
245
|
|
|
161
|
-
|
|
246
|
+
onChange({ modality: currentModality })
|
|
247
|
+
|
|
248
|
+
const handler = () => onChange({ modality: currentModality })
|
|
249
|
+
|
|
250
|
+
changeHandlers.add(handler)
|
|
162
251
|
return () => {
|
|
163
|
-
|
|
252
|
+
changeHandlers.delete(handler)
|
|
164
253
|
}
|
|
165
254
|
}
|
|
166
255
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
256
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
257
|
+
|
|
258
|
+
export function isFocusVisible(): boolean {
|
|
259
|
+
return currentModality === "keyboard"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface FocusVisibleChangeDetails {
|
|
263
|
+
/** Whether keyboard focus is visible globally. */
|
|
264
|
+
isFocusVisible: boolean
|
|
265
|
+
/** The modality of the interaction that caused the focus to be visible. */
|
|
266
|
+
modality: Modality | null
|
|
170
267
|
}
|
|
171
268
|
|
|
172
|
-
export
|
|
173
|
-
|
|
269
|
+
export interface FocusVisibleProps {
|
|
270
|
+
/** The root element to track focus visibility for. */
|
|
271
|
+
root?: RootNode
|
|
272
|
+
/** Whether the element is a text input. */
|
|
273
|
+
isTextInput?: boolean
|
|
274
|
+
/** Whether the element will be auto focused. */
|
|
275
|
+
autoFocus?: boolean
|
|
276
|
+
/** Callback to be called when the focus visibility changes. */
|
|
277
|
+
onChange?: (details: FocusVisibleChangeDetails) => void
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function trackFocusVisible(props: FocusVisibleProps = {}): VoidFunction {
|
|
281
|
+
const { isTextInput, autoFocus, onChange, root } = props
|
|
282
|
+
|
|
283
|
+
setupGlobalFocusEvents(root)
|
|
284
|
+
|
|
285
|
+
onChange?.({ isFocusVisible: autoFocus || isFocusVisible(), modality: currentModality })
|
|
286
|
+
|
|
287
|
+
const handler = (modality: Modality, e: HandlerEvent) => {
|
|
288
|
+
if (!isKeyboardFocusEvent(!!isTextInput, modality, e)) return
|
|
289
|
+
onChange?.({ isFocusVisible: isFocusVisible(), modality })
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
changeHandlers.add(handler)
|
|
293
|
+
|
|
294
|
+
return () => {
|
|
295
|
+
changeHandlers.delete(handler)
|
|
296
|
+
}
|
|
174
297
|
}
|