@reshaped/utilities 3.9.1-canary.2 → 3.10.0-canary.4
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/a11y/Chain.d.ts +20 -0
- package/dist/a11y/Chain.js +60 -0
- package/dist/a11y/TrapFocus.d.ts +28 -0
- package/dist/a11y/TrapFocus.js +162 -0
- package/dist/a11y/TrapScreenReader.d.ts +15 -0
- package/dist/a11y/TrapScreenReader.js +42 -0
- package/dist/a11y/focus.d.ts +38 -0
- package/dist/a11y/focus.js +101 -0
- package/dist/a11y/index.d.ts +4 -0
- package/dist/a11y/index.js +3 -0
- package/dist/a11y/keyboardMode.d.ts +4 -0
- package/dist/a11y/keyboardMode.js +10 -0
- package/dist/a11y/tests/Chain.test.js +88 -0
- package/dist/a11y/tests/TrapFocus.test.d.ts +1 -0
- package/dist/a11y/tests/TrapFocus.test.js +313 -0
- package/dist/a11y/tests/TrapScreenReader.test.d.ts +1 -0
- package/dist/a11y/tests/TrapScreenReader.test.js +126 -0
- package/dist/a11y/tests/focus.test.d.ts +1 -0
- package/dist/a11y/tests/focus.test.js +278 -0
- package/dist/a11y/tests/keyboardMode.test.d.ts +1 -0
- package/dist/a11y/tests/keyboardMode.test.js +27 -0
- package/dist/a11y/types.d.ts +24 -0
- package/dist/a11y/types.js +1 -0
- package/dist/constants/keys.d.ts +11 -0
- package/dist/constants/keys.js +11 -0
- package/dist/css/StyleCache.d.ts +7 -0
- package/dist/css/StyleCache.js +19 -0
- package/dist/css/classNames.d.ts +7 -0
- package/dist/css/classNames.js +19 -0
- package/dist/css/index.d.ts +2 -0
- package/dist/css/index.js +4 -0
- package/dist/css/tests/StyleCache.test.d.ts +1 -0
- package/dist/css/tests/StyleCache.test.js +45 -0
- package/dist/css/tests/classNames.test.d.ts +1 -0
- package/dist/css/tests/classNames.test.js +63 -0
- package/dist/dom/findClosestScrollableContainer.d.ts +5 -0
- package/dist/dom/findClosestScrollableContainer.js +12 -0
- package/dist/dom/findParent.d.ts +2 -0
- package/dist/dom/findParent.js +10 -0
- package/dist/dom/index.d.ts +3 -0
- package/dist/dom/index.js +4 -0
- package/dist/dom/tests/findClosestScrollableContainer.test.d.ts +1 -0
- package/dist/dom/tests/findClosestScrollableContainer.test.js +61 -0
- package/dist/dom/tests/findParent.test.d.ts +1 -0
- package/dist/dom/tests/findParent.test.js +45 -0
- package/dist/flyout/Flyout.js +11 -9
- package/dist/flyout/constants.d.ts +1 -1
- package/dist/flyout/constants.js +1 -1
- package/dist/flyout/index.d.ts +1 -1
- package/dist/flyout/index.js +1 -1
- package/dist/flyout/tests/Flyout.test.js +1 -1
- package/dist/flyout/types.d.ts +1 -1
- package/dist/flyout/utilities/applyPosition.js +46 -26
- package/dist/flyout/utilities/calculateLayoutAdjustment.d.ts +19 -0
- package/dist/flyout/utilities/calculateLayoutAdjustment.js +73 -0
- package/dist/flyout/utilities/calculatePosition.d.ts +7 -20
- package/dist/flyout/utilities/calculatePosition.js +11 -87
- package/dist/flyout/utilities/isFullyVisible.d.ts +2 -4
- package/dist/flyout/utilities/isFullyVisible.js +11 -14
- package/dist/flyout/utilities/tests/applyPosition.test.js +1 -1
- package/dist/flyout/utilities/tests/calculateLayoutAdjustment.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/calculateLayoutAdjustment.test.js +384 -0
- package/dist/flyout/utilities/tests/calculatePosition.test.js +244 -293
- package/dist/flyout/utilities/tests/centerBySize.test.js +1 -1
- package/dist/flyout/utilities/tests/getPositionFallbacks.test.js +1 -1
- package/dist/flyout/utilities/tests/getRTLPosition.test.js +1 -1
- package/dist/flyout/utilities/tests/isFullyVisible.test.js +28 -52
- package/dist/helpers/classNames.d.ts +7 -0
- package/dist/helpers/classNames.js +19 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +2 -0
- package/dist/helpers/tests/classNames.test.d.ts +1 -0
- package/dist/helpers/tests/classNames.test.js +63 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +2 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/internal.d.ts +11 -0
- package/dist/internal.js +10 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +16 -0
- package/dist/scroll/disable.d.ts +7 -0
- package/dist/scroll/disable.js +15 -0
- package/dist/scroll/helpers.d.ts +1 -0
- package/dist/scroll/helpers.js +17 -0
- package/dist/scroll/index.d.ts +2 -0
- package/dist/scroll/index.js +4 -0
- package/dist/scroll/lock.d.ts +7 -0
- package/dist/scroll/lock.js +26 -0
- package/dist/scroll/lockSafari.d.ts +2 -0
- package/dist/scroll/lockSafari.js +20 -0
- package/dist/scroll/lockStandard.d.ts +4 -0
- package/dist/scroll/lockStandard.js +15 -0
- package/dist/scroll/tests/lock.test.d.ts +1 -0
- package/dist/scroll/tests/lock.test.js +81 -0
- package/package.json +6 -1
- package/dist/flyout/utilities/findClosestFixedContainer.d.ts +0 -5
- package/dist/flyout/utilities/findClosestFixedContainer.js +0 -18
- package/dist/flyout/utilities/tests/findClosestFixedContainer.test.js +0 -46
- /package/dist/{flyout/utilities/tests/findClosestFixedContainer.test.d.ts → a11y/tests/Chain.test.d.ts} +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { expect, test, describe, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import TrapFocus from "../TrapFocus.js";
|
|
3
|
+
describe("a11y/TrapFocus", () => {
|
|
4
|
+
let container;
|
|
5
|
+
let trigger;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
container = document.createElement("div");
|
|
8
|
+
document.body.appendChild(container);
|
|
9
|
+
trigger = document.createElement("button");
|
|
10
|
+
trigger.textContent = "Trigger";
|
|
11
|
+
document.body.appendChild(trigger);
|
|
12
|
+
trigger.focus();
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
document.body.removeChild(container);
|
|
16
|
+
document.body.removeChild(trigger);
|
|
17
|
+
});
|
|
18
|
+
describe("basic functionality", () => {
|
|
19
|
+
test("traps focus within root element", () => {
|
|
20
|
+
container.innerHTML = `
|
|
21
|
+
<button id="btn1">Button 1</button>
|
|
22
|
+
<button id="btn2">Button 2</button>
|
|
23
|
+
`;
|
|
24
|
+
const trap = new TrapFocus();
|
|
25
|
+
trap.trap(container);
|
|
26
|
+
expect(trap.trapped).toBe(true);
|
|
27
|
+
// Should focus first element
|
|
28
|
+
expect(document.activeElement?.id).toBe("btn1");
|
|
29
|
+
});
|
|
30
|
+
test("releases trap and returns focus to trigger", () => {
|
|
31
|
+
container.innerHTML = `
|
|
32
|
+
<button id="btn1">Button 1</button>
|
|
33
|
+
<button id="btn2">Button 2</button>
|
|
34
|
+
`;
|
|
35
|
+
const trap = new TrapFocus();
|
|
36
|
+
trap.trap(container);
|
|
37
|
+
trap.release();
|
|
38
|
+
expect(trap.trapped).toBe(false);
|
|
39
|
+
expect(document.activeElement).toBe(trigger);
|
|
40
|
+
});
|
|
41
|
+
test("does not trap when no focusable elements", () => {
|
|
42
|
+
container.innerHTML = `<div>No focusable content</div>`;
|
|
43
|
+
const trap = new TrapFocus();
|
|
44
|
+
trap.trap(container);
|
|
45
|
+
expect(trap.trapped).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
test("focuses initialFocusEl when provided", () => {
|
|
48
|
+
container.innerHTML = `
|
|
49
|
+
<button id="btn1">Button 1</button>
|
|
50
|
+
<button id="btn2">Button 2</button>
|
|
51
|
+
`;
|
|
52
|
+
const btn2 = container.querySelector("#btn2");
|
|
53
|
+
const trap = new TrapFocus();
|
|
54
|
+
trap.trap(container, { initialFocusEl: btn2 });
|
|
55
|
+
expect(document.activeElement).toBe(btn2);
|
|
56
|
+
});
|
|
57
|
+
test("calls onRelease callback when trap is released by Tab navigation", () => {
|
|
58
|
+
container.innerHTML = `<button id="btn1">Button 1</button>`;
|
|
59
|
+
const onRelease = vi.fn();
|
|
60
|
+
const trap = new TrapFocus();
|
|
61
|
+
trap.trap(container, { mode: "action-menu", onRelease });
|
|
62
|
+
const btn1 = container.querySelector("#btn1");
|
|
63
|
+
btn1.focus();
|
|
64
|
+
// Tab should release action-menu mode
|
|
65
|
+
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true });
|
|
66
|
+
document.dispatchEvent(event);
|
|
67
|
+
expect(onRelease).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("dialog mode", () => {
|
|
71
|
+
test("navigates forward with Tab key", () => {
|
|
72
|
+
container.innerHTML = `
|
|
73
|
+
<button id="btn1">Button 1</button>
|
|
74
|
+
<button id="btn2">Button 2</button>
|
|
75
|
+
<button id="btn3">Button 3</button>
|
|
76
|
+
`;
|
|
77
|
+
const trap = new TrapFocus();
|
|
78
|
+
trap.trap(container, { mode: "dialog" });
|
|
79
|
+
const btn1 = container.querySelector("#btn1");
|
|
80
|
+
const btn2 = container.querySelector("#btn2");
|
|
81
|
+
btn1.focus();
|
|
82
|
+
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
|
83
|
+
document.dispatchEvent(event);
|
|
84
|
+
expect(document.activeElement).toBe(btn2);
|
|
85
|
+
});
|
|
86
|
+
test("navigates backward with Shift+Tab", () => {
|
|
87
|
+
container.innerHTML = `
|
|
88
|
+
<button id="btn1">Button 1</button>
|
|
89
|
+
<button id="btn2">Button 2</button>
|
|
90
|
+
<button id="btn3">Button 3</button>
|
|
91
|
+
`;
|
|
92
|
+
const trap = new TrapFocus();
|
|
93
|
+
trap.trap(container, { mode: "dialog" });
|
|
94
|
+
const btn1 = container.querySelector("#btn1");
|
|
95
|
+
const btn2 = container.querySelector("#btn2");
|
|
96
|
+
btn2.focus();
|
|
97
|
+
const event = new KeyboardEvent("keydown", {
|
|
98
|
+
key: "Tab",
|
|
99
|
+
shiftKey: true,
|
|
100
|
+
bubbles: true,
|
|
101
|
+
cancelable: true,
|
|
102
|
+
});
|
|
103
|
+
document.dispatchEvent(event);
|
|
104
|
+
expect(document.activeElement).toBe(btn1);
|
|
105
|
+
});
|
|
106
|
+
test("wraps focus to first element when tabbing from last", () => {
|
|
107
|
+
container.innerHTML = `
|
|
108
|
+
<button id="btn1">Button 1</button>
|
|
109
|
+
<button id="btn2">Button 2</button>
|
|
110
|
+
`;
|
|
111
|
+
const trap = new TrapFocus();
|
|
112
|
+
trap.trap(container, { mode: "dialog" });
|
|
113
|
+
const btn1 = container.querySelector("#btn1");
|
|
114
|
+
const btn2 = container.querySelector("#btn2");
|
|
115
|
+
btn2.focus();
|
|
116
|
+
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
|
117
|
+
document.dispatchEvent(event);
|
|
118
|
+
expect(document.activeElement).toBe(btn1);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("action-menu mode", () => {
|
|
122
|
+
test("navigates with ArrowDown key", () => {
|
|
123
|
+
container.innerHTML = `
|
|
124
|
+
<button id="btn1">Button 1</button>
|
|
125
|
+
<button id="btn2">Button 2</button>
|
|
126
|
+
`;
|
|
127
|
+
const trap = new TrapFocus();
|
|
128
|
+
trap.trap(container, { mode: "action-menu" });
|
|
129
|
+
const btn1 = container.querySelector("#btn1");
|
|
130
|
+
const btn2 = container.querySelector("#btn2");
|
|
131
|
+
btn1.focus();
|
|
132
|
+
const event = new KeyboardEvent("keydown", {
|
|
133
|
+
key: "ArrowDown",
|
|
134
|
+
bubbles: true,
|
|
135
|
+
cancelable: true,
|
|
136
|
+
});
|
|
137
|
+
document.dispatchEvent(event);
|
|
138
|
+
expect(document.activeElement).toBe(btn2);
|
|
139
|
+
});
|
|
140
|
+
test("navigates with ArrowUp key", () => {
|
|
141
|
+
container.innerHTML = `
|
|
142
|
+
<button id="btn1">Button 1</button>
|
|
143
|
+
<button id="btn2">Button 2</button>
|
|
144
|
+
`;
|
|
145
|
+
const trap = new TrapFocus();
|
|
146
|
+
trap.trap(container, { mode: "action-menu" });
|
|
147
|
+
const btn1 = container.querySelector("#btn1");
|
|
148
|
+
const btn2 = container.querySelector("#btn2");
|
|
149
|
+
btn2.focus();
|
|
150
|
+
const event = new KeyboardEvent("keydown", {
|
|
151
|
+
key: "ArrowUp",
|
|
152
|
+
bubbles: true,
|
|
153
|
+
cancelable: true,
|
|
154
|
+
});
|
|
155
|
+
document.dispatchEvent(event);
|
|
156
|
+
expect(document.activeElement).toBe(btn1);
|
|
157
|
+
});
|
|
158
|
+
test("releases trap when Tab is pressed", () => {
|
|
159
|
+
container.innerHTML = `<button id="btn1">Button 1</button>`;
|
|
160
|
+
const trap = new TrapFocus();
|
|
161
|
+
trap.trap(container, { mode: "action-menu" });
|
|
162
|
+
const btn1 = container.querySelector("#btn1");
|
|
163
|
+
btn1.focus();
|
|
164
|
+
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
|
165
|
+
document.dispatchEvent(event);
|
|
166
|
+
expect(trap.trapped).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe("action-bar mode", () => {
|
|
170
|
+
test("navigates with ArrowRight key", () => {
|
|
171
|
+
container.innerHTML = `
|
|
172
|
+
<button id="btn1">Button 1</button>
|
|
173
|
+
<button id="btn2">Button 2</button>
|
|
174
|
+
`;
|
|
175
|
+
const trap = new TrapFocus();
|
|
176
|
+
trap.trap(container, { mode: "action-bar" });
|
|
177
|
+
const btn1 = container.querySelector("#btn1");
|
|
178
|
+
const btn2 = container.querySelector("#btn2");
|
|
179
|
+
btn1.focus();
|
|
180
|
+
const event = new KeyboardEvent("keydown", {
|
|
181
|
+
key: "ArrowRight",
|
|
182
|
+
bubbles: true,
|
|
183
|
+
cancelable: true,
|
|
184
|
+
});
|
|
185
|
+
document.dispatchEvent(event);
|
|
186
|
+
expect(document.activeElement).toBe(btn2);
|
|
187
|
+
});
|
|
188
|
+
test("navigates with ArrowLeft key", () => {
|
|
189
|
+
container.innerHTML = `
|
|
190
|
+
<button id="btn1">Button 1</button>
|
|
191
|
+
<button id="btn2">Button 2</button>
|
|
192
|
+
`;
|
|
193
|
+
const trap = new TrapFocus();
|
|
194
|
+
trap.trap(container, { mode: "action-bar" });
|
|
195
|
+
const btn1 = container.querySelector("#btn1");
|
|
196
|
+
const btn2 = container.querySelector("#btn2");
|
|
197
|
+
btn2.focus();
|
|
198
|
+
const event = new KeyboardEvent("keydown", {
|
|
199
|
+
key: "ArrowLeft",
|
|
200
|
+
bubbles: true,
|
|
201
|
+
cancelable: true,
|
|
202
|
+
});
|
|
203
|
+
document.dispatchEvent(event);
|
|
204
|
+
expect(document.activeElement).toBe(btn1);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe("content-menu mode", () => {
|
|
208
|
+
test("releases trap when tabbing past last element", () => {
|
|
209
|
+
container.innerHTML = `
|
|
210
|
+
<button id="btn1">Button 1</button>
|
|
211
|
+
<button id="btn2">Button 2</button>
|
|
212
|
+
`;
|
|
213
|
+
const trap = new TrapFocus();
|
|
214
|
+
trap.trap(container, { mode: "content-menu" });
|
|
215
|
+
const btn2 = container.querySelector("#btn2");
|
|
216
|
+
btn2.focus();
|
|
217
|
+
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
|
218
|
+
document.dispatchEvent(event);
|
|
219
|
+
expect(trap.trapped).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
test("continues navigation when not at boundaries", () => {
|
|
222
|
+
container.innerHTML = `
|
|
223
|
+
<button id="btn1">Button 1</button>
|
|
224
|
+
<button id="btn2">Button 2</button>
|
|
225
|
+
<button id="btn3">Button 3</button>
|
|
226
|
+
`;
|
|
227
|
+
const trap = new TrapFocus();
|
|
228
|
+
trap.trap(container, { mode: "content-menu" });
|
|
229
|
+
const btn1 = container.querySelector("#btn1");
|
|
230
|
+
const btn2 = container.querySelector("#btn2");
|
|
231
|
+
btn1.focus();
|
|
232
|
+
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
|
233
|
+
document.dispatchEvent(event);
|
|
234
|
+
expect(document.activeElement).toBe(btn2);
|
|
235
|
+
expect(trap.trapped).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
describe("selection-menu mode", () => {
|
|
239
|
+
test("uses pseudo focus with data-rs-focus attribute", () => {
|
|
240
|
+
container.innerHTML = `
|
|
241
|
+
<button id="btn1">Button 1</button>
|
|
242
|
+
<button id="btn2">Button 2</button>
|
|
243
|
+
`;
|
|
244
|
+
const trap = new TrapFocus();
|
|
245
|
+
trap.trap(container, { mode: "selection-menu" });
|
|
246
|
+
const btn1 = container.querySelector("#btn1");
|
|
247
|
+
const btn2 = container.querySelector("#btn2");
|
|
248
|
+
// First element should have pseudo focus
|
|
249
|
+
expect(btn1.getAttribute("data-rs-focus")).toBe("true");
|
|
250
|
+
// Navigate with arrow down
|
|
251
|
+
const event = new KeyboardEvent("keydown", {
|
|
252
|
+
key: "ArrowDown",
|
|
253
|
+
bubbles: true,
|
|
254
|
+
cancelable: true,
|
|
255
|
+
});
|
|
256
|
+
document.dispatchEvent(event);
|
|
257
|
+
// Pseudo focus should move
|
|
258
|
+
expect(btn1.hasAttribute("data-rs-focus")).toBe(false);
|
|
259
|
+
expect(btn2.getAttribute("data-rs-focus")).toBe("true");
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
describe("includeTrigger option", () => {
|
|
263
|
+
test("includes trigger in focus navigation when enabled", () => {
|
|
264
|
+
container.innerHTML = `<button id="btn1">Button 1</button>`;
|
|
265
|
+
const trap = new TrapFocus();
|
|
266
|
+
trap.trap(container, { mode: "dialog", includeTrigger: true });
|
|
267
|
+
const btn1 = container.querySelector("#btn1");
|
|
268
|
+
// Trigger should be first in navigation order
|
|
269
|
+
expect(document.activeElement).toBe(trigger);
|
|
270
|
+
// Tab should move to btn1
|
|
271
|
+
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
|
272
|
+
document.dispatchEvent(event);
|
|
273
|
+
expect(document.activeElement).toBe(btn1);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
describe("chain management", () => {
|
|
277
|
+
test("manages multiple traps in a chain", () => {
|
|
278
|
+
const container1 = document.createElement("div");
|
|
279
|
+
container1.innerHTML = `<button id="btn1">Container 1</button>`;
|
|
280
|
+
document.body.appendChild(container1);
|
|
281
|
+
const container2 = document.createElement("div");
|
|
282
|
+
container2.innerHTML = `<button id="btn2">Container 2</button>`;
|
|
283
|
+
document.body.appendChild(container2);
|
|
284
|
+
const trap1 = new TrapFocus();
|
|
285
|
+
const trap2 = new TrapFocus();
|
|
286
|
+
trap1.trap(container1);
|
|
287
|
+
const btn1 = container1.querySelector("#btn1");
|
|
288
|
+
btn1.focus();
|
|
289
|
+
trap2.trap(container2);
|
|
290
|
+
// Only the last trap should respond to events
|
|
291
|
+
const btn2 = container2.querySelector("#btn2");
|
|
292
|
+
expect(document.activeElement).toBe(btn2);
|
|
293
|
+
// Release second trap
|
|
294
|
+
trap2.release();
|
|
295
|
+
// First trap should be restored
|
|
296
|
+
expect(document.activeElement).toBe(btn1);
|
|
297
|
+
trap1.release();
|
|
298
|
+
document.body.removeChild(container1);
|
|
299
|
+
document.body.removeChild(container2);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
describe("release options", () => {
|
|
303
|
+
test("does not return focus to trigger with withoutFocusReturn option", () => {
|
|
304
|
+
container.innerHTML = `<button id="btn1">Button 1</button>`;
|
|
305
|
+
const btn1 = container.querySelector("#btn1");
|
|
306
|
+
const trap = new TrapFocus();
|
|
307
|
+
trap.trap(container);
|
|
308
|
+
btn1.focus();
|
|
309
|
+
trap.release({ withoutFocusReturn: true });
|
|
310
|
+
expect(document.activeElement).not.toBe(trigger);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { expect, test, describe, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import TrapScreenReader from "../TrapScreenReader.js";
|
|
3
|
+
describe("a11y/TrapScreenReader", () => {
|
|
4
|
+
let container;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
container = document.createElement("div");
|
|
7
|
+
document.body.appendChild(container);
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
document.body.removeChild(container);
|
|
11
|
+
});
|
|
12
|
+
test("hides siblings from screen reader when trap is activated", () => {
|
|
13
|
+
container.innerHTML = `
|
|
14
|
+
<div id="sibling1">Sibling 1</div>
|
|
15
|
+
<div id="target">Target</div>
|
|
16
|
+
<div id="sibling2">Sibling 2</div>
|
|
17
|
+
`;
|
|
18
|
+
const target = container.querySelector("#target");
|
|
19
|
+
const sibling1 = container.querySelector("#sibling1");
|
|
20
|
+
const sibling2 = container.querySelector("#sibling2");
|
|
21
|
+
const trap = new TrapScreenReader(target);
|
|
22
|
+
trap.trap();
|
|
23
|
+
expect(sibling1.getAttribute("aria-hidden")).toBe("true");
|
|
24
|
+
expect(sibling2.getAttribute("aria-hidden")).toBe("true");
|
|
25
|
+
expect(target.hasAttribute("aria-hidden")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
test("releases trapped elements and removes aria-hidden", () => {
|
|
28
|
+
container.innerHTML = `
|
|
29
|
+
<div id="sibling1">Sibling 1</div>
|
|
30
|
+
<div id="target">Target</div>
|
|
31
|
+
<div id="sibling2">Sibling 2</div>
|
|
32
|
+
`;
|
|
33
|
+
const target = container.querySelector("#target");
|
|
34
|
+
const sibling1 = container.querySelector("#sibling1");
|
|
35
|
+
const sibling2 = container.querySelector("#sibling2");
|
|
36
|
+
const trap = new TrapScreenReader(target);
|
|
37
|
+
trap.trap();
|
|
38
|
+
trap.release();
|
|
39
|
+
expect(sibling1.hasAttribute("aria-hidden")).toBe(false);
|
|
40
|
+
expect(sibling2.hasAttribute("aria-hidden")).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
test("traverses up the DOM tree and hides siblings at each level", () => {
|
|
43
|
+
container.innerHTML = `
|
|
44
|
+
<div id="outer-sibling">Outer Sibling</div>
|
|
45
|
+
<div id="parent">
|
|
46
|
+
<div id="inner-sibling">Inner Sibling</div>
|
|
47
|
+
<div id="target">Target</div>
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
const target = container.querySelector("#target");
|
|
51
|
+
const innerSibling = container.querySelector("#inner-sibling");
|
|
52
|
+
const outerSibling = container.querySelector("#outer-sibling");
|
|
53
|
+
const parent = container.querySelector("#parent");
|
|
54
|
+
const trap = new TrapScreenReader(target);
|
|
55
|
+
trap.trap();
|
|
56
|
+
// Inner sibling should be hidden
|
|
57
|
+
expect(innerSibling.getAttribute("aria-hidden")).toBe("true");
|
|
58
|
+
// Outer sibling should be hidden
|
|
59
|
+
expect(outerSibling.getAttribute("aria-hidden")).toBe("true");
|
|
60
|
+
// Parent and target should not be hidden
|
|
61
|
+
expect(parent.hasAttribute("aria-hidden")).toBe(false);
|
|
62
|
+
expect(target.hasAttribute("aria-hidden")).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
test("does not hide elements that already have aria-hidden", () => {
|
|
65
|
+
container.innerHTML = `
|
|
66
|
+
<div id="sibling1" aria-hidden="true">Already Hidden</div>
|
|
67
|
+
<div id="target">Target</div>
|
|
68
|
+
<div id="sibling2">Sibling 2</div>
|
|
69
|
+
`;
|
|
70
|
+
const target = container.querySelector("#target");
|
|
71
|
+
const sibling1 = container.querySelector("#sibling1");
|
|
72
|
+
const sibling2 = container.querySelector("#sibling2");
|
|
73
|
+
const trap = new TrapScreenReader(target);
|
|
74
|
+
trap.trap();
|
|
75
|
+
// sibling1 should still be aria-hidden but not tracked
|
|
76
|
+
expect(sibling1.getAttribute("aria-hidden")).toBe("true");
|
|
77
|
+
expect(sibling2.getAttribute("aria-hidden")).toBe("true");
|
|
78
|
+
trap.release();
|
|
79
|
+
// sibling1 should still have aria-hidden (wasn't added by trap)
|
|
80
|
+
expect(sibling1.getAttribute("aria-hidden")).toBe("true");
|
|
81
|
+
// sibling2 should have aria-hidden removed (was added by trap)
|
|
82
|
+
expect(sibling2.hasAttribute("aria-hidden")).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
test("stops at body level and does not hide body siblings", () => {
|
|
85
|
+
const outsideContainer = document.createElement("div");
|
|
86
|
+
outsideContainer.id = "outside";
|
|
87
|
+
document.body.appendChild(outsideContainer);
|
|
88
|
+
container.innerHTML = `<div id="target">Target</div>`;
|
|
89
|
+
const target = container.querySelector("#target");
|
|
90
|
+
const trap = new TrapScreenReader(target);
|
|
91
|
+
trap.trap();
|
|
92
|
+
// Container's sibling (outsideContainer) should be hidden
|
|
93
|
+
expect(outsideContainer.getAttribute("aria-hidden")).toBe("true");
|
|
94
|
+
trap.release();
|
|
95
|
+
document.body.removeChild(outsideContainer);
|
|
96
|
+
});
|
|
97
|
+
test("releases previous trap when trap is called again", () => {
|
|
98
|
+
container.innerHTML = `
|
|
99
|
+
<div id="sibling1">Sibling 1</div>
|
|
100
|
+
<div id="target">Target</div>
|
|
101
|
+
<div id="sibling2">Sibling 2</div>
|
|
102
|
+
`;
|
|
103
|
+
const target = container.querySelector("#target");
|
|
104
|
+
const sibling1 = container.querySelector("#sibling1");
|
|
105
|
+
const sibling2 = container.querySelector("#sibling2");
|
|
106
|
+
const trap = new TrapScreenReader(target);
|
|
107
|
+
// First trap
|
|
108
|
+
trap.trap();
|
|
109
|
+
expect(sibling1.getAttribute("aria-hidden")).toBe("true");
|
|
110
|
+
// Second trap should release first
|
|
111
|
+
trap.trap();
|
|
112
|
+
expect(sibling1.getAttribute("aria-hidden")).toBe("true");
|
|
113
|
+
expect(sibling2.getAttribute("aria-hidden")).toBe("true");
|
|
114
|
+
});
|
|
115
|
+
test("ignores non-element nodes", () => {
|
|
116
|
+
const parent = document.createElement("div");
|
|
117
|
+
parent.appendChild(document.createTextNode("Text node"));
|
|
118
|
+
const target = document.createElement("div");
|
|
119
|
+
target.textContent = "Target";
|
|
120
|
+
parent.appendChild(target);
|
|
121
|
+
container.appendChild(parent);
|
|
122
|
+
const trap = new TrapScreenReader(target);
|
|
123
|
+
// Should not throw error when encountering text nodes
|
|
124
|
+
expect(() => trap.trap()).not.toThrow();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|