@ponchia/ui 0.6.7 → 0.6.9
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 +129 -4
- package/README.md +4 -4
- package/annotations/index.d.ts.map +1 -1
- package/annotations/index.js +26 -9
- package/behaviors/carousel.d.ts.map +1 -1
- package/behaviors/carousel.js +145 -49
- package/behaviors/combobox.d.ts.map +1 -1
- package/behaviors/combobox.js +220 -92
- package/behaviors/command.d.ts.map +1 -1
- package/behaviors/command.js +74 -14
- package/behaviors/connectors.d.ts.map +1 -1
- package/behaviors/connectors.js +131 -32
- package/behaviors/crosshair.d.ts.map +1 -1
- package/behaviors/crosshair.js +47 -1
- package/behaviors/dialog.d.ts.map +1 -1
- package/behaviors/dialog.js +24 -9
- package/behaviors/disclosure.d.ts.map +1 -1
- package/behaviors/disclosure.js +33 -3
- package/behaviors/dismissible.d.ts.map +1 -1
- package/behaviors/dismissible.js +3 -2
- package/behaviors/forms.d.ts.map +1 -1
- package/behaviors/forms.js +211 -140
- package/behaviors/glyph.d.ts.map +1 -1
- package/behaviors/glyph.js +172 -132
- package/behaviors/inert.d.ts +1 -1
- package/behaviors/inert.d.ts.map +1 -1
- package/behaviors/inert.js +4 -3
- package/behaviors/internal.d.ts.map +1 -1
- package/behaviors/internal.js +4 -3
- package/behaviors/legend.d.ts +0 -5
- package/behaviors/legend.d.ts.map +1 -1
- package/behaviors/legend.js +45 -13
- package/behaviors/menu.d.ts.map +1 -1
- package/behaviors/menu.js +13 -8
- package/behaviors/modal.d.ts.map +1 -1
- package/behaviors/modal.js +77 -19
- package/behaviors/popover.d.ts +4 -3
- package/behaviors/popover.d.ts.map +1 -1
- package/behaviors/popover.js +94 -14
- package/behaviors/sources.d.ts.map +1 -1
- package/behaviors/sources.js +14 -2
- package/behaviors/splitter.d.ts.map +1 -1
- package/behaviors/splitter.js +149 -110
- package/behaviors/spotlight.d.ts.map +1 -1
- package/behaviors/spotlight.js +28 -2
- package/behaviors/table.d.ts +1 -1
- package/behaviors/table.d.ts.map +1 -1
- package/behaviors/table.js +108 -17
- package/behaviors/tabs.d.ts.map +1 -1
- package/behaviors/tabs.js +84 -20
- package/behaviors/theme.d.ts.map +1 -1
- package/behaviors/theme.js +25 -5
- package/behaviors/toast.js +5 -5
- package/classes/index.d.ts +15 -2
- package/classes/index.js +48 -35
- package/connectors/index.d.ts +41 -8
- package/connectors/index.d.ts.map +1 -1
- package/connectors/index.js +74 -19
- package/css/annotations.css +12 -0
- package/css/app.css +3 -4
- package/css/base.css +1 -1
- package/css/content.css +3 -3
- package/css/crosshair.css +27 -2
- package/css/disclosure.css +3 -3
- package/css/dots.css +4 -4
- package/css/feedback.css +8 -37
- package/css/forms.css +9 -12
- package/css/legend.css +1 -1
- package/css/marks.css +1 -1
- package/css/motion.css +6 -6
- package/css/navigation.css +12 -0
- package/css/overlay.css +5 -7
- package/css/primitives.css +14 -16
- package/css/sidenote.css +2 -2
- package/css/table.css +2 -2
- package/css/tokens.css +16 -0
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -1
- package/dist/css/annotations.css +1 -1
- package/dist/css/crosshair.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/navigation.css +1 -1
- package/dist/css/report-kit.css +1 -1
- package/dist/css/tokens.css +1 -1
- package/docs/adr/0001-color-system.md +3 -2
- package/docs/annotations.md +21 -1
- package/docs/architecture.md +74 -13
- package/docs/command.md +4 -1
- package/docs/connectors.md +16 -0
- package/docs/crosshair.md +1 -1
- package/docs/dots.md +4 -1
- package/docs/glyphs.md +11 -0
- package/docs/interop/react-flow.md +89 -0
- package/docs/migrations/0.2-to-0.3.md +1 -1
- package/docs/package-contract.md +7 -5
- package/docs/reporting.md +23 -12
- package/docs/stability.md +85 -9
- package/docs/theming.md +2 -2
- package/docs/usage.md +16 -2
- package/docs/vega.md +4 -4
- package/glyphs/glyphs.js +43 -33
- package/llms.txt +19 -8
- package/package.json +23 -4
- package/schemas/report-claims.v1.schema.json +1 -1
- package/svelte/index.d.ts +71 -45
- package/svelte/index.d.ts.map +1 -1
- package/svelte/index.js +29 -2
- package/tokens/index.js +2 -2
- package/vue/index.d.ts +42 -5
- package/vue/index.d.ts.map +1 -1
- package/vue/index.js +32 -1
package/behaviors/combobox.js
CHANGED
|
@@ -7,8 +7,128 @@ import {
|
|
|
7
7
|
scrollIntoViewSafe,
|
|
8
8
|
wrapIndex,
|
|
9
9
|
collectHosts,
|
|
10
|
+
closestSafe,
|
|
10
11
|
} from './internal.js';
|
|
11
12
|
|
|
13
|
+
const COMBOBOX_OPTION_SELECTOR = '[role="option"], .ui-combobox__option';
|
|
14
|
+
|
|
15
|
+
const snapshotAttrs = (el, names) => {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
out[name] = {
|
|
19
|
+
had: el.hasAttribute(name),
|
|
20
|
+
value: el.getAttribute(name),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const restoreAttrs = (el, attrs) => {
|
|
27
|
+
for (const [name, attr] of Object.entries(attrs)) {
|
|
28
|
+
if (attr.had) el.setAttribute(name, attr.value);
|
|
29
|
+
else el.removeAttribute(name);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const inputLabel = (input) =>
|
|
34
|
+
input.getAttribute('aria-label') || input.labels?.[0]?.textContent?.trim();
|
|
35
|
+
|
|
36
|
+
const inputHasAccessibleName = (input) =>
|
|
37
|
+
input.hasAttribute('aria-label') ||
|
|
38
|
+
input.hasAttribute('aria-labelledby') ||
|
|
39
|
+
!!input.labels?.length ||
|
|
40
|
+
input.hasAttribute('title');
|
|
41
|
+
|
|
42
|
+
function mirrorListboxLabel(input, list) {
|
|
43
|
+
if (list.hasAttribute('aria-label') || list.hasAttribute('aria-labelledby')) return;
|
|
44
|
+
const name = inputLabel(input);
|
|
45
|
+
if (name) list.setAttribute('aria-label', name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function warnNamelessCombobox(input) {
|
|
49
|
+
if (inputHasAccessibleName(input) || typeof console === 'undefined') return;
|
|
50
|
+
console.warn(
|
|
51
|
+
'[bronto] initCombobox(): the combobox input has no accessible name — add a <label>, aria-label, or aria-labelledby (a placeholder is not enough).',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function prepareEmptyState(empty) {
|
|
56
|
+
if (!empty) return;
|
|
57
|
+
empty.hidden = true;
|
|
58
|
+
if (!empty.hasAttribute('role')) empty.setAttribute('role', 'status');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function liveOptionObserver(box, list, relist) {
|
|
62
|
+
if (!box.hasAttribute('data-bronto-combobox-live')) return null;
|
|
63
|
+
if (typeof MutationObserver !== 'function') return null;
|
|
64
|
+
const observer = new MutationObserver(relist);
|
|
65
|
+
observer.observe(list, { childList: true, subtree: true });
|
|
66
|
+
return observer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function bindComboboxLifecycle({
|
|
70
|
+
box,
|
|
71
|
+
input,
|
|
72
|
+
list,
|
|
73
|
+
empty,
|
|
74
|
+
rememberState,
|
|
75
|
+
restoreState,
|
|
76
|
+
assignListId,
|
|
77
|
+
syncOptions,
|
|
78
|
+
close,
|
|
79
|
+
relist,
|
|
80
|
+
onInput,
|
|
81
|
+
onKey,
|
|
82
|
+
onOptionClick,
|
|
83
|
+
onDocClick,
|
|
84
|
+
resetActive,
|
|
85
|
+
}) {
|
|
86
|
+
const state = rememberState();
|
|
87
|
+
const listId = assignListId();
|
|
88
|
+
syncOptions();
|
|
89
|
+
list.setAttribute('role', 'listbox');
|
|
90
|
+
// Give the listbox its own accessible name (a bare role=listbox is unnamed
|
|
91
|
+
// to a screen reader) by mirroring the input's real name.
|
|
92
|
+
// The placeholder is deliberately NOT in this chain: the input warning below
|
|
93
|
+
// already rejects a placeholder as an inadequate name, so papering the
|
|
94
|
+
// listbox over with it would contradict that. If there is no real name, the
|
|
95
|
+
// listbox stays unnamed and the warning is the signal.
|
|
96
|
+
mirrorListboxLabel(input, list);
|
|
97
|
+
// A `role="combobox"` with no accessible name is a silent AT failure. A
|
|
98
|
+
// placeholder is not a robust name (it can vanish and is ignored by some
|
|
99
|
+
// AT), so warn unless there is a real label/aria-label/aria-labelledby/title
|
|
100
|
+
// We cannot invent a good name, hence a dev-time warning, not a guess.
|
|
101
|
+
warnNamelessCombobox(input);
|
|
102
|
+
input.setAttribute('role', 'combobox');
|
|
103
|
+
input.setAttribute('aria-controls', listId);
|
|
104
|
+
input.setAttribute('aria-autocomplete', 'list');
|
|
105
|
+
input.setAttribute('aria-expanded', 'false');
|
|
106
|
+
input.setAttribute('autocomplete', 'off');
|
|
107
|
+
// Hide the empty-state at rest: it must only appear once a filter yields no
|
|
108
|
+
// matches, never on an idle combobox. Without this an author who omits
|
|
109
|
+
// `hidden` on `.ui-combobox__empty` ships a box that reads "No matches"
|
|
110
|
+
// before the user has typed anything. Make it a status live region so its
|
|
111
|
+
// appearance is announced.
|
|
112
|
+
prepareEmptyState(empty);
|
|
113
|
+
close();
|
|
114
|
+
input.addEventListener('input', onInput);
|
|
115
|
+
input.addEventListener('keydown', onKey);
|
|
116
|
+
list.addEventListener('click', onOptionClick);
|
|
117
|
+
document.addEventListener('click', onDocClick);
|
|
118
|
+
// Opt-in: keep options in sync with a list mutated after init (async /
|
|
119
|
+
// remote results). Off by default so the common static case stays free.
|
|
120
|
+
const observer = liveOptionObserver(box, list, relist);
|
|
121
|
+
return () => {
|
|
122
|
+
observer?.disconnect();
|
|
123
|
+
input.removeEventListener('input', onInput);
|
|
124
|
+
input.removeEventListener('keydown', onKey);
|
|
125
|
+
list.removeEventListener('click', onOptionClick);
|
|
126
|
+
document.removeEventListener('click', onDocClick);
|
|
127
|
+
restoreState(state);
|
|
128
|
+
resetActive();
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
12
132
|
/**
|
|
13
133
|
* Editable combobox with a filtered listbox popup, implementing the
|
|
14
134
|
* WAI-ARIA APG combobox pattern (the widget the framework most lacked
|
|
@@ -59,59 +179,69 @@ export function initCombobox({ root } = {}) {
|
|
|
59
179
|
const list = box.querySelector('[role="listbox"], .ui-combobox__list');
|
|
60
180
|
if (!input || !list) continue;
|
|
61
181
|
const empty = box.querySelector('.ui-combobox__empty');
|
|
62
|
-
const
|
|
182
|
+
const optionStates = new WeakMap();
|
|
183
|
+
let listId = '';
|
|
184
|
+
|
|
185
|
+
const rememberOptionState = (option) => {
|
|
186
|
+
if (optionStates.has(option)) return;
|
|
187
|
+
optionStates.set(option, {
|
|
188
|
+
hidden: option.hidden,
|
|
189
|
+
active: option.classList.contains('is-active'),
|
|
190
|
+
attrs: snapshotAttrs(option, ['id', 'role', 'aria-selected']),
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const rememberState = () => ({
|
|
195
|
+
input: snapshotAttrs(input, [
|
|
196
|
+
'role',
|
|
197
|
+
'aria-controls',
|
|
198
|
+
'aria-autocomplete',
|
|
199
|
+
'aria-expanded',
|
|
200
|
+
'aria-activedescendant',
|
|
201
|
+
'autocomplete',
|
|
202
|
+
]),
|
|
203
|
+
list: {
|
|
204
|
+
hidden: list.hidden,
|
|
205
|
+
attrs: snapshotAttrs(list, ['id', 'role', 'aria-label']),
|
|
206
|
+
},
|
|
207
|
+
empty: empty
|
|
208
|
+
? {
|
|
209
|
+
hidden: empty.hidden,
|
|
210
|
+
attrs: snapshotAttrs(empty, ['role']),
|
|
211
|
+
}
|
|
212
|
+
: null,
|
|
213
|
+
options: optionStates,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const restoreState = (state) => {
|
|
217
|
+
restoreAttrs(input, state.input);
|
|
218
|
+
list.hidden = state.list.hidden;
|
|
219
|
+
restoreAttrs(list, state.list.attrs);
|
|
220
|
+
if (empty && state.empty) {
|
|
221
|
+
empty.hidden = state.empty.hidden;
|
|
222
|
+
restoreAttrs(empty, state.empty.attrs);
|
|
223
|
+
}
|
|
224
|
+
for (const option of options) {
|
|
225
|
+
const optionState = state.options.get(option);
|
|
226
|
+
if (!optionState) continue;
|
|
227
|
+
option.hidden = optionState.hidden;
|
|
228
|
+
option.classList.toggle('is-active', optionState.active);
|
|
229
|
+
restoreAttrs(option, optionState.attrs);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
63
233
|
// Re-readable so the opt-in MutationObserver (`data-bronto-combobox-live`)
|
|
64
234
|
// can pick up async/replaced option nodes without a full re-init. `visible`,
|
|
65
235
|
// `filter`, `move`, etc. close over this binding, so reassigning it is enough.
|
|
66
236
|
let options = [];
|
|
67
237
|
const syncOptions = () => {
|
|
68
|
-
options = [...list.querySelectorAll(
|
|
238
|
+
options = [...list.querySelectorAll(COMBOBOX_OPTION_SELECTOR)];
|
|
69
239
|
options.forEach((o, i) => {
|
|
240
|
+
rememberOptionState(o);
|
|
70
241
|
if (!o.id) o.id = `${listId}-opt-${i}`;
|
|
71
242
|
o.setAttribute('role', 'option');
|
|
72
243
|
});
|
|
73
244
|
};
|
|
74
|
-
syncOptions();
|
|
75
|
-
list.setAttribute('role', 'listbox');
|
|
76
|
-
// Give the listbox its own accessible name (a bare role=listbox is unnamed
|
|
77
|
-
// to a screen reader) by mirroring the input's REAL name. (a11y review C30.)
|
|
78
|
-
// The placeholder is deliberately NOT in this chain: the input warning below
|
|
79
|
-
// already rejects a placeholder as an inadequate name, so papering the
|
|
80
|
-
// listbox over with it would contradict that — if there's no real name the
|
|
81
|
-
// listbox stays unnamed and the warning is the signal. (component audit C28.)
|
|
82
|
-
if (!list.hasAttribute('aria-label') && !list.hasAttribute('aria-labelledby')) {
|
|
83
|
-
const name = input.getAttribute('aria-label') || input.labels?.[0]?.textContent?.trim();
|
|
84
|
-
if (name) list.setAttribute('aria-label', name);
|
|
85
|
-
}
|
|
86
|
-
// A `role="combobox"` with no accessible name is a silent AT failure. A
|
|
87
|
-
// placeholder is not a robust name (it can vanish and is ignored by some
|
|
88
|
-
// AT), so warn unless there is a real label/aria-label/aria-labelledby/title
|
|
89
|
-
// (C7). We can't invent a good name, hence a dev-time warning, not a guess.
|
|
90
|
-
const inputNamed =
|
|
91
|
-
input.hasAttribute('aria-label') ||
|
|
92
|
-
input.hasAttribute('aria-labelledby') ||
|
|
93
|
-
!!input.labels?.length ||
|
|
94
|
-
input.hasAttribute('title');
|
|
95
|
-
if (!inputNamed && typeof console !== 'undefined') {
|
|
96
|
-
console.warn(
|
|
97
|
-
'[bronto] initCombobox(): the combobox input has no accessible name — add a <label>, aria-label, or aria-labelledby (a placeholder is not enough).',
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
input.setAttribute('role', 'combobox');
|
|
101
|
-
input.setAttribute('aria-controls', listId);
|
|
102
|
-
input.setAttribute('aria-autocomplete', 'list');
|
|
103
|
-
input.setAttribute('aria-expanded', 'false');
|
|
104
|
-
input.setAttribute('autocomplete', 'off');
|
|
105
|
-
list.hidden = true;
|
|
106
|
-
// Hide the empty-state at rest: it must only appear once a filter yields no
|
|
107
|
-
// matches, never on an idle combobox. Without this an author who omits
|
|
108
|
-
// `hidden` on `.ui-combobox__empty` ships a box that reads "No matches"
|
|
109
|
-
// before the user has typed anything. (component audit C10.) Make it a
|
|
110
|
-
// status live region so its appearance is announced. (component audit C38.)
|
|
111
|
-
if (empty) {
|
|
112
|
-
empty.hidden = true;
|
|
113
|
-
if (!empty.hasAttribute('role')) empty.setAttribute('role', 'status');
|
|
114
|
-
}
|
|
115
245
|
|
|
116
246
|
let active = -1;
|
|
117
247
|
const visible = () => options.filter((o) => !o.hidden);
|
|
@@ -161,7 +291,6 @@ export function initCombobox({ root } = {}) {
|
|
|
161
291
|
// Show the human LABEL in the input; emit the `data-value` CODE in the
|
|
162
292
|
// event. The natural pattern is code in `data-value`, label in the text —
|
|
163
293
|
// putting the code in the visible input silently shows the user a raw code.
|
|
164
|
-
// (component audit C10.)
|
|
165
294
|
const label = opt.textContent.trim();
|
|
166
295
|
const value = opt.dataset.value ?? label;
|
|
167
296
|
input.value = label;
|
|
@@ -214,62 +343,61 @@ export function initCombobox({ root } = {}) {
|
|
|
214
343
|
|
|
215
344
|
const onInput = () => filter();
|
|
216
345
|
const onKey = (e) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
case 'Tab':
|
|
239
|
-
close();
|
|
240
|
-
break;
|
|
241
|
-
default:
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
346
|
+
const handled = keyHandlers[e.key]?.();
|
|
347
|
+
if (handled) e.preventDefault();
|
|
348
|
+
};
|
|
349
|
+
const keyHandlers = {
|
|
350
|
+
ArrowDown: () => {
|
|
351
|
+
if (list.hidden) filter();
|
|
352
|
+
else move(1);
|
|
353
|
+
return true;
|
|
354
|
+
},
|
|
355
|
+
ArrowUp: () => {
|
|
356
|
+
move(-1);
|
|
357
|
+
return true;
|
|
358
|
+
},
|
|
359
|
+
Home: () => activateEdge('first'),
|
|
360
|
+
End: () => activateEdge('last'),
|
|
361
|
+
Enter: () => selectActive(),
|
|
362
|
+
Escape: () => closeIfOpen(),
|
|
363
|
+
Tab: () => {
|
|
364
|
+
close();
|
|
365
|
+
return false;
|
|
366
|
+
},
|
|
244
367
|
};
|
|
245
368
|
const onOptionClick = (e) => {
|
|
246
|
-
const opt = e.target
|
|
369
|
+
const opt = closestSafe(e.target, COMBOBOX_OPTION_SELECTOR);
|
|
247
370
|
if (opt) select(opt);
|
|
248
371
|
};
|
|
249
372
|
const onDocClick = (e) => {
|
|
250
373
|
if (!box.contains(e.target)) close();
|
|
251
374
|
};
|
|
375
|
+
const assignListId = () => {
|
|
376
|
+
listId = list.id || (list.id = `bronto-cb-list-${nextFieldUid()}`);
|
|
377
|
+
return listId;
|
|
378
|
+
};
|
|
252
379
|
|
|
253
|
-
const bound = bindOnce(box, 'combobox', () =>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
380
|
+
const bound = bindOnce(box, 'combobox', () =>
|
|
381
|
+
bindComboboxLifecycle({
|
|
382
|
+
box,
|
|
383
|
+
input,
|
|
384
|
+
list,
|
|
385
|
+
empty,
|
|
386
|
+
rememberState,
|
|
387
|
+
restoreState,
|
|
388
|
+
assignListId,
|
|
389
|
+
syncOptions,
|
|
390
|
+
close,
|
|
391
|
+
relist,
|
|
392
|
+
onInput,
|
|
393
|
+
onKey,
|
|
394
|
+
onOptionClick,
|
|
395
|
+
onDocClick,
|
|
396
|
+
resetActive: () => {
|
|
397
|
+
active = -1;
|
|
398
|
+
},
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
273
401
|
cleanups.push(bound);
|
|
274
402
|
}
|
|
275
403
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["command.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["command.js"],"names":[],"mappings":"AAYA;;;;GAIG;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA0N3C;;;;;WApPa,MAAM;;;;WACN,MAAM"}
|
package/behaviors/command.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
collectHosts,
|
|
8
8
|
scrollIntoViewSafe,
|
|
9
9
|
wrapIndex,
|
|
10
|
+
closestSafe,
|
|
10
11
|
} from './internal.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -46,6 +47,24 @@ export function initCommand({ root } = {}) {
|
|
|
46
47
|
const palettes = collectHosts(host, '[data-bronto-command]');
|
|
47
48
|
const cleanups = [];
|
|
48
49
|
|
|
50
|
+
const snapshotAttrs = (el, names) => {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
out[name] = {
|
|
54
|
+
had: el.hasAttribute(name),
|
|
55
|
+
value: el.getAttribute(name),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const restoreAttrs = (el, attrs) => {
|
|
62
|
+
for (const [name, attr] of Object.entries(attrs)) {
|
|
63
|
+
if (attr.had) el.setAttribute(name, attr.value);
|
|
64
|
+
else el.removeAttribute(name);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
49
68
|
for (const box of palettes) {
|
|
50
69
|
const input = box.querySelector('.ui-command__input, input');
|
|
51
70
|
const list = box.querySelector('.ui-command__list, [role="listbox"]');
|
|
@@ -54,18 +73,44 @@ export function initCommand({ root } = {}) {
|
|
|
54
73
|
const items = [...list.querySelectorAll('.ui-command__item, [role="option"]')];
|
|
55
74
|
const groups = [...list.querySelectorAll('.ui-command__group')];
|
|
56
75
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
76
|
+
const rememberState = () => ({
|
|
77
|
+
input: snapshotAttrs(input, [
|
|
78
|
+
'role',
|
|
79
|
+
'aria-controls',
|
|
80
|
+
'aria-autocomplete',
|
|
81
|
+
'aria-expanded',
|
|
82
|
+
'aria-activedescendant',
|
|
83
|
+
'autocomplete',
|
|
84
|
+
]),
|
|
85
|
+
list: snapshotAttrs(list, ['id', 'role']),
|
|
86
|
+
empty: empty ? { hidden: empty.hidden } : null,
|
|
87
|
+
groups: groups.map((g) => ({
|
|
88
|
+
el: g,
|
|
89
|
+
hidden: g.hidden,
|
|
90
|
+
attrs: snapshotAttrs(g, ['role']),
|
|
91
|
+
})),
|
|
92
|
+
items: items.map((it) => ({
|
|
93
|
+
el: it,
|
|
94
|
+
hidden: it.hidden,
|
|
95
|
+
active: it.classList.contains('is-active'),
|
|
96
|
+
attrs: snapshotAttrs(it, ['id', 'role', 'aria-selected']),
|
|
97
|
+
})),
|
|
61
98
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
|
|
100
|
+
const restoreState = (state) => {
|
|
101
|
+
restoreAttrs(input, state.input);
|
|
102
|
+
restoreAttrs(list, state.list);
|
|
103
|
+
if (empty && state.empty) empty.hidden = state.empty.hidden;
|
|
104
|
+
for (const group of state.groups) {
|
|
105
|
+
group.el.hidden = group.hidden;
|
|
106
|
+
restoreAttrs(group.el, group.attrs);
|
|
107
|
+
}
|
|
108
|
+
for (const item of state.items) {
|
|
109
|
+
item.el.hidden = item.hidden;
|
|
110
|
+
item.el.classList.toggle('is-active', item.active);
|
|
111
|
+
restoreAttrs(item.el, item.attrs);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
69
114
|
|
|
70
115
|
let active = -1;
|
|
71
116
|
const visible = () => items.filter((it) => !it.hidden);
|
|
@@ -176,22 +221,37 @@ export function initCommand({ root } = {}) {
|
|
|
176
221
|
}
|
|
177
222
|
};
|
|
178
223
|
const onClick = (e) => {
|
|
179
|
-
const item = e.target
|
|
224
|
+
const item = closestSafe(e.target, '.ui-command__item, [role="option"]');
|
|
180
225
|
if (item && list.contains(item)) choose(item);
|
|
181
226
|
};
|
|
182
227
|
|
|
183
228
|
const bound = bindOnce(box, 'command', () => {
|
|
229
|
+
const state = rememberState();
|
|
230
|
+
const listId = list.id || (list.id = `bronto-cmd-${nextFieldUid()}`);
|
|
231
|
+
items.forEach((it, i) => {
|
|
232
|
+
if (!it.id) it.id = `${listId}-opt-${i}`;
|
|
233
|
+
it.setAttribute('role', 'option');
|
|
234
|
+
});
|
|
235
|
+
groups.forEach((g) => g.setAttribute('role', 'presentation'));
|
|
236
|
+
list.setAttribute('role', 'listbox');
|
|
237
|
+
input.setAttribute('role', 'combobox');
|
|
238
|
+
input.setAttribute('aria-controls', listId);
|
|
239
|
+
input.setAttribute('aria-autocomplete', 'list');
|
|
240
|
+
input.setAttribute('aria-expanded', 'true');
|
|
241
|
+
input.setAttribute('autocomplete', 'off');
|
|
184
242
|
input.addEventListener('input', onInput);
|
|
185
243
|
input.addEventListener('keydown', onKey);
|
|
186
244
|
list.addEventListener('click', onClick);
|
|
245
|
+
// Seed the initial active item (first visible).
|
|
246
|
+
filter();
|
|
187
247
|
return () => {
|
|
188
248
|
input.removeEventListener('input', onInput);
|
|
189
249
|
input.removeEventListener('keydown', onKey);
|
|
190
250
|
list.removeEventListener('click', onClick);
|
|
251
|
+
restoreState(state);
|
|
252
|
+
active = -1;
|
|
191
253
|
};
|
|
192
254
|
});
|
|
193
|
-
// Seed the initial active item (first visible).
|
|
194
|
-
filter();
|
|
195
255
|
cleanups.push(bound);
|
|
196
256
|
}
|
|
197
257
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectors.d.ts","sourceRoot":"","sources":["connectors.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"connectors.d.ts","sourceRoot":"","sources":["connectors.js"],"names":[],"mappings":"AA+EA;;;;;;;;;;;;;;GAcG;AACH,0CAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAwG3C"}
|