@lessonkit/accessibility 1.3.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 CHANGED
@@ -93,7 +93,23 @@ function focusFirst(container) {
93
93
  }
94
94
  function getFocusableElements(container) {
95
95
  const candidates = Array.from(container.querySelectorAll(FOCUSABLE_SELECTORS));
96
- return candidates.filter((n) => isHTMLElement(n) && !n.hasAttribute("disabled"));
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;
97
113
  }
98
114
  function trapFocus(container, opts) {
99
115
  const restoreFocus = opts?.restoreFocus ?? true;
@@ -180,28 +196,41 @@ function createRovingTabIndex(opts) {
180
196
  const loop = opts.loop ?? true;
181
197
  const orientation = opts.orientation ?? "both";
182
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
+ };
183
208
  const setActiveIndex = (next) => {
184
209
  activeIndex = clampIndex(next, itemCount);
210
+ notifyActiveChange();
211
+ focusActiveItem();
185
212
  };
186
213
  const move = (delta) => {
187
214
  if (!itemCount) return;
188
215
  const next = activeIndex + delta;
189
216
  if (loop) {
190
217
  activeIndex = (next % itemCount + itemCount) % itemCount;
191
- return;
218
+ } else {
219
+ activeIndex = clampIndex(next, itemCount);
192
220
  }
193
- activeIndex = clampIndex(next, itemCount);
221
+ notifyActiveChange();
222
+ focusActiveItem();
194
223
  };
195
224
  const onKeyDown = (e) => {
196
225
  if (!itemCount) return;
197
226
  switch (e.key) {
198
227
  case "Home":
199
228
  e.preventDefault();
200
- activeIndex = 0;
229
+ setActiveIndex(0);
201
230
  return;
202
231
  case "End":
203
232
  e.preventDefault();
204
- activeIndex = itemCount - 1;
233
+ setActiveIndex(itemCount - 1);
205
234
  return;
206
235
  case "ArrowLeft":
207
236
  if (orientation === "vertical") return;
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;
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;
package/dist/index.js CHANGED
@@ -60,7 +60,23 @@ function focusFirst(container) {
60
60
  }
61
61
  function getFocusableElements(container) {
62
62
  const candidates = Array.from(container.querySelectorAll(FOCUSABLE_SELECTORS));
63
- return candidates.filter((n) => isHTMLElement(n) && !n.hasAttribute("disabled"));
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;
64
80
  }
65
81
  function trapFocus(container, opts) {
66
82
  const restoreFocus = opts?.restoreFocus ?? true;
@@ -147,28 +163,41 @@ function createRovingTabIndex(opts) {
147
163
  const loop = opts.loop ?? true;
148
164
  const orientation = opts.orientation ?? "both";
149
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
+ };
150
175
  const setActiveIndex = (next) => {
151
176
  activeIndex = clampIndex(next, itemCount);
177
+ notifyActiveChange();
178
+ focusActiveItem();
152
179
  };
153
180
  const move = (delta) => {
154
181
  if (!itemCount) return;
155
182
  const next = activeIndex + delta;
156
183
  if (loop) {
157
184
  activeIndex = (next % itemCount + itemCount) % itemCount;
158
- return;
185
+ } else {
186
+ activeIndex = clampIndex(next, itemCount);
159
187
  }
160
- activeIndex = clampIndex(next, itemCount);
188
+ notifyActiveChange();
189
+ focusActiveItem();
161
190
  };
162
191
  const onKeyDown = (e) => {
163
192
  if (!itemCount) return;
164
193
  switch (e.key) {
165
194
  case "Home":
166
195
  e.preventDefault();
167
- activeIndex = 0;
196
+ setActiveIndex(0);
168
197
  return;
169
198
  case "End":
170
199
  e.preventDefault();
171
- activeIndex = itemCount - 1;
200
+ setActiveIndex(itemCount - 1);
172
201
  return;
173
202
  case "ArrowLeft":
174
203
  if (orientation === "vertical") return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/accessibility",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "private": false,
5
5
  "description": "Accessibility utilities for LessonKit packages and apps.",
6
6
  "license": "Apache-2.0",