@reshaped/utilities 3.9.1-canary.3 → 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.
Files changed (91) hide show
  1. package/dist/a11y/Chain.d.ts +20 -0
  2. package/dist/a11y/Chain.js +60 -0
  3. package/dist/a11y/TrapFocus.d.ts +28 -0
  4. package/dist/a11y/TrapFocus.js +162 -0
  5. package/dist/a11y/TrapScreenReader.d.ts +15 -0
  6. package/dist/a11y/TrapScreenReader.js +42 -0
  7. package/dist/a11y/focus.d.ts +38 -0
  8. package/dist/a11y/focus.js +101 -0
  9. package/dist/a11y/index.d.ts +4 -0
  10. package/dist/a11y/index.js +3 -0
  11. package/dist/a11y/keyboardMode.d.ts +4 -0
  12. package/dist/a11y/keyboardMode.js +10 -0
  13. package/dist/a11y/tests/Chain.test.js +88 -0
  14. package/dist/a11y/tests/TrapFocus.test.d.ts +1 -0
  15. package/dist/a11y/tests/TrapFocus.test.js +313 -0
  16. package/dist/a11y/tests/TrapScreenReader.test.d.ts +1 -0
  17. package/dist/a11y/tests/TrapScreenReader.test.js +126 -0
  18. package/dist/a11y/tests/focus.test.d.ts +1 -0
  19. package/dist/a11y/tests/focus.test.js +278 -0
  20. package/dist/a11y/tests/keyboardMode.test.d.ts +1 -0
  21. package/dist/a11y/tests/keyboardMode.test.js +27 -0
  22. package/dist/a11y/types.d.ts +24 -0
  23. package/dist/a11y/types.js +1 -0
  24. package/dist/constants/keys.d.ts +11 -0
  25. package/dist/constants/keys.js +11 -0
  26. package/dist/css/StyleCache.d.ts +7 -0
  27. package/dist/css/StyleCache.js +19 -0
  28. package/dist/css/classNames.d.ts +7 -0
  29. package/dist/css/classNames.js +19 -0
  30. package/dist/css/index.d.ts +2 -0
  31. package/dist/css/index.js +4 -0
  32. package/dist/css/tests/StyleCache.test.d.ts +1 -0
  33. package/dist/css/tests/StyleCache.test.js +45 -0
  34. package/dist/css/tests/classNames.test.d.ts +1 -0
  35. package/dist/css/tests/classNames.test.js +63 -0
  36. package/dist/dom/findClosestScrollableContainer.d.ts +5 -0
  37. package/dist/dom/findClosestScrollableContainer.js +12 -0
  38. package/dist/dom/findParent.d.ts +2 -0
  39. package/dist/dom/findParent.js +10 -0
  40. package/dist/dom/index.d.ts +3 -0
  41. package/dist/dom/index.js +4 -0
  42. package/dist/dom/tests/findClosestScrollableContainer.test.d.ts +1 -0
  43. package/dist/dom/tests/findClosestScrollableContainer.test.js +61 -0
  44. package/dist/dom/tests/findParent.test.d.ts +1 -0
  45. package/dist/dom/tests/findParent.test.js +45 -0
  46. package/dist/flyout/Flyout.js +2 -2
  47. package/dist/flyout/index.d.ts +1 -1
  48. package/dist/flyout/index.js +1 -1
  49. package/dist/flyout/tests/Flyout.test.js +1 -1
  50. package/dist/flyout/types.d.ts +1 -1
  51. package/dist/flyout/utilities/applyPosition.js +1 -1
  52. package/dist/flyout/utilities/tests/applyPosition.test.js +1 -1
  53. package/dist/flyout/utilities/tests/calculateLayoutAdjustment.test.js +2 -2
  54. package/dist/flyout/utilities/tests/calculatePosition.test.js +1 -1
  55. package/dist/flyout/utilities/tests/centerBySize.test.js +1 -1
  56. package/dist/flyout/utilities/tests/getPositionFallbacks.test.js +1 -1
  57. package/dist/flyout/utilities/tests/getRTLPosition.test.js +1 -1
  58. package/dist/flyout/utilities/tests/isFullyVisible.test.js +1 -1
  59. package/dist/helpers/classNames.d.ts +7 -0
  60. package/dist/helpers/classNames.js +19 -0
  61. package/dist/helpers/index.d.ts +1 -0
  62. package/dist/helpers/index.js +2 -0
  63. package/dist/helpers/tests/classNames.test.d.ts +1 -0
  64. package/dist/helpers/tests/classNames.test.js +63 -0
  65. package/dist/i18n/index.d.ts +1 -0
  66. package/dist/i18n/index.js +2 -0
  67. package/dist/index.d.ts +5 -1
  68. package/dist/index.js +5 -1
  69. package/dist/internal.d.ts +11 -0
  70. package/dist/internal.js +10 -0
  71. package/dist/platform/index.d.ts +1 -0
  72. package/dist/platform/index.js +16 -0
  73. package/dist/scroll/disable.d.ts +7 -0
  74. package/dist/scroll/disable.js +15 -0
  75. package/dist/scroll/helpers.d.ts +1 -0
  76. package/dist/scroll/helpers.js +17 -0
  77. package/dist/scroll/index.d.ts +2 -0
  78. package/dist/scroll/index.js +4 -0
  79. package/dist/scroll/lock.d.ts +7 -0
  80. package/dist/scroll/lock.js +26 -0
  81. package/dist/scroll/lockSafari.d.ts +2 -0
  82. package/dist/scroll/lockSafari.js +20 -0
  83. package/dist/scroll/lockStandard.d.ts +4 -0
  84. package/dist/scroll/lockStandard.js +15 -0
  85. package/dist/scroll/tests/lock.test.d.ts +1 -0
  86. package/dist/scroll/tests/lock.test.js +81 -0
  87. package/package.json +6 -1
  88. package/dist/flyout/utilities/findClosestFixedContainer.d.ts +0 -5
  89. package/dist/flyout/utilities/findClosestFixedContainer.js +0 -18
  90. package/dist/flyout/utilities/tests/findClosestFixedContainer.test.js +0 -46
  91. /package/dist/{flyout/utilities/tests/findClosestFixedContainer.test.d.ts → a11y/tests/Chain.test.d.ts} +0 -0
