mtrl 0.9.5 → 0.9.8

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 (176) hide show
  1. package/README.md +20 -0
  2. package/dist/README.md +20 -0
  3. package/dist/components/bottom-app-bar/bottom-app-bar.js +2 -2
  4. package/dist/components/bottom-sheet/features/structure.js +4 -3
  5. package/dist/components/button/api.js +3 -2
  6. package/dist/components/button/features/progress.js +26 -21
  7. package/dist/components/button-group/config.d.ts +11 -3
  8. package/dist/components/button-group/config.js +1 -1
  9. package/dist/components/card/content.js +5 -4
  10. package/dist/components/carousel/features/slides.js +2 -1
  11. package/dist/components/checkbox/checkbox.js +22 -0
  12. package/dist/components/checkbox/config.d.ts +3 -3
  13. package/dist/components/checkbox/config.js +10 -9
  14. package/dist/components/checkbox/types.d.ts +19 -4
  15. package/dist/components/chips/api.js +3 -1
  16. package/dist/components/chips/chip/api.js +3 -2
  17. package/dist/components/chips/chip/chip.js +6 -4
  18. package/dist/components/chips/config.js +1 -1
  19. package/dist/components/datepicker/api.d.ts +8 -2
  20. package/dist/components/datepicker/api.js +1 -0
  21. package/dist/components/datepicker/config.d.ts +14 -4
  22. package/dist/components/datepicker/datepicker.js +5 -2
  23. package/dist/components/datepicker/types.d.ts +14 -0
  24. package/dist/components/datepicker/utils.js +4 -6
  25. package/dist/components/dialog/config.js +0 -1
  26. package/dist/components/dialog/features.js +10 -8
  27. package/dist/components/dialog/types.d.ts +3 -2
  28. package/dist/components/drawer/features/items.js +3 -2
  29. package/dist/components/list/features/renderer.js +4 -3
  30. package/dist/components/menu/api.d.ts +1 -1
  31. package/dist/components/menu/config.js +2 -2
  32. package/dist/components/menu/features/controller.js +41 -26
  33. package/dist/components/menu/features/keyboard.js +7 -5
  34. package/dist/components/menu/features/opener.js +19 -12
  35. package/dist/components/menu/features/position.js +1 -1
  36. package/dist/components/menu/features/submenu.js +7 -1
  37. package/dist/components/menu/types.d.ts +13 -2
  38. package/dist/components/navigation/nav-item.js +4 -3
  39. package/dist/components/navigation/system/mobile.js +3 -2
  40. package/dist/components/navigation-rail/navigation-rail.js +3 -2
  41. package/dist/components/progress/features/canvas.js +0 -2
  42. package/dist/components/search/features/states.js +3 -3
  43. package/dist/components/search/features/structure.js +3 -0
  44. package/dist/components/search/features/suggestions.js +2 -2
  45. package/dist/components/search/types.d.ts +6 -0
  46. package/dist/components/segmented-button/config.d.ts +4 -1
  47. package/dist/components/segmented-button/config.js +1 -1
  48. package/dist/components/segmented-button/segment.d.ts +2 -5
  49. package/dist/components/segmented-button/segment.js +7 -3
  50. package/dist/components/select/features.js +267 -44
  51. package/dist/components/side-sheet/features/structure.js +5 -4
  52. package/dist/components/slider/features/controller.js +7 -0
  53. package/dist/components/slider/features/dom.d.ts +1 -0
  54. package/dist/components/slider/features/dom.js +11 -0
  55. package/dist/components/slider/features/handlers.js +8 -3
  56. package/dist/components/slider/features/states.js +8 -0
  57. package/dist/components/slider/types.d.ts +7 -0
  58. package/dist/components/switch/config.d.ts +3 -3
  59. package/dist/components/switch/config.js +7 -7
  60. package/dist/components/switch/features.js +5 -5
  61. package/dist/components/switch/switch.js +1 -1
  62. package/dist/components/switch/types.d.ts +10 -0
  63. package/dist/components/tabs/features.d.ts +1 -0
  64. package/dist/components/tabs/scroll-indicators.js +3 -2
  65. package/dist/components/tabs/tab.d.ts +0 -5
  66. package/dist/components/tabs/tab.js +12 -3
  67. package/dist/components/tabs/tabs.js +10 -1
  68. package/dist/components/tabs/utils.d.ts +8 -0
  69. package/dist/components/tabs/utils.js +63 -18
  70. package/dist/components/textfield/api.js +12 -4
  71. package/dist/components/textfield/features/error.d.ts +1 -0
  72. package/dist/components/textfield/features/error.js +10 -0
  73. package/dist/components/textfield/features/leading-icon.js +32 -33
  74. package/dist/components/textfield/features/prefix-text.js +30 -18
  75. package/dist/components/textfield/features/suffix-text.js +30 -14
  76. package/dist/components/textfield/features/supporting-text.d.ts +1 -0
  77. package/dist/components/textfield/features/supporting-text.js +25 -0
  78. package/dist/components/textfield/features/trailing-icon.js +36 -19
  79. package/dist/components/timepicker/api.d.ts +1 -1
  80. package/dist/components/timepicker/api.js +16 -11
  81. package/dist/components/timepicker/render.js +27 -24
  82. package/dist/components/timepicker/timepicker.js +4 -2
  83. package/dist/components/timepicker/types.d.ts +7 -0
  84. package/dist/components/timepicker/utils.d.ts +14 -0
  85. package/dist/components/timepicker/utils.js +4 -0
  86. package/dist/components/tooltip/api.js +27 -2
  87. package/dist/components/top-app-bar/top-app-bar.js +3 -2
  88. package/dist/core/compose/component.d.ts +1 -1
  89. package/dist/core/compose/component.js +1 -1
  90. package/dist/core/compose/features/icon.js +3 -2
  91. package/dist/core/compose/features/input.js +1 -1
  92. package/dist/core/compose/features/lifecycle.js +10 -11
  93. package/dist/core/compose/features/textlabel.js +3 -0
  94. package/dist/core/compose/features/track.js +2 -1
  95. package/dist/core/config/component.d.ts +1 -1
  96. package/dist/core/config/component.js +1 -1
  97. package/dist/core/dom/create.js +5 -4
  98. package/dist/core/dom/form-value.d.ts +24 -0
  99. package/dist/core/dom/form-value.js +14 -0
  100. package/dist/core/dom/html.d.ts +34 -0
  101. package/dist/core/dom/html.js +13 -0
  102. package/dist/core/dom/index.d.ts +2 -0
  103. package/dist/core/dom/index.js +1 -0
  104. package/dist/core/dom/utils.js +1 -1
  105. package/dist/core/state/emitter.js +8 -1
  106. package/dist/core/state/index.d.ts +1 -1
  107. package/dist/core/state/store.d.ts +10 -2
  108. package/dist/core/state/store.js +2 -1
  109. package/dist/index.cjs +0 -0
  110. package/dist/package.json +5 -2
  111. package/dist/styles/badge.css +1 -1
  112. package/dist/styles/base.css +2 -2
  113. package/dist/styles/bottom-app-bar.css +1 -1
  114. package/dist/styles/bottom-sheet.css +2 -2
  115. package/dist/styles/button-group.css +1 -1
  116. package/dist/styles/button.css +2 -2
  117. package/dist/styles/card.css +2 -2
  118. package/dist/styles/carousel.css +1 -1
  119. package/dist/styles/checkbox.css +2 -2
  120. package/dist/styles/chips.css +2 -2
  121. package/dist/styles/dialog.css +2 -2
  122. package/dist/styles/divider.css +1 -1
  123. package/dist/styles/drawer.css +2 -2
  124. package/dist/styles/extended-fab.css +2 -2
  125. package/dist/styles/fab.css +2 -2
  126. package/dist/styles/icon-button.css +2 -2
  127. package/dist/styles/list.css +2 -2
  128. package/dist/styles/loading-indicator.css +1 -1
  129. package/dist/styles/menu.css +2 -2
  130. package/dist/styles/navigation-rail.css +1 -1
  131. package/dist/styles/navigation.css +2 -2
  132. package/dist/styles/progress.css +2 -2
  133. package/dist/styles/radios.css +1 -1
  134. package/dist/styles/search.css +1 -1
  135. package/dist/styles/segmented-button.css +2 -2
  136. package/dist/styles/select.css +2 -2
  137. package/dist/styles/side-sheet.css +2 -2
  138. package/dist/styles/slider.css +1 -1
  139. package/dist/styles/snackbar.css +1 -1
  140. package/dist/styles/split-button.css +2 -2
  141. package/dist/styles/switch.css +2 -2
  142. package/dist/styles/tabs.css +1 -1
  143. package/dist/styles/textfield.css +2 -2
  144. package/dist/styles/timepicker.css +1 -1
  145. package/dist/styles/tooltip.css +2 -2
  146. package/dist/styles/top-app-bar.css +1 -1
  147. package/dist/styles/utilities.css +1 -1
  148. package/dist/styles.css +2 -2
  149. package/dist/styles.d.ts +1 -0
  150. package/dist/themes/autumn.css +1 -1
  151. package/dist/themes/baseline.css +1 -1
  152. package/dist/themes/brownbeige.css +1 -1
  153. package/dist/themes/browngreen.css +1 -1
  154. package/dist/themes/desert.css +1 -1
  155. package/dist/themes/forest.css +1 -1
  156. package/dist/themes/highcontrast.css +1 -1
  157. package/dist/themes/legacy.css +1 -1
  158. package/dist/themes/material.css +1 -1
  159. package/dist/themes/ocean.css +1 -1
  160. package/dist/themes/sageivory.css +1 -1
  161. package/dist/themes/spring.css +1 -1
  162. package/dist/themes/summer.css +1 -1
  163. package/dist/themes/sunset.css +1 -1
  164. package/dist/themes/tealcaramel.css +1 -1
  165. package/dist/themes/winter.css +1 -1
  166. package/package.json +7 -3
  167. package/src/styles/abstract/_mixins.scss +26 -0
  168. package/src/styles/abstract/_variables.scss +25 -11
  169. package/src/styles/base/_reset.scss +9 -2
  170. package/src/styles/components/_bottom-sheet.scss +23 -7
  171. package/src/styles/components/_button.scss +1 -12
  172. package/src/styles/components/_dialog.scss +59 -26
  173. package/src/styles/components/_drawer.scss +60 -7
  174. package/src/styles/components/_menu.scss +12 -7
  175. package/src/styles/components/_side-sheet.scss +33 -4
  176. package/src/styles/components/_tooltip.scss +3 -0
