@lessonkit/accessibility 1.2.0 → 1.3.1
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.cjs +56 -15
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +54 -15
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -23,9 +23,11 @@ __export(index_exports, {
|
|
|
23
23
|
createRovingTabIndex: () => createRovingTabIndex,
|
|
24
24
|
focusFirst: () => focusFirst,
|
|
25
25
|
getFocusableElements: () => getFocusableElements,
|
|
26
|
+
getPrefersReducedMotionSnapshot: () => getPrefersReducedMotionSnapshot,
|
|
26
27
|
getReducedMotionPreference: () => getReducedMotionPreference,
|
|
27
28
|
prefersReducedMotion: () => prefersReducedMotion,
|
|
28
29
|
shouldAnimate: () => shouldAnimate,
|
|
30
|
+
subscribeReducedMotion: () => subscribeReducedMotion,
|
|
29
31
|
trapFocus: () => trapFocus,
|
|
30
32
|
visuallyHiddenStyle: () => visuallyHiddenStyle
|
|
31
33
|
});
|
|
@@ -56,8 +58,23 @@ function isHTMLElement(v) {
|
|
|
56
58
|
return typeof HTMLElement !== "undefined" && v instanceof HTMLElement;
|
|
57
59
|
}
|
|
58
60
|
function prefersReducedMotion() {
|
|
61
|
+
return getPrefersReducedMotionSnapshot();
|
|
62
|
+
}
|
|
63
|
+
function getPrefersReducedMotionSnapshot() {
|
|
59
64
|
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
60
65
|
}
|
|
66
|
+
function subscribeReducedMotion(listener) {
|
|
67
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
68
|
+
listener(false);
|
|
69
|
+
return () => {
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
73
|
+
const onChange = () => listener(mq.matches);
|
|
74
|
+
listener(mq.matches);
|
|
75
|
+
mq.addEventListener("change", onChange);
|
|
76
|
+
return () => mq.removeEventListener("change", onChange);
|
|
77
|
+
}
|
|
61
78
|
function getReducedMotionPreference() {
|
|
62
79
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return "unknown";
|
|
63
80
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "reduce" : "no-preference";
|
|
@@ -70,22 +87,29 @@ function shouldAnimate(opts) {
|
|
|
70
87
|
}
|
|
71
88
|
function focusFirst(container) {
|
|
72
89
|
if (!container) return false;
|
|
73
|
-
const el = container.querySelector(
|
|
74
|
-
[
|
|
75
|
-
"a[href]",
|
|
76
|
-
"button:not([disabled])",
|
|
77
|
-
"input:not([disabled])",
|
|
78
|
-
"select:not([disabled])",
|
|
79
|
-
"textarea:not([disabled])",
|
|
80
|
-
"[tabindex]:not([tabindex='-1'])"
|
|
81
|
-
].join(",")
|
|
82
|
-
);
|
|
90
|
+
const el = container.querySelector(FOCUSABLE_SELECTORS);
|
|
83
91
|
el?.focus();
|
|
84
92
|
return Boolean(el);
|
|
85
93
|
}
|
|
86
94
|
function getFocusableElements(container) {
|
|
87
95
|
const candidates = Array.from(container.querySelectorAll(FOCUSABLE_SELECTORS));
|
|
88
|
-
return candidates.filter(
|
|
96
|
+
return candidates.filter(
|
|
97
|
+
(n) => isHTMLElement(n) && !n.hasAttribute("disabled") && isVisibleFocusable(n)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
function isVisibleFocusable(el) {
|
|
101
|
+
if (el.closest("[inert], [aria-hidden='true']")) return false;
|
|
102
|
+
if (typeof el.checkVisibility === "function") {
|
|
103
|
+
try {
|
|
104
|
+
return el.checkVisibility();
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (typeof window !== "undefined" && typeof window.getComputedStyle === "function") {
|
|
109
|
+
const style = window.getComputedStyle(el);
|
|
110
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
89
113
|
}
|
|
90
114
|
function trapFocus(container, opts) {
|
|
91
115
|
const restoreFocus = opts?.restoreFocus ?? true;
|
|
@@ -140,10 +164,12 @@ function trapFocus(container, opts) {
|
|
|
140
164
|
prevFocused = restoreFocus && isHTMLElement(document.activeElement) ? document.activeElement : null;
|
|
141
165
|
document.addEventListener("keydown", onKeyDown);
|
|
142
166
|
document.addEventListener("focusin", onFocusIn);
|
|
167
|
+
document.addEventListener("pointerdown", onPointerDownCapture, true);
|
|
143
168
|
document.addEventListener("mousedown", onPointerDownCapture, true);
|
|
144
169
|
removeListeners = () => {
|
|
145
170
|
document.removeEventListener("keydown", onKeyDown);
|
|
146
171
|
document.removeEventListener("focusin", onFocusIn);
|
|
172
|
+
document.removeEventListener("pointerdown", onPointerDownCapture, true);
|
|
147
173
|
document.removeEventListener("mousedown", onPointerDownCapture, true);
|
|
148
174
|
};
|
|
149
175
|
const focusables = getFocusableElements(container);
|
|
@@ -170,28 +196,41 @@ function createRovingTabIndex(opts) {
|
|
|
170
196
|
const loop = opts.loop ?? true;
|
|
171
197
|
const orientation = opts.orientation ?? "both";
|
|
172
198
|
let activeIndex = clampIndex(opts.initialIndex ?? 0, itemCount);
|
|
199
|
+
const focusActiveItem = () => {
|
|
200
|
+
if (!opts.getId || typeof document === "undefined") return;
|
|
201
|
+
const id = opts.getId(activeIndex);
|
|
202
|
+
if (!id) return;
|
|
203
|
+
document.getElementById(id)?.focus();
|
|
204
|
+
};
|
|
205
|
+
const notifyActiveChange = () => {
|
|
206
|
+
opts.onActiveIndexChange?.(activeIndex);
|
|
207
|
+
};
|
|
173
208
|
const setActiveIndex = (next) => {
|
|
174
209
|
activeIndex = clampIndex(next, itemCount);
|
|
210
|
+
notifyActiveChange();
|
|
211
|
+
focusActiveItem();
|
|
175
212
|
};
|
|
176
213
|
const move = (delta) => {
|
|
177
214
|
if (!itemCount) return;
|
|
178
215
|
const next = activeIndex + delta;
|
|
179
216
|
if (loop) {
|
|
180
217
|
activeIndex = (next % itemCount + itemCount) % itemCount;
|
|
181
|
-
|
|
218
|
+
} else {
|
|
219
|
+
activeIndex = clampIndex(next, itemCount);
|
|
182
220
|
}
|
|
183
|
-
|
|
221
|
+
notifyActiveChange();
|
|
222
|
+
focusActiveItem();
|
|
184
223
|
};
|
|
185
224
|
const onKeyDown = (e) => {
|
|
186
225
|
if (!itemCount) return;
|
|
187
226
|
switch (e.key) {
|
|
188
227
|
case "Home":
|
|
189
228
|
e.preventDefault();
|
|
190
|
-
|
|
229
|
+
setActiveIndex(0);
|
|
191
230
|
return;
|
|
192
231
|
case "End":
|
|
193
232
|
e.preventDefault();
|
|
194
|
-
|
|
233
|
+
setActiveIndex(itemCount - 1);
|
|
195
234
|
return;
|
|
196
235
|
case "ArrowLeft":
|
|
197
236
|
if (orientation === "vertical") return;
|
|
@@ -242,9 +281,11 @@ function clampIndex(index, itemCount) {
|
|
|
242
281
|
createRovingTabIndex,
|
|
243
282
|
focusFirst,
|
|
244
283
|
getFocusableElements,
|
|
284
|
+
getPrefersReducedMotionSnapshot,
|
|
245
285
|
getReducedMotionPreference,
|
|
246
286
|
prefersReducedMotion,
|
|
247
287
|
shouldAnimate,
|
|
288
|
+
subscribeReducedMotion,
|
|
248
289
|
trapFocus,
|
|
249
290
|
visuallyHiddenStyle
|
|
250
291
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -23,6 +23,8 @@ type RovingTabIndexOptions = {
|
|
|
23
23
|
orientation?: "horizontal" | "vertical" | "both";
|
|
24
24
|
loop?: boolean;
|
|
25
25
|
initialIndex?: number;
|
|
26
|
+
/** Called when the active index changes so consumers can re-render. */
|
|
27
|
+
onActiveIndexChange?: (index: number) => void;
|
|
26
28
|
};
|
|
27
29
|
/** Screen-reader-only styles (no external CSS required). */
|
|
28
30
|
declare const visuallyHiddenStyle: VisuallyHiddenStyle;
|
|
@@ -30,6 +32,10 @@ type FocusContainer = {
|
|
|
30
32
|
querySelector<T>(selectors: string): T | null;
|
|
31
33
|
};
|
|
32
34
|
declare function prefersReducedMotion(): boolean;
|
|
35
|
+
/** One-shot read of the prefers-reduced-motion media query. */
|
|
36
|
+
declare function getPrefersReducedMotionSnapshot(): boolean;
|
|
37
|
+
/** Subscribe to OS reduced-motion preference changes; returns an unsubscribe function. */
|
|
38
|
+
declare function subscribeReducedMotion(listener: (reduce: boolean) => void): () => void;
|
|
33
39
|
declare function getReducedMotionPreference(): "reduce" | "no-preference" | "unknown";
|
|
34
40
|
declare function shouldAnimate(opts?: {
|
|
35
41
|
default?: boolean;
|
|
@@ -54,4 +60,4 @@ declare function createRovingTabIndex(opts: RovingTabIndexOptions): {
|
|
|
54
60
|
};
|
|
55
61
|
};
|
|
56
62
|
|
|
57
|
-
export { type FocusContainer, type Focusable, type RovingTabIndexOptions, type TrapFocusOptions, type VisuallyHiddenStyle, createRovingTabIndex, focusFirst, getFocusableElements, getReducedMotionPreference, prefersReducedMotion, shouldAnimate, trapFocus, visuallyHiddenStyle };
|
|
63
|
+
export { type FocusContainer, type Focusable, type RovingTabIndexOptions, type TrapFocusOptions, type VisuallyHiddenStyle, createRovingTabIndex, focusFirst, getFocusableElements, getPrefersReducedMotionSnapshot, getReducedMotionPreference, prefersReducedMotion, shouldAnimate, subscribeReducedMotion, trapFocus, visuallyHiddenStyle };
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ type RovingTabIndexOptions = {
|
|
|
23
23
|
orientation?: "horizontal" | "vertical" | "both";
|
|
24
24
|
loop?: boolean;
|
|
25
25
|
initialIndex?: number;
|
|
26
|
+
/** Called when the active index changes so consumers can re-render. */
|
|
27
|
+
onActiveIndexChange?: (index: number) => void;
|
|
26
28
|
};
|
|
27
29
|
/** Screen-reader-only styles (no external CSS required). */
|
|
28
30
|
declare const visuallyHiddenStyle: VisuallyHiddenStyle;
|
|
@@ -30,6 +32,10 @@ type FocusContainer = {
|
|
|
30
32
|
querySelector<T>(selectors: string): T | null;
|
|
31
33
|
};
|
|
32
34
|
declare function prefersReducedMotion(): boolean;
|
|
35
|
+
/** One-shot read of the prefers-reduced-motion media query. */
|
|
36
|
+
declare function getPrefersReducedMotionSnapshot(): boolean;
|
|
37
|
+
/** Subscribe to OS reduced-motion preference changes; returns an unsubscribe function. */
|
|
38
|
+
declare function subscribeReducedMotion(listener: (reduce: boolean) => void): () => void;
|
|
33
39
|
declare function getReducedMotionPreference(): "reduce" | "no-preference" | "unknown";
|
|
34
40
|
declare function shouldAnimate(opts?: {
|
|
35
41
|
default?: boolean;
|
|
@@ -54,4 +60,4 @@ declare function createRovingTabIndex(opts: RovingTabIndexOptions): {
|
|
|
54
60
|
};
|
|
55
61
|
};
|
|
56
62
|
|
|
57
|
-
export { type FocusContainer, type Focusable, type RovingTabIndexOptions, type TrapFocusOptions, type VisuallyHiddenStyle, createRovingTabIndex, focusFirst, getFocusableElements, getReducedMotionPreference, prefersReducedMotion, shouldAnimate, trapFocus, visuallyHiddenStyle };
|
|
63
|
+
export { type FocusContainer, type Focusable, type RovingTabIndexOptions, type TrapFocusOptions, type VisuallyHiddenStyle, createRovingTabIndex, focusFirst, getFocusableElements, getPrefersReducedMotionSnapshot, getReducedMotionPreference, prefersReducedMotion, shouldAnimate, subscribeReducedMotion, trapFocus, visuallyHiddenStyle };
|
package/dist/index.js
CHANGED
|
@@ -25,8 +25,23 @@ function isHTMLElement(v) {
|
|
|
25
25
|
return typeof HTMLElement !== "undefined" && v instanceof HTMLElement;
|
|
26
26
|
}
|
|
27
27
|
function prefersReducedMotion() {
|
|
28
|
+
return getPrefersReducedMotionSnapshot();
|
|
29
|
+
}
|
|
30
|
+
function getPrefersReducedMotionSnapshot() {
|
|
28
31
|
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
29
32
|
}
|
|
33
|
+
function subscribeReducedMotion(listener) {
|
|
34
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
35
|
+
listener(false);
|
|
36
|
+
return () => {
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
40
|
+
const onChange = () => listener(mq.matches);
|
|
41
|
+
listener(mq.matches);
|
|
42
|
+
mq.addEventListener("change", onChange);
|
|
43
|
+
return () => mq.removeEventListener("change", onChange);
|
|
44
|
+
}
|
|
30
45
|
function getReducedMotionPreference() {
|
|
31
46
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return "unknown";
|
|
32
47
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "reduce" : "no-preference";
|
|
@@ -39,22 +54,29 @@ function shouldAnimate(opts) {
|
|
|
39
54
|
}
|
|
40
55
|
function focusFirst(container) {
|
|
41
56
|
if (!container) return false;
|
|
42
|
-
const el = container.querySelector(
|
|
43
|
-
[
|
|
44
|
-
"a[href]",
|
|
45
|
-
"button:not([disabled])",
|
|
46
|
-
"input:not([disabled])",
|
|
47
|
-
"select:not([disabled])",
|
|
48
|
-
"textarea:not([disabled])",
|
|
49
|
-
"[tabindex]:not([tabindex='-1'])"
|
|
50
|
-
].join(",")
|
|
51
|
-
);
|
|
57
|
+
const el = container.querySelector(FOCUSABLE_SELECTORS);
|
|
52
58
|
el?.focus();
|
|
53
59
|
return Boolean(el);
|
|
54
60
|
}
|
|
55
61
|
function getFocusableElements(container) {
|
|
56
62
|
const candidates = Array.from(container.querySelectorAll(FOCUSABLE_SELECTORS));
|
|
57
|
-
return candidates.filter(
|
|
63
|
+
return candidates.filter(
|
|
64
|
+
(n) => isHTMLElement(n) && !n.hasAttribute("disabled") && isVisibleFocusable(n)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function isVisibleFocusable(el) {
|
|
68
|
+
if (el.closest("[inert], [aria-hidden='true']")) return false;
|
|
69
|
+
if (typeof el.checkVisibility === "function") {
|
|
70
|
+
try {
|
|
71
|
+
return el.checkVisibility();
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (typeof window !== "undefined" && typeof window.getComputedStyle === "function") {
|
|
76
|
+
const style = window.getComputedStyle(el);
|
|
77
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
58
80
|
}
|
|
59
81
|
function trapFocus(container, opts) {
|
|
60
82
|
const restoreFocus = opts?.restoreFocus ?? true;
|
|
@@ -109,10 +131,12 @@ function trapFocus(container, opts) {
|
|
|
109
131
|
prevFocused = restoreFocus && isHTMLElement(document.activeElement) ? document.activeElement : null;
|
|
110
132
|
document.addEventListener("keydown", onKeyDown);
|
|
111
133
|
document.addEventListener("focusin", onFocusIn);
|
|
134
|
+
document.addEventListener("pointerdown", onPointerDownCapture, true);
|
|
112
135
|
document.addEventListener("mousedown", onPointerDownCapture, true);
|
|
113
136
|
removeListeners = () => {
|
|
114
137
|
document.removeEventListener("keydown", onKeyDown);
|
|
115
138
|
document.removeEventListener("focusin", onFocusIn);
|
|
139
|
+
document.removeEventListener("pointerdown", onPointerDownCapture, true);
|
|
116
140
|
document.removeEventListener("mousedown", onPointerDownCapture, true);
|
|
117
141
|
};
|
|
118
142
|
const focusables = getFocusableElements(container);
|
|
@@ -139,28 +163,41 @@ function createRovingTabIndex(opts) {
|
|
|
139
163
|
const loop = opts.loop ?? true;
|
|
140
164
|
const orientation = opts.orientation ?? "both";
|
|
141
165
|
let activeIndex = clampIndex(opts.initialIndex ?? 0, itemCount);
|
|
166
|
+
const focusActiveItem = () => {
|
|
167
|
+
if (!opts.getId || typeof document === "undefined") return;
|
|
168
|
+
const id = opts.getId(activeIndex);
|
|
169
|
+
if (!id) return;
|
|
170
|
+
document.getElementById(id)?.focus();
|
|
171
|
+
};
|
|
172
|
+
const notifyActiveChange = () => {
|
|
173
|
+
opts.onActiveIndexChange?.(activeIndex);
|
|
174
|
+
};
|
|
142
175
|
const setActiveIndex = (next) => {
|
|
143
176
|
activeIndex = clampIndex(next, itemCount);
|
|
177
|
+
notifyActiveChange();
|
|
178
|
+
focusActiveItem();
|
|
144
179
|
};
|
|
145
180
|
const move = (delta) => {
|
|
146
181
|
if (!itemCount) return;
|
|
147
182
|
const next = activeIndex + delta;
|
|
148
183
|
if (loop) {
|
|
149
184
|
activeIndex = (next % itemCount + itemCount) % itemCount;
|
|
150
|
-
|
|
185
|
+
} else {
|
|
186
|
+
activeIndex = clampIndex(next, itemCount);
|
|
151
187
|
}
|
|
152
|
-
|
|
188
|
+
notifyActiveChange();
|
|
189
|
+
focusActiveItem();
|
|
153
190
|
};
|
|
154
191
|
const onKeyDown = (e) => {
|
|
155
192
|
if (!itemCount) return;
|
|
156
193
|
switch (e.key) {
|
|
157
194
|
case "Home":
|
|
158
195
|
e.preventDefault();
|
|
159
|
-
|
|
196
|
+
setActiveIndex(0);
|
|
160
197
|
return;
|
|
161
198
|
case "End":
|
|
162
199
|
e.preventDefault();
|
|
163
|
-
|
|
200
|
+
setActiveIndex(itemCount - 1);
|
|
164
201
|
return;
|
|
165
202
|
case "ArrowLeft":
|
|
166
203
|
if (orientation === "vertical") return;
|
|
@@ -210,9 +247,11 @@ export {
|
|
|
210
247
|
createRovingTabIndex,
|
|
211
248
|
focusFirst,
|
|
212
249
|
getFocusableElements,
|
|
250
|
+
getPrefersReducedMotionSnapshot,
|
|
213
251
|
getReducedMotionPreference,
|
|
214
252
|
prefersReducedMotion,
|
|
215
253
|
shouldAnimate,
|
|
254
|
+
subscribeReducedMotion,
|
|
216
255
|
trapFocus,
|
|
217
256
|
visuallyHiddenStyle
|
|
218
257
|
};
|