@zag-js/focus-visible 0.68.1 → 0.69.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 CHANGED
@@ -1,8 +1,39 @@
1
1
  type Modality = "keyboard" | "pointer" | "virtual";
2
- type FocusVisibleCallback = (isFocusVisible: boolean) => void;
3
- declare function trackFocusVisible(fn: FocusVisibleCallback): () => void;
4
- declare function trackInteractionModality(fn: (value: Modality | null) => void): () => void;
5
- declare function setInteractionModality(value: Modality): void;
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 FocusVisibleCallback = (isFocusVisible: boolean) => void;
3
- declare function trackFocusVisible(fn: FocusVisibleCallback): () => void;
4
- declare function trackInteractionModality(fn: (value: Modality | null) => void): () => void;
5
- declare function setInteractionModality(value: Modality): void;
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
- var hasSetup = false;
31
- var modality = null;
32
- var hasEventBeforeFocus = false;
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
- function onKeyboardEvent(event) {
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(event)) {
45
- modality = "keyboard";
46
- trigger("keyboard", event);
64
+ if (isValidKey(e)) {
65
+ currentModality = "keyboard";
66
+ triggerChangeHandlers("keyboard", e);
47
67
  }
48
68
  }
49
- function onPointerEvent(event) {
50
- modality = "pointer";
51
- if (event.type === "mousedown" || event.type === "pointerdown") {
69
+ function handlePointerEvent(e) {
70
+ currentModality = "pointer";
71
+ if (e.type === "mousedown" || e.type === "pointerdown") {
52
72
  hasEventBeforeFocus = true;
53
- const target = event.composedPath ? event.composedPath()[0] : event.target;
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 isVirtualClick(event) {
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
- modality = "virtual";
79
+ currentModality = "virtual";
71
80
  }
72
81
  }
73
- function onWindowFocus(event) {
74
- if (event.target === window || event.target === document) {
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
- modality = "virtual";
82
- trigger("virtual", event);
87
+ currentModality = "virtual";
88
+ triggerChangeHandlers("virtual", e);
83
89
  }
84
90
  hasEventBeforeFocus = false;
85
91
  hasBlurredWindowRecently = false;
86
92
  }
87
- function onWindowBlur() {
93
+ function handleWindowBlur() {
88
94
  hasEventBeforeFocus = false;
89
95
  hasBlurredWindowRecently = true;
90
96
  }
91
- function isFocusVisible() {
92
- return modality !== "pointer";
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 { focus } = HTMLElement.prototype;
99
- HTMLElement.prototype.focus = function focusElement(...args) {
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, args);
108
+ focus.apply(this, arguments);
102
109
  };
103
- document.addEventListener("keydown", onKeyboardEvent, true);
104
- document.addEventListener("keyup", onKeyboardEvent, true);
105
- document.addEventListener("click", onClickEvent, true);
106
- window.addEventListener("focus", onWindowFocus, true);
107
- window.addEventListener("blur", onWindowBlur, false);
108
- if (typeof PointerEvent !== "undefined") {
109
- document.addEventListener("pointerdown", onPointerEvent, true);
110
- document.addEventListener("pointermove", onPointerEvent, true);
111
- document.addEventListener("pointerup", onPointerEvent, true);
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
- document.addEventListener("mousedown", onPointerEvent, true);
114
- document.addEventListener("mousemove", onPointerEvent, true);
115
- document.addEventListener("mouseup", onPointerEvent, true);
120
+ doc.addEventListener("mousedown", handlePointerEvent, true);
121
+ doc.addEventListener("mousemove", handlePointerEvent, true);
122
+ doc.addEventListener("mouseup", handlePointerEvent, true);
116
123
  }
117
- hasSetup = true;
124
+ win.addEventListener(
125
+ "beforeunload",
126
+ () => {
127
+ tearDownWindowFocusTracking(root);
128
+ },
129
+ { once: true }
130
+ );
131
+ listenerMap.set(win, { focus });
118
132
  }
119
- function trackFocusVisible(fn) {
120
- setupGlobalFocusEvents();
121
- fn(isFocusVisible());
122
- const handler = () => fn(isFocusVisible());
123
- handlers.add(handler);
124
- return () => {
125
- handlers.delete(handler);
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 trackInteractionModality(fn) {
129
- setupGlobalFocusEvents();
130
- fn(modality);
131
- const handler = () => fn(modality);
132
- handlers.add(handler);
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
- handlers.delete(handler);
173
+ changeHandlers.delete(handler);
135
174
  };
136
175
  }
137
- function setInteractionModality(value) {
138
- modality = value;
139
- trigger(value, null);
176
+ function isFocusVisible() {
177
+ return currentModality === "keyboard";
140
178
  }
141
- function getInteractionModality() {
142
- return modality;
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 { isDom } from "@zag-js/dom-query";
3
- var hasSetup = false;
4
- var modality = null;
5
- var hasEventBeforeFocus = false;
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
- function onKeyboardEvent(event) {
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(event)) {
18
- modality = "keyboard";
19
- trigger("keyboard", event);
35
+ if (isValidKey(e)) {
36
+ currentModality = "keyboard";
37
+ triggerChangeHandlers("keyboard", e);
20
38
  }
21
39
  }
22
- function onPointerEvent(event) {
23
- modality = "pointer";
24
- if (event.type === "mousedown" || event.type === "pointerdown") {
40
+ function handlePointerEvent(e) {
41
+ currentModality = "pointer";
42
+ if (e.type === "mousedown" || e.type === "pointerdown") {
25
43
  hasEventBeforeFocus = true;
26
- const target = event.composedPath ? event.composedPath()[0] : event.target;
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 isVirtualClick(event) {
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
- modality = "virtual";
50
+ currentModality = "virtual";
44
51
  }
45
52
  }
46
- function onWindowFocus(event) {
47
- if (event.target === window || event.target === document) {
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
- modality = "virtual";
55
- trigger("virtual", event);
58
+ currentModality = "virtual";
59
+ triggerChangeHandlers("virtual", e);
56
60
  }
57
61
  hasEventBeforeFocus = false;
58
62
  hasBlurredWindowRecently = false;
59
63
  }
60
- function onWindowBlur() {
64
+ function handleWindowBlur() {
61
65
  hasEventBeforeFocus = false;
62
66
  hasBlurredWindowRecently = true;
63
67
  }
64
- function isFocusVisible() {
65
- return modality !== "pointer";
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 { focus } = HTMLElement.prototype;
72
- HTMLElement.prototype.focus = function focusElement(...args) {
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, args);
79
+ focus.apply(this, arguments);
75
80
  };
76
- document.addEventListener("keydown", onKeyboardEvent, true);
77
- document.addEventListener("keyup", onKeyboardEvent, true);
78
- document.addEventListener("click", onClickEvent, true);
79
- window.addEventListener("focus", onWindowFocus, true);
80
- window.addEventListener("blur", onWindowBlur, false);
81
- if (typeof PointerEvent !== "undefined") {
82
- document.addEventListener("pointerdown", onPointerEvent, true);
83
- document.addEventListener("pointermove", onPointerEvent, true);
84
- document.addEventListener("pointerup", onPointerEvent, true);
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
- document.addEventListener("mousedown", onPointerEvent, true);
87
- document.addEventListener("mousemove", onPointerEvent, true);
88
- document.addEventListener("mouseup", onPointerEvent, true);
91
+ doc.addEventListener("mousedown", handlePointerEvent, true);
92
+ doc.addEventListener("mousemove", handlePointerEvent, true);
93
+ doc.addEventListener("mouseup", handlePointerEvent, true);
89
94
  }
90
- hasSetup = true;
95
+ win.addEventListener(
96
+ "beforeunload",
97
+ () => {
98
+ tearDownWindowFocusTracking(root);
99
+ },
100
+ { once: true }
101
+ );
102
+ listenerMap.set(win, { focus });
91
103
  }
92
- function trackFocusVisible(fn) {
93
- setupGlobalFocusEvents();
94
- fn(isFocusVisible());
95
- const handler = () => fn(isFocusVisible());
96
- handlers.add(handler);
97
- return () => {
98
- handlers.delete(handler);
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 trackInteractionModality(fn) {
102
- setupGlobalFocusEvents();
103
- fn(modality);
104
- const handler = () => fn(modality);
105
- handlers.add(handler);
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
- handlers.delete(handler);
144
+ changeHandlers.delete(handler);
108
145
  };
109
146
  }
110
- function setInteractionModality(value) {
111
- modality = value;
112
- trigger(value, null);
147
+ function isFocusVisible() {
148
+ return currentModality === "keyboard";
113
149
  }
114
- function getInteractionModality() {
115
- return modality;
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
@@ -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.68.1",
3
+ "version": "0.69.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.68.1"
29
+ "@zag-js/dom-query": "0.69.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "clean-package": "2.2.0"
package/src/index.ts CHANGED
@@ -1,27 +1,18 @@
1
- import { isDom } from "@zag-js/dom-query"
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
- type Modality = "keyboard" | "pointer" | "virtual"
4
- type HandlerEvent = PointerEvent | MouseEvent | KeyboardEvent | FocusEvent
5
- type Handler = (modality: Modality, e: HandlerEvent | null) => void
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
- function onKeyboardEvent(event: KeyboardEvent) {
33
- hasEventBeforeFocus = true
34
- if (isValidKey(event)) {
35
- modality = "keyboard"
36
- trigger("keyboard", event)
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
- function onPointerEvent(event: PointerEvent | MouseEvent) {
41
- modality = "pointer"
47
+ /////////////////////////////////////////////////////////////////////////////////////////////
42
48
 
43
- if (event.type === "mousedown" || event.type === "pointerdown") {
44
- hasEventBeforeFocus = true
45
- const target = event.composedPath ? event.composedPath()[0] : event.target
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
- let matches = false
48
- try {
49
- matches = (target as any).matches(":focus-visible")
50
- } catch {}
59
+ let currentModality: Modality | null = null
51
60
 
52
- if (matches) return
53
- trigger("pointer", event)
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 isVirtualClick(event: MouseEvent | PointerEvent): boolean {
58
- // JAWS/NVDA with Firefox.
59
- if ((event as any).mozInputSource === 0 && event.isTrusted) return true
60
- return event.detail === 0 && !(event as PointerEvent).pointerType
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 onClickEvent(e: MouseEvent) {
100
+ function handleClickEvent(e: MouseEvent) {
64
101
  if (isVirtualClick(e)) {
65
102
  hasEventBeforeFocus = true
66
- modality = "virtual"
103
+ currentModality = "virtual"
67
104
  }
68
105
  }
69
106
 
70
- function onWindowFocus(event: FocusEvent) {
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 (event.target === window || event.target === document) {
111
+ if (e.target === getWindow(e.target as Element) || e.target === getDocument(e.target as Element)) {
75
112
  return
76
113
  }
77
114
 
78
- // An extra event is fired when the user first clicks inside an element with tabindex attribute.
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
- modality = "virtual"
88
- trigger("virtual", event)
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 onWindowBlur() {
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
- function isFocusVisible() {
103
- return modality !== "pointer"
104
- }
105
-
106
- function setupGlobalFocusEvents() {
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
- // Programmatic focus() calls shouldn't affect the current input modality.
112
- // However, we need to detect other cases when a focus event occurs without
113
- // a preceding user event (e.g. screen reader focus). Overriding the focus
114
- // method on HTMLElement.prototype is a bit hacky, but works.
115
- const { focus } = HTMLElement.prototype
116
- HTMLElement.prototype.focus = function focusElement(...args) {
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, args)
153
+ focus.apply(this, arguments as unknown as [options?: FocusOptions | undefined])
119
154
  }
120
155
 
121
- document.addEventListener("keydown", onKeyboardEvent, true)
122
- document.addEventListener("keyup", onKeyboardEvent, true)
123
- document.addEventListener("click", onClickEvent, true)
156
+ doc.addEventListener("keydown", handleKeyboardEvent, true)
157
+ doc.addEventListener("keyup", handleKeyboardEvent, true)
158
+ doc.addEventListener("click", handleClickEvent, true)
124
159
 
125
- // Register focus events on the window so they are sure to happen
126
- // before React's event listeners (registered on the document).
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
- document.addEventListener("pointerdown", onPointerEvent, true)
132
- document.addEventListener("pointermove", onPointerEvent, true)
133
- document.addEventListener("pointerup", onPointerEvent, true)
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
- document.addEventListener("mousedown", onPointerEvent, true)
136
- document.addEventListener("mousemove", onPointerEvent, true)
137
- document.addEventListener("mouseup", onPointerEvent, true)
168
+ doc.addEventListener("mousedown", handlePointerEvent, true)
169
+ doc.addEventListener("mousemove", handlePointerEvent, true)
170
+ doc.addEventListener("mouseup", handlePointerEvent, true)
138
171
  }
139
172
 
140
- hasSetup = true
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
- export function trackFocusVisible(fn: FocusVisibleCallback) {
144
- setupGlobalFocusEvents()
185
+ const tearDownWindowFocusTracking = (root?: RootNode, loadListener?: () => void) => {
186
+ const win = getWindow(root)
187
+ const doc = getDocument(root)
145
188
 
146
- fn(isFocusVisible())
147
- const handler = () => fn(isFocusVisible())
189
+ if (loadListener) {
190
+ doc.removeEventListener("DOMContentLoaded", loadListener)
191
+ }
148
192
 
149
- handlers.add(handler)
150
- return () => {
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(fn: (value: Modality | null) => void) {
156
- setupGlobalFocusEvents()
241
+ export function trackInteractionModality(props: InteractionModalityProps): VoidFunction {
242
+ const { onChange, root } = props
157
243
 
158
- fn(modality)
159
- const handler = () => fn(modality)
244
+ setupGlobalFocusEvents(root)
160
245
 
161
- handlers.add(handler)
246
+ onChange({ modality: currentModality })
247
+
248
+ const handler = () => onChange({ modality: currentModality })
249
+
250
+ changeHandlers.add(handler)
162
251
  return () => {
163
- handlers.delete(handler)
252
+ changeHandlers.delete(handler)
164
253
  }
165
254
  }
166
255
 
167
- export function setInteractionModality(value: Modality) {
168
- modality = value
169
- trigger(value, null)
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 function getInteractionModality() {
173
- return modality
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
  }