@neuravision/construct 2.0.2 → 2.1.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +272 -0
  2. package/README.md +148 -152
  3. package/behaviors/combobox.js +447 -0
  4. package/behaviors/drawer.js +18 -0
  5. package/behaviors/dropdown.js +296 -0
  6. package/behaviors/index.d.ts +333 -0
  7. package/behaviors/index.js +9 -0
  8. package/behaviors/internal/dialog-controller.js +351 -0
  9. package/behaviors/internal/dom.js +377 -0
  10. package/behaviors/internal/overlay.js +241 -0
  11. package/behaviors/internal/popup-layer.js +188 -0
  12. package/behaviors/modal.js +17 -0
  13. package/behaviors/popover.js +269 -0
  14. package/behaviors/select-menu.js +326 -0
  15. package/behaviors/tabs.js +230 -0
  16. package/behaviors/toggle-group.js +234 -0
  17. package/behaviors/tooltip.js +233 -0
  18. package/components/README.md +279 -83
  19. package/components/_shared.css +11 -10
  20. package/components/_shell-shared.css +20 -4
  21. package/components/app-shell-v2.css +21 -14
  22. package/components/app-shell.css +23 -14
  23. package/components/badge.css +15 -0
  24. package/components/button.css +103 -15
  25. package/components/card.css +53 -8
  26. package/components/checkbox.css +3 -2
  27. package/components/chip.css +1 -1
  28. package/components/datepicker.css +3 -1
  29. package/components/datum.css +204 -0
  30. package/components/index.css +5 -0
  31. package/components/meter.css +182 -0
  32. package/components/navbar.css +88 -3
  33. package/components/slider.css +2 -1
  34. package/components/status.css +107 -0
  35. package/components/switch.css +4 -3
  36. package/components/tooltip.css +4 -0
  37. package/docs/architecture.md +79 -0
  38. package/docs/behaviors.md +101 -0
  39. package/docs/fonts.md +70 -0
  40. package/docs/governance.md +85 -0
  41. package/docs/guidelines.md +42 -23
  42. package/examples/react/README.md +20 -0
  43. package/examples/react/SettingsModal.jsx +66 -0
  44. package/examples/react/construct.css +5 -0
  45. package/fonts.css +10 -0
  46. package/foundations.css +8 -1
  47. package/package.json +72 -21
  48. package/schemas/components.schema.json +107 -0
  49. package/schemas/examples/theme.example.json +27 -0
  50. package/schemas/primitives.schema.json +135 -0
  51. package/schemas/semantic.schema.json +223 -0
  52. package/schemas/theme.schema.json +63 -0
  53. package/scripts/build-tokens.mjs +1417 -0
  54. package/scripts/construct-theme.mjs +43 -0
  55. package/scripts/validate-tokens.mjs +25 -0
  56. package/standalone/accordion.css +3 -0
  57. package/standalone/alert.css +3 -0
  58. package/standalone/app-shell-v2.css +4 -0
  59. package/standalone/app-shell.css +4 -0
  60. package/standalone/avatar.css +3 -0
  61. package/standalone/badge.css +3 -0
  62. package/standalone/banner.css +3 -0
  63. package/standalone/breadcrumbs.css +3 -0
  64. package/standalone/button.css +3 -0
  65. package/standalone/card.css +3 -0
  66. package/standalone/chart.css +3 -0
  67. package/standalone/checkbox.css +3 -0
  68. package/standalone/chip.css +3 -0
  69. package/standalone/combobox.css +3 -0
  70. package/standalone/core.css +4 -0
  71. package/standalone/data-table.css +3 -0
  72. package/standalone/datepicker.css +3 -0
  73. package/standalone/datum.css +3 -0
  74. package/standalone/divider.css +3 -0
  75. package/standalone/drawer.css +3 -0
  76. package/standalone/dropdown.css +3 -0
  77. package/standalone/empty-state.css +3 -0
  78. package/standalone/field.css +3 -0
  79. package/standalone/file-upload.css +3 -0
  80. package/standalone/icon.css +3 -0
  81. package/standalone/input.css +3 -0
  82. package/standalone/list.css +3 -0
  83. package/standalone/meter.css +3 -0
  84. package/standalone/modal.css +3 -0
  85. package/standalone/navbar.css +3 -0
  86. package/standalone/pagination.css +3 -0
  87. package/standalone/popover.css +3 -0
  88. package/standalone/progress-bar.css +3 -0
  89. package/standalone/select-menu.css +3 -0
  90. package/standalone/select.css +3 -0
  91. package/standalone/sidebar.css +3 -0
  92. package/standalone/skeleton.css +3 -0
  93. package/standalone/skip-link.css +3 -0
  94. package/standalone/slider.css +3 -0
  95. package/standalone/spinner.css +3 -0
  96. package/standalone/status.css +3 -0
  97. package/standalone/switch.css +3 -0
  98. package/standalone/table.css +3 -0
  99. package/standalone/tabs.css +3 -0
  100. package/standalone/textarea.css +3 -0
  101. package/standalone/toast.css +3 -0
  102. package/standalone/toggle-group.css +3 -0
  103. package/standalone/toolbar.css +3 -0
  104. package/standalone/tooltip.css +3 -0
  105. package/standalone/tree.css +3 -0
  106. package/tokens/README.md +130 -126
  107. package/tokens/components.json +54 -0
  108. package/tokens/primitives.json +268 -0
  109. package/tokens/semantic.dark.json +93 -0
  110. package/tokens/semantic.high-contrast.json +93 -0
  111. package/tokens/semantic.light.json +178 -0
  112. package/tokens/tokens.css +532 -28
  113. package/tokens/tokens.js +335 -25
  114. package/tokens/tokens.json +764 -21
  115. package/tokens/tokens.ts +335 -25
