mtrl 0.9.5 → 0.9.6
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/dist/components/button/features/progress.js +2 -0
- package/dist/components/dialog/config.js +0 -1
- package/dist/components/dialog/features.js +5 -4
- package/dist/components/dialog/types.d.ts +3 -2
- package/dist/components/menu/config.js +2 -2
- package/dist/components/menu/features/controller.js +37 -23
- package/dist/components/menu/features/opener.js +4 -2
- package/dist/components/menu/types.d.ts +12 -1
- package/dist/components/select/features.js +244 -23
- package/dist/components/switch/switch.js +1 -1
- package/dist/components/tabs/tabs.js +10 -1
- package/dist/components/tabs/utils.d.ts +8 -0
- package/dist/components/tabs/utils.js +42 -17
- package/dist/components/textfield/features/error.d.ts +1 -0
- package/dist/components/textfield/features/error.js +10 -0
- package/dist/components/textfield/features/supporting-text.d.ts +1 -0
- package/dist/components/textfield/features/supporting-text.js +25 -0
- package/dist/components/tooltip/api.js +19 -0
- package/dist/core/compose/features/input.js +1 -1
- package/dist/core/compose/features/textlabel.js +3 -0
- package/dist/index.cjs +0 -0
- package/dist/package.json +1 -1
- package/dist/styles/badge.css +1 -1
- package/dist/styles/base.css +2 -2
- package/dist/styles/bottom-app-bar.css +1 -1
- package/dist/styles/bottom-sheet.css +2 -2
- package/dist/styles/button-group.css +1 -1
- package/dist/styles/button.css +2 -2
- package/dist/styles/card.css +2 -2
- package/dist/styles/carousel.css +1 -1
- package/dist/styles/checkbox.css +2 -2
- package/dist/styles/chips.css +2 -2
- package/dist/styles/dialog.css +2 -2
- package/dist/styles/divider.css +1 -1
- package/dist/styles/drawer.css +2 -2
- package/dist/styles/extended-fab.css +2 -2
- package/dist/styles/fab.css +2 -2
- package/dist/styles/icon-button.css +2 -2
- package/dist/styles/list.css +2 -2
- package/dist/styles/loading-indicator.css +1 -1
- package/dist/styles/menu.css +2 -2
- package/dist/styles/navigation-rail.css +1 -1
- package/dist/styles/navigation.css +2 -2
- package/dist/styles/progress.css +2 -2
- package/dist/styles/radios.css +1 -1
- package/dist/styles/search.css +1 -1
- package/dist/styles/segmented-button.css +2 -2
- package/dist/styles/select.css +2 -2
- package/dist/styles/side-sheet.css +2 -2
- package/dist/styles/slider.css +1 -1
- package/dist/styles/snackbar.css +1 -1
- package/dist/styles/split-button.css +2 -2
- package/dist/styles/switch.css +2 -2
- package/dist/styles/tabs.css +1 -1
- package/dist/styles/textfield.css +2 -2
- package/dist/styles/timepicker.css +1 -1
- package/dist/styles/tooltip.css +2 -2
- package/dist/styles/top-app-bar.css +1 -1
- package/dist/styles/utilities.css +1 -1
- package/dist/styles.css +2 -2
- package/dist/themes/autumn.css +1 -1
- package/dist/themes/baseline.css +1 -1
- package/dist/themes/brownbeige.css +1 -1
- package/dist/themes/browngreen.css +1 -1
- package/dist/themes/desert.css +1 -1
- package/dist/themes/forest.css +1 -1
- package/dist/themes/highcontrast.css +1 -1
- package/dist/themes/legacy.css +1 -1
- package/dist/themes/material.css +1 -1
- package/dist/themes/ocean.css +1 -1
- package/dist/themes/sageivory.css +1 -1
- package/dist/themes/spring.css +1 -1
- package/dist/themes/summer.css +1 -1
- package/dist/themes/sunset.css +1 -1
- package/dist/themes/tealcaramel.css +1 -1
- package/dist/themes/winter.css +1 -1
- package/package.json +1 -1
- package/src/styles/abstract/_mixins.scss +26 -0
- package/src/styles/abstract/_variables.scss +25 -11
- package/src/styles/components/_bottom-sheet.scss +23 -7
- package/src/styles/components/_dialog.scss +59 -26
- package/src/styles/components/_drawer.scss +60 -7
- package/src/styles/components/_menu.scss +12 -7
- package/src/styles/components/_side-sheet.scss +33 -4
- package/src/styles/components/_tooltip.scss +3 -0
|
@@ -120,6 +120,7 @@ export const withProgress = (config) => (component) => {
|
|
|
120
120
|
if (loading && !isLoading) {
|
|
121
121
|
originalText = readLabel() ?? "";
|
|
122
122
|
isLoading = true;
|
|
123
|
+
component.element.setAttribute("aria-busy", "true");
|
|
123
124
|
await component.showProgress();
|
|
124
125
|
if (component.disabled?.disable) {
|
|
125
126
|
component.disabled.disable();
|
|
@@ -129,6 +130,7 @@ export const withProgress = (config) => (component) => {
|
|
|
129
130
|
}
|
|
130
131
|
else if (!loading && isLoading) {
|
|
131
132
|
isLoading = false;
|
|
133
|
+
component.element.removeAttribute("aria-busy");
|
|
132
134
|
await component.hideProgress();
|
|
133
135
|
if (component.disabled?.enable) {
|
|
134
136
|
component.disabled.enable();
|
|
@@ -236,7 +236,8 @@ const addButton = (footer, buttonConfig, component) => {
|
|
|
236
236
|
};
|
|
237
237
|
export const withVisibility = () => (component) => {
|
|
238
238
|
const isOpen = component.config.open === true;
|
|
239
|
-
const
|
|
239
|
+
const openDuration = component.config.animationDuration ?? 500;
|
|
240
|
+
const closeDuration = component.config.animationDuration ?? 150;
|
|
240
241
|
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
241
242
|
let previouslyFocusedElement = null;
|
|
242
243
|
let mouseDownOnOverlay = false;
|
|
@@ -370,8 +371,8 @@ export const withVisibility = () => (component) => {
|
|
|
370
371
|
const container = component.structure.container || document.body;
|
|
371
372
|
container.appendChild(component.overlay);
|
|
372
373
|
}
|
|
373
|
-
addClass(component.overlay, `${component.getClass("dialog-overlay")}--visible`);
|
|
374
374
|
setTimeout(() => {
|
|
375
|
+
addClass(component.overlay, `${component.getClass("dialog-overlay")}--visible`);
|
|
375
376
|
addClass(component.element, `${component.getClass("dialog")}--visible`);
|
|
376
377
|
trapFocus();
|
|
377
378
|
setupEvents();
|
|
@@ -379,7 +380,7 @@ export const withVisibility = () => (component) => {
|
|
|
379
380
|
component.emit(DIALOG_EVENTS.OPEN, { dialog: component });
|
|
380
381
|
setTimeout(() => {
|
|
381
382
|
component.emit(DIALOG_EVENTS.AFTER_OPEN, { dialog: component });
|
|
382
|
-
},
|
|
383
|
+
}, openDuration);
|
|
383
384
|
}
|
|
384
385
|
}, 10);
|
|
385
386
|
},
|
|
@@ -414,7 +415,7 @@ export const withVisibility = () => (component) => {
|
|
|
414
415
|
if (typeof component.emit === "function") {
|
|
415
416
|
component.emit(DIALOG_EVENTS.AFTER_CLOSE, { dialog: component });
|
|
416
417
|
}
|
|
417
|
-
},
|
|
418
|
+
}, closeDuration);
|
|
418
419
|
},
|
|
419
420
|
toggle(open) {
|
|
420
421
|
if (open === undefined) {
|
|
@@ -164,8 +164,9 @@ export interface DialogConfig {
|
|
|
164
164
|
*/
|
|
165
165
|
zIndex?: number;
|
|
166
166
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
167
|
+
* How long `afteropen` and `afterclose` wait after opening and closing, in
|
|
168
|
+
* milliseconds, and when a closed dialog leaves the DOM. By default they
|
|
169
|
+
* follow the stylesheet: 450 after opening, 200 after closing.
|
|
169
170
|
*/
|
|
170
171
|
animationDuration?: number;
|
|
171
172
|
/**
|
|
@@ -26,8 +26,8 @@ export const getElementConfig = (config) => {
|
|
|
26
26
|
styles.maxHeight = config.maxHeight;
|
|
27
27
|
}
|
|
28
28
|
const attributes = {
|
|
29
|
-
role: "menu",
|
|
30
|
-
tabindex: "-1",
|
|
29
|
+
role: config.listbox ? "presentation" : "menu",
|
|
30
|
+
...(config.listbox ? {} : { tabindex: "-1" }),
|
|
31
31
|
"aria-hidden": (!config.visible).toString(),
|
|
32
32
|
style: Object.entries(styles)
|
|
33
33
|
.map(([key, value]) => `${key}: ${value}`)
|
|
@@ -6,6 +6,11 @@ const withController = (config) => (component) => {
|
|
|
6
6
|
return component;
|
|
7
7
|
}
|
|
8
8
|
const tasks = createMenuTasks();
|
|
9
|
+
const listbox = config.listbox === true;
|
|
10
|
+
const optionIdPrefix = `${component.getClass("menu")}-${Math.random().toString(36).slice(2, 9)}`;
|
|
11
|
+
if (listbox) {
|
|
12
|
+
component.element.addEventListener("mousedown", (e) => e.preventDefault());
|
|
13
|
+
}
|
|
9
14
|
const state = {
|
|
10
15
|
visible: config.visible || false,
|
|
11
16
|
items: config.items || [],
|
|
@@ -51,8 +56,14 @@ const withController = (config) => (component) => {
|
|
|
51
56
|
const itemElement = document.createElement("li");
|
|
52
57
|
const itemClass = `${component.getClass("menu-item")}`;
|
|
53
58
|
itemElement.className = itemClass;
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
if (listbox) {
|
|
60
|
+
itemElement.setAttribute("role", "option");
|
|
61
|
+
itemElement.id = `${optionIdPrefix}-option-${index}`;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
itemElement.setAttribute("role", "menuitem");
|
|
65
|
+
itemElement.setAttribute("tabindex", "-1");
|
|
66
|
+
}
|
|
56
67
|
itemElement.setAttribute("data-id", item.id);
|
|
57
68
|
itemElement.setAttribute("data-index", index.toString());
|
|
58
69
|
if (item.disabled) {
|
|
@@ -144,7 +155,9 @@ const withController = (config) => (component) => {
|
|
|
144
155
|
const renderMenuItems = () => {
|
|
145
156
|
const menuList = document.createElement("ul");
|
|
146
157
|
menuList.className = `${component.getClass("menu-list")}`;
|
|
147
|
-
menuList.setAttribute("role", "menu");
|
|
158
|
+
menuList.setAttribute("role", listbox ? "listbox" : "menu");
|
|
159
|
+
if (listbox)
|
|
160
|
+
menuList.id = `${optionIdPrefix}-listbox`;
|
|
148
161
|
let target = menuList;
|
|
149
162
|
const startGroup = () => {
|
|
150
163
|
const group = document.createElement("li");
|
|
@@ -256,31 +269,32 @@ const withController = (config) => (component) => {
|
|
|
256
269
|
component.element.style.opacity = "";
|
|
257
270
|
void component.element.getBoundingClientRect();
|
|
258
271
|
component.element.classList.add(`${component.getClass("menu--visible")}`);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
component.keyboard.handleInitialFocus
|
|
262
|
-
|
|
263
|
-
else {
|
|
264
|
-
const items = Array.from(component.element.querySelectorAll(`.${component.getClass("menu-item")}`));
|
|
265
|
-
items.forEach((item) => {
|
|
266
|
-
item.tabIndex = -1;
|
|
267
|
-
});
|
|
268
|
-
if (items.length > 0)
|
|
269
|
-
items[0].tabIndex = 0;
|
|
270
|
-
if (interactionType === "keyboard" && items.length > 0) {
|
|
271
|
-
items[0].focus();
|
|
272
|
+
if (!listbox)
|
|
273
|
+
tasks.setTimeout(() => {
|
|
274
|
+
if (component.keyboard && component.keyboard.handleInitialFocus) {
|
|
275
|
+
component.keyboard.handleInitialFocus(component.element, interactionType);
|
|
272
276
|
}
|
|
273
277
|
else {
|
|
274
|
-
component.element.
|
|
275
|
-
|
|
278
|
+
const items = Array.from(component.element.querySelectorAll(`.${component.getClass("menu-item")}`));
|
|
279
|
+
items.forEach((item) => {
|
|
280
|
+
item.tabIndex = -1;
|
|
281
|
+
});
|
|
282
|
+
if (items.length > 0)
|
|
283
|
+
items[0].tabIndex = 0;
|
|
284
|
+
if (interactionType === "keyboard" && items.length > 0) {
|
|
285
|
+
items[0].focus();
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
component.element.tabIndex = -1;
|
|
289
|
+
component.element.focus();
|
|
290
|
+
}
|
|
276
291
|
}
|
|
277
|
-
}
|
|
278
|
-
}, 100);
|
|
292
|
+
}, 100);
|
|
279
293
|
tasks.setTimeout(() => {
|
|
280
294
|
if (config.closeOnClickOutside && state.visible) {
|
|
281
295
|
document.addEventListener("click", handleDocumentClick);
|
|
282
296
|
}
|
|
283
|
-
if (config.closeOnEscape) {
|
|
297
|
+
if (config.closeOnEscape && !listbox) {
|
|
284
298
|
document.addEventListener("keydown", handleDocumentKeydown);
|
|
285
299
|
}
|
|
286
300
|
window.addEventListener("resize", handleWindowResize, {
|
|
@@ -432,7 +446,7 @@ const withController = (config) => (component) => {
|
|
|
432
446
|
};
|
|
433
447
|
const initMenu = () => {
|
|
434
448
|
renderMenuItems();
|
|
435
|
-
if (component.keyboard && component.keyboard.setupKeyboardHandlers) {
|
|
449
|
+
if (!listbox && component.keyboard && component.keyboard.setupKeyboardHandlers) {
|
|
436
450
|
component.keyboard.setupKeyboardHandlers(component.element, state, {
|
|
437
451
|
closeMenu,
|
|
438
452
|
closeSubmenu: component.submenu ? component.submenu.closeSubmenu : null,
|
|
@@ -454,7 +468,7 @@ const withController = (config) => (component) => {
|
|
|
454
468
|
if (config.closeOnClickOutside) {
|
|
455
469
|
document.addEventListener("click", handleDocumentClick);
|
|
456
470
|
}
|
|
457
|
-
if (config.closeOnEscape) {
|
|
471
|
+
if (config.closeOnEscape && !listbox) {
|
|
458
472
|
document.addEventListener("keydown", handleDocumentKeydown);
|
|
459
473
|
}
|
|
460
474
|
window.addEventListener("resize", handleWindowResize);
|
|
@@ -82,6 +82,8 @@ const withOpener = (config) => (component) => {
|
|
|
82
82
|
openerElement.addEventListener("keydown", handleOpenerKeydown);
|
|
83
83
|
openerElement.addEventListener("blur", handleOpenerBlur);
|
|
84
84
|
}
|
|
85
|
+
if (config.listbox)
|
|
86
|
+
return;
|
|
85
87
|
openerElement.setAttribute("aria-haspopup", "true");
|
|
86
88
|
openerElement.setAttribute("aria-expanded", "false");
|
|
87
89
|
let menuId = component.element.id;
|
|
@@ -256,13 +258,13 @@ const withOpener = (config) => (component) => {
|
|
|
256
258
|
};
|
|
257
259
|
}
|
|
258
260
|
component.on("open", () => {
|
|
259
|
-
if (state.openerElement) {
|
|
261
|
+
if (state.openerElement && !config.listbox) {
|
|
260
262
|
state.openerElement.setAttribute("aria-expanded", "true");
|
|
261
263
|
setOpenerActive(true);
|
|
262
264
|
}
|
|
263
265
|
});
|
|
264
266
|
component.on("close", (event) => {
|
|
265
|
-
if (state.openerElement) {
|
|
267
|
+
if (state.openerElement && !config.listbox) {
|
|
266
268
|
state.openerElement.setAttribute("aria-expanded", "false");
|
|
267
269
|
setOpenerActive(false);
|
|
268
270
|
if (event.restoreFocus && !isTabNavigation) {
|
|
@@ -232,6 +232,15 @@ export interface MenuConfig {
|
|
|
232
232
|
* @default false
|
|
233
233
|
*/
|
|
234
234
|
manualOpen?: boolean;
|
|
235
|
+
/**
|
|
236
|
+
* Renders the menu as the listbox popup of a combobox (WAI-ARIA select-only
|
|
237
|
+
* combobox): the list is `role="listbox"` and each item `role="option"` with
|
|
238
|
+
* its own id, focus stays with the combobox, which owns the keyboard and
|
|
239
|
+
* points at the active option with `aria-activedescendant`, and the opener
|
|
240
|
+
* gets no ARIA from the menu. Pair it with `manualOpen`.
|
|
241
|
+
* @default false
|
|
242
|
+
*/
|
|
243
|
+
listbox?: boolean;
|
|
235
244
|
/**
|
|
236
245
|
* When true, renders a compact menu with smaller items and tighter spacing.
|
|
237
246
|
* Useful in dense UIs like toolbars and action bars where the standard
|
|
@@ -317,9 +326,11 @@ export interface MenuComponent {
|
|
|
317
326
|
/**
|
|
318
327
|
* Closes the menu
|
|
319
328
|
* @param event - Optional event that triggered the close
|
|
329
|
+
* @param restoreFocus - Whether focus returns to the opener (default true)
|
|
330
|
+
* @param skipAnimation - Whether to close without the exit animation
|
|
320
331
|
* @returns The menu component for chaining
|
|
321
332
|
*/
|
|
322
|
-
close: (event?: Event) => MenuComponent;
|
|
333
|
+
close: (event?: Event, restoreFocus?: boolean, skipAnimation?: boolean) => MenuComponent;
|
|
323
334
|
/**
|
|
324
335
|
* Toggles the menu's open state
|
|
325
336
|
* @param event - Optional event that triggered the toggle
|
|
@@ -75,6 +75,217 @@ const processMenuItems = (options) => {
|
|
|
75
75
|
return menuItem;
|
|
76
76
|
});
|
|
77
77
|
};
|
|
78
|
+
const setupCombobox = (component, menu, state, choose, prefix) => {
|
|
79
|
+
const input = component.textfield.input;
|
|
80
|
+
const field = component.textfield.element;
|
|
81
|
+
const activeClass = `${prefix}-menu-item--active`;
|
|
82
|
+
const TYPEAHEAD_DELAY = 500;
|
|
83
|
+
const PAGE = 10;
|
|
84
|
+
input.setAttribute("role", "combobox");
|
|
85
|
+
input.setAttribute("aria-haspopup", "listbox");
|
|
86
|
+
input.setAttribute("aria-expanded", "false");
|
|
87
|
+
let active = null;
|
|
88
|
+
let pending = "selected";
|
|
89
|
+
let buffer = "";
|
|
90
|
+
let bufferTimer = null;
|
|
91
|
+
const options = () => Array.from(menu.element.querySelectorAll('[role="option"]'));
|
|
92
|
+
const enabled = () => options().filter((option) => option.getAttribute("aria-disabled") !== "true");
|
|
93
|
+
const optionFor = (element) => element
|
|
94
|
+
? state.options.find((option) => String(option.id) === element.getAttribute("data-id"))
|
|
95
|
+
: undefined;
|
|
96
|
+
const setActive = (element) => {
|
|
97
|
+
active = element;
|
|
98
|
+
options().forEach((option) => option.classList.toggle(activeClass, option === element));
|
|
99
|
+
if (element) {
|
|
100
|
+
input.setAttribute("aria-activedescendant", element.id);
|
|
101
|
+
element.scrollIntoView?.({ block: "nearest" });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
input.removeAttribute("aria-activedescendant");
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const placeActive = () => {
|
|
108
|
+
const list = enabled();
|
|
109
|
+
if (!list.length)
|
|
110
|
+
return setActive(null);
|
|
111
|
+
if (pending === "first")
|
|
112
|
+
return setActive(list[0]);
|
|
113
|
+
if (pending === "last")
|
|
114
|
+
return setActive(list[list.length - 1]);
|
|
115
|
+
const selectedId = state.selectedOption ? String(state.selectedOption.id) : null;
|
|
116
|
+
setActive(list.find((option) => option.getAttribute("data-id") === selectedId) ?? list[0]);
|
|
117
|
+
};
|
|
118
|
+
const open = (start, event) => {
|
|
119
|
+
pending = start;
|
|
120
|
+
menu.open(event, "keyboard");
|
|
121
|
+
};
|
|
122
|
+
const close = (event) => {
|
|
123
|
+
menu.close(event, false);
|
|
124
|
+
};
|
|
125
|
+
const commit = (event) => {
|
|
126
|
+
const option = optionFor(active);
|
|
127
|
+
if (option)
|
|
128
|
+
choose(option, event);
|
|
129
|
+
close(event);
|
|
130
|
+
};
|
|
131
|
+
const move = (index) => {
|
|
132
|
+
const list = enabled();
|
|
133
|
+
if (!list.length)
|
|
134
|
+
return;
|
|
135
|
+
setActive(list[Math.max(0, Math.min(index, list.length - 1))]);
|
|
136
|
+
};
|
|
137
|
+
const typeahead = (key) => {
|
|
138
|
+
if (bufferTimer)
|
|
139
|
+
clearTimeout(bufferTimer);
|
|
140
|
+
bufferTimer = setTimeout(() => {
|
|
141
|
+
buffer = "";
|
|
142
|
+
bufferTimer = null;
|
|
143
|
+
}, TYPEAHEAD_DELAY);
|
|
144
|
+
buffer += key.toLowerCase();
|
|
145
|
+
const list = enabled();
|
|
146
|
+
const start = active ? list.indexOf(active) + (buffer.length === 1 ? 1 : 0) : 0;
|
|
147
|
+
const ordered = [...list.slice(start), ...list.slice(0, start)];
|
|
148
|
+
const match = ordered.find((option) => (option.textContent || "").trim().toLowerCase().startsWith(buffer));
|
|
149
|
+
if (match)
|
|
150
|
+
setActive(match);
|
|
151
|
+
return !!match;
|
|
152
|
+
};
|
|
153
|
+
menu.on("open", () => {
|
|
154
|
+
input.setAttribute("aria-expanded", "true");
|
|
155
|
+
const connect = () => {
|
|
156
|
+
const list = menu.element.querySelector('[role="listbox"]');
|
|
157
|
+
if (list?.id)
|
|
158
|
+
input.setAttribute("aria-controls", list.id);
|
|
159
|
+
placeActive();
|
|
160
|
+
};
|
|
161
|
+
if (options().length)
|
|
162
|
+
connect();
|
|
163
|
+
else
|
|
164
|
+
setTimeout(connect, 0);
|
|
165
|
+
});
|
|
166
|
+
menu.on("close", () => {
|
|
167
|
+
input.setAttribute("aria-expanded", "false");
|
|
168
|
+
input.removeAttribute("aria-controls");
|
|
169
|
+
setActive(null);
|
|
170
|
+
pending = "selected";
|
|
171
|
+
});
|
|
172
|
+
input.addEventListener("keydown", (e) => {
|
|
173
|
+
if (input.disabled)
|
|
174
|
+
return;
|
|
175
|
+
const isOpen = menu.isOpen();
|
|
176
|
+
const list = enabled();
|
|
177
|
+
const index = active ? list.indexOf(active) : -1;
|
|
178
|
+
if (e.altKey && e.key === "ArrowDown") {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
if (!isOpen)
|
|
181
|
+
open("selected", e);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (e.altKey && e.key === "ArrowUp") {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
if (isOpen)
|
|
187
|
+
commit(e);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
switch (e.key) {
|
|
191
|
+
case "ArrowDown":
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
if (!isOpen)
|
|
194
|
+
open("selected", e);
|
|
195
|
+
else
|
|
196
|
+
move(index + 1);
|
|
197
|
+
return;
|
|
198
|
+
case "ArrowUp":
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
if (!isOpen)
|
|
201
|
+
open("selected", e);
|
|
202
|
+
else
|
|
203
|
+
move(index - 1);
|
|
204
|
+
return;
|
|
205
|
+
case "Home":
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
if (!isOpen)
|
|
208
|
+
open("first", e);
|
|
209
|
+
else
|
|
210
|
+
move(0);
|
|
211
|
+
return;
|
|
212
|
+
case "End":
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
if (!isOpen)
|
|
215
|
+
open("last", e);
|
|
216
|
+
else
|
|
217
|
+
move(list.length - 1);
|
|
218
|
+
return;
|
|
219
|
+
case "PageDown":
|
|
220
|
+
if (!isOpen)
|
|
221
|
+
return;
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
move(index + PAGE);
|
|
224
|
+
return;
|
|
225
|
+
case "PageUp":
|
|
226
|
+
if (!isOpen)
|
|
227
|
+
return;
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
move(index - PAGE);
|
|
230
|
+
return;
|
|
231
|
+
case "Enter":
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
if (isOpen)
|
|
234
|
+
commit(e);
|
|
235
|
+
else
|
|
236
|
+
open("selected", e);
|
|
237
|
+
return;
|
|
238
|
+
case "Escape":
|
|
239
|
+
if (!isOpen)
|
|
240
|
+
return;
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
close(e);
|
|
243
|
+
return;
|
|
244
|
+
case "Tab":
|
|
245
|
+
if (isOpen)
|
|
246
|
+
commit(e);
|
|
247
|
+
return;
|
|
248
|
+
default:
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (e.key === " " && !buffer) {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
if (isOpen)
|
|
254
|
+
commit(e);
|
|
255
|
+
else
|
|
256
|
+
open("selected", e);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
if (!isOpen) {
|
|
262
|
+
open("selected", e);
|
|
263
|
+
setTimeout(() => typeahead(e.key), 0);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
typeahead(e.key);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
field.addEventListener("mousedown", (e) => {
|
|
271
|
+
if (input.disabled || menu.element.contains(e.target) || e.target === input)
|
|
272
|
+
return;
|
|
273
|
+
e.preventDefault();
|
|
274
|
+
input.focus();
|
|
275
|
+
});
|
|
276
|
+
field.addEventListener("click", (e) => {
|
|
277
|
+
if (input.disabled || menu.element.contains(e.target))
|
|
278
|
+
return;
|
|
279
|
+
if (menu.isOpen())
|
|
280
|
+
close(e);
|
|
281
|
+
else
|
|
282
|
+
open("selected", e);
|
|
283
|
+
});
|
|
284
|
+
input.addEventListener("blur", (e) => {
|
|
285
|
+
if (menu.isOpen() && !field.contains(e.relatedTarget))
|
|
286
|
+
close(e);
|
|
287
|
+
});
|
|
288
|
+
};
|
|
78
289
|
export const withMenu = (config) => (component) => {
|
|
79
290
|
if (!component.textfield) {
|
|
80
291
|
console.warn("Cannot add menu: textfield not found");
|
|
@@ -88,6 +299,7 @@ export const withMenu = (config) => (component) => {
|
|
|
88
299
|
state.selectedOption = state.options.find((opt) => opt.id === config.value);
|
|
89
300
|
}
|
|
90
301
|
const menuItems = processMenuItems(state.options);
|
|
302
|
+
const listbox = !state.options.some((option) => option && option.hasSubmenu);
|
|
91
303
|
const menu = createMenu({
|
|
92
304
|
opener: component.textfield,
|
|
93
305
|
items: menuItems,
|
|
@@ -100,47 +312,56 @@ export const withMenu = (config) => (component) => {
|
|
|
100
312
|
closeOnResize: true,
|
|
101
313
|
offset: 0,
|
|
102
314
|
container: component.element,
|
|
315
|
+
...(listbox ? { manualOpen: true, listbox: true, closeOnEscape: false } : {}),
|
|
103
316
|
...(config.menu ?? {}),
|
|
104
317
|
});
|
|
105
|
-
|
|
106
|
-
if (!event.item || event.item.hasSubmenu) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const option = event.item.data;
|
|
110
|
-
if (!isSelectOption(option)) {
|
|
111
|
-
console.warn("Invalid menu selection: missing required data properties");
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
318
|
+
const choose = (option, originalEvent) => {
|
|
114
319
|
state.selectedOption = option;
|
|
115
320
|
component.textfield.setValue(option.text);
|
|
116
321
|
menu.setSelected(option.id);
|
|
117
322
|
if (component.emit) {
|
|
118
|
-
|
|
323
|
+
const changeEvent = {
|
|
119
324
|
select: component,
|
|
120
325
|
value: option.id,
|
|
121
326
|
text: option.text,
|
|
122
327
|
option,
|
|
123
|
-
originalEvent
|
|
328
|
+
originalEvent,
|
|
124
329
|
preventDefault: () => {
|
|
125
|
-
|
|
330
|
+
changeEvent.defaultPrevented = true;
|
|
126
331
|
},
|
|
127
332
|
defaultPrevented: false,
|
|
128
|
-
}
|
|
333
|
+
};
|
|
334
|
+
component.emit("change", changeEvent);
|
|
129
335
|
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (
|
|
336
|
+
};
|
|
337
|
+
menu.on("select", (event) => {
|
|
338
|
+
if (!event.item || event.item.hasSubmenu) {
|
|
133
339
|
return;
|
|
134
|
-
if ((e.key === "Enter" || e.key === " " || e.key === "ArrowDown") &&
|
|
135
|
-
!menu.isOpen()) {
|
|
136
|
-
e.preventDefault();
|
|
137
|
-
menu.open(e, "keyboard");
|
|
138
340
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
341
|
+
const option = event.item.data;
|
|
342
|
+
if (!isSelectOption(option)) {
|
|
343
|
+
console.warn("Invalid menu selection: missing required data properties");
|
|
344
|
+
return;
|
|
142
345
|
}
|
|
346
|
+
choose(option, event.originalEvent);
|
|
143
347
|
});
|
|
348
|
+
if (listbox) {
|
|
349
|
+
setupCombobox(component, menu, state, choose, config.prefix || "mtrl");
|
|
350
|
+
}
|
|
351
|
+
if (!listbox)
|
|
352
|
+
component.textfield.element.addEventListener("keydown", (e) => {
|
|
353
|
+
if (component.textfield.input.disabled)
|
|
354
|
+
return;
|
|
355
|
+
if ((e.key === "Enter" || e.key === " " || e.key === "ArrowDown") &&
|
|
356
|
+
!menu.isOpen()) {
|
|
357
|
+
e.preventDefault();
|
|
358
|
+
menu.open(e, "keyboard");
|
|
359
|
+
}
|
|
360
|
+
else if (e.key === "Escape" && menu.isOpen()) {
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
menu.close(e);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
144
365
|
menu.on("open", () => {
|
|
145
366
|
if (component.emit) {
|
|
146
367
|
component.emit("open", {
|
|
@@ -7,7 +7,7 @@ import { createBaseConfig, getElementConfig, getApiConfig } from "./config.js";
|
|
|
7
7
|
const createSwitch = (config = {}) => {
|
|
8
8
|
const baseConfig = createBaseConfig(config);
|
|
9
9
|
try {
|
|
10
|
-
const switchComponent = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)),
|
|
10
|
+
const switchComponent = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)), withInput(baseConfig), withTextLabel(baseConfig), withTrack(baseConfig), withSupportingText(baseConfig), withCheckable(baseConfig), withDisabled(baseConfig), withLifecycle(), (comp) => withAPI(getApiConfig(comp))(comp))(baseConfig);
|
|
11
11
|
return switchComponent;
|
|
12
12
|
}
|
|
13
13
|
catch (error) {
|
|
@@ -5,7 +5,7 @@ import { withAPI, getApiConfig } from "./api.js";
|
|
|
5
5
|
import { withTabsManagement, withScrollable, withDivider, withIndicator } from "./features.js";
|
|
6
6
|
import { createTabsConfig, getTabsElementConfig } from "./config.js";
|
|
7
7
|
import { addTabStateStyles } from "./state.js";
|
|
8
|
-
import { setupKeyboardNavigation } from "./utils.js";
|
|
8
|
+
import { setupKeyboardNavigation, syncTabStops } from "./utils.js";
|
|
9
9
|
const createTabs = (config = {}) => {
|
|
10
10
|
const baseConfig = createTabsConfig(config);
|
|
11
11
|
addTabStateStyles();
|
|
@@ -18,6 +18,15 @@ const createTabs = (config = {}) => {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
setupKeyboardNavigation(component);
|
|
21
|
+
component.on('change', () => syncTabStops(component));
|
|
22
|
+
for (const method of ['addTab', 'add', 'removeTab', 'setActiveTab']) {
|
|
23
|
+
const original = component[method];
|
|
24
|
+
component[method] = (...args) => {
|
|
25
|
+
const result = original.apply(component, args);
|
|
26
|
+
syncTabStops(component);
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
21
30
|
return component;
|
|
22
31
|
}
|
|
23
32
|
catch (error) {
|
|
@@ -8,6 +8,7 @@ interface TabsHost {
|
|
|
8
8
|
getTabs?: () => TabComponent[];
|
|
9
9
|
getActiveTab?: () => TabComponent | null;
|
|
10
10
|
setActiveTab?: (tabOrValue: TabComponent | string) => unknown;
|
|
11
|
+
handleTabClick?: (event: unknown, tab: TabComponent) => void;
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
14
|
* Gets the active tab from a component
|
|
@@ -20,6 +21,13 @@ export declare function getActiveTab(component: TabsHost): TabComponent | null;
|
|
|
20
21
|
* @param component - Component with tabs
|
|
21
22
|
*/
|
|
22
23
|
export declare function updateTabPanels(component: TabsHost): void;
|
|
24
|
+
/**
|
|
25
|
+
* Gives the tablist a single tab stop: the active tab, or the first enabled
|
|
26
|
+
* one when none is active. Every other tab takes tabindex -1 and is reached
|
|
27
|
+
* with the arrow keys (WAI-ARIA tabs pattern).
|
|
28
|
+
* @param component - Tabs component
|
|
29
|
+
*/
|
|
30
|
+
export declare function syncTabStops(component: TabsHost): void;
|
|
23
31
|
/**
|
|
24
32
|
* Sets up keyboard navigation for tabs
|
|
25
33
|
* @param component - Tabs component
|