@zag-js/focus-visible 0.9.2 → 0.10.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.
Files changed (2) hide show
  1. package/package.json +3 -2
  2. package/src/index.ts +166 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/focus-visible",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "Focus visible polyfill utility based on WICG",
5
5
  "keywords": [
6
6
  "js",
@@ -14,7 +14,8 @@
14
14
  "repository": "https://github.com/chakra-ui/zag/tree/main/packages/utilities/focus-visible",
15
15
  "sideEffects": false,
16
16
  "files": [
17
- "dist/**/*"
17
+ "dist",
18
+ "src"
18
19
  ],
19
20
  "publishConfig": {
20
21
  "access": "public"
package/src/index.ts ADDED
@@ -0,0 +1,166 @@
1
+ type Modality = "keyboard" | "pointer" | "virtual"
2
+ type HandlerEvent = PointerEvent | MouseEvent | KeyboardEvent | FocusEvent
3
+ type Handler = (modality: Modality, e: HandlerEvent | null) => void
4
+ type FocusVisibleCallback = (isFocusVisible: boolean) => void
5
+
6
+ let hasSetup = false
7
+ let modality: Modality | null = null
8
+ let hasEventBeforeFocus = false
9
+ let hasBlurredWindowRecently = false
10
+
11
+ const handlers = new Set<Handler>()
12
+
13
+ function trigger(modality: Modality, event: HandlerEvent | null) {
14
+ handlers.forEach((handler) => handler(modality, event))
15
+ }
16
+
17
+ const isMac = typeof window !== "undefined" && window.navigator != null ? /^Mac/.test(window.navigator.platform) : false
18
+
19
+ function isValidKey(e: KeyboardEvent) {
20
+ return !(
21
+ e.metaKey ||
22
+ (!isMac && e.altKey) ||
23
+ e.ctrlKey ||
24
+ e.key === "Control" ||
25
+ e.key === "Shift" ||
26
+ e.key === "Meta"
27
+ )
28
+ }
29
+
30
+ function onKeyboardEvent(event: KeyboardEvent) {
31
+ hasEventBeforeFocus = true
32
+ if (isValidKey(event)) {
33
+ modality = "keyboard"
34
+ trigger("keyboard", event)
35
+ }
36
+ }
37
+
38
+ function onPointerEvent(event: PointerEvent | MouseEvent) {
39
+ modality = "pointer"
40
+
41
+ if (event.type === "mousedown" || event.type === "pointerdown") {
42
+ hasEventBeforeFocus = true
43
+ const target = event.composedPath ? event.composedPath()[0] : event.target
44
+
45
+ let matches = false
46
+ try {
47
+ matches = (target as any).matches(":focus-visible")
48
+ } catch {}
49
+
50
+ if (matches) return
51
+ trigger("pointer", event)
52
+ }
53
+ }
54
+
55
+ function isVirtualClick(event: MouseEvent | PointerEvent): boolean {
56
+ // JAWS/NVDA with Firefox.
57
+ if ((event as any).mozInputSource === 0 && event.isTrusted) return true
58
+ return event.detail === 0 && !(event as PointerEvent).pointerType
59
+ }
60
+
61
+ function onClickEvent(e: MouseEvent) {
62
+ if (isVirtualClick(e)) {
63
+ hasEventBeforeFocus = true
64
+ modality = "virtual"
65
+ }
66
+ }
67
+
68
+ function onWindowFocus(event: FocusEvent) {
69
+ // Firefox fires two extra focus events when the user first clicks into an iframe:
70
+ // first on the window, then on the document. We ignore these events so they don't
71
+ // cause keyboard focus rings to appear.
72
+ if (event.target === window || event.target === document) {
73
+ return
74
+ }
75
+
76
+ // If a focus event occurs without a preceding keyboard or pointer event, switch to keyboard modality.
77
+ // This occurs, for example, when navigating a form with the next/previous buttons on iOS.
78
+ if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {
79
+ modality = "virtual"
80
+ trigger("virtual", event)
81
+ }
82
+
83
+ hasEventBeforeFocus = false
84
+ hasBlurredWindowRecently = false
85
+ }
86
+
87
+ function onWindowBlur() {
88
+ // When the window is blurred, reset state. This is necessary when tabbing out of the window,
89
+ // for example, since a subsequent focus event won't be fired.
90
+ hasEventBeforeFocus = false
91
+ hasBlurredWindowRecently = true
92
+ }
93
+
94
+ function isFocusVisible() {
95
+ return modality !== "pointer"
96
+ }
97
+
98
+ function setupGlobalFocusEvents() {
99
+ if (typeof window === "undefined" || hasSetup) {
100
+ return
101
+ }
102
+
103
+ // Programmatic focus() calls shouldn't affect the current input modality.
104
+ // However, we need to detect other cases when a focus event occurs without
105
+ // a preceding user event (e.g. screen reader focus). Overriding the focus
106
+ // method on HTMLElement.prototype is a bit hacky, but works.
107
+ const { focus } = HTMLElement.prototype
108
+ HTMLElement.prototype.focus = function focusElement(...args) {
109
+ hasEventBeforeFocus = true
110
+ focus.apply(this, args)
111
+ }
112
+
113
+ document.addEventListener("keydown", onKeyboardEvent, true)
114
+ document.addEventListener("keyup", onKeyboardEvent, true)
115
+ document.addEventListener("click", onClickEvent, true)
116
+
117
+ // Register focus events on the window so they are sure to happen
118
+ // before React's event listeners (registered on the document).
119
+ window.addEventListener("focus", onWindowFocus, true)
120
+ window.addEventListener("blur", onWindowBlur, false)
121
+
122
+ if (typeof PointerEvent !== "undefined") {
123
+ document.addEventListener("pointerdown", onPointerEvent, true)
124
+ document.addEventListener("pointermove", onPointerEvent, true)
125
+ document.addEventListener("pointerup", onPointerEvent, true)
126
+ } else {
127
+ document.addEventListener("mousedown", onPointerEvent, true)
128
+ document.addEventListener("mousemove", onPointerEvent, true)
129
+ document.addEventListener("mouseup", onPointerEvent, true)
130
+ }
131
+
132
+ hasSetup = true
133
+ }
134
+
135
+ export function trackFocusVisible(fn: FocusVisibleCallback) {
136
+ setupGlobalFocusEvents()
137
+
138
+ fn(isFocusVisible())
139
+ const handler = () => fn(isFocusVisible())
140
+
141
+ handlers.add(handler)
142
+ return () => {
143
+ handlers.delete(handler)
144
+ }
145
+ }
146
+
147
+ export function trackInteractionModality(fn: (value: Modality | null) => void) {
148
+ setupGlobalFocusEvents()
149
+
150
+ fn(modality)
151
+ const handler = () => fn(modality)
152
+
153
+ handlers.add(handler)
154
+ return () => {
155
+ handlers.delete(handler)
156
+ }
157
+ }
158
+
159
+ export function setInteractionModality(value: Modality) {
160
+ modality = value
161
+ trigger(value, null)
162
+ }
163
+
164
+ export function getInteractionModality() {
165
+ return modality
166
+ }