@neuravision/construct 2.0.3 → 2.1.2
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/CHANGELOG.md +318 -0
- package/README.md +158 -149
- package/behaviors/combobox.js +486 -0
- package/behaviors/drawer.js +18 -0
- package/behaviors/dropdown.js +310 -0
- package/behaviors/index.d.ts +335 -0
- package/behaviors/index.js +9 -0
- package/behaviors/internal/dialog-controller.js +421 -0
- package/behaviors/internal/dom.js +408 -0
- package/behaviors/internal/overlay.js +241 -0
- package/behaviors/internal/popup-layer.js +190 -0
- package/behaviors/modal.js +17 -0
- package/behaviors/popover.js +278 -0
- package/behaviors/select-menu.js +344 -0
- package/behaviors/tabs.js +241 -0
- package/behaviors/toggle-group.js +266 -0
- package/behaviors/tooltip.js +257 -0
- package/components/README.md +319 -83
- package/components/_shared.css +22 -10
- package/components/_shell-shared.css +20 -4
- package/components/accordion.css +8 -0
- package/components/app-shell-v2.css +28 -14
- package/components/app-shell.css +30 -14
- package/components/badge.css +15 -0
- package/components/banner.css +32 -1
- package/components/button.css +103 -15
- package/components/card.css +67 -8
- package/components/chart.css +26 -0
- package/components/checkbox.css +35 -2
- package/components/chip.css +30 -2
- package/components/combobox.css +21 -0
- package/components/datepicker.css +58 -9
- package/components/datum.css +277 -0
- package/components/dropdown.css +23 -3
- package/components/field.css +6 -4
- package/components/file-upload.css +1 -1
- package/components/index.css +5 -0
- package/components/list.css +28 -2
- package/components/meter.css +182 -0
- package/components/navbar.css +102 -4
- package/components/pagination.css +18 -0
- package/components/progress-bar.css +22 -1
- package/components/select-menu.css +8 -0
- package/components/select.css +8 -0
- package/components/sidebar.css +15 -2
- package/components/skip-link.css +6 -2
- package/components/slider.css +7 -1
- package/components/status.css +107 -0
- package/components/switch.css +66 -5
- package/components/table.css +53 -4
- package/components/tabs.css +55 -3
- package/components/toast.css +10 -1
- package/components/toggle-group.css +16 -5
- package/components/toolbar.css +17 -1
- package/components/tooltip.css +4 -0
- package/components/tree.css +30 -0
- package/docs/architecture.md +79 -0
- package/docs/behaviors.md +122 -0
- package/docs/fonts.md +70 -0
- package/docs/governance.md +85 -0
- package/docs/guidelines.md +72 -25
- package/examples/react/README.md +20 -0
- package/examples/react/SettingsModal.jsx +66 -0
- package/examples/react/construct.css +5 -0
- package/fonts.css +10 -0
- package/foundations.css +14 -1
- package/package.json +76 -21
- package/schemas/components.schema.json +122 -0
- package/schemas/examples/theme.example.json +27 -0
- package/schemas/primitives.schema.json +135 -0
- package/schemas/semantic.schema.json +224 -0
- package/schemas/theme.schema.json +63 -0
- package/scripts/build-tokens.mjs +1499 -0
- package/scripts/construct-theme.mjs +43 -0
- package/scripts/validate-tokens.mjs +25 -0
- package/standalone/accordion.css +3 -0
- package/standalone/alert.css +3 -0
- package/standalone/app-shell-v2.css +4 -0
- package/standalone/app-shell.css +4 -0
- package/standalone/avatar.css +3 -0
- package/standalone/badge.css +3 -0
- package/standalone/banner.css +3 -0
- package/standalone/breadcrumbs.css +3 -0
- package/standalone/button.css +3 -0
- package/standalone/card.css +3 -0
- package/standalone/chart.css +3 -0
- package/standalone/checkbox.css +3 -0
- package/standalone/chip.css +3 -0
- package/standalone/combobox.css +3 -0
- package/standalone/core.css +4 -0
- package/standalone/data-table.css +3 -0
- package/standalone/datepicker.css +3 -0
- package/standalone/datum.css +3 -0
- package/standalone/divider.css +3 -0
- package/standalone/drawer.css +3 -0
- package/standalone/dropdown.css +3 -0
- package/standalone/empty-state.css +3 -0
- package/standalone/field.css +3 -0
- package/standalone/file-upload.css +3 -0
- package/standalone/icon.css +3 -0
- package/standalone/input.css +3 -0
- package/standalone/list.css +3 -0
- package/standalone/meter.css +3 -0
- package/standalone/modal.css +3 -0
- package/standalone/navbar.css +3 -0
- package/standalone/pagination.css +3 -0
- package/standalone/popover.css +3 -0
- package/standalone/progress-bar.css +3 -0
- package/standalone/select-menu.css +3 -0
- package/standalone/select.css +3 -0
- package/standalone/sidebar.css +3 -0
- package/standalone/skeleton.css +3 -0
- package/standalone/skip-link.css +3 -0
- package/standalone/slider.css +3 -0
- package/standalone/spinner.css +3 -0
- package/standalone/status.css +3 -0
- package/standalone/switch.css +3 -0
- package/standalone/table.css +3 -0
- package/standalone/tabs.css +3 -0
- package/standalone/textarea.css +3 -0
- package/standalone/toast.css +3 -0
- package/standalone/toggle-group.css +3 -0
- package/standalone/toolbar.css +3 -0
- package/standalone/tooltip.css +3 -0
- package/standalone/tree.css +3 -0
- package/themes/README.md +37 -0
- package/themes/walnut.theme.json +106 -0
- package/tokens/README.md +135 -123
- package/tokens/components.json +64 -0
- package/tokens/primitives.json +268 -0
- package/tokens/semantic.dark.json +94 -0
- package/tokens/semantic.high-contrast-dark.json +94 -0
- package/tokens/semantic.high-contrast.json +94 -0
- package/tokens/semantic.light.json +179 -0
- package/tokens/tokens.css +960 -58
- package/tokens/tokens.js +729 -120
- package/tokens/tokens.json +1158 -116
- package/tokens/tokens.ts +729 -120
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertChoice,
|
|
3
|
+
assertElement,
|
|
4
|
+
assertElements,
|
|
5
|
+
assertFunction,
|
|
6
|
+
assertSameDocument,
|
|
7
|
+
createAttributeStore,
|
|
8
|
+
createControllerLifecycle,
|
|
9
|
+
createDisposables,
|
|
10
|
+
dispatch,
|
|
11
|
+
ensureId,
|
|
12
|
+
focusElement,
|
|
13
|
+
focusIntoView,
|
|
14
|
+
getFocusableElements,
|
|
15
|
+
isComposingEvent,
|
|
16
|
+
isElement,
|
|
17
|
+
isDisabled,
|
|
18
|
+
warnAccessibility,
|
|
19
|
+
} from './dom.js';
|
|
20
|
+
import {
|
|
21
|
+
closeOwnedOverlays,
|
|
22
|
+
consumeTopOverlayEvent,
|
|
23
|
+
destroyOwnedOverlays,
|
|
24
|
+
getModalFocusScopes,
|
|
25
|
+
isInModalFocusScope,
|
|
26
|
+
isTopModalOverlay,
|
|
27
|
+
lockBodyScroll,
|
|
28
|
+
pushOverlay,
|
|
29
|
+
topOverlayHandlesModalTab,
|
|
30
|
+
trapTabKey,
|
|
31
|
+
} from './overlay.js';
|
|
32
|
+
|
|
33
|
+
function resolveInitialFocus(initialFocus, dialog) {
|
|
34
|
+
let target = initialFocus;
|
|
35
|
+
if (typeof target === 'function') target = target();
|
|
36
|
+
if (typeof target === 'string') {
|
|
37
|
+
target = dialog.querySelector(target);
|
|
38
|
+
if (!target) throw new TypeError('initialFocus selector did not match an Element inside dialog.');
|
|
39
|
+
}
|
|
40
|
+
if (target == null) target = dialog.querySelector('[autofocus]');
|
|
41
|
+
if (target == null) target = getFocusableElements(dialog)[0];
|
|
42
|
+
|
|
43
|
+
if (target != null && (!isElement(target) || !dialog.contains(target))) {
|
|
44
|
+
throw new TypeError('initialFocus must resolve to an Element inside dialog.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return target;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateInitialFocus(initialFocus, dialog) {
|
|
51
|
+
if (initialFocus == null || typeof initialFocus === 'function') return;
|
|
52
|
+
if (typeof initialFocus === 'string') {
|
|
53
|
+
if (!dialog.querySelector(initialFocus)) {
|
|
54
|
+
throw new TypeError('initialFocus selector did not match an Element inside dialog.');
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!isElement(initialFocus) || !dialog.contains(initialFocus)) {
|
|
59
|
+
throw new TypeError('initialFocus must be an Element inside dialog, a selector, or a function.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createDialogController(options, registry, defaults) {
|
|
64
|
+
if (!options || typeof options !== 'object') {
|
|
65
|
+
throw new TypeError('Dialog controller options are required.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const container = assertElement(options.container, 'options.container');
|
|
69
|
+
const existing = registry.get(container);
|
|
70
|
+
if (existing) return existing;
|
|
71
|
+
|
|
72
|
+
const dialog = assertElement(options.dialog ?? container, 'options.dialog');
|
|
73
|
+
if (dialog !== container && !container.contains(dialog)) {
|
|
74
|
+
throw new TypeError('options.dialog must be options.container or a descendant of it.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const trigger = options.trigger == null ? null : assertElement(options.trigger, 'options.trigger');
|
|
78
|
+
const backdrop = options.backdrop === false
|
|
79
|
+
? null
|
|
80
|
+
: assertElement(options.backdrop ?? container, 'options.backdrop');
|
|
81
|
+
const closeButtons = options.closeButtons == null
|
|
82
|
+
? [...dialog.querySelectorAll('[data-ct-dismiss]')]
|
|
83
|
+
: assertElements(options.closeButtons, 'options.closeButtons');
|
|
84
|
+
assertSameDocument(closeButtons, container.ownerDocument, 'options.closeButtons');
|
|
85
|
+
if (trigger) assertSameDocument([trigger], container.ownerDocument, 'options.trigger');
|
|
86
|
+
if (backdrop) assertSameDocument([backdrop], container.ownerDocument, 'options.backdrop');
|
|
87
|
+
const triggerAction = assertChoice(
|
|
88
|
+
options.triggerAction ?? defaults.triggerAction,
|
|
89
|
+
['open', 'toggle'],
|
|
90
|
+
'options.triggerAction',
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
assertFunction(options.onOpen, 'options.onOpen');
|
|
94
|
+
assertFunction(options.onClose, 'options.onClose');
|
|
95
|
+
validateInitialFocus(options.initialFocus, dialog);
|
|
96
|
+
|
|
97
|
+
const attributes = createAttributeStore();
|
|
98
|
+
const lifecycle = createControllerLifecycle(defaults.name);
|
|
99
|
+
const disposables = createDisposables();
|
|
100
|
+
const document = container.ownerDocument;
|
|
101
|
+
const token = {};
|
|
102
|
+
const closeOnEscape = options.closeOnEscape !== false;
|
|
103
|
+
const closeOnBackdrop = options.closeOnBackdrop !== false;
|
|
104
|
+
const trapFocus = options.trapFocus !== false;
|
|
105
|
+
const returnFocus = options.returnFocus !== false;
|
|
106
|
+
const lockScroll = options.lockScroll !== false;
|
|
107
|
+
const inertBackground = options.inertBackground === true;
|
|
108
|
+
let open = options.open ?? container.getAttribute('data-state') === 'open';
|
|
109
|
+
let previouslyFocused = null;
|
|
110
|
+
let releaseBodyLock = null;
|
|
111
|
+
let removeFromStack = null;
|
|
112
|
+
let inertedElements = null;
|
|
113
|
+
let warnedMissingName = false;
|
|
114
|
+
|
|
115
|
+
const dialogId = ensureId(dialog, `${defaults.idPrefix}-dialog`, attributes);
|
|
116
|
+
attributes.set(dialog, 'role', dialog.getAttribute('role') ?? 'dialog');
|
|
117
|
+
attributes.set(dialog, 'aria-modal', 'true');
|
|
118
|
+
if (trigger) {
|
|
119
|
+
attributes.set(trigger, 'aria-haspopup', 'dialog');
|
|
120
|
+
attributes.set(trigger, 'aria-controls', dialogId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function syncState() {
|
|
124
|
+
attributes.set(container, 'data-state', open ? 'open' : 'closed');
|
|
125
|
+
attributes.set(container, 'hidden', open ? null : '');
|
|
126
|
+
if (trigger) attributes.set(trigger, 'aria-expanded', String(open));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function releaseOpenResources() {
|
|
130
|
+
removeFromStack?.();
|
|
131
|
+
removeFromStack = null;
|
|
132
|
+
releaseBodyLock?.();
|
|
133
|
+
releaseBodyLock = null;
|
|
134
|
+
releaseBackgroundInert();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* aria-modal hides background content from modern AT, but legacy virtual
|
|
138
|
+
cursors can still wander behind the dialog. Opt-in `inertBackground`
|
|
139
|
+
additionally inerts the container's DOM siblings up to <body>. Nodes
|
|
140
|
+
appended after opening (e.g. portaled popups) are unaffected. */
|
|
141
|
+
function applyBackgroundInert() {
|
|
142
|
+
if (!inertBackground || inertedElements) return;
|
|
143
|
+
inertedElements = [];
|
|
144
|
+
for (let node = container; node.parentElement && node !== document.body; node = node.parentElement) {
|
|
145
|
+
for (const sibling of node.parentElement.children) {
|
|
146
|
+
if (sibling === node || sibling.inert) continue;
|
|
147
|
+
const tag = sibling.tagName;
|
|
148
|
+
if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'LINK' || tag === 'TEMPLATE') continue;
|
|
149
|
+
sibling.inert = true;
|
|
150
|
+
inertedElements.push(sibling);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function releaseBackgroundInert() {
|
|
156
|
+
if (!inertedElements) return;
|
|
157
|
+
for (const element of inertedElements) element.inert = false;
|
|
158
|
+
inertedElements = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function warnWhenUnnamed() {
|
|
162
|
+
if (warnedMissingName) return;
|
|
163
|
+
warnedMissingName = true;
|
|
164
|
+
if (!dialog.hasAttribute('aria-label') && !dialog.hasAttribute('aria-labelledby')) {
|
|
165
|
+
warnAccessibility(
|
|
166
|
+
`${defaults.name}: the dialog element has neither aria-labelledby nor aria-label. ` +
|
|
167
|
+
'Dialogs must expose an accessible name (WCAG 4.1.2).',
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function registerDialogOverlay() {
|
|
173
|
+
return pushOverlay(document, token, {
|
|
174
|
+
modal: true,
|
|
175
|
+
onOwnerClose: () => closeDialog('owner-close', { restoreFocus: false }),
|
|
176
|
+
onOwnerDestroy: () => controller.destroy(),
|
|
177
|
+
scope: dialog,
|
|
178
|
+
trigger,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function focusInside(checkpoint = lifecycle.assertAlive) {
|
|
183
|
+
const target = resolveInitialFocus(options.initialFocus, dialog);
|
|
184
|
+
checkpoint();
|
|
185
|
+
if (target && focusIntoView(target)) {
|
|
186
|
+
checkpoint();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!dialog.hasAttribute('tabindex')) attributes.set(dialog, 'tabindex', '-1');
|
|
191
|
+
focusElement(dialog);
|
|
192
|
+
checkpoint();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function restoreDialogFocus(target) {
|
|
196
|
+
if (focusIntoView(target)) return true;
|
|
197
|
+
if (target !== trigger && focusIntoView(trigger)) return true;
|
|
198
|
+
|
|
199
|
+
const active = document.activeElement;
|
|
200
|
+
if (isElement(active) && dialog.contains(active) && typeof active.blur === 'function') {
|
|
201
|
+
active.blur();
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function openDialog(reason = 'programmatic') {
|
|
207
|
+
return lifecycle.run('open', (checkpoint) => {
|
|
208
|
+
if (open) return false;
|
|
209
|
+
|
|
210
|
+
const detail = { controller, reason };
|
|
211
|
+
const allowed = dispatch(container, 'ct:beforeopen', detail, true);
|
|
212
|
+
checkpoint();
|
|
213
|
+
if (!allowed) return false;
|
|
214
|
+
|
|
215
|
+
previouslyFocused = isElement(document.activeElement) ? document.activeElement : null;
|
|
216
|
+
open = true;
|
|
217
|
+
syncState();
|
|
218
|
+
warnWhenUnnamed();
|
|
219
|
+
removeFromStack = registerDialogOverlay();
|
|
220
|
+
if (lockScroll) releaseBodyLock = lockBodyScroll(document);
|
|
221
|
+
applyBackgroundInert();
|
|
222
|
+
try {
|
|
223
|
+
focusInside(checkpoint);
|
|
224
|
+
options.onOpen?.(detail);
|
|
225
|
+
checkpoint();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (lifecycle.isAlive) {
|
|
228
|
+
open = false;
|
|
229
|
+
syncState();
|
|
230
|
+
releaseOpenResources();
|
|
231
|
+
const focusTarget = previouslyFocused?.isConnected ? previouslyFocused : trigger;
|
|
232
|
+
restoreDialogFocus(focusTarget);
|
|
233
|
+
previouslyFocused = null;
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
dispatch(container, 'ct:open', detail);
|
|
238
|
+
checkpoint();
|
|
239
|
+
return true;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function closeDialog(reason = 'programmatic', { restoreFocus = returnFocus } = {}) {
|
|
244
|
+
return lifecycle.run('close', (checkpoint) => {
|
|
245
|
+
if (!open) return false;
|
|
246
|
+
|
|
247
|
+
const detail = { controller, reason };
|
|
248
|
+
const allowed = dispatch(container, 'ct:beforeclose', detail, true);
|
|
249
|
+
checkpoint();
|
|
250
|
+
if (!allowed) return false;
|
|
251
|
+
if (!closeOwnedOverlays(document, token)) return false;
|
|
252
|
+
checkpoint();
|
|
253
|
+
|
|
254
|
+
open = false;
|
|
255
|
+
syncState();
|
|
256
|
+
releaseOpenResources();
|
|
257
|
+
|
|
258
|
+
const focusTarget = previouslyFocused?.isConnected ? previouslyFocused : trigger;
|
|
259
|
+
previouslyFocused = null;
|
|
260
|
+
if (restoreFocus) {
|
|
261
|
+
restoreDialogFocus(focusTarget);
|
|
262
|
+
checkpoint();
|
|
263
|
+
}
|
|
264
|
+
options.onClose?.(detail);
|
|
265
|
+
checkpoint();
|
|
266
|
+
dispatch(container, 'ct:close', detail);
|
|
267
|
+
checkpoint();
|
|
268
|
+
return true;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function toggleDialog(reason = 'programmatic') {
|
|
273
|
+
lifecycle.assertAlive();
|
|
274
|
+
return open ? closeDialog(reason) : openDialog(reason);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function onDocumentKeyDown(event) {
|
|
278
|
+
if (!open) return;
|
|
279
|
+
/* Never treat keys as dialog shortcuts while an IME composition is
|
|
280
|
+
active — Escape/Tab are cancelling or committing composed text. */
|
|
281
|
+
if (isComposingEvent(event)) return;
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
event.key === 'Escape' &&
|
|
285
|
+
closeOnEscape &&
|
|
286
|
+
consumeTopOverlayEvent(event, document, token)
|
|
287
|
+
) {
|
|
288
|
+
event.preventDefault();
|
|
289
|
+
event.stopPropagation();
|
|
290
|
+
closeDialog('escape');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (
|
|
295
|
+
trapFocus &&
|
|
296
|
+
isTopModalOverlay(document, token) &&
|
|
297
|
+
!topOverlayHandlesModalTab(document, token)
|
|
298
|
+
) {
|
|
299
|
+
trapTabKey(event, getModalFocusScopes(document, token));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function onDocumentFocusIn(event) {
|
|
304
|
+
if (
|
|
305
|
+
!open ||
|
|
306
|
+
!trapFocus ||
|
|
307
|
+
!isTopModalOverlay(document, token) ||
|
|
308
|
+
isInModalFocusScope(document, token, event.target)
|
|
309
|
+
) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
focusInside();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (trigger) {
|
|
317
|
+
disposables.listen(trigger, 'click', (event) => {
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
if (isDisabled(trigger)) return;
|
|
320
|
+
if (triggerAction === 'toggle') toggleDialog('trigger');
|
|
321
|
+
else openDialog('trigger');
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
closeButtons.forEach((button) => {
|
|
326
|
+
disposables.listen(button, 'click', (event) => {
|
|
327
|
+
event.preventDefault();
|
|
328
|
+
if (!isDisabled(button)) closeDialog('dismiss');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (backdrop) {
|
|
333
|
+
/* Close only when the pointer both went down AND came up on the backdrop
|
|
334
|
+
itself. A plain click listener also fires when a drag (e.g. selecting
|
|
335
|
+
text in a form field) starts inside the dialog and is released over
|
|
336
|
+
the backdrop — the click retargets to their common ancestor, which is
|
|
337
|
+
the backdrop — and would discard the user's in-progress work. Routing
|
|
338
|
+
the pointerdown through the overlay stack also keeps a single gesture
|
|
339
|
+
from dismissing a popup and this dialog together. */
|
|
340
|
+
let pointerDownOnBackdrop = false;
|
|
341
|
+
disposables.listen(backdrop, 'pointerdown', (event) => {
|
|
342
|
+
pointerDownOnBackdrop =
|
|
343
|
+
open &&
|
|
344
|
+
closeOnBackdrop &&
|
|
345
|
+
event.target === backdrop &&
|
|
346
|
+
consumeTopOverlayEvent(event, document, token);
|
|
347
|
+
});
|
|
348
|
+
disposables.listen(backdrop, 'pointercancel', () => {
|
|
349
|
+
pointerDownOnBackdrop = false;
|
|
350
|
+
});
|
|
351
|
+
disposables.listen(backdrop, 'pointerup', (event) => {
|
|
352
|
+
const shouldClose = pointerDownOnBackdrop && event.target === backdrop;
|
|
353
|
+
pointerDownOnBackdrop = false;
|
|
354
|
+
if (shouldClose && open) closeDialog('backdrop');
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
disposables.listen(document, 'keydown', onDocumentKeyDown, true);
|
|
359
|
+
disposables.listen(document, 'focusin', onDocumentFocusIn, true);
|
|
360
|
+
|
|
361
|
+
const controller = Object.freeze({
|
|
362
|
+
get container() {
|
|
363
|
+
return container;
|
|
364
|
+
},
|
|
365
|
+
get dialog() {
|
|
366
|
+
return dialog;
|
|
367
|
+
},
|
|
368
|
+
get trigger() {
|
|
369
|
+
return trigger;
|
|
370
|
+
},
|
|
371
|
+
get isOpen() {
|
|
372
|
+
return open;
|
|
373
|
+
},
|
|
374
|
+
open: openDialog,
|
|
375
|
+
close: closeDialog,
|
|
376
|
+
toggle: toggleDialog,
|
|
377
|
+
destroy() {
|
|
378
|
+
if (!lifecycle.isAlive) return;
|
|
379
|
+
destroyOwnedOverlays(document, token);
|
|
380
|
+
if (!lifecycle.destroy()) return;
|
|
381
|
+
if (open) {
|
|
382
|
+
open = false;
|
|
383
|
+
/* Frameworks often unmount an open dialog without closing it first;
|
|
384
|
+
without a hand-off the browser drops focus to <body> and silently
|
|
385
|
+
teleports keyboard/AT users to the top of the page. */
|
|
386
|
+
const active = document.activeElement;
|
|
387
|
+
if (isElement(active) && dialog.contains(active)) {
|
|
388
|
+
restoreDialogFocus(previouslyFocused?.isConnected ? previouslyFocused : trigger);
|
|
389
|
+
}
|
|
390
|
+
previouslyFocused = null;
|
|
391
|
+
releaseOpenResources();
|
|
392
|
+
}
|
|
393
|
+
disposables.dispose();
|
|
394
|
+
attributes.restore();
|
|
395
|
+
registry.delete(container);
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
registry.set(container, controller);
|
|
400
|
+
syncState();
|
|
401
|
+
if (open) {
|
|
402
|
+
previouslyFocused = isElement(document.activeElement) ? document.activeElement : null;
|
|
403
|
+
warnWhenUnnamed();
|
|
404
|
+
removeFromStack = registerDialogOverlay();
|
|
405
|
+
if (lockScroll) releaseBodyLock = lockBodyScroll(document);
|
|
406
|
+
applyBackgroundInert();
|
|
407
|
+
try {
|
|
408
|
+
focusInside();
|
|
409
|
+
} catch (error) {
|
|
410
|
+
open = false;
|
|
411
|
+
releaseOpenResources();
|
|
412
|
+
lifecycle.destroy();
|
|
413
|
+
disposables.dispose();
|
|
414
|
+
attributes.restore();
|
|
415
|
+
registry.delete(container);
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return controller;
|
|
421
|
+
}
|