mtrl 0.9.6 → 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 (156) 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 +24 -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/features.js +5 -4
  26. package/dist/components/drawer/features/items.js +3 -2
  27. package/dist/components/list/features/renderer.js +4 -3
  28. package/dist/components/menu/api.d.ts +1 -1
  29. package/dist/components/menu/features/controller.js +4 -3
  30. package/dist/components/menu/features/keyboard.js +7 -5
  31. package/dist/components/menu/features/opener.js +15 -10
  32. package/dist/components/menu/features/position.js +1 -1
  33. package/dist/components/menu/features/submenu.js +7 -1
  34. package/dist/components/menu/types.d.ts +1 -1
  35. package/dist/components/navigation/nav-item.js +4 -3
  36. package/dist/components/navigation/system/mobile.js +3 -2
  37. package/dist/components/navigation-rail/navigation-rail.js +3 -2
  38. package/dist/components/progress/features/canvas.js +0 -2
  39. package/dist/components/search/features/states.js +3 -3
  40. package/dist/components/search/features/structure.js +3 -0
  41. package/dist/components/search/features/suggestions.js +2 -2
  42. package/dist/components/search/types.d.ts +6 -0
  43. package/dist/components/segmented-button/config.d.ts +4 -1
  44. package/dist/components/segmented-button/config.js +1 -1
  45. package/dist/components/segmented-button/segment.d.ts +2 -5
  46. package/dist/components/segmented-button/segment.js +7 -3
  47. package/dist/components/select/features.js +29 -27
  48. package/dist/components/side-sheet/features/structure.js +5 -4
  49. package/dist/components/slider/features/controller.js +7 -0
  50. package/dist/components/slider/features/dom.d.ts +1 -0
  51. package/dist/components/slider/features/dom.js +11 -0
  52. package/dist/components/slider/features/handlers.js +8 -3
  53. package/dist/components/slider/features/states.js +8 -0
  54. package/dist/components/slider/types.d.ts +7 -0
  55. package/dist/components/switch/config.d.ts +3 -3
  56. package/dist/components/switch/config.js +7 -7
  57. package/dist/components/switch/features.js +5 -5
  58. package/dist/components/switch/types.d.ts +10 -0
  59. package/dist/components/tabs/features.d.ts +1 -0
  60. package/dist/components/tabs/scroll-indicators.js +3 -2
  61. package/dist/components/tabs/tab.d.ts +0 -5
  62. package/dist/components/tabs/tab.js +12 -3
  63. package/dist/components/tabs/utils.js +21 -1
  64. package/dist/components/textfield/api.js +12 -4
  65. package/dist/components/textfield/features/leading-icon.js +32 -33
  66. package/dist/components/textfield/features/prefix-text.js +30 -18
  67. package/dist/components/textfield/features/suffix-text.js +30 -14
  68. package/dist/components/textfield/features/trailing-icon.js +36 -19
  69. package/dist/components/timepicker/api.d.ts +1 -1
  70. package/dist/components/timepicker/api.js +16 -11
  71. package/dist/components/timepicker/render.js +27 -24
  72. package/dist/components/timepicker/timepicker.js +4 -2
  73. package/dist/components/timepicker/types.d.ts +7 -0
  74. package/dist/components/timepicker/utils.d.ts +14 -0
  75. package/dist/components/timepicker/utils.js +4 -0
  76. package/dist/components/tooltip/api.js +8 -2
  77. package/dist/components/top-app-bar/top-app-bar.js +3 -2
  78. package/dist/core/compose/component.d.ts +1 -1
  79. package/dist/core/compose/component.js +1 -1
  80. package/dist/core/compose/features/icon.js +3 -2
  81. package/dist/core/compose/features/lifecycle.js +10 -11
  82. package/dist/core/compose/features/track.js +2 -1
  83. package/dist/core/config/component.d.ts +1 -1
  84. package/dist/core/config/component.js +1 -1
  85. package/dist/core/dom/create.js +5 -4
  86. package/dist/core/dom/form-value.d.ts +24 -0
  87. package/dist/core/dom/form-value.js +14 -0
  88. package/dist/core/dom/html.d.ts +34 -0
  89. package/dist/core/dom/html.js +13 -0
  90. package/dist/core/dom/index.d.ts +2 -0
  91. package/dist/core/dom/index.js +1 -0
  92. package/dist/core/dom/utils.js +1 -1
  93. package/dist/core/state/emitter.js +8 -1
  94. package/dist/core/state/index.d.ts +1 -1
  95. package/dist/core/state/store.d.ts +10 -2
  96. package/dist/core/state/store.js +2 -1
  97. package/dist/index.cjs +0 -0
  98. package/dist/package.json +5 -2
  99. package/dist/styles/badge.css +1 -1
  100. package/dist/styles/base.css +2 -2
  101. package/dist/styles/bottom-app-bar.css +1 -1
  102. package/dist/styles/bottom-sheet.css +1 -1
  103. package/dist/styles/button-group.css +1 -1
  104. package/dist/styles/button.css +2 -2
  105. package/dist/styles/card.css +1 -1
  106. package/dist/styles/carousel.css +1 -1
  107. package/dist/styles/checkbox.css +1 -1
  108. package/dist/styles/chips.css +1 -1
  109. package/dist/styles/dialog.css +1 -1
  110. package/dist/styles/divider.css +1 -1
  111. package/dist/styles/drawer.css +1 -1
  112. package/dist/styles/extended-fab.css +1 -1
  113. package/dist/styles/fab.css +1 -1
  114. package/dist/styles/icon-button.css +1 -1
  115. package/dist/styles/list.css +1 -1
  116. package/dist/styles/loading-indicator.css +1 -1
  117. package/dist/styles/menu.css +1 -1
  118. package/dist/styles/navigation-rail.css +1 -1
  119. package/dist/styles/navigation.css +1 -1
  120. package/dist/styles/progress.css +1 -1
  121. package/dist/styles/radios.css +1 -1
  122. package/dist/styles/search.css +1 -1
  123. package/dist/styles/segmented-button.css +1 -1
  124. package/dist/styles/select.css +1 -1
  125. package/dist/styles/side-sheet.css +1 -1
  126. package/dist/styles/slider.css +1 -1
  127. package/dist/styles/snackbar.css +1 -1
  128. package/dist/styles/split-button.css +1 -1
  129. package/dist/styles/switch.css +1 -1
  130. package/dist/styles/tabs.css +1 -1
  131. package/dist/styles/textfield.css +1 -1
  132. package/dist/styles/timepicker.css +1 -1
  133. package/dist/styles/tooltip.css +1 -1
  134. package/dist/styles/top-app-bar.css +1 -1
  135. package/dist/styles/utilities.css +1 -1
  136. package/dist/styles.css +2 -2
  137. package/dist/styles.d.ts +1 -0
  138. package/dist/themes/autumn.css +1 -1
  139. package/dist/themes/baseline.css +1 -1
  140. package/dist/themes/brownbeige.css +1 -1
  141. package/dist/themes/browngreen.css +1 -1
  142. package/dist/themes/desert.css +1 -1
  143. package/dist/themes/forest.css +1 -1
  144. package/dist/themes/highcontrast.css +1 -1
  145. package/dist/themes/legacy.css +1 -1
  146. package/dist/themes/material.css +1 -1
  147. package/dist/themes/ocean.css +1 -1
  148. package/dist/themes/sageivory.css +1 -1
  149. package/dist/themes/spring.css +1 -1
  150. package/dist/themes/summer.css +1 -1
  151. package/dist/themes/sunset.css +1 -1
  152. package/dist/themes/tealcaramel.css +1 -1
  153. package/dist/themes/winter.css +1 -1
  154. package/package.json +7 -3
  155. package/src/styles/base/_reset.scss +9 -2
  156. package/src/styles/components/_button.scss +1 -12
