mtrl 0.9.4 → 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 (106) hide show
  1. package/dist/components/badge/api.d.ts +0 -1
  2. package/dist/components/badge/api.js +3 -1
  3. package/dist/components/bottom-app-bar/bottom-app-bar.js +9 -2
  4. package/dist/components/button/features/progress.d.ts +5 -0
  5. package/dist/components/button/features/progress.js +16 -12
  6. package/dist/components/card/api.js +17 -14
  7. package/dist/components/card/config.js +1 -1
  8. package/dist/components/card/content.js +2 -5
  9. package/dist/components/dialog/config.js +0 -1
  10. package/dist/components/dialog/features.js +5 -4
  11. package/dist/components/dialog/types.d.ts +3 -2
  12. package/dist/components/divider/features.d.ts +2 -46
  13. package/dist/components/divider/features.js +54 -115
  14. package/dist/components/menu/config.js +2 -2
  15. package/dist/components/menu/features/controller.js +37 -23
  16. package/dist/components/menu/features/opener.js +4 -2
  17. package/dist/components/menu/types.d.ts +12 -1
  18. package/dist/components/radios/api.d.ts +4 -6
  19. package/dist/components/radios/api.js +5 -25
  20. package/dist/components/radios/radio.js +8 -33
  21. package/dist/components/search/features/states.js +2 -0
  22. package/dist/components/search/features/suggestions.js +3 -0
  23. package/dist/components/select/api.js +2 -0
  24. package/dist/components/select/features.js +252 -31
  25. package/dist/components/slider/features/controller.js +11 -2
  26. package/dist/components/switch/switch.js +1 -1
  27. package/dist/components/tabs/api.js +12 -6
  28. package/dist/components/tabs/features.js +5 -7
  29. package/dist/components/tabs/tabs.js +16 -1
  30. package/dist/components/tabs/utils.d.ts +8 -0
  31. package/dist/components/tabs/utils.js +42 -17
  32. package/dist/components/textfield/features/error.d.ts +2 -0
  33. package/dist/components/textfield/features/error.js +22 -0
  34. package/dist/components/textfield/features/supporting-text.d.ts +1 -0
  35. package/dist/components/textfield/features/supporting-text.js +25 -0
  36. package/dist/components/tooltip/api.js +42 -7
  37. package/dist/components/top-app-bar/top-app-bar.js +3 -0
  38. package/dist/core/compose/features/input.js +1 -1
  39. package/dist/core/compose/features/textinput.d.ts +4 -0
  40. package/dist/core/compose/features/textinput.js +1 -0
  41. package/dist/core/compose/features/textlabel.js +3 -0
  42. package/dist/index.cjs +0 -0
  43. package/dist/package.json +1 -1
  44. package/dist/styles/badge.css +1 -1
  45. package/dist/styles/base.css +2 -2
  46. package/dist/styles/bottom-app-bar.css +1 -1
  47. package/dist/styles/bottom-sheet.css +2 -2
  48. package/dist/styles/button-group.css +1 -1
  49. package/dist/styles/button.css +2 -2
  50. package/dist/styles/card.css +2 -2
  51. package/dist/styles/carousel.css +1 -1
  52. package/dist/styles/checkbox.css +2 -2
  53. package/dist/styles/chips.css +2 -2
  54. package/dist/styles/dialog.css +2 -2
  55. package/dist/styles/divider.css +1 -1
  56. package/dist/styles/drawer.css +2 -2
  57. package/dist/styles/extended-fab.css +2 -2
  58. package/dist/styles/fab.css +2 -2
  59. package/dist/styles/icon-button.css +2 -2
  60. package/dist/styles/list.css +2 -2
  61. package/dist/styles/loading-indicator.css +1 -1
  62. package/dist/styles/menu.css +2 -2
  63. package/dist/styles/navigation-rail.css +1 -1
  64. package/dist/styles/navigation.css +2 -2
  65. package/dist/styles/progress.css +2 -2
  66. package/dist/styles/radios.css +1 -1
  67. package/dist/styles/search.css +1 -1
  68. package/dist/styles/segmented-button.css +2 -2
  69. package/dist/styles/select.css +2 -2
  70. package/dist/styles/side-sheet.css +2 -2
  71. package/dist/styles/slider.css +1 -1
  72. package/dist/styles/snackbar.css +1 -1
  73. package/dist/styles/split-button.css +2 -2
  74. package/dist/styles/switch.css +2 -2
  75. package/dist/styles/tabs.css +1 -1
  76. package/dist/styles/textfield.css +2 -2
  77. package/dist/styles/timepicker.css +1 -1
  78. package/dist/styles/tooltip.css +2 -2
  79. package/dist/styles/top-app-bar.css +1 -1
  80. package/dist/styles/utilities.css +1 -1
  81. package/dist/styles.css +2 -2
  82. package/dist/themes/autumn.css +1 -1
  83. package/dist/themes/baseline.css +1 -1
  84. package/dist/themes/brownbeige.css +1 -1
  85. package/dist/themes/browngreen.css +1 -1
  86. package/dist/themes/desert.css +1 -1
  87. package/dist/themes/forest.css +1 -1
  88. package/dist/themes/highcontrast.css +1 -1
  89. package/dist/themes/legacy.css +1 -1
  90. package/dist/themes/material.css +1 -1
  91. package/dist/themes/ocean.css +1 -1
  92. package/dist/themes/sageivory.css +1 -1
  93. package/dist/themes/spring.css +1 -1
  94. package/dist/themes/summer.css +1 -1
  95. package/dist/themes/sunset.css +1 -1
  96. package/dist/themes/tealcaramel.css +1 -1
  97. package/dist/themes/winter.css +1 -1
  98. package/package.json +1 -1
  99. package/src/styles/abstract/_mixins.scss +26 -0
  100. package/src/styles/abstract/_variables.scss +25 -11
  101. package/src/styles/components/_bottom-sheet.scss +23 -7
  102. package/src/styles/components/_dialog.scss +59 -26
  103. package/src/styles/components/_drawer.scss +60 -7
  104. package/src/styles/components/_menu.scss +12 -7
  105. package/src/styles/components/_side-sheet.scss +33 -4
  106. package/src/styles/components/_tooltip.scss +3 -0
