@neuravision/construct 2.0.3 → 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.
- package/CHANGELOG.md +272 -0
- package/README.md +148 -152
- package/behaviors/combobox.js +447 -0
- package/behaviors/drawer.js +18 -0
- package/behaviors/dropdown.js +296 -0
- package/behaviors/index.d.ts +333 -0
- package/behaviors/index.js +9 -0
- package/behaviors/internal/dialog-controller.js +351 -0
- package/behaviors/internal/dom.js +377 -0
- package/behaviors/internal/overlay.js +241 -0
- package/behaviors/internal/popup-layer.js +188 -0
- package/behaviors/modal.js +17 -0
- package/behaviors/popover.js +269 -0
- package/behaviors/select-menu.js +326 -0
- package/behaviors/tabs.js +230 -0
- package/behaviors/toggle-group.js +234 -0
- package/behaviors/tooltip.js +233 -0
- package/components/README.md +279 -83
- package/components/_shared.css +11 -10
- package/components/_shell-shared.css +20 -4
- package/components/app-shell-v2.css +21 -14
- package/components/app-shell.css +23 -14
- package/components/badge.css +15 -0
- package/components/button.css +103 -15
- package/components/card.css +53 -8
- package/components/checkbox.css +3 -2
- package/components/chip.css +1 -1
- package/components/datepicker.css +3 -1
- package/components/datum.css +204 -0
- package/components/index.css +5 -0
- package/components/meter.css +182 -0
- package/components/navbar.css +88 -3
- package/components/slider.css +2 -1
- package/components/status.css +107 -0
- package/components/switch.css +4 -3
- package/components/tooltip.css +4 -0
- package/docs/architecture.md +79 -0
- package/docs/behaviors.md +101 -0
- package/docs/fonts.md +70 -0
- package/docs/governance.md +85 -0
- package/docs/guidelines.md +42 -23
- 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 +8 -1
- package/package.json +72 -21
- package/schemas/components.schema.json +107 -0
- package/schemas/examples/theme.example.json +27 -0
- package/schemas/primitives.schema.json +135 -0
- package/schemas/semantic.schema.json +223 -0
- package/schemas/theme.schema.json +63 -0
- package/scripts/build-tokens.mjs +1417 -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/tokens/README.md +130 -126
- package/tokens/components.json +54 -0
- package/tokens/primitives.json +268 -0
- package/tokens/semantic.dark.json +93 -0
- package/tokens/semantic.high-contrast.json +93 -0
- package/tokens/semantic.light.json +178 -0
- package/tokens/tokens.css +532 -28
- package/tokens/tokens.js +335 -25
- package/tokens/tokens.json +764 -21
- package/tokens/tokens.ts +335 -25
|
@@ -0,0 +1,351 @@
|
|
|
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
|
+
getFocusableElements,
|
|
14
|
+
isElement,
|
|
15
|
+
isDisabled,
|
|
16
|
+
} from './dom.js';
|
|
17
|
+
import {
|
|
18
|
+
closeOwnedOverlays,
|
|
19
|
+
consumeTopOverlayEvent,
|
|
20
|
+
destroyOwnedOverlays,
|
|
21
|
+
getModalFocusScopes,
|
|
22
|
+
isInModalFocusScope,
|
|
23
|
+
isTopModalOverlay,
|
|
24
|
+
lockBodyScroll,
|
|
25
|
+
pushOverlay,
|
|
26
|
+
topOverlayHandlesModalTab,
|
|
27
|
+
trapTabKey,
|
|
28
|
+
} from './overlay.js';
|
|
29
|
+
|
|
30
|
+
function resolveInitialFocus(initialFocus, dialog) {
|
|
31
|
+
let target = initialFocus;
|
|
32
|
+
if (typeof target === 'function') target = target();
|
|
33
|
+
if (typeof target === 'string') {
|
|
34
|
+
target = dialog.querySelector(target);
|
|
35
|
+
if (!target) throw new TypeError('initialFocus selector did not match an Element inside dialog.');
|
|
36
|
+
}
|
|
37
|
+
if (target == null) target = dialog.querySelector('[autofocus]');
|
|
38
|
+
if (target == null) target = getFocusableElements(dialog)[0];
|
|
39
|
+
|
|
40
|
+
if (target != null && (!isElement(target) || !dialog.contains(target))) {
|
|
41
|
+
throw new TypeError('initialFocus must resolve to an Element inside dialog.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return target;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateInitialFocus(initialFocus, dialog) {
|
|
48
|
+
if (initialFocus == null || typeof initialFocus === 'function') return;
|
|
49
|
+
if (typeof initialFocus === 'string') {
|
|
50
|
+
if (!dialog.querySelector(initialFocus)) {
|
|
51
|
+
throw new TypeError('initialFocus selector did not match an Element inside dialog.');
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!isElement(initialFocus) || !dialog.contains(initialFocus)) {
|
|
56
|
+
throw new TypeError('initialFocus must be an Element inside dialog, a selector, or a function.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createDialogController(options, registry, defaults) {
|
|
61
|
+
if (!options || typeof options !== 'object') {
|
|
62
|
+
throw new TypeError('Dialog controller options are required.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const container = assertElement(options.container, 'options.container');
|
|
66
|
+
const existing = registry.get(container);
|
|
67
|
+
if (existing) return existing;
|
|
68
|
+
|
|
69
|
+
const dialog = assertElement(options.dialog ?? container, 'options.dialog');
|
|
70
|
+
if (dialog !== container && !container.contains(dialog)) {
|
|
71
|
+
throw new TypeError('options.dialog must be options.container or a descendant of it.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const trigger = options.trigger == null ? null : assertElement(options.trigger, 'options.trigger');
|
|
75
|
+
const backdrop = options.backdrop === false
|
|
76
|
+
? null
|
|
77
|
+
: assertElement(options.backdrop ?? container, 'options.backdrop');
|
|
78
|
+
const closeButtons = options.closeButtons == null
|
|
79
|
+
? [...dialog.querySelectorAll('[data-ct-dismiss]')]
|
|
80
|
+
: assertElements(options.closeButtons, 'options.closeButtons');
|
|
81
|
+
assertSameDocument(closeButtons, container.ownerDocument, 'options.closeButtons');
|
|
82
|
+
if (trigger) assertSameDocument([trigger], container.ownerDocument, 'options.trigger');
|
|
83
|
+
if (backdrop) assertSameDocument([backdrop], container.ownerDocument, 'options.backdrop');
|
|
84
|
+
const triggerAction = assertChoice(
|
|
85
|
+
options.triggerAction ?? defaults.triggerAction,
|
|
86
|
+
['open', 'toggle'],
|
|
87
|
+
'options.triggerAction',
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
assertFunction(options.onOpen, 'options.onOpen');
|
|
91
|
+
assertFunction(options.onClose, 'options.onClose');
|
|
92
|
+
validateInitialFocus(options.initialFocus, dialog);
|
|
93
|
+
|
|
94
|
+
const attributes = createAttributeStore();
|
|
95
|
+
const lifecycle = createControllerLifecycle(defaults.name);
|
|
96
|
+
const disposables = createDisposables();
|
|
97
|
+
const document = container.ownerDocument;
|
|
98
|
+
const token = {};
|
|
99
|
+
const closeOnEscape = options.closeOnEscape !== false;
|
|
100
|
+
const closeOnBackdrop = options.closeOnBackdrop !== false;
|
|
101
|
+
const trapFocus = options.trapFocus !== false;
|
|
102
|
+
const returnFocus = options.returnFocus !== false;
|
|
103
|
+
const lockScroll = options.lockScroll !== false;
|
|
104
|
+
let open = options.open ?? container.getAttribute('data-state') === 'open';
|
|
105
|
+
let previouslyFocused = null;
|
|
106
|
+
let releaseBodyLock = null;
|
|
107
|
+
let removeFromStack = null;
|
|
108
|
+
|
|
109
|
+
const dialogId = ensureId(dialog, `${defaults.idPrefix}-dialog`, attributes);
|
|
110
|
+
attributes.set(dialog, 'role', dialog.getAttribute('role') ?? 'dialog');
|
|
111
|
+
attributes.set(dialog, 'aria-modal', 'true');
|
|
112
|
+
if (trigger) {
|
|
113
|
+
attributes.set(trigger, 'aria-haspopup', 'dialog');
|
|
114
|
+
attributes.set(trigger, 'aria-controls', dialogId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function syncState() {
|
|
118
|
+
attributes.set(container, 'data-state', open ? 'open' : 'closed');
|
|
119
|
+
attributes.set(container, 'hidden', open ? null : '');
|
|
120
|
+
if (trigger) attributes.set(trigger, 'aria-expanded', String(open));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function releaseOpenResources() {
|
|
124
|
+
removeFromStack?.();
|
|
125
|
+
removeFromStack = null;
|
|
126
|
+
releaseBodyLock?.();
|
|
127
|
+
releaseBodyLock = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function registerDialogOverlay() {
|
|
131
|
+
return pushOverlay(document, token, {
|
|
132
|
+
modal: true,
|
|
133
|
+
onOwnerClose: () => closeDialog('owner-close', { restoreFocus: false }),
|
|
134
|
+
onOwnerDestroy: () => controller.destroy(),
|
|
135
|
+
scope: dialog,
|
|
136
|
+
trigger,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function focusInside(checkpoint = lifecycle.assertAlive) {
|
|
141
|
+
const target = resolveInitialFocus(options.initialFocus, dialog);
|
|
142
|
+
checkpoint();
|
|
143
|
+
if (target && focusElement(target)) {
|
|
144
|
+
checkpoint();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!dialog.hasAttribute('tabindex')) attributes.set(dialog, 'tabindex', '-1');
|
|
149
|
+
focusElement(dialog);
|
|
150
|
+
checkpoint();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function restoreDialogFocus(target) {
|
|
154
|
+
if (focusElement(target)) return true;
|
|
155
|
+
if (target !== trigger && focusElement(trigger)) return true;
|
|
156
|
+
|
|
157
|
+
const active = document.activeElement;
|
|
158
|
+
if (isElement(active) && dialog.contains(active) && typeof active.blur === 'function') {
|
|
159
|
+
active.blur();
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function openDialog(reason = 'programmatic') {
|
|
165
|
+
return lifecycle.run('open', (checkpoint) => {
|
|
166
|
+
if (open) return false;
|
|
167
|
+
|
|
168
|
+
const detail = { controller, reason };
|
|
169
|
+
const allowed = dispatch(container, 'ct:beforeopen', detail, true);
|
|
170
|
+
checkpoint();
|
|
171
|
+
if (!allowed) return false;
|
|
172
|
+
|
|
173
|
+
previouslyFocused = isElement(document.activeElement) ? document.activeElement : null;
|
|
174
|
+
open = true;
|
|
175
|
+
syncState();
|
|
176
|
+
removeFromStack = registerDialogOverlay();
|
|
177
|
+
if (lockScroll) releaseBodyLock = lockBodyScroll(document);
|
|
178
|
+
try {
|
|
179
|
+
focusInside(checkpoint);
|
|
180
|
+
options.onOpen?.(detail);
|
|
181
|
+
checkpoint();
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (lifecycle.isAlive) {
|
|
184
|
+
open = false;
|
|
185
|
+
syncState();
|
|
186
|
+
releaseOpenResources();
|
|
187
|
+
const focusTarget = previouslyFocused?.isConnected ? previouslyFocused : trigger;
|
|
188
|
+
restoreDialogFocus(focusTarget);
|
|
189
|
+
previouslyFocused = null;
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
dispatch(container, 'ct:open', detail);
|
|
194
|
+
checkpoint();
|
|
195
|
+
return true;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function closeDialog(reason = 'programmatic', { restoreFocus = returnFocus } = {}) {
|
|
200
|
+
return lifecycle.run('close', (checkpoint) => {
|
|
201
|
+
if (!open) return false;
|
|
202
|
+
|
|
203
|
+
const detail = { controller, reason };
|
|
204
|
+
const allowed = dispatch(container, 'ct:beforeclose', detail, true);
|
|
205
|
+
checkpoint();
|
|
206
|
+
if (!allowed) return false;
|
|
207
|
+
if (!closeOwnedOverlays(document, token)) return false;
|
|
208
|
+
checkpoint();
|
|
209
|
+
|
|
210
|
+
open = false;
|
|
211
|
+
syncState();
|
|
212
|
+
releaseOpenResources();
|
|
213
|
+
|
|
214
|
+
const focusTarget = previouslyFocused?.isConnected ? previouslyFocused : trigger;
|
|
215
|
+
previouslyFocused = null;
|
|
216
|
+
if (restoreFocus) {
|
|
217
|
+
restoreDialogFocus(focusTarget);
|
|
218
|
+
checkpoint();
|
|
219
|
+
}
|
|
220
|
+
options.onClose?.(detail);
|
|
221
|
+
checkpoint();
|
|
222
|
+
dispatch(container, 'ct:close', detail);
|
|
223
|
+
checkpoint();
|
|
224
|
+
return true;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function toggleDialog(reason = 'programmatic') {
|
|
229
|
+
lifecycle.assertAlive();
|
|
230
|
+
return open ? closeDialog(reason) : openDialog(reason);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function onDocumentKeyDown(event) {
|
|
234
|
+
if (!open) return;
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
event.key === 'Escape' &&
|
|
238
|
+
closeOnEscape &&
|
|
239
|
+
consumeTopOverlayEvent(event, document, token)
|
|
240
|
+
) {
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
event.stopPropagation();
|
|
243
|
+
closeDialog('escape');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
trapFocus &&
|
|
249
|
+
isTopModalOverlay(document, token) &&
|
|
250
|
+
!topOverlayHandlesModalTab(document, token)
|
|
251
|
+
) {
|
|
252
|
+
trapTabKey(event, getModalFocusScopes(document, token));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function onDocumentFocusIn(event) {
|
|
257
|
+
if (
|
|
258
|
+
!open ||
|
|
259
|
+
!trapFocus ||
|
|
260
|
+
!isTopModalOverlay(document, token) ||
|
|
261
|
+
isInModalFocusScope(document, token, event.target)
|
|
262
|
+
) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
focusInside();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (trigger) {
|
|
270
|
+
disposables.listen(trigger, 'click', (event) => {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
if (isDisabled(trigger)) return;
|
|
273
|
+
if (triggerAction === 'toggle') toggleDialog('trigger');
|
|
274
|
+
else openDialog('trigger');
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
closeButtons.forEach((button) => {
|
|
279
|
+
disposables.listen(button, 'click', (event) => {
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
if (!isDisabled(button)) closeDialog('dismiss');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (backdrop) {
|
|
286
|
+
disposables.listen(backdrop, 'click', (event) => {
|
|
287
|
+
if (
|
|
288
|
+
open &&
|
|
289
|
+
closeOnBackdrop &&
|
|
290
|
+
event.target === backdrop &&
|
|
291
|
+
consumeTopOverlayEvent(event, document, token)
|
|
292
|
+
) {
|
|
293
|
+
closeDialog('backdrop');
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
disposables.listen(document, 'keydown', onDocumentKeyDown, true);
|
|
299
|
+
disposables.listen(document, 'focusin', onDocumentFocusIn, true);
|
|
300
|
+
|
|
301
|
+
const controller = Object.freeze({
|
|
302
|
+
get container() {
|
|
303
|
+
return container;
|
|
304
|
+
},
|
|
305
|
+
get dialog() {
|
|
306
|
+
return dialog;
|
|
307
|
+
},
|
|
308
|
+
get trigger() {
|
|
309
|
+
return trigger;
|
|
310
|
+
},
|
|
311
|
+
get isOpen() {
|
|
312
|
+
return open;
|
|
313
|
+
},
|
|
314
|
+
open: openDialog,
|
|
315
|
+
close: closeDialog,
|
|
316
|
+
toggle: toggleDialog,
|
|
317
|
+
destroy() {
|
|
318
|
+
if (!lifecycle.isAlive) return;
|
|
319
|
+
destroyOwnedOverlays(document, token);
|
|
320
|
+
if (!lifecycle.destroy()) return;
|
|
321
|
+
if (open) {
|
|
322
|
+
open = false;
|
|
323
|
+
releaseOpenResources();
|
|
324
|
+
}
|
|
325
|
+
disposables.dispose();
|
|
326
|
+
attributes.restore();
|
|
327
|
+
registry.delete(container);
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
registry.set(container, controller);
|
|
332
|
+
syncState();
|
|
333
|
+
if (open) {
|
|
334
|
+
previouslyFocused = isElement(document.activeElement) ? document.activeElement : null;
|
|
335
|
+
removeFromStack = registerDialogOverlay();
|
|
336
|
+
if (lockScroll) releaseBodyLock = lockBodyScroll(document);
|
|
337
|
+
try {
|
|
338
|
+
focusInside();
|
|
339
|
+
} catch (error) {
|
|
340
|
+
open = false;
|
|
341
|
+
releaseOpenResources();
|
|
342
|
+
lifecycle.destroy();
|
|
343
|
+
disposables.dispose();
|
|
344
|
+
attributes.restore();
|
|
345
|
+
registry.delete(container);
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return controller;
|
|
351
|
+
}
|