@@ -0,0 +1,278 @@
1
+ import { expect, test, describe, beforeEach, afterEach } from "vitest";
2
+ import { getActiveElement, focusElement, getFocusableElements, getFocusData, focusNextElement, focusPreviousElement, focusFirstElement, focusLastElement, } from "../focus.js";
3
+ describe("a11y/focus", () => {
4
+ let container;
5
+ beforeEach(() => {
6
+ container = document.createElement("div");
7
+ document.body.appendChild(container);
8
+ });
9
+ afterEach(() => {
10
+ document.body.removeChild(container);
11
+ document.body.focus();
12
+ });
13
+ describe("getActiveElement", () => {
14
+ test("returns document.activeElement when no pseudo focus is set", () => {
15
+ const button = document.createElement("button");
16
+ container.appendChild(button);
17
+ button.focus();
18
+ expect(getActiveElement()).toBe(button);
19
+ });
20
+ test("returns pseudo focused element when data-rs-focus is set", () => {
21
+ const button1 = document.createElement("button");
22
+ const button2 = document.createElement("button");
23
+ container.appendChild(button1);
24
+ container.appendChild(button2);
25
+ button1.focus();
26
+ button2.setAttribute("data-rs-focus", "true");
27
+ expect(getActiveElement()).toBe(button2);
28
+ });
29
+ });
30
+ describe("focusElement", () => {
31
+ test("focuses element with real focus by default", () => {
32
+ const button = document.createElement("button");
33
+ container.appendChild(button);
34
+ focusElement(button);
35
+ expect(document.activeElement).toBe(button);
36
+ expect(button.hasAttribute("data-rs-focus")).toBe(false);
37
+ });
38
+ test("focuses element with pseudo focus when option is set", () => {
39
+ const button = document.createElement("button");
40
+ container.appendChild(button);
41
+ focusElement(button, { pseudoFocus: true });
42
+ expect(button.getAttribute("data-rs-focus")).toBe("true");
43
+ });
44
+ test("removes previous pseudo focus when focusing new element", () => {
45
+ const button1 = document.createElement("button");
46
+ const button2 = document.createElement("button");
47
+ container.appendChild(button1);
48
+ container.appendChild(button2);
49
+ focusElement(button1, { pseudoFocus: true });
50
+ focusElement(button2, { pseudoFocus: true });
51
+ expect(button1.hasAttribute("data-rs-focus")).toBe(false);
52
+ expect(button2.getAttribute("data-rs-focus")).toBe("true");
53
+ });
54
+ });
55
+ describe("getFocusableElements", () => {
56
+ test("returns all focusable elements", () => {
57
+ container.innerHTML = `
58
+ <div>content</div>
59
+ <button>Button 1</button>
60
+ <input type="text" />
61
+ <textarea></textarea>
62
+ <button>Button 2</button>
63
+ <textarea></textarea>
64
+ <select><option>Option 1</option></select>
65
+ <details><summary>Summary</summary>Content</details>
66
+ <div contenteditable>Contenteditable</div>
67
+ <div tabindex="0">Negative tabindex</div>
68
+ `;
69
+ const focusable = getFocusableElements(container);
70
+ expect(focusable).toHaveLength(9);
71
+ });
72
+ test("filters out disabled elements", () => {
73
+ container.innerHTML = `
74
+ <button>Enabled</button>
75
+ <button disabled>Disabled</button>
76
+ `;
77
+ const focusable = getFocusableElements(container);
78
+ expect(focusable).toHaveLength(1);
79
+ expect(focusable[0].textContent).toBe("Enabled");
80
+ });
81
+ test("filters out hidden elements (clientHeight === 0)", () => {
82
+ const button1 = document.createElement("button");
83
+ const button2 = document.createElement("button");
84
+ button1.textContent = "Visible";
85
+ button2.textContent = "Hidden";
86
+ button2.style.display = "none";
87
+ container.appendChild(button1);
88
+ container.appendChild(button2);
89
+ const focusable = getFocusableElements(container);
90
+ expect(focusable).toHaveLength(1);
91
+ expect(focusable[0].textContent).toBe("Visible");
92
+ });
93
+ test("filters out elements with tabindex=-1 by default", () => {
94
+ container.innerHTML = `
95
+ <button>Regular</button>
96
+ <button tabindex="-1">Negative tabindex</button>
97
+ `;
98
+ const focusable = getFocusableElements(container);
99
+ expect(focusable).toHaveLength(1);
100
+ expect(focusable[0].textContent).toBe("Regular");
101
+ });
102
+ test("includes elements with tabindex=-1 when option is set", () => {
103
+ container.innerHTML = `
104
+ <button>Regular</button>
105
+ <button tabindex="-1">Negative tabindex</button>
106
+ `;
107
+ const focusable = getFocusableElements(container, { includeNegativeTabIndex: true });
108
+ expect(focusable).toHaveLength(2);
109
+ });
110
+ test("handles radio buttons in a group", () => {
111
+ container.innerHTML = `
112
+ <form>
113
+ <input type="radio" name="group1" value="1" />
114
+ <input type="radio" name="group1" value="2" checked />
115
+ <input type="radio" name="group1" value="3" />
116
+ </form>
117
+ `;
118
+ const focusable = getFocusableElements(container);
119
+ // Only the checked radio should be focusable
120
+ expect(focusable).toHaveLength(1);
121
+ expect(focusable[0].value).toBe("2");
122
+ });
123
+ test("includes first radio when none are checked", () => {
124
+ container.innerHTML = `
125
+ <form>
126
+ <input type="radio" name="group1" value="1" />
127
+ <input type="radio" name="group1" value="2" />
128
+ <input type="radio" name="group1" value="3" />
129
+ </form>
130
+ `;
131
+ const focusable = getFocusableElements(container);
132
+ expect(focusable).toHaveLength(1);
133
+ expect(focusable[0].value).toBe("1");
134
+ });
135
+ test("adds additional element at the beginning when provided", () => {
136
+ container.innerHTML = `
137
+ <button>Button 1</button>
138
+ <button>Button 2</button>
139
+ `;
140
+ const additionalButton = document.createElement("button");
141
+ additionalButton.textContent = "Additional";
142
+ document.body.appendChild(additionalButton);
143
+ const focusable = getFocusableElements(container, { additionalElement: additionalButton });
144
+ expect(focusable).toHaveLength(3);
145
+ expect(focusable[0]).toBe(additionalButton);
146
+ document.body.removeChild(additionalButton);
147
+ });
148
+ });
149
+ describe("getFocusData", () => {
150
+ beforeEach(() => {
151
+ container.innerHTML = `
152
+ <button>Button 1</button>
153
+ <button>Button 2</button>
154
+ <button>Button 3</button>
155
+ `;
156
+ });
157
+ test("returns next element data", () => {
158
+ const buttons = container.querySelectorAll("button");
159
+ buttons[0].focus();
160
+ const data = getFocusData({ root: container, target: "next" });
161
+ expect(data.el).toBe(buttons[1]);
162
+ expect(data.overflow).toBe(false);
163
+ });
164
+ test("returns previous element data", () => {
165
+ const buttons = container.querySelectorAll("button");
166
+ buttons[1].focus();
167
+ const data = getFocusData({ root: container, target: "prev" });
168
+ expect(data.el).toBe(buttons[0]);
169
+ expect(data.overflow).toBe(false);
170
+ });
171
+ test("returns first element data", () => {
172
+ const buttons = container.querySelectorAll("button");
173
+ const data = getFocusData({ root: container, target: "first" });
174
+ expect(data.el).toBe(buttons[0]);
175
+ });
176
+ test("returns last element data", () => {
177
+ const buttons = container.querySelectorAll("button");
178
+ const data = getFocusData({ root: container, target: "last" });
179
+ expect(data.el).toBe(buttons[2]);
180
+ });
181
+ test("handles overflow at end without circular option", () => {
182
+ const buttons = container.querySelectorAll("button");
183
+ buttons[2].focus();
184
+ const data = getFocusData({ root: container, target: "next" });
185
+ expect(data.el).toBe(buttons[2]);
186
+ expect(data.overflow).toBe(true);
187
+ });
188
+ test("handles overflow at start without circular option", () => {
189
+ const buttons = container.querySelectorAll("button");
190
+ buttons[0].focus();
191
+ const data = getFocusData({ root: container, target: "prev" });
192
+ expect(data.el).toBe(buttons[0]);
193
+ expect(data.overflow).toBe(true);
194
+ });
195
+ test("wraps to first when going next from last with circular option", () => {
196
+ const buttons = container.querySelectorAll("button");
197
+ buttons[2].focus();
198
+ const data = getFocusData({ root: container, target: "next", options: { circular: true } });
199
+ expect(data.el).toBe(buttons[0]);
200
+ expect(data.overflow).toBe(true);
201
+ });
202
+ test("wraps to last when going prev from first with circular option", () => {
203
+ const buttons = container.querySelectorAll("button");
204
+ buttons[0].focus();
205
+ const data = getFocusData({ root: container, target: "prev", options: { circular: true } });
206
+ expect(data.el).toBe(buttons[2]);
207
+ expect(data.overflow).toBe(true);
208
+ });
209
+ });
210
+ describe("focusNextElement", () => {
211
+ test("focuses next element in sequence", () => {
212
+ container.innerHTML = `
213
+ <button>Button 1</button>
214
+ <button>Button 2</button>
215
+ `;
216
+ const buttons = container.querySelectorAll("button");
217
+ buttons[0].focus();
218
+ focusNextElement(container);
219
+ expect(document.activeElement).toBe(buttons[1]);
220
+ });
221
+ test("wraps to first element with circular option", () => {
222
+ container.innerHTML = `
223
+ <button>Button 1</button>
224
+ <button>Button 2</button>
225
+ `;
226
+ const buttons = container.querySelectorAll("button");
227
+ buttons[1].focus();
228
+ focusNextElement(container, { circular: true });
229
+ expect(document.activeElement).toBe(buttons[0]);
230
+ });
231
+ });
232
+ describe("focusPreviousElement", () => {
233
+ test("focuses previous element in sequence", () => {
234
+ container.innerHTML = `
235
+ <button>Button 1</button>
236
+ <button>Button 2</button>
237
+ `;
238
+ const buttons = container.querySelectorAll("button");
239
+ buttons[1].focus();
240
+ focusPreviousElement(container);
241
+ expect(document.activeElement).toBe(buttons[0]);
242
+ });
243
+ test("wraps to last element with circular option", () => {
244
+ container.innerHTML = `
245
+ <button>Button 1</button>
246
+ <button>Button 2</button>
247
+ `;
248
+ const buttons = container.querySelectorAll("button");
249
+ buttons[0].focus();
250
+ focusPreviousElement(container, { circular: true });
251
+ expect(document.activeElement).toBe(buttons[1]);
252
+ });
253
+ });
254
+ describe("focusFirstElement", () => {
255
+ test("focuses first focusable element", () => {
256
+ container.innerHTML = `
257
+ <button>Button 1</button>
258
+ <button>Button 2</button>
259
+ <button>Button 3</button>
260
+ `;
261
+ const buttons = container.querySelectorAll("button");
262
+ focusFirstElement(container);
263
+ expect(document.activeElement).toBe(buttons[0]);
264
+ });
265
+ });
266
+ describe("focusLastElement", () => {
267
+ test("focuses last focusable element", () => {
268
+ container.innerHTML = `
269
+ <button>Button 1</button>
270
+ <button>Button 2</button>
271
+ <button>Button 3</button>
272
+ `;
273
+ const buttons = container.querySelectorAll("button");
274
+ focusLastElement(container);
275
+ expect(document.activeElement).toBe(buttons[2]);
276
+ });
277
+ });
278
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { expect, test, describe, beforeEach, afterEach } from "vitest";
2
+ import { activateKeyboardMode, deactivateKeyboardMode, checkKeyboardMode, keyboardModeAttribute, } from "../keyboardMode.js";
3
+ describe("a11y/keyboardMode", () => {
4
+ beforeEach(() => {
5
+ document.documentElement.removeAttribute(keyboardModeAttribute);
6
+ });
7
+ afterEach(() => {
8
+ document.documentElement.removeAttribute(keyboardModeAttribute);
9
+ });
10
+ test("activates keyboard mode by setting attribute", () => {
11
+ expect(document.documentElement.hasAttribute(keyboardModeAttribute)).toBe(false);
12
+ activateKeyboardMode();
13
+ expect(document.documentElement.getAttribute(keyboardModeAttribute)).toBe("true");
14
+ });
15
+ test("deactivates keyboard mode by removing attribute", () => {
16
+ document.documentElement.setAttribute(keyboardModeAttribute, "true");
17
+ deactivateKeyboardMode();
18
+ expect(document.documentElement.hasAttribute(keyboardModeAttribute)).toBe(false);
19
+ });
20
+ test("checks keyboard mode correctly", () => {
21
+ expect(checkKeyboardMode()).toBe(false);
22
+ activateKeyboardMode();
23
+ expect(checkKeyboardMode()).toBe(true);
24
+ deactivateKeyboardMode();
25
+ expect(checkKeyboardMode()).toBe(false);
26
+ });
27
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * dialog: Completely trap the focus inside for tab navigation until content is closed
3
+ * example: Modal
4
+ *
5
+ * action-menu: Trap the arrow navigation, while tab moves the focus to
6
+ * the next element on the page after the trigger
7
+ * example: Dropdown Menu
8
+ *
9
+ * action-bar: Same as action-menu but with horizontal keyboard arrow navigation
10
+ *
11
+ * content-menu: Include dropdown content into the tab navigation flow and move the focus to
12
+ * the next element on the page after the trigger after navigation through the trapped content
13
+ * example: Header navigation dropdowns
14
+ *
15
+ * selection-menu: Keep the focus on the trigger and enable arrow key navigation with pseudo focusing with data-attributes
16
+ * without moving the focus away from the trigger
17
+ * example: Autocomplete
18
+ */
19
+ export type TrapMode = "dialog" | "action-menu" | "action-bar" | "content-menu" | "selection-menu";
20
+ export type FocusableElement = HTMLButtonElement | HTMLInputElement;
21
+ export type FocusableOptions = {
22
+ additionalElement?: FocusableElement | null;
23
+ includeNegativeTabIndex?: boolean;
24
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ export declare const ESC = "Escape";
2
+ export declare const SPACE = " ";
3
+ export declare const ENTER = "Enter";
4
+ export declare const TAB = "Tab";
5
+ export declare const UP = "ArrowUp";
6
+ export declare const DOWN = "ArrowDown";
7
+ export declare const RIGHT = "ArrowRight";
8
+ export declare const LEFT = "ArrowLeft";
9
+ export declare const HOME = "Home";
10
+ export declare const END = "End";
11
+ export declare const BACKSPACE = "Backspace";
@@ -0,0 +1,11 @@
1
+ export const ESC = "Escape";
2
+ export const SPACE = " ";
3
+ export const ENTER = "Enter";
4
+ export const TAB = "Tab";
5
+ export const UP = "ArrowUp";
6
+ export const DOWN = "ArrowDown";
7
+ export const RIGHT = "ArrowRight";
8
+ export const LEFT = "ArrowLeft";
9
+ export const HOME = "Home";
10
+ export const END = "End";
11
+ export const BACKSPACE = "Backspace";
@@ -0,0 +1,7 @@
1
+ type Styles = Record<string, string>;
2
+ declare class StyleCache {
3
+ cache: Map<HTMLElement, Record<string, string>>;
4
+ set: (el: HTMLElement, styles: Styles) => void;
5
+ reset: () => void;
6
+ }
7
+ export default StyleCache;
@@ -0,0 +1,19 @@
1
+ class StyleCache {
2
+ cache = new Map();
3
+ set = (el, styles) => {
4
+ const originalStyles = {};
5
+ const cachedStyles = this.cache.get(el);
6
+ Object.keys(styles).forEach((key) => {
7
+ originalStyles[key] = el.style.getPropertyValue(key);
8
+ });
9
+ this.cache.set(el, { ...originalStyles, ...cachedStyles });
10
+ Object.assign(el.style, styles);
11
+ };
12
+ reset = () => {
13
+ for (const [el, styles] of this.cache.entries()) {
14
+ Object.assign(el.style, styles);
15
+ }
16
+ this.cache.clear();
17
+ };
18
+ }
19
+ export default StyleCache;
@@ -0,0 +1,7 @@
1
+ type ClassNameValue = string | null | undefined | false;
2
+ export type ClassName = ClassNameValue | ClassNameValue[] | ClassName[];
3
+ /**
4
+ * Resolve an array of values into a classname string
5
+ */
6
+ declare const classNames: (...args: ClassName[]) => string;
7
+ export default classNames;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Resolve an array of values into a classname string
3
+ */
4
+ const classNames = (...args) => {
5
+ return args
6
+ .reduce((acc, cur) => {
7
+ if (Array.isArray(cur)) {
8
+ const className = classNames(...cur);
9
+ if (!className)
10
+ return acc;
11
+ return `${acc} ${className}`;
12
+ }
13
+ if (cur)
14
+ return `${acc} ${cur}`;
15
+ return acc;
16
+ }, "")
17
+ .trim();
18
+ };
19
+ export default classNames;
@@ -0,0 +1,2 @@
1
+ export { default as classNames, type ClassName } from "./classNames";
2
+ export { default as StyleCache } from "./StyleCache";
@@ -0,0 +1,4 @@
1
+ // External
2
+ export { default as classNames } from "./classNames.js";
3
+ // Internal
4
+ export { default as StyleCache } from "./StyleCache.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import { expect, test, describe, beforeEach } from "vitest";
2
+ import StyleCache from "../StyleCache.js";
3
+ describe("css/StyleCache", () => {
4
+ let styleCache;
5
+ beforeEach(() => {
6
+ styleCache = new StyleCache();
7
+ });
8
+ test("applies styles and restores original styles on reset", () => {
9
+ const el = document.createElement("div");
10
+ el.style.color = "red";
11
+ el.style.overflow = "visible";
12
+ styleCache.set(el, { color: "blue", overflow: "hidden" });
13
+ expect(el.style.color).toBe("blue");
14
+ expect(el.style.overflow).toBe("hidden");
15
+ styleCache.reset();
16
+ expect(el.style.color).toBe("red");
17
+ expect(el.style.overflow).toBe("visible");
18
+ });
19
+ test("preserves original styles across multiple set calls", () => {
20
+ const el = document.createElement("div");
21
+ el.style.color = "red";
22
+ styleCache.set(el, { color: "blue" });
23
+ styleCache.set(el, { color: "green" });
24
+ styleCache.reset();
25
+ expect(el.style.color).toBe("red");
26
+ });
27
+ test("handles multiple elements", () => {
28
+ const el1 = document.createElement("div");
29
+ const el2 = document.createElement("div");
30
+ el1.style.color = "red";
31
+ el2.style.color = "blue";
32
+ styleCache.set(el1, { color: "green" });
33
+ styleCache.set(el2, { color: "yellow" });
34
+ styleCache.reset();
35
+ expect(el1.style.color).toBe("red");
36
+ expect(el2.style.color).toBe("blue");
37
+ });
38
+ test("clears cache after reset", () => {
39
+ const el = document.createElement("div");
40
+ el.style.color = "red";
41
+ styleCache.set(el, { color: "blue" });
42
+ styleCache.reset();
43
+ expect(styleCache.cache.size).toBe(0);
44
+ });
45
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import { expect, test, describe } from "vitest";
2
+ import classNames from "../classNames.js";
3
+ describe("helpers/classNames", () => {
4
+ test("returns empty string when no arguments are provided", () => {
5
+ expect(classNames()).toBe("");
6
+ });
7
+ test("returns a single class name", () => {
8
+ expect(classNames("foo")).toBe("foo");
9
+ });
10
+ test("concatenates multiple class names", () => {
11
+ expect(classNames("foo", "bar", "baz")).toBe("foo bar baz");
12
+ });
13
+ test("filters out null values", () => {
14
+ expect(classNames("foo", null, "bar")).toBe("foo bar");
15
+ expect(classNames(null, "foo", null)).toBe("foo");
16
+ expect(classNames(null, null)).toBe("");
17
+ });
18
+ test("filters out undefined values", () => {
19
+ expect(classNames("foo", undefined, "bar")).toBe("foo bar");
20
+ expect(classNames(undefined, "foo", undefined)).toBe("foo");
21
+ expect(classNames(undefined, undefined)).toBe("");
22
+ });
23
+ test("filters out false values", () => {
24
+ expect(classNames("foo", false, "bar")).toBe("foo bar");
25
+ expect(classNames(false, "foo", false)).toBe("foo");
26
+ expect(classNames(false, false)).toBe("");
27
+ });
28
+ test("handles arrays of class names", () => {
29
+ expect(classNames(["foo", "bar"])).toBe("foo bar");
30
+ expect(classNames(["foo", "bar"], "baz")).toBe("foo bar baz");
31
+ expect(classNames("foo", ["bar", "baz"])).toBe("foo bar baz");
32
+ });
33
+ test("handles nested arrays", () => {
34
+ expect(classNames(["foo", ["bar", "baz"]])).toBe("foo bar baz");
35
+ expect(classNames([["foo", "bar"], "baz"])).toBe("foo bar baz");
36
+ expect(classNames([["foo"], ["bar"], ["baz"]])).toBe("foo bar baz");
37
+ });
38
+ test("filters out falsy values in arrays", () => {
39
+ expect(classNames(["foo", null, "bar"])).toBe("foo bar");
40
+ expect(classNames(["foo", undefined, "bar"])).toBe("foo bar");
41
+ expect(classNames(["foo", false, "bar"])).toBe("foo bar");
42
+ expect(classNames([null, undefined, false])).toBe("");
43
+ });
44
+ test("handles deeply nested arrays with falsy values", () => {
45
+ expect(classNames([["foo", null], [undefined, "bar"], false])).toBe("foo bar");
46
+ expect(classNames(["foo", [null, undefined, ["bar", false, "baz"]]])).toBe("foo bar baz");
47
+ });
48
+ test("handles empty arrays", () => {
49
+ expect(classNames([])).toBe("");
50
+ expect(classNames("foo", [], "bar")).toBe("foo bar");
51
+ expect(classNames([[], []])).toBe("");
52
+ });
53
+ test("handles empty strings", () => {
54
+ expect(classNames("", "foo")).toBe("foo");
55
+ expect(classNames("foo", "", "bar")).toBe("foo bar");
56
+ expect(classNames("", "", "")).toBe("");
57
+ });
58
+ test("handles conditional class names", () => {
59
+ const isActive = true;
60
+ const isDisabled = false;
61
+ expect(classNames("button", isActive && "active", isDisabled && "disabled")).toBe("button active");
62
+ });
63
+ });
@@ -0,0 +1,5 @@
1
+ type Args = {
2
+ el: HTMLElement;
3
+ };
4
+ declare const _default: (args: Args) => HTMLElement | null;
5
+ export default _default;
@@ -0,0 +1,12 @@
1
+ const findClosestScrollableContainer = (args) => {
2
+ const { el, iteration } = args;
3
+ const style = el && window.getComputedStyle(el);
4
+ const overflowY = style.overflowY;
5
+ const isScrollable = overflowY.includes("scroll") || overflowY.includes("auto");
6
+ if (!el.parentElement)
7
+ return null;
8
+ if (isScrollable && el.scrollHeight > el.clientHeight)
9
+ return el;
10
+ return findClosestScrollableContainer({ el: el.parentElement, iteration: iteration + 1 });
11
+ };
12
+ export default (args) => findClosestScrollableContainer({ ...args, iteration: 0 });
@@ -0,0 +1,2 @@
1
+ declare const findParent: (element: HTMLElement, condition: (el: HTMLElement) => boolean) => HTMLElement | null;
2
+ export default findParent;
@@ -0,0 +1,10 @@
1
+ const findParent = (element, condition) => {
2
+ let currentElement = element.parentElement;
3
+ while (currentElement) {
4
+ if (condition(currentElement))
5
+ return currentElement;
6
+ currentElement = currentElement.parentElement;
7
+ }
8
+ return null;
9
+ };
10
+ export default findParent;
@@ -0,0 +1,3 @@
1
+ export { default as findClosestScrollableContainer } from "./findClosestScrollableContainer";
2
+ export { default as findParent } from "./findParent";
3
+ export { default as getShadowRoot } from "./getShadowRoot";
@@ -0,0 +1,4 @@
1
+ // Internal
2
+ export { default as findClosestScrollableContainer } from "./findClosestScrollableContainer.js";
3
+ export { default as findParent } from "./findParent.js";
4
+ export { default as getShadowRoot } from "./getShadowRoot.js";
@@ -0,0 +1,61 @@
1
+ import { expect, test, describe, afterEach } from "vitest";
2
+ import findClosestScrollableContainer from "../findClosestScrollableContainer.js";
3
+ describe("dom/findClosestScrollableContainer", () => {
4
+ afterEach(() => {
5
+ document.body.innerHTML = "";
6
+ });
7
+ test("returns null when no scrollable container is found", () => {
8
+ const container = document.createElement("div");
9
+ const child = document.createElement("div");
10
+ container.appendChild(child);
11
+ document.body.appendChild(container);
12
+ const result = findClosestScrollableContainer({ el: child });
13
+ expect(result).toBe(null);
14
+ });
15
+ test("returns element with overflow auto and scrollable content", () => {
16
+ const scrollable = document.createElement("div");
17
+ scrollable.style.overflowY = "auto";
18
+ scrollable.style.height = "100px";
19
+ const content = document.createElement("div");
20
+ content.style.height = "200px";
21
+ scrollable.appendChild(content);
22
+ document.body.appendChild(scrollable);
23
+ const result = findClosestScrollableContainer({ el: content });
24
+ expect(result).toBe(scrollable);
25
+ });
26
+ test("returns element with overflow scroll and scrollable content", () => {
27
+ const scrollable = document.createElement("div");
28
+ scrollable.style.overflowY = "scroll";
29
+ scrollable.style.height = "100px";
30
+ const content = document.createElement("div");
31
+ content.style.height = "200px";
32
+ scrollable.appendChild(content);
33
+ document.body.appendChild(scrollable);
34
+ const result = findClosestScrollableContainer({ el: content });
35
+ expect(result).toBe(scrollable);
36
+ });
37
+ test("returns first scrollable ancestor", () => {
38
+ const scrollable = document.createElement("div");
39
+ scrollable.style.overflowY = "auto";
40
+ scrollable.style.height = "100px";
41
+ const middle = document.createElement("div");
42
+ const child = document.createElement("div");
43
+ child.style.height = "200px";
44
+ scrollable.appendChild(middle);
45
+ middle.appendChild(child);
46
+ document.body.appendChild(scrollable);
47
+ const result = findClosestScrollableContainer({ el: child });
48
+ expect(result).toBe(scrollable);
49
+ });
50
+ test("skips element with overflow but no scrollable content", () => {
51
+ const notScrollable = document.createElement("div");
52
+ notScrollable.style.overflowY = "auto";
53
+ notScrollable.style.height = "200px";
54
+ const content = document.createElement("div");
55
+ content.style.height = "100px";
56
+ notScrollable.appendChild(content);
57
+ document.body.appendChild(notScrollable);
58
+ const result = findClosestScrollableContainer({ el: content });
59
+ expect(result).toBe(null);
60
+ });
61
+ });