@@ -6,6 +6,7 @@ import { createEmitter } from "../../core/state/emitter.js";
6
6
  import { mountRailRipple } from "./ripple.js";
7
7
  import { createBaseConfig } from "./config.js";
8
8
  import { safeUrl } from "../../core/utils/url.js";
9
+ import { setHTML } from "../../core/dom/html.js";
9
10
  const copyItems = (items) => {
10
11
  const ids = new Set();
11
12
  let selected = false;
@@ -94,7 +95,7 @@ export default function createNavigationRail(config = {}) {
94
95
  else
95
96
  element.removeAttribute('aria-current');
96
97
  const icon = element.querySelector(`.${cls('__icon')}`);
97
- icon.innerHTML = item.active && item.activeIcon ? item.activeIcon : item.icon;
98
+ setHTML(icon, item.active && item.activeIcon ? item.activeIcon : item.icon);
98
99
  }
99
100
  };
100
101
  const render = () => {
@@ -163,7 +164,7 @@ export default function createNavigationRail(config = {}) {
163
164
  toggle?.setAttribute('aria-expanded', String(expanded));
164
165
  toggle?.setAttribute('aria-label', (expanded ? options.collapseLabel : options.expandLabel));
165
166
  if (toggle)
166
- toggle.innerHTML = (expanded ? options.collapseIcon : options.expandIcon);
167
+ setHTML(toggle, (expanded ? options.collapseIcon : options.expandIcon));
167
168
  if (dialog) {
168
169
  if (expanded)
169
170
  showModal();
@@ -78,7 +78,6 @@ export const withCanvas = (config) => (component) => {
78
78
  return false;
79
79
  context = { canvas, ctx, width: 0, height: 0, pixelRatio: view?.devicePixelRatio || 1 };
80
80
  measure();
81
- component.ctx = ctx;
82
81
  return true;
83
82
  };
84
83
  const currentAmplitude = () => {
@@ -293,7 +292,6 @@ export const withCanvas = (config) => (component) => {
293
292
  return {
294
293
  ...component,
295
294
  canvas,
296
- ctx: context?.ctx,
297
295
  draw,
298
296
  resize,
299
297
  };
@@ -1,4 +1,5 @@
1
1
  import { SEARCH_STATES, SEARCH_VIEW_MODES, SEARCH_CLASSES, SEARCH_ICONS, } from "../constants.js";
2
+ import { setHTML } from "../../../core/dom/html.js";
2
3
  export const withStates = (config) => (component) => {
3
4
  let currentState = config.initialState || SEARCH_STATES.BAR;
4
5
  let currentViewMode = config.viewMode || SEARCH_VIEW_MODES.DOCKED;
@@ -16,7 +17,7 @@ export const withStates = (config) => (component) => {
16
17
  element.classList.remove(getClass(SEARCH_CLASSES.STATE_BAR));
17
18
  element.classList.add(getClass(SEARCH_CLASSES.STATE_VIEW));
18
19
  if (structure?.leadingIcon) {
19
- structure.leadingIcon.innerHTML = SEARCH_ICONS.BACK;
20
+ setHTML(structure.leadingIcon, SEARCH_ICONS.BACK);
20
21
  structure.leadingIcon.setAttribute("aria-label", "Go back");
21
22
  }
22
23
  if (structure?.divider && !element.contains(structure.divider)) {
@@ -50,8 +51,7 @@ export const withStates = (config) => (component) => {
50
51
  element.classList.remove(getClass(SEARCH_CLASSES.STATE_VIEW));
51
52
  element.classList.add(getClass(SEARCH_CLASSES.STATE_BAR));
52
53
  if (structure?.leadingIcon) {
53
- structure.leadingIcon.innerHTML =
54
- config.leadingIcon || SEARCH_ICONS.SEARCH;
54
+ setHTML(structure.leadingIcon, config.leadingIcon || SEARCH_ICONS.SEARCH);
55
55
  structure.leadingIcon.setAttribute("aria-label", "Search");
56
56
  }
57
57
  if (structure?.divider && element.contains(structure.divider)) {
@@ -62,6 +62,9 @@ export const withStructure = (config) => (component) => {
62
62
  if (value) {
63
63
  inputAttributes.value = value;
64
64
  }
65
+ if (config.name) {
66
+ inputAttributes.name = config.name;
67
+ }
65
68
  if (isDisabled) {
66
69
  inputAttributes.disabled = "disabled";
67
70
  }
@@ -83,7 +83,7 @@ export const withSuggestions = () => (component) => {
83
83
  return;
84
84
  const suggestions = getSuggestions();
85
85
  const query = component.input?.getValue?.() || "";
86
- suggestionsList.innerHTML = "";
86
+ suggestionsList.replaceChildren();
87
87
  if (suggestions.length === 0) {
88
88
  return;
89
89
  }
@@ -112,7 +112,7 @@ export const withSuggestions = () => (component) => {
112
112
  const clearRenderedSuggestions = () => {
113
113
  const suggestionsList = component.structure?.suggestionsList;
114
114
  if (suggestionsList) {
115
- suggestionsList.innerHTML = "";
115
+ suggestionsList.replaceChildren();
116
116
  }
117
117
  highlightedIndex = -1;
118
118
  };
@@ -74,6 +74,12 @@ export interface SearchConfig {
74
74
  placeholder?: string;
75
75
  /** Initial input value */
76
76
  value?: string;
77
+ /**
78
+ * Form field name. Set on the text input itself, so the search takes part
79
+ * in a surrounding form and its value appears in `FormData`. Without it the
80
+ * search submits nothing.
81
+ */
82
+ name?: string;
77
83
  /** Custom leading icon HTML (replaces default search icon) */
78
84
  leadingIcon?: string;
79
85
  /** Trailing content items (icons, avatar) */
@@ -10,7 +10,10 @@ export declare const DEFAULT_CONFIG: SegmentedButtonConfig;
10
10
  * @returns {SegmentedButtonConfig} Complete configuration with defaults applied
11
11
  * @internal
12
12
  */
13
- export declare const createBaseConfig: (config?: SegmentedButtonConfig) => SegmentedButtonConfig;
13
+ export declare const createBaseConfig: (config?: SegmentedButtonConfig) => SegmentedButtonConfig & {
14
+ componentName: string;
15
+ prefix: string;
16
+ };
14
17
  /**
15
18
  * Generates element configuration for the Segmented Button container
16
19
  * @param {SegmentedButtonConfig} config - Segmented Button configuration
@@ -24,7 +24,7 @@ export const getContainerConfig = (config) => {
24
24
  density !== Density.DEFAULT
25
25
  ? `${config.prefix}-segmented-button--${density}`
26
26
  : null,
27
- ],
27
+ ].filter((name) => Boolean(name)),
28
28
  interactive: true,
29
29
  };
30
30
  };
@@ -1,4 +1,4 @@
1
- import { SegmentConfig, Segment } from "./types.js";
1
+ import { SegmentConfig, Segment, SegmentedButtonConfig } from "./types.js";
2
2
  /**
3
3
  * Creates a segment for the segmented button using the button component
4
4
  *
@@ -16,7 +16,4 @@ import { SegmentConfig, Segment } from "./types.js";
16
16
  * @returns {Segment} Segment object
17
17
  * @internal
18
18
  */
19
- export declare const createSegment: (config: SegmentConfig, container: HTMLElement, prefix: string, groupDisabled?: boolean, options?: {
20
- ripple: boolean;
21
- rippleConfig: {};
22
- }) => Segment;
19
+ export declare const createSegment: (config: SegmentConfig, container: HTMLElement, prefix: string, groupDisabled?: boolean, options?: Pick<SegmentedButtonConfig, "ripple" | "rippleConfig">) => Segment;
@@ -1,12 +1,16 @@
1
1
  import createButton from "../button/index.js";
2
2
  import { DEFAULT_CHECKMARK_ICON } from "./constants.js";
3
+ import { setHTML } from "../../core/dom/html.js";
3
4
  const createCheckmarkElement = (prefix, icon) => {
4
5
  const checkmark = document.createElement("span");
5
6
  checkmark.className = `${prefix}-segment-checkmark`;
6
- checkmark.innerHTML = icon || DEFAULT_CHECKMARK_ICON;
7
+ setHTML(checkmark, icon || DEFAULT_CHECKMARK_ICON);
7
8
  return checkmark;
8
9
  };
9
- export const createSegment = (config, container, prefix, groupDisabled = false, options = { ripple: true, rippleConfig: {} }) => {
10
+ export const createSegment = (config, container, prefix, groupDisabled = false, options = {
11
+ ripple: true,
12
+ rippleConfig: {},
13
+ }) => {
10
14
  const isDisabled = groupDisabled || config.disabled;
11
15
  const originalIcon = config.icon;
12
16
  const hasText = Boolean(config.text);
@@ -53,7 +57,7 @@ export const createSegment = (config, container, prefix, groupDisabled = false,
53
57
  button.element.setAttribute("aria-pressed", selected ? "true" : "false");
54
58
  if (isIconOnly) {
55
59
  }
56
- else if (hasIcon && hasText) {
60
+ else if (originalIcon && hasText) {
57
61
  if (selected) {
58
62
  button.setIcon(checkmarkIcon);
59
63
  }
@@ -75,19 +75,232 @@ const processMenuItems = (options) => {
75
75
  return menuItem;
76
76
  });
77
77
  };
78
+ const setupCombobox = (component, textfield, menu, state, choose, prefix) => {
79
+ const input = textfield.input;
80
+ const field = 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");
81
292
  return component;
82
293
  }
294
+ const textfield = component.textfield;
83
295
  const state = {
84
296
  options: config.options || [],
85
297
  selectedOption: null,
86
298
  };
87
299
  if (config.value) {
88
- state.selectedOption = state.options.find((opt) => opt.id === config.value);
300
+ state.selectedOption = state.options.find((opt) => opt.id === config.value) ?? null;
89
301
  }
90
302
  const menuItems = processMenuItems(state.options);
303
+ const listbox = !state.options.some((option) => option && option.hasSubmenu);
91
304
  const menu = createMenu({
92
305
  opener: component.textfield,
93
306
  items: menuItems,
@@ -100,47 +313,56 @@ export const withMenu = (config) => (component) => {
100
313
  closeOnResize: true,
101
314
  offset: 0,
102
315
  container: component.element,
316
+ ...(listbox ? { manualOpen: true, listbox: true, closeOnEscape: false } : {}),
103
317
  ...(config.menu ?? {}),
104
318
  });
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
- }
319
+ const choose = (option, originalEvent) => {
114
320
  state.selectedOption = option;
115
- component.textfield.setValue(option.text);
321
+ textfield.setValue(option.text);
116
322
  menu.setSelected(option.id);
117
323
  if (component.emit) {
118
- component.emit("change", {
324
+ const changeEvent = {
119
325
  select: component,
120
326
  value: option.id,
121
327
  text: option.text,
122
328
  option,
123
- originalEvent: event.originalEvent,
329
+ originalEvent,
124
330
  preventDefault: () => {
125
- event.defaultPrevented = true;
331
+ changeEvent.defaultPrevented = true;
126
332
  },
127
333
  defaultPrevented: false,
128
- });
334
+ };
335
+ component.emit("change", changeEvent);
129
336
  }
130
- });
131
- component.textfield.element.addEventListener("keydown", (e) => {
132
- if (component.textfield.input.disabled)
337
+ };
338
+ menu.on("select", (event) => {
339
+ if (!event.item || event.item.hasSubmenu) {
133
340
  return;
134
- if ((e.key === "Enter" || e.key === " " || e.key === "ArrowDown") &&
135
- !menu.isOpen()) {
136
- e.preventDefault();
137
- menu.open(e, "keyboard");
138
341
  }
139
- else if (e.key === "Escape" && menu.isOpen()) {
140
- e.preventDefault();
141
- menu.close(e);
342
+ const option = event.item.data;
343
+ if (!isSelectOption(option)) {
344
+ console.warn("Invalid menu selection: missing required data properties");
345
+ return;
142
346
  }
347
+ choose(option, event.originalEvent);
143
348
  });
349
+ if (listbox) {
350
+ setupCombobox(component, textfield, menu, state, choose, config.prefix || "mtrl");
351
+ }
352
+ if (!listbox)
353
+ textfield.element.addEventListener("keydown", (e) => {
354
+ if (textfield.input.disabled)
355
+ return;
356
+ if ((e.key === "Enter" || e.key === " " || e.key === "ArrowDown") &&
357
+ !menu.isOpen()) {
358
+ e.preventDefault();
359
+ menu.open(e, "keyboard");
360
+ }
361
+ else if (e.key === "Escape" && menu.isOpen()) {
362
+ e.preventDefault();
363
+ menu.close(e);
364
+ }
365
+ });
144
366
  menu.on("open", () => {
145
367
  if (component.emit) {
146
368
  component.emit("open", {
@@ -150,29 +372,29 @@ export const withMenu = (config) => (component) => {
150
372
  defaultPrevented: false,
151
373
  });
152
374
  }
153
- component.textfield.element.classList.add(`${config.prefix || "mtrl"}-select--open`);
375
+ textfield.element.classList.add(`${config.prefix || "mtrl"}-select--open`);
154
376
  const PREFIX = config.prefix || "mtrl";
155
- component.textfield.element.classList.add(`${PREFIX}-textfield--focused`);
156
- if (component.textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
157
- component.textfield.element.classList.add(`${PREFIX}-textfield--filled-focused`);
377
+ textfield.element.classList.add(`${PREFIX}-textfield--focused`);
378
+ if (textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
379
+ textfield.element.classList.add(`${PREFIX}-textfield--filled-focused`);
158
380
  }
159
381
  });
160
382
  menu.on("close", (event) => {
161
- component.textfield.element.classList.remove(`${config.prefix || "mtrl"}-select--open`);
383
+ textfield.element.classList.remove(`${config.prefix || "mtrl"}-select--open`);
162
384
  setTimeout(() => {
163
385
  const PREFIX = config.prefix || "mtrl";
164
- const isFocused = document.activeElement === component.textfield.input ||
165
- component.textfield.element.contains(document.activeElement);
386
+ const isFocused = document.activeElement === textfield.input ||
387
+ textfield.element.contains(document.activeElement);
166
388
  if (isFocused) {
167
- component.textfield.element.classList.add(`${PREFIX}-textfield--focused`);
168
- if (component.textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
169
- component.textfield.element.classList.add(`${PREFIX}-textfield--filled-focused`);
389
+ textfield.element.classList.add(`${PREFIX}-textfield--focused`);
390
+ if (textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
391
+ textfield.element.classList.add(`${PREFIX}-textfield--filled-focused`);
170
392
  }
171
393
  }
172
394
  else {
173
- component.textfield.element.classList.remove(`${PREFIX}-textfield--focused`);
174
- if (component.textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
175
- component.textfield.element.classList.remove(`${PREFIX}-textfield--filled-focused`);
395
+ textfield.element.classList.remove(`${PREFIX}-textfield--focused`);
396
+ if (textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
397
+ textfield.element.classList.remove(`${PREFIX}-textfield--filled-focused`);
176
398
  }
177
399
  }
178
400
  }, 10);
@@ -201,21 +423,21 @@ export const withMenu = (config) => (component) => {
201
423
  setValue: (value) => {
202
424
  if (value === null || value === undefined || value === "") {
203
425
  state.selectedOption = null;
204
- component.textfield.setValue("");
426
+ textfield.setValue("");
205
427
  menu.setSelected(null);
206
428
  return component;
207
429
  }
208
430
  const option = state.options.find((opt) => "id" in opt && opt.id === value);
209
431
  if (option && "text" in option) {
210
432
  state.selectedOption = option;
211
- component.textfield.setValue(option.text);
433
+ textfield.setValue(option.text);
212
434
  menu.setSelected(option.id);
213
435
  }
214
436
  return component;
215
437
  },
216
438
  clear: () => {
217
439
  state.selectedOption = null;
218
- component.textfield.setValue("");
440
+ textfield.setValue("");
219
441
  menu.setSelected(null);
220
442
  return component;
221
443
  },
@@ -226,10 +448,11 @@ export const withMenu = (config) => (component) => {
226
448
  state.options = options;
227
449
  const menuItems = processMenuItems(options);
228
450
  menu.setItems(menuItems);
229
- if (state.selectedOption &&
230
- !options.find((opt) => "id" in opt && opt.id === state.selectedOption.id)) {
451
+ const selected = state.selectedOption;
452
+ if (selected &&
453
+ !options.find((opt) => "id" in opt && opt.id === selected.id)) {
231
454
  state.selectedOption = null;
232
- component.textfield.setValue("");
455
+ textfield.setValue("");
233
456
  }
234
457
  return component;
235
458
  },
@@ -1,4 +1,5 @@
1
1
  import { SIDE_SHEET_CLASSES, SIDE_SHEET_VARIANTS } from "../constants.js";
2
+ import { setHTML } from "../../../core/dom/html.js";
2
3
  const CLOSE_ICON = `<svg viewBox="0 -960 960 960" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>`;
3
4
  export const withStructure = (config) => (component) => {
4
5
  const { element, getClass } = component;
@@ -42,7 +43,7 @@ export const withStructure = (config) => (component) => {
42
43
  closeButton = document.createElement("button");
43
44
  closeButton.type = "button";
44
45
  closeButton.className = getClass(SIDE_SHEET_CLASSES.CLOSE);
45
- closeButton.innerHTML = CLOSE_ICON;
46
+ setHTML(closeButton, CLOSE_ICON);
46
47
  closeButton.setAttribute("aria-label", "Close");
47
48
  header.appendChild(closeButton);
48
49
  }
@@ -53,7 +54,7 @@ export const withStructure = (config) => (component) => {
53
54
  if (config.content instanceof HTMLElement)
54
55
  content.appendChild(config.content);
55
56
  else if (typeof config.content === "string")
56
- content.innerHTML = config.content;
57
+ setHTML(content, config.content);
57
58
  container.appendChild(content);
58
59
  element.appendChild(container);
59
60
  (config.container || document.body).appendChild(element);
@@ -66,11 +67,11 @@ export const withStructure = (config) => (component) => {
66
67
  closeButton,
67
68
  content,
68
69
  setContent(next) {
69
- content.innerHTML = "";
70
+ content.replaceChildren();
70
71
  if (next instanceof HTMLElement)
71
72
  content.appendChild(next);
72
73
  else
73
- content.innerHTML = next;
74
+ setHTML(content, next);
74
75
  },
75
76
  setTitle(next) {
76
77
  if (title) {
@@ -2,6 +2,7 @@ import { SLIDER_EVENTS } from "../types.js";
2
2
  import { SLIDER_MEASUREMENTS } from "../constants.js";
3
3
  import { defaultConfig } from "../config.js";
4
4
  import { createHandlers } from "./handlers.js";
5
+ import { setFormValue } from "../../../core/dom/form-value.js";
5
6
  export const withController = (config) => (component) => {
6
7
  if (!component.element) {
7
8
  console.warn("Cannot initialize slider controller: missing element");
@@ -143,6 +144,12 @@ export const withController = (config) => (component) => {
143
144
  try {
144
145
  updateHandlePositions();
145
146
  updateValueBubbles();
147
+ if (component.formFields) {
148
+ setFormValue(component.formFields[0], String(state.value));
149
+ if (state.secondValue !== null) {
150
+ setFormValue(component.formFields[1], String(state.secondValue));
151
+ }
152
+ }
146
153
  if (component.renderTracks) {
147
154
  component.renderTracks(state);
148
155
  }
@@ -13,6 +13,7 @@ export declare const withDom: (config: SliderConfig) => <T extends ElementCompon
13
13
  valueBubble: HTMLElement;
14
14
  secondHandle?: HTMLElement;
15
15
  secondValueBubble?: HTMLElement;
16
+ formFields: (HTMLInputElement | null)[] | null;
16
17
  getContainer: () => HTMLElement;
17
18
  getHandle: () => HTMLElement;
18
19
  getValueBubble: () => HTMLElement;