@@ -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
@@ -21,11 +21,9 @@ interface ComponentWithRadio {
21
21
  enableOption: (value: string) => void;
22
22
  disableOption: (value: string) => void;
23
23
  getClass: (name: string) => string;
24
- events: {
25
- on: (event: string, handler: Function) => void;
26
- off: (event: string, handler: Function) => void;
27
- emit?: (event: string, data: unknown) => void;
28
- };
24
+ on: (event: string, handler: Function) => void;
25
+ off: (event: string, handler: Function) => void;
26
+ emit?: (event: string, data: unknown) => void;
29
27
  }
30
28
  /**
31
29
  * Enhances a radios component with API methods
@@ -33,5 +31,5 @@ interface ComponentWithRadio {
33
31
  * @returns {Function} Higher-order function that adds API methods to component
34
32
  * @internal This is an internal utility for the Radios component
35
33
  */
36
- export declare const withAPI: ({ disabled, lifecycle }: ApiOptions) => (component: ComponentWithRadio) => RadiosComponent;
34
+ export declare const withAPI: ({ lifecycle }: ApiOptions) => (component: ComponentWithRadio) => RadiosComponent;
37
35
  export {};
@@ -1,24 +1,4 @@
1
- export const withAPI = ({ disabled, lifecycle }) => (component) => {
2
- if (!component.events) {
3
- component.events = {
4
- on: (event, handler) => {
5
- component.element.addEventListener(event, ((e) => handler(e.detail)));
6
- },
7
- off: (event, handler) => {
8
- component.element.removeEventListener(event, handler);
9
- },
10
- emit: (event, data) => {
11
- const customEvent = new CustomEvent(event, { detail: data });
12
- component.element.dispatchEvent(customEvent);
13
- },
14
- };
15
- }
16
- else if (!component.events.emit) {
17
- component.events.emit = (event, data) => {
18
- const customEvent = new CustomEvent(event, { detail: data });
19
- component.element.dispatchEvent(customEvent);
20
- };
21
- }
1
+ export const withAPI = ({ lifecycle }) => (component) => {
22
2
  const radiosComponent = {
23
3
  element: component.element,
24
4
  radios: component.radios,
@@ -41,11 +21,11 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => {
41
21
  return this;
42
22
  },
43
23
  enable() {
44
- disabled.enable();
24
+ component.enable();
45
25
  return this;
46
26
  },
47
27
  disable() {
48
- disabled.disable();
28
+ component.disable();
49
29
  return this;
50
30
  },
51
31
  enableOption(value) {
@@ -57,11 +37,11 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => {
57
37
  return this;
58
38
  },
59
39
  on(event, handler) {
60
- component.events.on(event, handler);
40
+ component.on(event, handler);
61
41
  return this;
62
42
  },
63
43
  off(event, handler) {
64
- component.events.off(event, handler);
44
+ component.off(event, handler);
65
45
  return this;
66
46
  },
67
47
  destroy() {
@@ -58,23 +58,11 @@ export const withRadio = (config) => (component) => {
58
58
  radio.input.checked = false;
59
59
  }
60
60
  });
61
- if (component.events && typeof component.events.emit === 'function') {
62
- component.events.emit('change', {
63
- value: option.value,
64
- originalEvent: e,
65
- option
66
- });
67
- }
68
- else if (typeof component.on === 'function') {
69
- const changeEvent = new CustomEvent('change', {
70
- detail: {
71
- value: option.value,
72
- originalEvent: e,
73
- option
74
- }
75
- });
76
- component.element.dispatchEvent(changeEvent);
77
- }
61
+ component.emit?.('change', {
62
+ value: option.value,
63
+ originalEvent: e,
64
+ option
65
+ });
78
66
  }
79
67
  };
80
68
  input.addEventListener('change', handleChange);
@@ -112,28 +100,15 @@ export const withRadio = (config) => (component) => {
112
100
  if (config.options && Array.isArray(config.options)) {
113
101
  config.options.forEach(option => addOption(option));
114
102
  }
115
- if (!component.events) {
116
- component.events = {
117
- on: (event, handler) => {
118
- component.element.addEventListener(event, (e) => handler(e.detail));
119
- },
120
- off: (event, handler) => {
121
- component.element.removeEventListener(event, handler);
122
- },
123
- emit: (event, data) => {
124
- const customEvent = new CustomEvent(event, { detail: data });
125
- component.element.dispatchEvent(customEvent);
126
- }
127
- };
128
- }
129
103
  return {
130
104
  ...component,
131
105
  radios,
132
106
  getValue: () => selectedValue,
133
107
  setValue: (value) => {
134
- selectedValue = value;
108
+ const known = radios.some(radio => radio.config.value === value);
109
+ selectedValue = known ? value : '';
135
110
  radios.forEach(radio => {
136
- radio.input.checked = radio.config.value === value;
111
+ radio.input.checked = known && radio.config.value === value;
137
112
  });
138
113
  return component;
139
114
  },
@@ -38,6 +38,7 @@ export const withStates = (config) => (component) => {
38
38
  viewMode: currentViewMode,
39
39
  });
40
40
  }
41
+ config.onExpand?.();
41
42
  };
