ds-tis 1.0.0-beta.10
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/LICENSE +49 -0
- package/README.md +125 -0
- package/css/base/forced-colors.css +62 -0
- package/css/base/icons.css +73 -0
- package/css/base/index.css +4 -0
- package/css/base/reset.css +80 -0
- package/css/base/typography.css +211 -0
- package/css/components/accordion.css +153 -0
- package/css/components/alert.css +161 -0
- package/css/components/avatar.css +76 -0
- package/css/components/badge.css +83 -0
- package/css/components/breadcrumb.css +58 -0
- package/css/components/button.css +364 -0
- package/css/components/card.css +159 -0
- package/css/components/checkbox.css +296 -0
- package/css/components/combobox.css +330 -0
- package/css/components/divider.css +20 -0
- package/css/components/form-field.css +137 -0
- package/css/components/index.css +28 -0
- package/css/components/input.css +356 -0
- package/css/components/link.css +67 -0
- package/css/components/menu.css +246 -0
- package/css/components/modal.css +236 -0
- package/css/components/pagination.css +132 -0
- package/css/components/radio.css +280 -0
- package/css/components/select.css +399 -0
- package/css/components/skeleton.css +52 -0
- package/css/components/spinner.css +59 -0
- package/css/components/tabs.css +79 -0
- package/css/components/textarea.css +218 -0
- package/css/components/toggle.css +202 -0
- package/css/components/tooltip.css +116 -0
- package/css/design-system.css +13 -0
- package/css/tokens/generated/component.css +720 -0
- package/css/tokens/generated/foundation.css +282 -0
- package/css/tokens/generated/index.css +5 -0
- package/css/tokens/generated/theme-dark.css +243 -0
- package/css/tokens/generated/theme-light.css +244 -0
- package/css/tokens/index.css +6 -0
- package/css/utilities/elevation.css +24 -0
- package/css/utilities/index.css +2 -0
- package/css/utilities/layout.css +132 -0
- package/docs/agent-consumer-usage.en.md +322 -0
- package/docs/agent-consumer-usage.md +294 -0
- package/docs/api/adrs.json +186 -0
- package/docs/api/components.json +2071 -0
- package/docs/api/consumer-context.json +66 -0
- package/docs/api/foundations.json +94 -0
- package/docs/api/tokens.json +6839 -0
- package/docs/llms-full.txt +23699 -0
- package/docs/llms.txt +109 -0
- package/docs/templates/contact.html +364 -0
- package/docs/templates/dashboard.html +318 -0
- package/docs/templates/index.html +232 -0
- package/docs/templates/login.html +286 -0
- package/docs/templates/settings.html +365 -0
- package/docs/templates/signup.html +350 -0
- package/js/accordion.js +192 -0
- package/js/combobox.js +263 -0
- package/js/menu.js +301 -0
- package/js/modal.js +256 -0
- package/js/package.json +3 -0
- package/js/tabs.js +200 -0
- package/js/theme/apply.js +75 -0
- package/js/theme/brand-contrast-audit.js +133 -0
- package/js/theme/color.js +149 -0
- package/js/theme/config-schema.js +40 -0
- package/js/theme/contrast.js +76 -0
- package/js/theme/export.js +118 -0
- package/js/theme/index.js +21 -0
- package/js/theme/overlay.js +29 -0
- package/js/theme/package.json +3 -0
- package/js/theme/palette.js +103 -0
- package/js/theme/radius.js +76 -0
- package/js/theme/semantic-mapper.js +138 -0
- package/js/theme/typography.js +33 -0
- package/js/theme/url-state.js +52 -0
- package/js/tooltip.js +227 -0
- package/package.json +139 -0
package/js/combobox.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
combobox.js — runtime público para Combobox (required)
|
|
3
|
+
|
|
4
|
+
O componente em css/components/combobox.css define anatomia e
|
|
5
|
+
estados visuais; este módulo implementa abertura/fechamento do
|
|
6
|
+
listbox, filtro, seleção e teclado. Use com a estrutura:
|
|
7
|
+
|
|
8
|
+
<div class="ds-field">
|
|
9
|
+
<label class="ds-field__label" for="id">…</label>
|
|
10
|
+
<div class="ds-combobox-anchor">
|
|
11
|
+
<div class="ds-combobox ds-combobox--md">…</div>
|
|
12
|
+
<ul class="ds-combobox__listbox" hidden>…</ul>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
Ciclo de vida:
|
|
17
|
+
const instances = initComboboxes(root);
|
|
18
|
+
destroyComboboxes(root); // ou instance.destroy()
|
|
19
|
+
============================================================ */
|
|
20
|
+
|
|
21
|
+
const instances = new Set();
|
|
22
|
+
let generatedId = 0;
|
|
23
|
+
|
|
24
|
+
function closeAllExcept(current) {
|
|
25
|
+
for (const inst of instances) {
|
|
26
|
+
if (inst !== current) inst.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getOptions(listbox) {
|
|
31
|
+
return Array.from(listbox.querySelectorAll('.ds-combobox__option'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureId(element, prefix) {
|
|
35
|
+
if (!element.id) {
|
|
36
|
+
generatedId += 1;
|
|
37
|
+
element.id = `${prefix}-${generatedId}`;
|
|
38
|
+
}
|
|
39
|
+
return element.id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sincroniza valor, estado filled e aria-selected das opções.
|
|
44
|
+
* @param {HTMLElement} root - .ds-combobox-anchor ou .ds-field legado
|
|
45
|
+
* @param {string} [value] - se omitido, lê do input
|
|
46
|
+
*/
|
|
47
|
+
export function syncComboboxState(root, value) {
|
|
48
|
+
const input = root.querySelector('.ds-combobox__input');
|
|
49
|
+
const combobox = root.querySelector('.ds-combobox');
|
|
50
|
+
const listbox = root.querySelector('.ds-combobox__listbox');
|
|
51
|
+
if (!input || !combobox || !listbox) return;
|
|
52
|
+
|
|
53
|
+
const current = value != null ? value : input.value.trim();
|
|
54
|
+
combobox.classList.toggle('ds-combobox--filled', current.length > 0);
|
|
55
|
+
for (const opt of getOptions(listbox)) {
|
|
56
|
+
opt.setAttribute('aria-selected', opt.textContent.trim() === current ? 'true' : 'false');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createInstance(root, { onChange } = {}) {
|
|
61
|
+
const combobox = root.querySelector('.ds-combobox');
|
|
62
|
+
const input = root.querySelector('.ds-combobox__input');
|
|
63
|
+
const listbox = root.querySelector('.ds-combobox__listbox');
|
|
64
|
+
if (!combobox || !input || !listbox) return null;
|
|
65
|
+
|
|
66
|
+
const listboxId = ensureId(listbox, 'ds-combobox-listbox');
|
|
67
|
+
input.setAttribute('aria-controls', listboxId);
|
|
68
|
+
for (const opt of getOptions(listbox)) ensureId(opt, `${listboxId}-option`);
|
|
69
|
+
|
|
70
|
+
const cleanups = [];
|
|
71
|
+
const on = (target, type, handler, options) => {
|
|
72
|
+
target.addEventListener(type, handler, options);
|
|
73
|
+
cleanups.push(() => target.removeEventListener(type, handler, options));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const inst = {
|
|
77
|
+
root,
|
|
78
|
+
close() {
|
|
79
|
+
listbox.hidden = true;
|
|
80
|
+
input.setAttribute('aria-expanded', 'false');
|
|
81
|
+
input.removeAttribute('aria-activedescendant');
|
|
82
|
+
combobox.classList.remove('ds-combobox--open');
|
|
83
|
+
for (const opt of getOptions(listbox)) opt.removeAttribute('data-active');
|
|
84
|
+
},
|
|
85
|
+
open() {
|
|
86
|
+
closeAllExcept(inst);
|
|
87
|
+
listbox.hidden = false;
|
|
88
|
+
input.setAttribute('aria-expanded', 'true');
|
|
89
|
+
combobox.classList.add('ds-combobox--open');
|
|
90
|
+
filterOptions();
|
|
91
|
+
},
|
|
92
|
+
destroy() {
|
|
93
|
+
inst.close();
|
|
94
|
+
while (cleanups.length) cleanups.pop()();
|
|
95
|
+
delete root.dataset.dsComboboxInit;
|
|
96
|
+
instances.delete(inst);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function filterOptions() {
|
|
101
|
+
const query = input.value.trim().toLowerCase();
|
|
102
|
+
for (const opt of getOptions(listbox)) {
|
|
103
|
+
const label = opt.textContent.trim().toLowerCase();
|
|
104
|
+
opt.hidden = query.length > 0 && !label.includes(query);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function setActiveOption(option) {
|
|
109
|
+
for (const opt of getOptions(listbox)) opt.removeAttribute('data-active');
|
|
110
|
+
if (!option) {
|
|
111
|
+
input.removeAttribute('aria-activedescendant');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
option.setAttribute('data-active', 'true');
|
|
115
|
+
input.setAttribute('aria-activedescendant', ensureId(option, `${listboxId}-option`));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function emitChange(option = null) {
|
|
119
|
+
syncComboboxState(root);
|
|
120
|
+
if (typeof onChange === 'function') onChange(input, root);
|
|
121
|
+
input.dispatchEvent(new CustomEvent('ds-combobox-change', {
|
|
122
|
+
bubbles: true,
|
|
123
|
+
detail: {
|
|
124
|
+
input,
|
|
125
|
+
root,
|
|
126
|
+
option,
|
|
127
|
+
value: input.value,
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function selectOption(opt) {
|
|
133
|
+
input.value = opt.textContent.trim();
|
|
134
|
+
inst.close();
|
|
135
|
+
emitChange(opt);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function visibleOptions() {
|
|
139
|
+
return getOptions(listbox).filter((opt) => !opt.hidden);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
on(input, 'focus', () => inst.open());
|
|
143
|
+
|
|
144
|
+
on(input, 'input', () => {
|
|
145
|
+
inst.open();
|
|
146
|
+
emitChange();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
on(input, 'change', () => emitChange());
|
|
150
|
+
|
|
151
|
+
on(input, 'blur', () => {
|
|
152
|
+
requestAnimationFrame(() => {
|
|
153
|
+
if (!instances.has(inst)) return;
|
|
154
|
+
if (!root.contains(document.activeElement)) inst.close();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
on(input, 'keydown', (e) => {
|
|
159
|
+
if (e.key === 'Escape') {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
inst.close();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (e.key === 'ArrowDown') {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
inst.open();
|
|
167
|
+
const visible = visibleOptions();
|
|
168
|
+
const activeIndex = visible.findIndex((opt) => opt.getAttribute('data-active') === 'true');
|
|
169
|
+
const next = visible[activeIndex < visible.length - 1 ? activeIndex + 1 : 0];
|
|
170
|
+
setActiveOption(next);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (e.key === 'ArrowUp') {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
inst.open();
|
|
176
|
+
const visible = visibleOptions();
|
|
177
|
+
const activeIndex = visible.findIndex((opt) => opt.getAttribute('data-active') === 'true');
|
|
178
|
+
const prev = visible[activeIndex > 0 ? activeIndex - 1 : visible.length - 1];
|
|
179
|
+
setActiveOption(prev);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (e.key === 'Enter') {
|
|
183
|
+
const visible = visibleOptions();
|
|
184
|
+
const active = visible.find((opt) => opt.getAttribute('data-active') === 'true');
|
|
185
|
+
if (active) {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
selectOption(active);
|
|
188
|
+
} else {
|
|
189
|
+
inst.close();
|
|
190
|
+
emitChange();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
on(listbox, 'mousedown', (e) => {
|
|
196
|
+
const opt = e.target.closest('.ds-combobox__option');
|
|
197
|
+
if (opt && listbox.contains(opt)) {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
selectOption(opt);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
on(document, 'click', (e) => {
|
|
204
|
+
if (!root.contains(e.target)) inst.close();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
syncComboboxState(root);
|
|
208
|
+
return inst;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isInside(root, node) {
|
|
212
|
+
return root === document || root === node || (typeof root.contains === 'function' && root.contains(node));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Inicializa comboboxes dentro de `root`.
|
|
217
|
+
* @param {ParentNode} [root]
|
|
218
|
+
* @param {{ onChange?: (input: HTMLInputElement, root: HTMLElement) => void }} [opts]
|
|
219
|
+
*/
|
|
220
|
+
export function initComboboxes(root = document, opts = {}) {
|
|
221
|
+
const created = [];
|
|
222
|
+
|
|
223
|
+
const anchors = [
|
|
224
|
+
...(root instanceof Element && root.matches('.ds-combobox-anchor') ? [root] : []),
|
|
225
|
+
...root.querySelectorAll('.ds-combobox-anchor'),
|
|
226
|
+
];
|
|
227
|
+
anchors.forEach((anchor) => {
|
|
228
|
+
if (anchor.dataset.dsComboboxInit === 'true') return;
|
|
229
|
+
const inst = createInstance(anchor, opts);
|
|
230
|
+
if (inst) {
|
|
231
|
+
anchor.dataset.dsComboboxInit = 'true';
|
|
232
|
+
instances.add(inst);
|
|
233
|
+
created.push(inst);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const fields = [
|
|
238
|
+
...(root instanceof Element && root.matches('.ds-field') ? [root] : []),
|
|
239
|
+
...root.querySelectorAll('.ds-field'),
|
|
240
|
+
];
|
|
241
|
+
fields.forEach((field) => {
|
|
242
|
+
if (field.querySelector('.ds-combobox-anchor')) return;
|
|
243
|
+
if (field.dataset.dsComboboxInit === 'true') return;
|
|
244
|
+
if (!field.querySelector(':scope > .ds-combobox__listbox')) return;
|
|
245
|
+
const inst = createInstance(field, opts);
|
|
246
|
+
if (inst) {
|
|
247
|
+
field.dataset.dsComboboxInit = 'true';
|
|
248
|
+
instances.add(inst);
|
|
249
|
+
created.push(inst);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return created;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {ParentNode} [root]
|
|
258
|
+
*/
|
|
259
|
+
export function destroyComboboxes(root = document) {
|
|
260
|
+
for (const inst of [...instances]) {
|
|
261
|
+
if (isInside(root, inst.root)) inst.destroy();
|
|
262
|
+
}
|
|
263
|
+
}
|
package/js/menu.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
menu.js — runtime público para Action Menu (required)
|
|
3
|
+
|
|
4
|
+
Anatomia e estados visuais: css/components/menu.css
|
|
5
|
+
Uso:
|
|
6
|
+
<div class="ds-action-menu">
|
|
7
|
+
<button class="ds-button ds-action-menu__trigger"
|
|
8
|
+
type="button"
|
|
9
|
+
aria-haspopup="menu"
|
|
10
|
+
aria-expanded="false"
|
|
11
|
+
aria-controls="menu-id">...</button>
|
|
12
|
+
<div class="ds-menu ds-action-menu__content" id="menu-id" role="menu">...</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
Ciclo de vida:
|
|
16
|
+
const instances = initActionMenus(root);
|
|
17
|
+
destroyActionMenus(root); // ou instance.destroy()
|
|
18
|
+
============================================================ */
|
|
19
|
+
|
|
20
|
+
const instances = new Set();
|
|
21
|
+
const MENU_ITEM_SELECTOR = [
|
|
22
|
+
'.ds-menu__item[role="menuitem"]',
|
|
23
|
+
'.ds-menu__item[role="menuitemradio"]',
|
|
24
|
+
'.ds-menu__item[role="menuitemcheckbox"]',
|
|
25
|
+
].join(', ');
|
|
26
|
+
|
|
27
|
+
function getMenuItems(menu) {
|
|
28
|
+
return [...menu.querySelectorAll(MENU_ITEM_SELECTOR)].filter((item) => !item.disabled);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isDisabled(item) {
|
|
32
|
+
return item.disabled || item.getAttribute('aria-disabled') === 'true';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function focusItem(items, index) {
|
|
36
|
+
const item = items[index];
|
|
37
|
+
if (!item) return;
|
|
38
|
+
items.forEach((node) => node.removeAttribute('data-active'));
|
|
39
|
+
item.setAttribute('data-active', 'true');
|
|
40
|
+
item.focus();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function emit(root, name, detail) {
|
|
44
|
+
root.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createInstance(root) {
|
|
48
|
+
const trigger = root.querySelector('.ds-action-menu__trigger');
|
|
49
|
+
const menu = root.querySelector('.ds-action-menu__content[role="menu"], .ds-menu[role="menu"]');
|
|
50
|
+
if (!trigger || !menu) return null;
|
|
51
|
+
|
|
52
|
+
const cleanups = [];
|
|
53
|
+
let typeaheadBuffer = '';
|
|
54
|
+
let typeaheadTimer = null;
|
|
55
|
+
let focusFrame = null;
|
|
56
|
+
const on = (target, type, handler, options) => {
|
|
57
|
+
target.addEventListener(type, handler, options);
|
|
58
|
+
cleanups.push(() => target.removeEventListener(type, handler, options));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const inst = {
|
|
62
|
+
root,
|
|
63
|
+
trigger,
|
|
64
|
+
menu,
|
|
65
|
+
open() {
|
|
66
|
+
if (root.dataset.open === 'true') return;
|
|
67
|
+
root.dataset.open = 'true';
|
|
68
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
69
|
+
const items = getMenuItems(menu);
|
|
70
|
+
const firstItem = items[0] || null;
|
|
71
|
+
if (firstItem) {
|
|
72
|
+
focusItem(items, 0);
|
|
73
|
+
focusFirstWhenVisible(firstItem);
|
|
74
|
+
}
|
|
75
|
+
emit(root, 'ds-menu-open', { root, trigger, menu, item: firstItem });
|
|
76
|
+
},
|
|
77
|
+
close() {
|
|
78
|
+
if (root.dataset.open !== 'true') return;
|
|
79
|
+
delete root.dataset.open;
|
|
80
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
81
|
+
cancelPendingFocus();
|
|
82
|
+
clearTypeahead();
|
|
83
|
+
getMenuItems(menu).forEach((item) => item.removeAttribute('data-active'));
|
|
84
|
+
trigger.focus();
|
|
85
|
+
emit(root, 'ds-menu-close', { root, trigger, menu });
|
|
86
|
+
},
|
|
87
|
+
toggle() {
|
|
88
|
+
if (root.dataset.open === 'true') inst.close();
|
|
89
|
+
else inst.open();
|
|
90
|
+
},
|
|
91
|
+
destroy() {
|
|
92
|
+
cancelPendingFocus();
|
|
93
|
+
clearTypeahead();
|
|
94
|
+
if (root.dataset.open === 'true') {
|
|
95
|
+
delete root.dataset.open;
|
|
96
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
97
|
+
getMenuItems(menu).forEach((item) => item.removeAttribute('data-active'));
|
|
98
|
+
}
|
|
99
|
+
while (cleanups.length) cleanups.pop()();
|
|
100
|
+
delete root.dataset.dsActionMenuInit;
|
|
101
|
+
instances.delete(inst);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function cancelPendingFocus() {
|
|
106
|
+
if (focusFrame !== null) cancelAnimationFrame(focusFrame);
|
|
107
|
+
focusFrame = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function focusFirstWhenVisible(firstItem) {
|
|
111
|
+
cancelPendingFocus();
|
|
112
|
+
let attempts = 0;
|
|
113
|
+
const attempt = () => {
|
|
114
|
+
focusFrame = null;
|
|
115
|
+
if (root.dataset.open !== 'true' || !instances.has(inst) || !firstItem.isConnected) return;
|
|
116
|
+
if (getComputedStyle(menu).visibility === 'visible') {
|
|
117
|
+
focusItem(getMenuItems(menu), 0);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
attempts += 1;
|
|
121
|
+
if (attempts >= 60) return;
|
|
122
|
+
focusFrame = requestAnimationFrame(attempt);
|
|
123
|
+
};
|
|
124
|
+
focusFrame = requestAnimationFrame(attempt);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clearTypeahead() {
|
|
128
|
+
if (typeaheadTimer !== null) clearTimeout(typeaheadTimer);
|
|
129
|
+
typeaheadTimer = null;
|
|
130
|
+
typeaheadBuffer = '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function scheduleTypeaheadReset() {
|
|
134
|
+
if (typeaheadTimer !== null) clearTimeout(typeaheadTimer);
|
|
135
|
+
typeaheadTimer = setTimeout(() => {
|
|
136
|
+
typeaheadTimer = null;
|
|
137
|
+
typeaheadBuffer = '';
|
|
138
|
+
}, 500);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function activateItem(item) {
|
|
142
|
+
if (isDisabled(item)) return false;
|
|
143
|
+
const role = item.getAttribute('role');
|
|
144
|
+
if (role === 'menuitemradio') {
|
|
145
|
+
for (const radio of menu.querySelectorAll('.ds-menu__item[role="menuitemradio"]')) {
|
|
146
|
+
radio.setAttribute('aria-checked', radio === item ? 'true' : 'false');
|
|
147
|
+
}
|
|
148
|
+
} else if (role === 'menuitemcheckbox') {
|
|
149
|
+
item.setAttribute('aria-checked', item.getAttribute('aria-checked') === 'true' ? 'false' : 'true');
|
|
150
|
+
}
|
|
151
|
+
inst.close();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
on(trigger, 'click', (e) => {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
inst.toggle();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
on(trigger, 'keydown', (e) => {
|
|
161
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
inst.open();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
on(menu, 'keydown', (e) => {
|
|
168
|
+
const items = getMenuItems(menu);
|
|
169
|
+
const activeIndex = items.findIndex((item) => item.getAttribute('data-active') === 'true');
|
|
170
|
+
|
|
171
|
+
if (e.key === 'Escape') {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
inst.close();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (e.key === 'ArrowDown') {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
const next = activeIndex < items.length - 1 ? activeIndex + 1 : 0;
|
|
179
|
+
focusItem(items, next);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (e.key === 'ArrowUp') {
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
const prev = activeIndex > 0 ? activeIndex - 1 : items.length - 1;
|
|
185
|
+
focusItem(items, prev);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (e.key === 'Home') {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
focusItem(items, 0);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (e.key === 'End') {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
focusItem(items, items.length - 1);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
199
|
+
typeaheadBuffer += e.key.toLowerCase();
|
|
200
|
+
let query = typeaheadBuffer;
|
|
201
|
+
let match = items.find((item, index) => {
|
|
202
|
+
if (index <= activeIndex) return false;
|
|
203
|
+
return item.textContent.trim().toLowerCase().startsWith(query);
|
|
204
|
+
}) || items.find((item) => item.textContent.trim().toLowerCase().startsWith(query));
|
|
205
|
+
if (!match && typeaheadBuffer.length > 1) {
|
|
206
|
+
typeaheadBuffer = e.key.toLowerCase();
|
|
207
|
+
query = typeaheadBuffer;
|
|
208
|
+
match = items.find((item) => item.textContent.trim().toLowerCase().startsWith(query));
|
|
209
|
+
}
|
|
210
|
+
if (match) {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
focusItem(items, items.indexOf(match));
|
|
213
|
+
}
|
|
214
|
+
scheduleTypeaheadReset();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
on(menu, 'click', (e) => {
|
|
219
|
+
const item = e.target.closest(MENU_ITEM_SELECTOR);
|
|
220
|
+
if (!item || !menu.contains(item) || !isDisabled(item)) return;
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
e.stopPropagation();
|
|
223
|
+
}, true);
|
|
224
|
+
|
|
225
|
+
on(menu, 'click', (e) => {
|
|
226
|
+
const item = e.target.closest(MENU_ITEM_SELECTOR);
|
|
227
|
+
if (item && menu.contains(item)) activateItem(item);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
on(document, 'click', (e) => {
|
|
231
|
+
if (!root.contains(e.target)) inst.close();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
on(document, 'keydown', (e) => {
|
|
235
|
+
if (root.dataset.open !== 'true') return;
|
|
236
|
+
if (e.key === 'Escape') {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
inst.close();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (root.dataset.open !== 'true') {
|
|
243
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return inst;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isInside(root, node) {
|
|
250
|
+
return root === document || root === node || (typeof root.contains === 'function' && root.contains(node));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {ParentNode} [root]
|
|
255
|
+
*/
|
|
256
|
+
export function initActionMenus(root = document) {
|
|
257
|
+
const created = [];
|
|
258
|
+
|
|
259
|
+
const actionMenus = [
|
|
260
|
+
...(root instanceof Element && root.matches('.ds-action-menu') ? [root] : []),
|
|
261
|
+
...root.querySelectorAll('.ds-action-menu'),
|
|
262
|
+
];
|
|
263
|
+
actionMenus.forEach((actionMenu) => {
|
|
264
|
+
if (actionMenu.dataset.dsActionMenuInit === 'true') return;
|
|
265
|
+
const inst = createInstance(actionMenu);
|
|
266
|
+
if (inst) {
|
|
267
|
+
actionMenu.dataset.dsActionMenuInit = 'true';
|
|
268
|
+
instances.add(inst);
|
|
269
|
+
created.push(inst);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return created;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {ParentNode} [root]
|
|
278
|
+
*/
|
|
279
|
+
export function destroyActionMenus(root = document) {
|
|
280
|
+
for (const inst of [...instances]) {
|
|
281
|
+
if (isInside(root, inst.root)) inst.destroy();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @param {HTMLElement} root
|
|
287
|
+
*/
|
|
288
|
+
export function openActionMenu(root) {
|
|
289
|
+
const inst = [...instances].find((item) => item.root === root);
|
|
290
|
+
inst?.open();
|
|
291
|
+
return inst;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @param {HTMLElement} root
|
|
296
|
+
*/
|
|
297
|
+
export function closeActionMenu(root) {
|
|
298
|
+
const inst = [...instances].find((item) => item.root === root);
|
|
299
|
+
inst?.close();
|
|
300
|
+
return inst;
|
|
301
|
+
}
|