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.
Files changed (85) hide show
  1. package/dist/components/button/features/progress.js +2 -0
  2. package/dist/components/dialog/config.js +0 -1
  3. package/dist/components/dialog/features.js +5 -4
  4. package/dist/components/dialog/types.d.ts +3 -2
  5. package/dist/components/menu/config.js +2 -2
  6. package/dist/components/menu/features/controller.js +37 -23
  7. package/dist/components/menu/features/opener.js +4 -2
  8. package/dist/components/menu/types.d.ts +12 -1
  9. package/dist/components/select/features.js +244 -23
  10. package/dist/components/switch/switch.js +1 -1
  11. package/dist/components/tabs/tabs.js +10 -1
  12. package/dist/components/tabs/utils.d.ts +8 -0
  13. package/dist/components/tabs/utils.js +42 -17
  14. package/dist/components/textfield/features/error.d.ts +1 -0
  15. package/dist/components/textfield/features/error.js +10 -0
  16. package/dist/components/textfield/features/supporting-text.d.ts +1 -0
  17. package/dist/components/textfield/features/supporting-text.js +25 -0
  18. package/dist/components/tooltip/api.js +19 -0
  19. package/dist/core/compose/features/input.js +1 -1
  20. package/dist/core/compose/features/textlabel.js +3 -0
  21. package/dist/index.cjs +0 -0
  22. package/dist/package.json +1 -1
  23. package/dist/styles/badge.css +1 -1
  24. package/dist/styles/base.css +2 -2
  25. package/dist/styles/bottom-app-bar.css +1 -1
  26. package/dist/styles/bottom-sheet.css +2 -2
  27. package/dist/styles/button-group.css +1 -1
  28. package/dist/styles/button.css +2 -2
  29. package/dist/styles/card.css +2 -2
  30. package/dist/styles/carousel.css +1 -1
  31. package/dist/styles/checkbox.css +2 -2
  32. package/dist/styles/chips.css +2 -2
  33. package/dist/styles/dialog.css +2 -2
  34. package/dist/styles/divider.css +1 -1
  35. package/dist/styles/drawer.css +2 -2
  36. package/dist/styles/extended-fab.css +2 -2
  37. package/dist/styles/fab.css +2 -2
  38. package/dist/styles/icon-button.css +2 -2
  39. package/dist/styles/list.css +2 -2
  40. package/dist/styles/loading-indicator.css +1 -1
  41. package/dist/styles/menu.css +2 -2
  42. package/dist/styles/navigation-rail.css +1 -1
  43. package/dist/styles/navigation.css +2 -2
  44. package/dist/styles/progress.css +2 -2
  45. package/dist/styles/radios.css +1 -1
  46. package/dist/styles/search.css +1 -1
  47. package/dist/styles/segmented-button.css +2 -2
  48. package/dist/styles/select.css +2 -2
  49. package/dist/styles/side-sheet.css +2 -2
  50. package/dist/styles/slider.css +1 -1
  51. package/dist/styles/snackbar.css +1 -1
  52. package/dist/styles/split-button.css +2 -2
  53. package/dist/styles/switch.css +2 -2
  54. package/dist/styles/tabs.css +1 -1
  55. package/dist/styles/textfield.css +2 -2
  56. package/dist/styles/timepicker.css +1 -1
  57. package/dist/styles/tooltip.css +2 -2
  58. package/dist/styles/top-app-bar.css +1 -1
  59. package/dist/styles/utilities.css +1 -1
  60. package/dist/styles.css +2 -2
  61. package/dist/themes/autumn.css +1 -1
  62. package/dist/themes/baseline.css +1 -1
  63. package/dist/themes/brownbeige.css +1 -1
  64. package/dist/themes/browngreen.css +1 -1
  65. package/dist/themes/desert.css +1 -1
  66. package/dist/themes/forest.css +1 -1
  67. package/dist/themes/highcontrast.css +1 -1
  68. package/dist/themes/legacy.css +1 -1
  69. package/dist/themes/material.css +1 -1
  70. package/dist/themes/ocean.css +1 -1
  71. package/dist/themes/sageivory.css +1 -1
  72. package/dist/themes/spring.css +1 -1
  73. package/dist/themes/summer.css +1 -1
  74. package/dist/themes/sunset.css +1 -1
  75. package/dist/themes/tealcaramel.css +1 -1
  76. package/dist/themes/winter.css +1 -1
  77. package/package.json +1 -1
  78. package/src/styles/abstract/_mixins.scss +26 -0
  79. package/src/styles/abstract/_variables.scss +25 -11
  80. package/src/styles/components/_bottom-sheet.scss +23 -7
  81. package/src/styles/components/_dialog.scss +59 -26
  82. package/src/styles/components/_drawer.scss +60 -7
  83. package/src/styles/components/_menu.scss +12 -7
  84. package/src/styles/components/_side-sheet.scss +33 -4
  85. 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();
@@ -10,7 +10,6 @@ export const defaultConfig = {
10
10
  autofocus: true,
11
11
  trapFocus: true,
12
12
  divider: false,
13
- animationDuration: 150,
14
13
  buttons: []
15
14
  };
16
15
  export const createBaseConfig = (config = {}) => createComponentConfig(defaultConfig, config, 'dialog');
@@ -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 animationDuration = component.config.animationDuration || 150;
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
- }, animationDuration);
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
- }, animationDuration);
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
- * Duration of open/close animations in milliseconds
168
- * @default 150
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
- itemElement.setAttribute("role", "menuitem");
55
- itemElement.setAttribute("tabindex", "-1");
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
- tasks.setTimeout(() => {
260
- if (component.keyboard && component.keyboard.handleInitialFocus) {
261
- component.keyboard.handleInitialFocus(component.element, interactionType);
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.tabIndex = -1;
275
- component.element.focus();
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
- menu.on("select", (event) => {
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
- component.emit("change", {
323
+ const changeEvent = {
119
324
  select: component,
120
325
  value: option.id,
121
326
  text: option.text,
122
327
  option,
123
- originalEvent: event.originalEvent,
328
+ originalEvent,
124
329
  preventDefault: () => {
125
- event.defaultPrevented = true;
330
+ changeEvent.defaultPrevented = true;
126
331
  },
127
332
  defaultPrevented: false,
128
- });
333
+ };
334
+ component.emit("change", changeEvent);
129
335
  }
130
- });
131
- component.textfield.element.addEventListener("keydown", (e) => {
132
- if (component.textfield.input.disabled)
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
- else if (e.key === "Escape" && menu.isOpen()) {
140
- e.preventDefault();
141
- menu.close(e);
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)), withTextLabel(baseConfig), withInput(baseConfig), withTrack(baseConfig), withSupportingText(baseConfig), withCheckable(baseConfig), withDisabled(baseConfig), withLifecycle(), (comp) => withAPI(getApiConfig(comp))(comp))(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