@@ -2,7 +2,7 @@ import { PREFIX } from "../../core/config.js";
2
2
  import { pipe } from "../../core/compose/index.js";
3
3
  import { createBase, withElement } from "../../core/compose/component.js";
4
4
  import { withEvents, withDisabled, withLifecycle, } from "../../core/compose/features/index.js";
5
- import { withAPI } from "./api.js";
5
+ import { withAPI, isReadyForApi } from "./api.js";
6
6
  import { createBaseConfig, getContainerConfig, getInputConfig, getCalendarConfig, getApiConfig, } from "./config.js";
7
7
  import { formatDate, parseDate } from "./utils.js";
8
8
  import { renderCalendar } from "./render.js";
@@ -45,7 +45,7 @@ const createDatePicker = (config = {}) => {
45
45
  updateCalendar() {
46
46
  if (!this.isOpen || !this.calendarElement)
47
47
  return;
48
- this.calendarElement.innerHTML = "";
48
+ this.calendarElement.replaceChildren();
49
49
  const calendar = renderCalendar(this, (...[event, data]) => {
50
50
  switch (event) {
51
51
  case "dateSelected":
@@ -234,6 +234,9 @@ const createDatePicker = (config = {}) => {
234
234
  }
235
235
  };
236
236
  document.addEventListener("click", state.outsideClickHandler);
237
+ if (!isReadyForApi(state)) {
238
+ throw new Error("DatePicker input or outside-click handler is missing");
239
+ }
237
240
  return withAPI(state, getApiConfig(component))(component);
238
241
  }
239
242
  catch (error) {
@@ -349,6 +349,20 @@ export interface DatePickerState {
349
349
  nextYear(): void;
350
350
  render(): void;
351
351
  }
352
+ /**
353
+ * State as it reaches the API step.
354
+ *
355
+ * `DatePickerState` marks these loose because the state object is built before
356
+ * the DOM is. By the time `withAPI` runs, the factory has created the input
357
+ * (throwing if it is not one) and installed the outside-click handler, so both
358
+ * are there — and the public `input` is declared non-optional.
359
+ * @category Components
360
+ * @internal
361
+ */
362
+ export type DatePickerApiState = DatePickerState & {
363
+ input: HTMLInputElement;
364
+ outsideClickHandler: EventListener;
365
+ };
352
366
  /**
353
367
  * Events the calendar renderer emits, each with its payload
354
368
  * @internal
@@ -75,13 +75,13 @@ export const isDateInRange = (date, startDate, endDate) => {
75
75
  export const generateCalendarDates = (year, month, selectedDate = null, rangeEndDate = null, minDate = null, maxDate = null) => {
76
76
  const today = new Date();
77
77
  today.setHours(0, 0, 0, 0);
78
+ const outOfRange = (date) => Boolean((minDate && date < minDate) || (maxDate && date > maxDate));
78
79
  const result = [];
79
80
  const firstDay = getFirstDayOfMonth(year, month);
80
81
  const daysInPrevMonth = getDaysInMonth(year, month - 1);
81
82
  for (let i = firstDay - 1; i >= 0; i--) {
82
83
  const date = new Date(year, month - 1, daysInPrevMonth - i);
83
- const isDisabled = (minDate && date < minDate) ||
84
- (maxDate && date > maxDate);
84
+ const isDisabled = outOfRange(date);
85
85
  result.push({
86
86
  date,
87
87
  day: date.getDate(),
@@ -95,8 +95,7 @@ export const generateCalendarDates = (year, month, selectedDate = null, rangeEnd
95
95
  for (let i = 1; i <= daysInMonth; i++) {
96
96
  const date = new Date(year, month, i);
97
97
  const isSelected = selectedDate ? isSameDay(date, selectedDate) : false;
98
- const isDisabled = (minDate && date < minDate) ||
99
- (maxDate && date > maxDate);
98
+ const isDisabled = outOfRange(date);
100
99
  const calendarDate = {
101
100
  date,
102
101
  day: i,
@@ -117,8 +116,7 @@ export const generateCalendarDates = (year, month, selectedDate = null, rangeEnd
117
116
  const remainingDays = totalDaysNeeded - result.length;
118
117
  for (let i = 1; i <= remainingDays; i++) {
119
118
  const date = new Date(year, month + 1, i);
120
- const isDisabled = (minDate && date < minDate) ||
121
- (maxDate && date > maxDate);
119
+ const isDisabled = outOfRange(date);
122
120
  result.push({
123
121
  date,
124
122
  day: i,
@@ -2,6 +2,7 @@ import { getOverlayConfig } from "./config.js";
2
2
  import createButton from "../button/index.js";
3
3
  import { createDivider } from "../divider/index.js";
4
4
  import { addClass, removeClass } from "../../core/dom/classes.js";
5
+ import { setHTML } from "../../core/dom/html.js";
5
6
  const DIALOG_EVENTS = {
6
7
  OPEN: "open",
7
8
  CLOSE: "close",
@@ -53,12 +54,12 @@ export const withStructure = (config) => (component) => {
53
54
  const closeButton = document.createElement("button");
54
55
  closeButton.classList.add(component.getClass("dialog-header-close"));
55
56
  closeButton.setAttribute("aria-label", "Close dialog");
56
- closeButton.innerHTML = `
57
+ setHTML(closeButton, `
57
58
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
58
59
  <line x1="18" y1="6" x2="6" y2="18"></line>
59
60
  <line x1="6" y1="6" x2="18" y2="18"></line>
60
61
  </svg>
61
- `;
62
+ `);
62
63
  closeButton.addEventListener("click", (e) => {
63
64
  e.preventDefault();
64
65
  e.stopPropagation();
@@ -75,7 +76,7 @@ export const withStructure = (config) => (component) => {
75
76
  content.classList.add(component.getClass("dialog-content"));
76
77
  content.id = contentId;
77
78
  if (config.content) {
78
- content.innerHTML = config.content;
79
+ setHTML(content, config.content);
79
80
  }
80
81
  return content;
81
82
  };
@@ -488,7 +489,7 @@ export const withContent = () => (component) => {
488
489
  return subtitleElement ? subtitleElement.textContent || "" : "";
489
490
  },
490
491
  setContent(content) {
491
- contentElement.innerHTML = content;
492
+ setHTML(contentElement, content);
492
493
  },
493
494
  getContent() {
494
495
  return contentElement.innerHTML;
@@ -1,5 +1,6 @@
1
1
  import { DRAWER_EVENTS } from "../constants.js";
2
2
  import { createRipple, } from "../../../core/compose/features/ripple.js";
3
+ import { setHTML } from "../../../core/dom/html.js";
3
4
  const createItemElement = (item, getClass) => {
4
5
  const el = document.createElement("button");
5
6
  el.className = getClass("drawer__item");
@@ -18,7 +19,7 @@ const createItemElement = (item, getClass) => {
18
19
  if (item.icon) {
19
20
  const iconEl = document.createElement("span");
20
21
  iconEl.className = getClass("drawer__item-icon");
21
- iconEl.innerHTML = item.icon;
22
+ setHTML(iconEl, item.icon);
22
23
  iconEl.setAttribute("aria-hidden", "true");
23
24
  el.appendChild(iconEl);
24
25
  }
@@ -70,7 +71,7 @@ export const withItems = (config) => (component) => {
70
71
  if (!itemsContainer)
71
72
  return;
72
73
  unmountRipples();
73
- itemsContainer.innerHTML = "";
74
+ itemsContainer.replaceChildren();
74
75
  let navIndex = 0;
75
76
  currentItems.forEach((item) => {
76
77
  const type = item.type || "item";
@@ -1,4 +1,5 @@
1
1
  import { LIST_CLASSES, LIST_EVENTS } from "../constants.js";
2
+ import { setHTML } from "../../../core/dom/html.js";
2
3
  export const withRenderer = (config) => component => {
3
4
  if (!component.element) {
4
5
  console.warn('Cannot initialize list renderer: missing element');
@@ -24,9 +25,9 @@ export const withRenderer = (config) => component => {
24
25
  return element;
25
26
  }
26
27
  const renderAllItems = () => {
27
- listContainer.innerHTML = '';
28
+ listContainer.replaceChildren();
28
29
  if (!items || items.length === 0) {
29
- listContainer.innerHTML = '<div class="mtrl-list-empty">No items</div>';
30
+ setHTML(listContainer, '<div class="mtrl-list-empty">No items</div>');
30
31
  return;
31
32
  }
32
33
  const fragment = document.createDocumentFragment();
@@ -84,7 +85,7 @@ export const withRenderer = (config) => component => {
84
85
  if (component.lifecycle?.destroy) {
85
86
  const originalDestroy = component.lifecycle.destroy;
86
87
  component.lifecycle.destroy = () => {
87
- listContainer.innerHTML = '';
88
+ listContainer.replaceChildren();
88
89
  originalDestroy();
89
90
  };
90
91
  }
@@ -15,7 +15,7 @@ interface ApiOptions {
15
15
  getItems: () => MenuContent[];
16
16
  setPosition: (position: MenuPosition) => void;
17
17
  getPosition: () => MenuPosition;
18
- setSelected: (itemId: string) => void;
18
+ setSelected: (itemId: string | null) => void;
19
19
  getSelected: () => string | null;
20
20
  };
21
21
  opener: {
@@ -1,5 +1,6 @@
1
1
  import { createMenuTasks } from "./tasks.js";
2
2
  import { menuOpened, menuClosed } from "./registry.js";
3
+ import { setHTML } from "../../../core/dom/html.js";
3
4
  const withController = (config) => (component) => {
4
5
  if (!component.element) {
5
6
  console.warn("Cannot initialize menu controller: missing element");
@@ -90,7 +91,7 @@ const withController = (config) => (component) => {
90
91
  if (item.icon) {
91
92
  const iconElement = document.createElement("span");
92
93
  iconElement.className = `${component.getClass("menu-item-icon")}`;
93
- iconElement.innerHTML = item.icon;
94
+ setHTML(iconElement, item.icon);
94
95
  contentContainer.appendChild(iconElement);
95
96
  }
96
97
  const textElement = document.createElement("span");
@@ -183,7 +184,7 @@ const withController = (config) => (component) => {
183
184
  target.appendChild(createMenuItem(item, index));
184
185
  }
185
186
  });
186
- component.element.innerHTML = "";
187
+ component.element.replaceChildren();
187
188
  component.element.appendChild(menuList);
188
189
  };
189
190
  const findItemById = (id) => {
@@ -192,7 +193,7 @@ const withController = (config) => (component) => {
192
193
  return item;
193
194
  }
194
195
  if ("submenu" in item && Array.isArray(item.submenu)) {
195
- for (const subItem of item.submenu) {
196
+ for (const subItem of item.submenu ?? []) {
196
197
  if ("id" in subItem && subItem.id === id) {
197
198
  return subItem;
198
199
  }
@@ -39,10 +39,12 @@ export const createKeyboardNavigation = (component) => {
39
39
  if (!parentId)
40
40
  return null;
41
41
  const level = parseInt(menuElement.getAttribute("data-level") || "1", 10);
42
+ const owner = component.element.id;
43
+ const ownerFilter = owner ? `[data-owner="${owner}"]` : "";
42
44
  const scope = level <= 1
43
45
  ? component.element
44
- : document.querySelector(`.${component.getClass("menu--submenu")}[data-level="${level - 1}"]`);
45
- return ((scope ?? document).querySelector(`.${component.getClass("menu-item")}[data-id="${parentId}"]`) ?? null);
46
+ : document.querySelector(`.${component.getClass("menu--submenu")}${ownerFilter}[data-level="${level - 1}"]`);
47
+ return ((scope ?? component.element).querySelector(`.${component.getClass("menu-item")}[data-id="${parentId}"]`) ?? null);
46
48
  };
47
49
  const menuItems = (menuElement) => Array.from(menuElement.querySelectorAll(`.${component.getClass("menu-item")}`));
48
50
  const resetTypeahead = () => {
@@ -173,10 +175,10 @@ export const createKeyboardNavigation = (component) => {
173
175
  if (focusedItemIndex >= 0 &&
174
176
  items[focusedItemIndex].classList.contains(`${component.getClass("menu-item--submenu")}`)) {
175
177
  const itemElement = items[focusedItemIndex];
176
- const itemIndex = parseInt(itemElement.getAttribute("data-index"), 10);
178
+ const itemIndex = parseInt(itemElement.getAttribute("data-index") ?? "0", 10);
177
179
  const parentMenu = itemElement.closest(`.${component.getClass("menu--submenu")}`);
178
180
  const parentItemId = parentMenu?.getAttribute("data-parent-item");
179
- const parentItem = actions.findItemById(parentItemId);
181
+ const parentItem = parentItemId ? actions.findItemById(parentItemId) : null;
180
182
  if (parentItem && parentItem.submenu) {
181
183
  const itemData = parentItem.submenu[itemIndex];
182
184
  actions.handleNestedSubmenuClick(itemData, itemIndex, itemElement);
@@ -187,7 +189,7 @@ export const createKeyboardNavigation = (component) => {
187
189
  if (focusedItemIndex >= 0 &&
188
190
  items[focusedItemIndex].classList.contains(`${component.getClass("menu-item--submenu")}`)) {
189
191
  const itemElement = items[focusedItemIndex];
190
- const itemIndex = parseInt(itemElement.getAttribute("data-index"), 10);
192
+ const itemIndex = parseInt(itemElement.getAttribute("data-index") ?? "0", 10);
191
193
  const itemData = state.items[itemIndex];
192
194
  actions.handleSubmenuClick(itemData, itemIndex, itemElement);
193
195
  }
@@ -128,23 +128,27 @@ const withOpener = (config) => (component) => {
128
128
  const restoreFocusToOpener = () => {
129
129
  if (!state.openerElement)
130
130
  return;
131
- if (state.openerComponent &&
132
- typeof state.openerComponent.focus === "function") {
131
+ const openerComponent = state.openerComponent;
132
+ if (openerComponent && typeof openerComponent.focus === "function") {
133
133
  tasks.requestAnimationFrame(() => {
134
- state.openerComponent.focus();
134
+ openerComponent.focus();
135
135
  });
136
136
  return;
137
137
  }
138
- if (state.openerComponent &&
139
- "input" in state.openerComponent &&
140
- state.openerComponent.input instanceof HTMLElement) {
138
+ if (openerComponent &&
139
+ "input" in openerComponent &&
140
+ openerComponent.input instanceof HTMLElement) {
141
+ const input = openerComponent.input;
141
142
  tasks.requestAnimationFrame(() => {
142
- state.openerComponent.input.focus();
143
+ input.focus();
143
144
  });
144
145
  return;
145
146
  }
147
+ const openerElement = state.openerElement;
148
+ if (!openerElement)
149
+ return;
146
150
  tasks.requestAnimationFrame(() => {
147
- state.openerElement.focus();
151
+ openerElement.focus();
148
152
  });
149
153
  };
150
154
  const handleOpenerClick = (e) => {
@@ -245,7 +249,8 @@ const withOpener = (config) => (component) => {
245
249
  state.activeClass = "";
246
250
  };
247
251
  const { element, component: openerComponent } = resolveOpener(config.opener);
248
- setupOpenerEvents({ element, component: openerComponent });
252
+ if (element)
253
+ setupOpenerEvents({ element, component: openerComponent });
249
254
  if (component.lifecycle) {
250
255
  const originalDestroy = component.lifecycle.destroy || (() => { });
251
256
  component.lifecycle.destroy = () => {
@@ -278,7 +283,7 @@ const withOpener = (config) => (component) => {
278
283
  setOpener(opener) {
279
284
  const resolved = resolveOpener(opener);
280
285
  if (resolved.element) {
281
- setupOpenerEvents(resolved);
286
+ setupOpenerEvents({ element: resolved.element, component: resolved.component });
282
287
  }
283
288
  return component;
284
289
  },
@@ -248,7 +248,7 @@ export const createPositioner = (component, config) => {
248
248
  const positionMenu = (openerElement) => {
249
249
  if (!openerElement || !component.element)
250
250
  return;
251
- positionElement(component.element, openerElement, config.position, false);
251
+ positionElement(component.element, openerElement, config.position ?? "bottom-start", false);
252
252
  };
253
253
  const positionSubmenu = (submenuElement, parentItemElement, level = 1) => {
254
254
  if (!submenuElement || !parentItemElement)
@@ -145,7 +145,7 @@ const withSubmenu = (config) => (component) => {
145
145
  const currentLevel = itemElement.closest(`.${component.getClass("menu--submenu")}`)
146
146
  ? parseInt(itemElement
147
147
  .closest(`.${component.getClass("menu--submenu")}`)
148
- .getAttribute("data-level") || "0", 10) + 1
148
+ ?.getAttribute("data-level") || "0", 10) + 1
149
149
  : 1;
150
150
  closeSubmenu(currentLevel);
151
151
  const existingSubmenuIndex = state.activeSubmenus.findIndex((s) => s.menuItem === itemElement && s.isOpening);
@@ -165,6 +165,12 @@ const withSubmenu = (config) => (component) => {
165
165
  submenuElement.setAttribute("tabindex", "-1");
166
166
  submenuElement.setAttribute("data-level", currentLevel.toString());
167
167
  submenuElement.setAttribute("data-parent-item", item.id);
168
+ let ownerId = component.element.id;
169
+ if (!ownerId) {
170
+ ownerId = `menu-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
171
+ component.element.id = ownerId;
172
+ }
173
+ submenuElement.setAttribute("data-owner", ownerId);
168
174
  submenuElement.style.zIndex = `${1000 + currentLevel * 10}`;
169
175
  const submenuList = document.createElement("ul");
170
176
  submenuList.className = `${component.getClass("menu-list")}`;
@@ -380,7 +380,7 @@ export interface MenuComponent {
380
380
  * @param itemId - ID of the menu item to mark as selected
381
381
  * @returns The menu component for chaining
382
382
  */
383
- setSelected: (itemId: string) => MenuComponent;
383
+ setSelected: (itemId: string | null) => MenuComponent;
384
384
  /**
385
385
  * Gets the currently selected menu item's ID
386
386
  * @returns ID of the selected menu item or null if none is selected
@@ -1,12 +1,13 @@
1
1
  import { NAV_CLASSES } from "./constants.js";
2
+ import { setHTML } from "../../core/dom/html.js";
2
3
  export const createExpandIcon = (prefix) => {
3
4
  const icon = document.createElement('span');
4
5
  icon.className = `${prefix}-${NAV_CLASSES.EXPAND_ICON}`;
5
- icon.innerHTML = `
6
+ setHTML(icon, `
6
7
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
7
8
  <polyline points="9 18 15 12 9 6"></polyline>
8
9
  </svg>
9
- `;
10
+ `);
10
11
  return icon;
11
12
  };
12
13
  export const createNestedContainer = (items, prefix, createItem) => {
@@ -56,7 +57,7 @@ export const createNavItem = (config, container, prefix) => {
56
57
  if (config.icon) {
57
58
  const icon = document.createElement('span');
58
59
  icon.className = `${prefix}-${NAV_CLASSES.ICON}`;
59
- icon.innerHTML = config.icon;
60
+ setHTML(icon, config.icon);
60
61
  itemElement.appendChild(icon);
61
62
  }
62
63
  if (config.label) {
@@ -1,4 +1,5 @@
1
1
  import { hasTouchSupport, normalizeEvent, TOUCH_CONFIG, TOUCH_TARGETS } from "../../../core/utils/mobile.js";
2
+ import { setHTML } from "../../../core/dom/html.js";
2
3
  export const createOverlay = (state, mobileConfig, hideDrawer) => {
3
4
  if (state.overlayElement)
4
5
  return state.overlayElement;
@@ -19,12 +20,12 @@ export const createCloseButton = (state, mobileConfig, hideDrawer) => {
19
20
  state.closeButtonElement = document.createElement('button');
20
21
  state.closeButtonElement.className = mobileConfig.closeButtonClass;
21
22
  state.closeButtonElement.setAttribute('aria-label', 'Close navigation');
22
- state.closeButtonElement.innerHTML = `
23
+ setHTML(state.closeButtonElement, `
23
24
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
24
25
  <line x1="18" y1="6" x2="6" y2="18"></line>
25
26
  <line x1="6" y1="6" x2="18" y2="18"></line>
26
27
  </svg>
27
- `;
28
+ `);
28
29
  state.closeButtonElement.addEventListener('click', () => {
29
30
  hideDrawer();
30
31
  });
@@ -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
  }