@@ -0,0 +1,377 @@
1
+ const documentCounters = new WeakMap();
2
+
3
+ const FOCUSABLE_SELECTOR = [
4
+ 'a[href]',
5
+ 'area[href]',
6
+ 'button:not([disabled])',
7
+ 'input:not([disabled]):not([type="hidden"])',
8
+ 'select:not([disabled])',
9
+ 'textarea:not([disabled])',
10
+ 'details > summary:first-of-type',
11
+ 'iframe',
12
+ 'object',
13
+ 'embed',
14
+ '[contenteditable]:not([contenteditable="false"])',
15
+ '[tabindex]:not([tabindex="-1"])',
16
+ ].join(',');
17
+
18
+ export function isElement(value) {
19
+ return value?.nodeType === 1 && typeof value.matches === 'function';
20
+ }
21
+
22
+ export function assertElement(value, name) {
23
+ if (!isElement(value)) {
24
+ throw new TypeError(`${name} must be a DOM Element.`);
25
+ }
26
+
27
+ return value;
28
+ }
29
+
30
+ export function assertElements(value, name) {
31
+ if (value == null) return [];
32
+
33
+ const elements = isElement(value) ? [value] : Array.from(value);
34
+ elements.forEach((element, index) => assertElement(element, `${name}[${index}]`));
35
+ return elements;
36
+ }
37
+
38
+ export function assertSameDocument(elements, document, name) {
39
+ elements.forEach((element, index) => {
40
+ if (element.ownerDocument !== document) {
41
+ throw new TypeError(`${name}[${index}] must belong to the same document.`);
42
+ }
43
+ });
44
+
45
+ return elements;
46
+ }
47
+
48
+ export function assertDescendants(elements, container, name) {
49
+ elements.forEach((element, index) => {
50
+ if (element === container || !container.contains(element)) {
51
+ throw new TypeError(`${name}[${index}] must be a descendant of its composite container.`);
52
+ }
53
+ });
54
+
55
+ return elements;
56
+ }
57
+
58
+ export function assertUniqueElements(elements, name) {
59
+ if (new Set(elements).size !== elements.length) {
60
+ throw new TypeError(`${name} must not contain duplicate Elements.`);
61
+ }
62
+
63
+ return elements;
64
+ }
65
+
66
+ export function assertChoice(value, choices, name) {
67
+ if (!choices.includes(value)) {
68
+ throw new TypeError(`${name} must be one of: ${choices.join(', ')}.`);
69
+ }
70
+
71
+ return value;
72
+ }
73
+
74
+ export function assertFunction(value, name) {
75
+ if (value != null && typeof value !== 'function') {
76
+ throw new TypeError(`${name} must be a function.`);
77
+ }
78
+
79
+ return value;
80
+ }
81
+
82
+ export function isDisabled(element) {
83
+ let nativelyDisabled = false;
84
+ try {
85
+ nativelyDisabled = element.matches(':disabled');
86
+ } catch {
87
+ // Non-HTML DOM implementations may not support the :disabled selector.
88
+ }
89
+
90
+ return Boolean(
91
+ nativelyDisabled ||
92
+ element.disabled ||
93
+ element.getAttribute('aria-disabled') === 'true' ||
94
+ element.closest('[inert]'),
95
+ );
96
+ }
97
+
98
+ export function isRendered(element) {
99
+ if (element.hidden || element.closest('[hidden]')) return false;
100
+
101
+ const view = element.ownerDocument.defaultView;
102
+ if (!view) return true;
103
+
104
+ for (let current = element; isElement(current); current = current.parentElement) {
105
+ const style = view.getComputedStyle(current);
106
+ if (style.display === 'none' || style.visibility === 'hidden') return false;
107
+ }
108
+
109
+ return element.getClientRects().length > 0;
110
+ }
111
+
112
+ export function getFocusableElements(container) {
113
+ assertElement(container, 'container');
114
+
115
+ const candidates = [];
116
+ if (container.matches(FOCUSABLE_SELECTOR)) candidates.push(container);
117
+ candidates.push(...container.querySelectorAll(FOCUSABLE_SELECTOR));
118
+
119
+ return candidates.filter(
120
+ (element) =>
121
+ !isDisabled(element) &&
122
+ element.getAttribute('aria-hidden') !== 'true' &&
123
+ isRendered(element),
124
+ );
125
+ }
126
+
127
+ export function focusElement(element) {
128
+ if (!isElement(element) || !element.isConnected || typeof element.focus !== 'function') {
129
+ return false;
130
+ }
131
+
132
+ element.focus({ preventScroll: true });
133
+ return element.ownerDocument.activeElement === element;
134
+ }
135
+
136
+ export function createAttributeStore() {
137
+ const originals = new Map();
138
+
139
+ function remember(element, name) {
140
+ let attributes = originals.get(element);
141
+ if (!attributes) {
142
+ attributes = new Map();
143
+ originals.set(element, attributes);
144
+ }
145
+
146
+ if (!attributes.has(name)) {
147
+ attributes.set(name, element.hasAttribute(name) ? element.getAttribute(name) : null);
148
+ }
149
+ }
150
+
151
+ return {
152
+ set(element, name, value = '') {
153
+ remember(element, name);
154
+ if (value == null || value === false) {
155
+ element.removeAttribute(name);
156
+ } else {
157
+ element.setAttribute(name, value === true ? '' : String(value));
158
+ }
159
+ },
160
+
161
+ restore() {
162
+ for (const [element, attributes] of originals) {
163
+ for (const [name, value] of attributes) {
164
+ if (value == null) element.removeAttribute(name);
165
+ else element.setAttribute(name, value);
166
+ }
167
+ }
168
+ originals.clear();
169
+ },
170
+ };
171
+ }
172
+
173
+ export function createDisposables() {
174
+ const disposables = [];
175
+ let disposed = false;
176
+
177
+ return {
178
+ add(disposable) {
179
+ if (typeof disposable !== 'function') {
180
+ throw new TypeError('A disposable must be a function.');
181
+ }
182
+ if (disposed) disposable();
183
+ else disposables.push(disposable);
184
+ return disposable;
185
+ },
186
+
187
+ listen(target, type, listener, options) {
188
+ target.addEventListener(type, listener, options);
189
+ return this.add(() => target.removeEventListener(type, listener, options));
190
+ },
191
+
192
+ dispose() {
193
+ if (disposed) return;
194
+ disposed = true;
195
+
196
+ const errors = [];
197
+ for (const disposable of disposables.splice(0).reverse()) {
198
+ try {
199
+ disposable();
200
+ } catch (error) {
201
+ errors.push(error);
202
+ }
203
+ }
204
+
205
+ if (errors.length === 1) throw errors[0];
206
+ if (errors.length > 1) throw new AggregateError(errors, 'Multiple cleanup operations failed.');
207
+ },
208
+ };
209
+ }
210
+
211
+ export function ensureId(element, prefix, attributes) {
212
+ if (element.id) return element.id;
213
+
214
+ const document = element.ownerDocument;
215
+ let next = documentCounters.get(document) ?? 0;
216
+ let id;
217
+ do {
218
+ next += 1;
219
+ id = `${prefix}-${next}`;
220
+ } while (document.getElementById(id));
221
+ documentCounters.set(document, next);
222
+ attributes.set(element, 'id', id);
223
+ return id;
224
+ }
225
+
226
+ export function getItemValue(element, index) {
227
+ return element.getAttribute('data-value') ?? element.getAttribute('value') ?? (element.id || String(index));
228
+ }
229
+
230
+ export function getTextLabel(element) {
231
+ return (element.getAttribute('aria-label') ?? element.textContent ?? '').trim().replace(/\s+/g, ' ');
232
+ }
233
+
234
+ export function nextEnabledIndex(items, currentIndex, delta, loop = true) {
235
+ if (!items.length) return -1;
236
+
237
+ let index = currentIndex;
238
+ for (let attempts = 0; attempts < items.length; attempts += 1) {
239
+ index += delta;
240
+ if (loop) index = (index + items.length) % items.length;
241
+ if (!loop && (index < 0 || index >= items.length)) return currentIndex;
242
+ if (!isDisabled(items[index])) return index;
243
+ }
244
+
245
+ return currentIndex;
246
+ }
247
+
248
+ export function boundaryEnabledIndex(items, fromEnd = false) {
249
+ const indices = fromEnd
250
+ ? Array.from({ length: items.length }, (_, index) => items.length - index - 1)
251
+ : Array.from(items.keys());
252
+ return indices.find((index) => !isDisabled(items[index])) ?? -1;
253
+ }
254
+
255
+ export function orientationDelta(key, orientation, element) {
256
+ if (orientation === 'vertical') {
257
+ if (key === 'ArrowDown') return 1;
258
+ if (key === 'ArrowUp') return -1;
259
+ return 0;
260
+ }
261
+
262
+ const direction = element.ownerDocument.defaultView?.getComputedStyle(element).direction;
263
+ const rtlMultiplier = direction === 'rtl' ? -1 : 1;
264
+ if (key === 'ArrowRight') return rtlMultiplier;
265
+ if (key === 'ArrowLeft') return -rtlMultiplier;
266
+ return 0;
267
+ }
268
+
269
+ export function createTypeahead({ timeout = 500 } = {}) {
270
+ if (!Number.isFinite(timeout) || timeout < 0) {
271
+ throw new RangeError('Typeahead timeout must be a non-negative finite number.');
272
+ }
273
+
274
+ let query = '';
275
+ let timer;
276
+
277
+ function reset() {
278
+ query = '';
279
+ if (timer) clearTimeout(timer);
280
+ timer = undefined;
281
+ }
282
+
283
+ return {
284
+ find(event, items, currentIndex = -1) {
285
+ if (
286
+ event.key.length !== 1 ||
287
+ event.altKey ||
288
+ event.ctrlKey ||
289
+ event.metaKey ||
290
+ event.isComposing
291
+ ) {
292
+ return -1;
293
+ }
294
+
295
+ const character = event.key.toLocaleLowerCase();
296
+ query = query.length === 1 && query === character ? character : query + character;
297
+ if (timer) clearTimeout(timer);
298
+ timer = setTimeout(reset, timeout);
299
+
300
+ const ordered = [
301
+ ...items.slice(currentIndex + 1),
302
+ ...items.slice(0, currentIndex + 1),
303
+ ];
304
+ const match = ordered.find(
305
+ (item) => !isDisabled(item) && getTextLabel(item).toLocaleLowerCase().startsWith(query),
306
+ );
307
+ return match ? items.indexOf(match) : -1;
308
+ },
309
+ reset,
310
+ };
311
+ }
312
+
313
+ export function createControllerLifecycle(name) {
314
+ let destroyed = false;
315
+ let activeTransaction = null;
316
+
317
+ function assertAlive() {
318
+ if (destroyed) throw new Error(`${name} controller has been destroyed.`);
319
+ }
320
+
321
+ return Object.freeze({
322
+ get isAlive() {
323
+ return !destroyed;
324
+ },
325
+
326
+ assertAlive,
327
+
328
+ run(operation, callback) {
329
+ assertAlive();
330
+ if (activeTransaction) return false;
331
+
332
+ const token = {};
333
+ activeTransaction = { operation, token };
334
+ const checkpoint = () => {
335
+ assertAlive();
336
+ if (activeTransaction?.token !== token) {
337
+ throw new Error(`${name} controller transaction is no longer active.`);
338
+ }
339
+ };
340
+
341
+ try {
342
+ const result = callback(checkpoint);
343
+ checkpoint();
344
+ return result;
345
+ } finally {
346
+ if (activeTransaction?.token === token) activeTransaction = null;
347
+ }
348
+ },
349
+
350
+ destroy() {
351
+ if (destroyed) return false;
352
+ destroyed = true;
353
+ activeTransaction = null;
354
+ return true;
355
+ },
356
+ });
357
+ }
358
+
359
+ export function dispatch(element, type, detail, cancelable = false) {
360
+ const EventConstructor = element.ownerDocument.defaultView?.CustomEvent;
361
+ if (!EventConstructor) return true;
362
+
363
+ return element.dispatchEvent(
364
+ new EventConstructor(type, {
365
+ bubbles: true,
366
+ cancelable,
367
+ detail,
368
+ }),
369
+ );
370
+ }
371
+
372
+ export function eventTargetsOutside(event, elements) {
373
+ const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
374
+ return elements.every(
375
+ (element) => !path.includes(element) && !element.contains(event.target),
376
+ );
377
+ }
@@ -0,0 +1,241 @@
1
+ import { focusElement, getFocusableElements, isElement } from './dom.js';
2
+
3
+ const bodyLocks = new WeakMap();
4
+ const overlayStates = new WeakMap();
5
+ const consumedEvents = new WeakSet();
6
+
7
+ function getOverlayState(document) {
8
+ let state = overlayStates.get(document);
9
+ if (!state) {
10
+ state = { dismissals: [], modals: [] };
11
+ overlayStates.set(document, state);
12
+ }
13
+ return state;
14
+ }
15
+
16
+ function targetInScopes(target, scopes) {
17
+ return isElement(target) && scopes.some((scope) => scope.contains(target));
18
+ }
19
+
20
+ function findOwningModal(state, trigger) {
21
+ for (let index = state.modals.length - 1; index >= 0; index -= 1) {
22
+ const modal = state.modals[index];
23
+ if (modal.scope.contains(trigger)) return modal.token;
24
+ if (
25
+ state.dismissals.some(
26
+ (entry) => entry.ownerModal === modal.token && targetInScopes(trigger, entry.focusScopes),
27
+ )
28
+ ) {
29
+ return modal.token;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ export function lockBodyScroll(document) {
36
+ const existing = bodyLocks.get(document);
37
+ if (existing) {
38
+ existing.count += 1;
39
+ let released = false;
40
+ return () => {
41
+ if (released) return;
42
+ released = true;
43
+ releaseBodyLock(document);
44
+ };
45
+ }
46
+
47
+ const body = document.body;
48
+ const view = document.defaultView;
49
+ const state = {
50
+ body,
51
+ count: 1,
52
+ overflow: body.style.overflow,
53
+ paddingRight: body.style.paddingRight,
54
+ };
55
+ bodyLocks.set(document, state);
56
+
57
+ const scrollbarWidth = view ? Math.max(0, view.innerWidth - document.documentElement.clientWidth) : 0;
58
+ body.style.overflow = 'hidden';
59
+ if (scrollbarWidth > 0 && view) {
60
+ const currentPadding = Number.parseFloat(view.getComputedStyle(body).paddingRight) || 0;
61
+ body.style.paddingRight = `${currentPadding + scrollbarWidth}px`;
62
+ }
63
+
64
+ let released = false;
65
+ return () => {
66
+ if (released) return;
67
+ released = true;
68
+ releaseBodyLock(document);
69
+ };
70
+ }
71
+
72
+ function releaseBodyLock(document) {
73
+ const state = bodyLocks.get(document);
74
+ if (!state) return;
75
+
76
+ state.count -= 1;
77
+ if (state.count > 0) return;
78
+
79
+ state.body.style.overflow = state.overflow;
80
+ state.body.style.paddingRight = state.paddingRight;
81
+ bodyLocks.delete(document);
82
+ }
83
+
84
+ export function pushOverlay(
85
+ document,
86
+ token,
87
+ {
88
+ focusScopes = [],
89
+ handlesTab = false,
90
+ modal = false,
91
+ onOwnerClose = null,
92
+ onOwnerDestroy = null,
93
+ scope = null,
94
+ trigger = null,
95
+ } = {},
96
+ ) {
97
+ const state = getOverlayState(document);
98
+ const existingIndex = state.dismissals.findIndex((entry) => entry.token === token);
99
+ if (existingIndex >= 0) state.dismissals.splice(existingIndex, 1);
100
+ const parentModal = trigger || scope ? findOwningModal(state, trigger ?? scope) : null;
101
+
102
+ let modalEntry = null;
103
+ if (modal) {
104
+ const existingModalIndex = state.modals.findIndex((entry) => entry.token === token);
105
+ if (existingModalIndex >= 0) state.modals.splice(existingModalIndex, 1);
106
+ modalEntry = { scope, token };
107
+ state.modals.push(modalEntry);
108
+ }
109
+
110
+ const entry = {
111
+ focusScopes: [...focusScopes],
112
+ handlesTab,
113
+ modal,
114
+ onOwnerClose,
115
+ onOwnerDestroy,
116
+ ownerModal: modal ? token : trigger ? findOwningModal(state, trigger) : null,
117
+ parentModal,
118
+ token,
119
+ };
120
+ state.dismissals.push(entry);
121
+
122
+ let removed = false;
123
+ return () => {
124
+ if (removed) return;
125
+ removed = true;
126
+ const dismissalIndex = state.dismissals.findIndex((candidate) => candidate.token === token);
127
+ if (dismissalIndex >= 0) state.dismissals.splice(dismissalIndex, 1);
128
+ if (modalEntry) {
129
+ const modalIndex = state.modals.findIndex((candidate) => candidate.token === token);
130
+ if (modalIndex >= 0) state.modals.splice(modalIndex, 1);
131
+ }
132
+ if (!state.dismissals.length && !state.modals.length) overlayStates.delete(document);
133
+ };
134
+ }
135
+
136
+ export function isTopOverlay(document, token) {
137
+ const state = overlayStates.get(document);
138
+ return Boolean(state?.dismissals.length && state.dismissals.at(-1).token === token);
139
+ }
140
+
141
+ export function isTopModalOverlay(document, token) {
142
+ const state = overlayStates.get(document);
143
+ return Boolean(state?.modals.length && state.modals.at(-1).token === token);
144
+ }
145
+
146
+ export function consumeTopOverlayEvent(event, document, token) {
147
+ if (consumedEvents.has(event) || !isTopOverlay(document, token)) return false;
148
+ consumedEvents.add(event);
149
+ return true;
150
+ }
151
+
152
+ export function getModalFocusScopes(document, token) {
153
+ const state = overlayStates.get(document);
154
+ const modal = state?.modals.find((entry) => entry.token === token);
155
+ if (!modal) return [];
156
+
157
+ return [
158
+ modal.scope,
159
+ ...state.dismissals
160
+ .filter((entry) => entry.ownerModal === token)
161
+ .flatMap((entry) => entry.focusScopes),
162
+ ];
163
+ }
164
+
165
+ export function isInModalFocusScope(document, token, target) {
166
+ return targetInScopes(target, getModalFocusScopes(document, token));
167
+ }
168
+
169
+ export function topOverlayHandlesModalTab(document, token) {
170
+ const top = overlayStates.get(document)?.dismissals.at(-1);
171
+ return Boolean(top && top.ownerModal === token && top.handlesTab);
172
+ }
173
+
174
+ function ownedByModal(entry, token) {
175
+ return entry.parentModal === token || (!entry.modal && entry.ownerModal === token);
176
+ }
177
+
178
+ export function closeOwnedOverlays(document, token) {
179
+ const state = overlayStates.get(document);
180
+ if (!state) return true;
181
+
182
+ const owned = state.dismissals.filter((entry) => ownedByModal(entry, token)).reverse();
183
+ for (const entry of owned) {
184
+ if (!state.dismissals.includes(entry)) continue;
185
+ entry.onOwnerClose?.();
186
+ if (state.dismissals.includes(entry)) return false;
187
+ }
188
+ return true;
189
+ }
190
+
191
+ export function destroyOwnedOverlays(document, token) {
192
+ const state = overlayStates.get(document);
193
+ if (!state) return;
194
+
195
+ const owned = state.dismissals.filter((entry) => ownedByModal(entry, token)).reverse();
196
+ for (const entry of owned) {
197
+ if (!state.dismissals.includes(entry)) continue;
198
+ entry.onOwnerDestroy?.();
199
+ }
200
+ }
201
+
202
+ export function trapTabKey(event, containers) {
203
+ if (event.key !== 'Tab') return false;
204
+
205
+ const scopes = Array.isArray(containers) ? containers : [containers];
206
+ const document = scopes[0].ownerDocument;
207
+ const NodeConstructor = document.defaultView?.Node;
208
+ const focusable = [...new Set(scopes.flatMap((scope) => getFocusableElements(scope)))];
209
+ if (NodeConstructor) {
210
+ focusable.sort((left, right) => {
211
+ const position = left.compareDocumentPosition(right);
212
+ if (position & NodeConstructor.DOCUMENT_POSITION_FOLLOWING) return -1;
213
+ if (position & NodeConstructor.DOCUMENT_POSITION_PRECEDING) return 1;
214
+ return 0;
215
+ });
216
+ }
217
+ if (!focusable.length) {
218
+ event.preventDefault();
219
+ focusElement(scopes[0]);
220
+ return true;
221
+ }
222
+
223
+ const first = focusable[0];
224
+ const last = focusable.at(-1);
225
+ const active = document.activeElement;
226
+ const activeInside = targetInScopes(active, scopes);
227
+
228
+ if (event.shiftKey && (!activeInside || active === first)) {
229
+ event.preventDefault();
230
+ focusElement(last);
231
+ return true;
232
+ }
233
+
234
+ if (!event.shiftKey && (!activeInside || active === last)) {
235
+ event.preventDefault();
236
+ focusElement(first);
237
+ return true;
238
+ }
239
+
240
+ return false;
241
+ }