42
43
  const collapseToBar = () => {
43
44
  if (currentState === SEARCH_STATES.BAR) {
@@ -67,6 +68,7 @@ export const withStates = (config) => (component) => {
67
68
  viewMode: currentViewMode,
68
69
  });
69
70
  }
71
+ config.onCollapse?.();
70
72
  };
71
73
  const setViewMode = (mode) => {
72
74
  if (mode === currentViewMode) {
@@ -207,7 +207,10 @@ export const withSuggestions = () => (component) => {
207
207
  };
208
208
  setTimeout(() => {
209
209
  setupKeyboardNavigation();
210
+ if (getSuggestions().length > 0)
211
+ renderSuggestions();
210
212
  }, 0);
213
+ component.on?.("expand", () => renderSuggestions());
211
214
  return {
212
215
  ...component,
213
216
  suggestions: {
@@ -20,6 +20,8 @@ export const withAPI = (options) => (component) => ({
20
20
  return this;
21
21
  },
22
22
  open(interactionType = "mouse") {
23
+ if (component.textfield?.input?.disabled)
24
+ return this;
23
25
  if (component.menu && typeof component.menu.open === "function") {
24
26
  component.menu.open(undefined, interactionType);
25
27
  }
@@ -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,56 +312,65 @@ 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
- if (component.emit) {
139
- component.emit("open", {
140
- select: component,
141
- originalEvent: e,
142
- preventDefault: () => { },
143
- defaultPrevented: false,
144
- });
145
- }
146
340
  }
147
- else if (e.key === "Escape" && menu.isOpen()) {
148
- e.preventDefault();
149
- 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;
150
345
  }
346
+ choose(option, event.originalEvent);
151
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
+ });
152
365
  menu.on("open", () => {
366
+ if (component.emit) {
367
+ component.emit("open", {
368
+ select: component,
369
+ originalEvent: null,
370
+ preventDefault: () => { },
371
+ defaultPrevented: false,
372
+ });
373
+ }
153
374
  component.textfield.element.classList.add(`${config.prefix || "mtrl"}-select--open`);
154
375
  const PREFIX = config.prefix || "mtrl";
155
376
  component.textfield.element.classList.add(`${PREFIX}-textfield--focused`);
@@ -1,5 +1,6 @@
1
1
  import { SLIDER_EVENTS } from "../types.js";
2
2
  import { SLIDER_MEASUREMENTS } from "../constants.js";
3
+ import { defaultConfig } from "../config.js";
3
4
  import { createHandlers } from "./handlers.js";
4
5
  export const withController = (config) => (component) => {
5
6
  if (!component.element) {
@@ -105,8 +106,16 @@ export const withController = (config) => (component) => {
105
106
  secondValueBubble.style.transform = "translateX(-50%)";
106
107
  }
107
108
  }
108
- if (handle) {
109
- handle.setAttribute("aria-valuenow", String(state.value));
109
+ updateHandleAria(handle, state.value);
110
+ if (config.range && secondHandle && state.secondValue !== null) {
111
+ updateHandleAria(secondHandle, state.secondValue);
112
+ }
113
+ };
114
+ const updateHandleAria = (handleElement, value) => {
115
+ handleElement.setAttribute("aria-valuenow", String(value));
116
+ if (config.valueFormatter &&
117
+ config.valueFormatter !== defaultConfig.valueFormatter) {
118
+ handleElement.setAttribute("aria-valuetext", config.valueFormatter(value));
110
119
  }
111
120
  };
112
121
  const updateValueBubbles = () => {
@@ -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) {
@@ -18,9 +18,11 @@ export const withAPI = ({ lifecycle }) => (component) => ({
18
18
  if (tab.on && typeof tab.on === 'function') {
19
19
  tab.on('click', (event) => component.handleTabClick(event, tab));
20
20
  }
21
- tab.element.addEventListener('click', (event) => {
22
- component.handleTabClick(event, tab);
23
- });
21
+ else {
22
+ tab.element.addEventListener('click', (event) => {
23
+ component.handleTabClick(event, tab);
24
+ });
25
+ }
24
26
  return tab;
25
27
  },
26
28
  add(tab) {
@@ -30,9 +32,11 @@ export const withAPI = ({ lifecycle }) => (component) => ({
30
32
  if (tab.on && typeof tab.on === 'function') {
31
33
  tab.on('click', (event) => component.handleTabClick(event, tab));
32
34
  }
33
- tab.element.addEventListener('click', (event) => {
34
- component.handleTabClick(event, tab);
35
- });
35
+ else {
36
+ tab.element.addEventListener('click', (event) => {
37
+ component.handleTabClick(event, tab);
38
+ });
39
+ }
36
40
  return this;
37
41
  },
38
42
  getTabs() {
@@ -47,6 +51,8 @@ export const withAPI = ({ lifecycle }) => (component) => ({
47
51
  : tabOrValue;
48
52
  if (!targetTab)
49
53
  return this;
54
+ if (targetTab.element.disabled)
55
+ return this;
50
56
  component.tabs.forEach(tab => tab.deactivate());
51
57
  targetTab.activate();
52
58
  if (component.emit) {
@@ -27,9 +27,7 @@ export const withTabsManagement = (config) => (component) => {
27
27
  if (isCancelable(event)) {
28
28
  event.preventDefault();
29
29
  }
30
- if (tab.disabled &&
31
- tab.disabled.isDisabled &&
32
- tab.disabled.isDisabled()) {
30
+ if (tab.element.disabled) {
33
31
  return;
34
32
  }
35
33
  tabs.forEach((t) => t.deactivate());
@@ -50,7 +48,9 @@ export const withTabsManagement = (config) => (component) => {
50
48
  if (tab.on && typeof tab.on === "function") {
51
49
  tab.on("click", (event) => handleTabClick(event, tab));
52
50
  }
53
- tab.element.addEventListener("click", (event) => handleTabClick(event, tab));
51
+ else {
52
+ tab.element.addEventListener("click", (event) => handleTabClick(event, tab));
53
+ }
54
54
  });
55
55
  return {
56
56
  ...component,
@@ -108,9 +108,7 @@ export const withIndicator = (config) => (component) => {
108
108
  scrollContainer.appendChild(indicator.element);
109
109
  const originalHandleTabClick = component.handleTabClick;
110
110
  component.handleTabClick = function (event, tab) {
111
- if (tab.disabled &&
112
- tab.disabled.isDisabled &&
113
- tab.disabled.isDisabled()) {
111
+ if (tab.element.disabled) {
114
112
  return;
115
113
  }
116
114
  originalHandleTabClick.call(this, event, tab);