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
@@ -75,9 +75,9 @@ 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;
78
+ const setupCombobox = (component, textfield, menu, state, choose, prefix) => {
79
+ const input = textfield.input;
80
+ const field = textfield.element;
81
81
  const activeClass = `${prefix}-menu-item--active`;
82
82
  const TYPEAHEAD_DELAY = 500;
83
83
  const PAGE = 10;
@@ -291,12 +291,13 @@ export const withMenu = (config) => (component) => {
291
291
  console.warn("Cannot add menu: textfield not found");
292
292
  return component;
293
293
  }
294
+ const textfield = component.textfield;
294
295
  const state = {
295
296
  options: config.options || [],
296
297
  selectedOption: null,
297
298
  };
298
299
  if (config.value) {
299
- state.selectedOption = state.options.find((opt) => opt.id === config.value);
300
+ state.selectedOption = state.options.find((opt) => opt.id === config.value) ?? null;
300
301
  }
301
302
  const menuItems = processMenuItems(state.options);
302
303
  const listbox = !state.options.some((option) => option && option.hasSubmenu);
@@ -317,7 +318,7 @@ export const withMenu = (config) => (component) => {
317
318
  });
318
319
  const choose = (option, originalEvent) => {
319
320
  state.selectedOption = option;
320
- component.textfield.setValue(option.text);
321
+ textfield.setValue(option.text);
321
322
  menu.setSelected(option.id);
322
323
  if (component.emit) {
323
324
  const changeEvent = {
@@ -346,11 +347,11 @@ export const withMenu = (config) => (component) => {
346
347
  choose(option, event.originalEvent);
347
348
  });
348
349
  if (listbox) {
349
- setupCombobox(component, menu, state, choose, config.prefix || "mtrl");
350
+ setupCombobox(component, textfield, menu, state, choose, config.prefix || "mtrl");
350
351
  }
351
352
  if (!listbox)
352
- component.textfield.element.addEventListener("keydown", (e) => {
353
- if (component.textfield.input.disabled)
353
+ textfield.element.addEventListener("keydown", (e) => {
354
+ if (textfield.input.disabled)
354
355
  return;
355
356
  if ((e.key === "Enter" || e.key === " " || e.key === "ArrowDown") &&
356
357
  !menu.isOpen()) {
@@ -371,29 +372,29 @@ export const withMenu = (config) => (component) => {
371
372
  defaultPrevented: false,
372
373
  });
373
374
  }
374
- component.textfield.element.classList.add(`${config.prefix || "mtrl"}-select--open`);
375
+ textfield.element.classList.add(`${config.prefix || "mtrl"}-select--open`);
375
376
  const PREFIX = config.prefix || "mtrl";
376
- component.textfield.element.classList.add(`${PREFIX}-textfield--focused`);
377
- if (component.textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
378
- 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`);
379
380
  }
380
381
  });
381
382
  menu.on("close", (event) => {
382
- component.textfield.element.classList.remove(`${config.prefix || "mtrl"}-select--open`);
383
+ textfield.element.classList.remove(`${config.prefix || "mtrl"}-select--open`);
383
384
  setTimeout(() => {
384
385
  const PREFIX = config.prefix || "mtrl";
385
- const isFocused = document.activeElement === component.textfield.input ||
386
- component.textfield.element.contains(document.activeElement);
386
+ const isFocused = document.activeElement === textfield.input ||
387
+ textfield.element.contains(document.activeElement);
387
388
  if (isFocused) {
388
- component.textfield.element.classList.add(`${PREFIX}-textfield--focused`);
389
- if (component.textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
390
- 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`);
391
392
  }
392
393
  }
393
394
  else {
394
- component.textfield.element.classList.remove(`${PREFIX}-textfield--focused`);
395
- if (component.textfield.element.classList.contains(`${PREFIX}-textfield--filled`)) {
396
- 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`);
397
398
  }
398
399
  }
399
400
  }, 10);
@@ -422,21 +423,21 @@ export const withMenu = (config) => (component) => {
422
423
  setValue: (value) => {
423
424
  if (value === null || value === undefined || value === "") {
424
425
  state.selectedOption = null;
425
- component.textfield.setValue("");
426
+ textfield.setValue("");
426
427
  menu.setSelected(null);
427
428
  return component;
428
429
  }
429
430
  const option = state.options.find((opt) => "id" in opt && opt.id === value);
430
431
  if (option && "text" in option) {
431
432
  state.selectedOption = option;
432
- component.textfield.setValue(option.text);
433
+ textfield.setValue(option.text);
433
434
  menu.setSelected(option.id);
434
435
  }
435
436
  return component;
436
437
  },
437
438
  clear: () => {
438
439
  state.selectedOption = null;
439
- component.textfield.setValue("");
440
+ textfield.setValue("");
440
441
  menu.setSelected(null);
441
442
  return component;
442
443
  },
@@ -447,10 +448,11 @@ export const withMenu = (config) => (component) => {
447
448
  state.options = options;
448
449
  const menuItems = processMenuItems(options);
449
450
  menu.setItems(menuItems);
450
- if (state.selectedOption &&
451
- !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)) {
452
454
  state.selectedOption = null;
453
- component.textfield.setValue("");
455
+ textfield.setValue("");
454
456
  }
455
457
  return component;
456
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;
@@ -1,5 +1,6 @@
1
1
  import { createElement } from "../../../core/dom/create.js";
2
2
  import { setStyles } from "../../../core/dom/utils.js";
3
+ import { createFormValue } from "../../../core/dom/form-value.js";
3
4
  export const withDom = (config) => (component) => {
4
5
  const getClass = (className) => component.getClass(className);
5
6
  const min = config.min || 0;
@@ -97,6 +98,15 @@ export const withDom = (config) => (component) => {
97
98
  left: `${secondValuePercent}%`,
98
99
  });
99
100
  }
101
+ const name = config.name;
102
+ const formFields = name
103
+ ? [
104
+ createFormValue(component.element, name, String(value), isDisabled),
105
+ config.range
106
+ ? createFormValue(component.element, `${name}-end`, String(config.secondValue ?? max), isDisabled)
107
+ : null,
108
+ ]
109
+ : null;
100
110
  return {
101
111
  ...component,
102
112
  container,
@@ -104,6 +114,7 @@ export const withDom = (config) => (component) => {
104
114
  valueBubble,
105
115
  secondHandle,
106
116
  secondValueBubble,
117
+ formFields,
107
118
  getContainer: () => container,
108
119
  getHandle: () => handle,
109
120
  getValueBubble: () => valueBubble,
@@ -51,11 +51,16 @@ export const createHandlers = (config, state, uiRenderer, eventHelpers) => {
51
51
  secondHandle.classList.remove(`${state.component.getClass("slider-handle")}--focused`);
52
52
  try {
53
53
  const focusClass = state.component.getClass("slider-handle--focused");
54
- document.querySelectorAll(`.${focusClass}`).forEach((el) => {
54
+ state.component.element
55
+ .querySelectorAll(`.${focusClass}`)
56
+ .forEach((el) => {
55
57
  el.classList.remove(focusClass);
56
58
  });
57
- if (document.activeElement?.classList.contains(state.component.getClass("slider-handle"))) {
58
- document.activeElement.blur();
59
+ const active = document.activeElement;
60
+ if (active &&
61
+ state.component.element.contains(active) &&
62
+ active.classList.contains(state.component.getClass("slider-handle"))) {
63
+ active.blur();
59
64
  }
60
65
  }
61
66
  catch (error) {
@@ -10,6 +10,12 @@ export const withStates = (config) => (component) => {
10
10
  disableComponent();
11
11
  }, 0);
12
12
  }
13
+ const setFieldsDisabled = (disabled) => {
14
+ for (const field of component.formFields ?? []) {
15
+ if (field)
16
+ field.disabled = disabled;
17
+ }
18
+ };
13
19
  function disableComponent() {
14
20
  component.element.classList.add(`${component.getClass("slider")}--disabled`);
15
21
  component.element.setAttribute("aria-disabled", "true");
@@ -21,6 +27,7 @@ export const withStates = (config) => (component) => {
21
27
  secondHandle.tabIndex = -1;
22
28
  secondHandle.setAttribute("aria-disabled", "true");
23
29
  }
30
+ setFieldsDisabled(true);
24
31
  if (component.renderTracks) {
25
32
  component.renderTracks();
26
33
  }
@@ -36,6 +43,7 @@ export const withStates = (config) => (component) => {
36
43
  secondHandle.tabIndex = 0;
37
44
  secondHandle.setAttribute("aria-disabled", "false");
38
45
  }
46
+ setFieldsDisabled(false);
39
47
  if (component.renderTracks) {
40
48
  component.renderTracks();
41
49
  }
@@ -26,6 +26,13 @@ export interface SliderConfig extends BaseComponentConfig {
26
26
  step?: number;
27
27
  /** Whether the slider is disabled */
28
28
  disabled?: boolean;
29
+ /**
30
+ * Form field name. A slider renders no form control of its own, so it
31
+ * submits nothing unless this is set; with it, a hidden input inside the
32
+ * slider carries the current value. A range slider submits two fields,
33
+ * `name` and `name-end`. A disabled slider submits neither.
34
+ */
35
+ name?: string;
29
36
  /** Color variant of the slider *
30
37
  * A stylesheet can override the drawn colours per slider with the custom
31
38
  * properties `--<prefix>-slider-color` and `--<prefix>-slider-on-color`,
@@ -1,4 +1,4 @@
1
- import { SwitchConfig, BaseComponent, ApiOptions } from "./types.js";
1
+ import { SwitchConfig, ApiComponent, ApiOptions } from "./types.js";
2
2
  /**
3
3
  * Default configuration for the Switch component
4
4
  */
@@ -38,8 +38,8 @@ export declare const getElementConfig: (config: SwitchConfig) => {
38
38
  };
39
39
  /**
40
40
  * Creates API configuration for the Switch component
41
- * @param {BaseComponent} comp - Component with disabled, lifecycle, and checkable features
41
+ * @param {ApiComponent} comp - Component with disabled, lifecycle, and checkable features
42
42
  * @returns {ApiOptions} API configuration object
43
43
  */
44
- export declare const getApiConfig: (comp: BaseComponent) => ApiOptions;
44
+ export declare const getApiConfig: (comp: ApiComponent) => ApiOptions;
45
45
  export default defaultConfig;
@@ -13,17 +13,17 @@ export const getElementConfig = (config) => createElementConfig(config, {
13
13
  });
14
14
  export const getApiConfig = (comp) => ({
15
15
  disabled: {
16
- enable: comp.disabled?.enable,
17
- disable: comp.disabled?.disable,
16
+ enable: comp.disabled.enable,
17
+ disable: comp.disabled.disable,
18
18
  },
19
19
  lifecycle: {
20
- destroy: comp.lifecycle?.destroy,
20
+ destroy: comp.lifecycle.destroy,
21
21
  },
22
22
  checkable: {
23
- check: comp.checkable?.check,
24
- uncheck: comp.checkable?.uncheck,
25
- toggle: comp.checkable?.toggle,
26
- isChecked: comp.checkable?.isChecked,
23
+ check: comp.checkable.check,
24
+ uncheck: comp.checkable.uncheck,
25
+ toggle: comp.checkable.toggle,
26
+ isChecked: comp.checkable.isChecked,
27
27
  },
28
28
  });
29
29
  export default defaultConfig;
@@ -2,9 +2,9 @@ import { withTrack as withTrackCore, } from "../../core/compose/features/index.j
2
2
  const ensureSwitchStructure = (component, prefix, componentName) => {
3
3
  const PREFIX = prefix || "mtrl";
4
4
  const COMPONENT = componentName || "switch";
5
- let container = component.element.querySelector(`.${PREFIX}-${COMPONENT}-container`);
6
- if (!container) {
7
- container = document.createElement("div");
5
+ const existingContainer = component.element.querySelector(`.${PREFIX}-${COMPONENT}-container`);
6
+ if (!existingContainer) {
7
+ const container = document.createElement("div");
8
8
  container.className = `${PREFIX}-${COMPONENT}-container`;
9
9
  const input = component.element.querySelector(`.${PREFIX}-${COMPONENT}-input`);
10
10
  const track = component.element.querySelector(`.${PREFIX}-${COMPONENT}-track`);
@@ -32,9 +32,9 @@ const ensureSwitchStructure = (component, prefix, componentName) => {
32
32
  if (label) {
33
33
  contentWrapper.appendChild(label);
34
34
  }
35
- container.insertBefore(contentWrapper, container.firstChild);
35
+ existingContainer.insertBefore(contentWrapper, existingContainer.firstChild);
36
36
  }
37
- return { container, contentWrapper };
37
+ return { container: existingContainer, contentWrapper };
38
38
  };
39
39
  export const withSupportingText = (config) => (component) => {
40
40
  const PREFIX = config.prefix || "mtrl";
@@ -130,3 +130,13 @@ export interface BaseComponent {
130
130
  setSupportingText?: (text: string, isError?: boolean) => void;
131
131
  removeSupportingText?: () => void;
132
132
  }
133
+ /**
134
+ * Component as it reaches the API step of the pipeline.
135
+ *
136
+ * `BaseComponent` marks the feature controllers optional because a component
137
+ * part-way through the pipeline has not got them yet. By the time
138
+ * `getApiConfig` runs, withCheckable, withDisabled and withLifecycle have all
139
+ * been applied, so they are there.
140
+ * @internal
141
+ */
142
+ export type ApiComponent = BaseComponent & Required<Pick<BaseComponent, "checkable" | "disabled" | "lifecycle">>;
@@ -125,5 +125,6 @@ export interface IndicatorComponent {
125
125
  */
126
126
  export declare const withIndicator: <T extends IndicatorFeatureConfig>(config: T) => <C extends ComponentBase & {
127
127
  tabs: TabComponent[];
128
+ handleTabClick: (event: unknown, tab: TabComponent) => void;
128
129
  }>(component: C) => C & IndicatorComponent;
129
130
  export {};
@@ -1,3 +1,4 @@
1
+ import { setHTML } from "../../core/dom/html.js";
1
2
  export const addScrollIndicators = (tabs, config = {}) => {
2
3
  const { enabled = true, showButtons = false, appearance = 'fade' } = config;
3
4
  if (!enabled)
@@ -17,11 +18,11 @@ export const addScrollIndicators = (tabs, config = {}) => {
17
18
  const leftButton = document.createElement('button');
18
19
  leftButton.className = `${tabs.getClass('tabs')}-scroll-button ${tabs.getClass('tabs')}-scroll-button--left`;
19
20
  leftButton.setAttribute('aria-label', 'Scroll tabs left');
20
- leftButton.innerHTML = '<svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>';
21
+ setHTML(leftButton, '<svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>');
21
22
  const rightButton = document.createElement('button');
22
23
  rightButton.className = `${tabs.getClass('tabs')}-scroll-button ${tabs.getClass('tabs')}-scroll-button--right`;
23
24
  rightButton.setAttribute('aria-label', 'Scroll tabs right');
24
- rightButton.innerHTML = '<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>';
25
+ setHTML(rightButton, '<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>');
25
26
  tabs.element.appendChild(leftButton);
26
27
  tabs.element.appendChild(rightButton);
27
28
  leftButton.addEventListener('click', () => {
@@ -1,7 +1,2 @@
1
1
  import { TabConfig, TabComponent } from "./types.js";
2
- /**
3
- * Creates a new Tab component following MD3 guidelines
4
- * @param {TabConfig} config - Tab configuration object
5
- * @returns {TabComponent} Tab component instance
6
- */
7
2
  export declare const createTab: (config?: TabConfig) => TabComponent;
@@ -5,6 +5,15 @@ import { TAB_LAYOUT } from "./constants.js";
5
5
  import { createTabConfig } from "./config.js";
6
6
  import createButton from "../button/index.js";
7
7
  import createBadge from "../badge/index.js";
8
+ function linkPanel(element, value) {
9
+ const panelId = `tabpanel-${value}`;
10
+ if (typeof document !== "undefined" && document.getElementById(panelId)) {
11
+ element.setAttribute("aria-controls", panelId);
12
+ }
13
+ else {
14
+ element.removeAttribute("aria-controls");
15
+ }
16
+ }
8
17
  export const createTab = (config = {}) => {
9
18
  const baseConfig = createTabConfig(config);
10
19
  try {
@@ -31,7 +40,7 @@ export const createTab = (config = {}) => {
31
40
  baseComponent.element.setAttribute("aria-selected", baseConfig.state === "active" ? "true" : "false");
32
41
  if (baseConfig.value) {
33
42
  baseComponent.element.setAttribute("id", `tab-${baseConfig.value}`);
34
- baseComponent.element.setAttribute("aria-controls", `tabpanel-${baseConfig.value}`);
43
+ linkPanel(baseComponent.element, baseConfig.value);
35
44
  }
36
45
  if (baseConfig.state === "active") {
37
46
  baseComponent.element.classList.add(`${baseComponent.getClass("tab")}--active`);
@@ -53,7 +62,7 @@ export const createTab = (config = {}) => {
53
62
  baseComponent.off(event, handler);
54
63
  return this;
55
64
  },
56
- badge: null,
65
+ badge: undefined,
57
66
  getValue() {
58
67
  return button.getValue();
59
68
  },
@@ -61,7 +70,7 @@ export const createTab = (config = {}) => {
61
70
  const safeValue = value || "";
62
71
  button.setValue(safeValue);
63
72
  this.element.setAttribute("id", `tab-${safeValue}`);
64
- this.element.setAttribute("aria-controls", `tabpanel-${safeValue}`);
73
+ linkPanel(this.element, safeValue);
65
74
  return this;
66
75
  },
67
76
  activate() {
@@ -3,7 +3,7 @@ export function getActiveTab(component) {
3
3
  return component.getActiveTab();
4
4
  }
5
5
  if (Array.isArray(component.tabs)) {
6
- return component.tabs.find((tab) => tab.isActive && tab.isActive());
6
+ return component.tabs.find((tab) => tab.isActive && tab.isActive()) ?? null;
7
7
  }
8
8
  return null;
9
9
  }
@@ -15,8 +15,11 @@ export function updateTabPanels(component) {
15
15
  return;
16
16
  const activeValue = activeTab.getValue();
17
17
  const tabPanels = document.querySelectorAll(`[role="tabpanel"]`);
18
+ const linked = new Set();
18
19
  tabPanels.forEach((panel) => {
19
20
  const forTab = panel.getAttribute("aria-labelledby")?.replace("tab-", "");
21
+ if (forTab)
22
+ linked.add(forTab);
20
23
  if (forTab === activeValue) {
21
24
  panel.removeAttribute("hidden");
22
25
  panel.setAttribute("tabindex", "0");
@@ -26,6 +29,23 @@ export function updateTabPanels(component) {
26
29
  panel.setAttribute("tabindex", "-1");
27
30
  }
28
31
  });
32
+ const allTabs = typeof component.getTabs === "function"
33
+ ? component.getTabs()
34
+ : component.tabs ?? [];
35
+ {
36
+ for (const tab of allTabs) {
37
+ if (typeof tab?.getValue !== "function" || !tab.element)
38
+ continue;
39
+ const value = tab.getValue();
40
+ const panelId = `tabpanel-${value}`;
41
+ if (linked.has(value) && document.getElementById(panelId)) {
42
+ tab.element.setAttribute("aria-controls", panelId);
43
+ }
44
+ else {
45
+ tab.element.removeAttribute("aria-controls");
46
+ }
47
+ }
48
+ }
29
49
  }
30
50
  export function syncTabStops(component) {
31
51
  if (typeof component.getTabs !== "function")
@@ -45,7 +45,9 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
45
45
  getLabel() {
46
46
  return component.label?.getText() || "";
47
47
  },
48
- leadingIcon: component.leadingIcon || null,
48
+ get leadingIcon() {
49
+ return component.leadingIcon ?? null;
50
+ },
49
51
  setLeadingIcon(html) {
50
52
  if (component.setLeadingIcon) {
51
53
  component.setLeadingIcon(html);
@@ -64,7 +66,9 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
64
66
  }
65
67
  return this;
66
68
  },
67
- trailingIcon: component.trailingIcon || null,
69
+ get trailingIcon() {
70
+ return component.trailingIcon ?? null;
71
+ },
68
72
  setTrailingIcon(html) {
69
73
  if (component.setTrailingIcon) {
70
74
  component.setTrailingIcon(html);
@@ -96,7 +100,9 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
96
100
  }
97
101
  return this;
98
102
  },
99
- prefixTextElement: component.prefixTextElement || null,
103
+ get prefixTextElement() {
104
+ return component.prefixTextElement ?? null;
105
+ },
100
106
  setPrefixText(text) {
101
107
  if (component.setPrefixText) {
102
108
  component.setPrefixText(text);
@@ -115,7 +121,9 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
115
121
  }
116
122
  return this;
117
123
  },
118
- suffixTextElement: component.suffixTextElement || null,
124
+ get suffixTextElement() {
125
+ return component.suffixTextElement ?? null;
126
+ },
119
127
  setSuffixText(text) {
120
128
  if (component.setSuffixText) {
121
129
  component.setSuffixText(text);
@@ -1,52 +1,51 @@
1
+ import { setHTML } from "../../../core/dom/html.js";
1
2
  export const withLeadingIcon = (config) => (component) => {
2
- if (!config.leadingIcon) {
3
- return component;
4
- }
5
3
  const PREFIX = config.prefix || 'mtrl';
6
- const iconElement = document.createElement('span');
7
- iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-leading-icon`;
8
- iconElement.innerHTML = config.leadingIcon;
9
- component.element.appendChild(iconElement);
10
- component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
11
- if (component.input) {
12
- component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
4
+ const NAME = config.componentName || 'textfield';
5
+ let iconElement = null;
6
+ const ensureIcon = () => {
7
+ if (iconElement && iconElement.parentNode)
8
+ return iconElement;
9
+ const element = document.createElement('span');
10
+ element.className = `${PREFIX}-${NAME}-leading-icon`;
11
+ component.element.appendChild(element);
12
+ component.element.classList.add(`${PREFIX}-${NAME}--with-leading-icon`);
13
+ if (component.input) {
14
+ component.input.classList.add(`${PREFIX}-${NAME}-input--with-leading-icon`);
15
+ }
16
+ iconElement = element;
17
+ return element;
18
+ };
19
+ const detachIcon = () => {
20
+ iconElement?.remove();
21
+ iconElement = null;
22
+ component.element.classList.remove(`${PREFIX}-${NAME}--with-leading-icon`);
23
+ if (component.input) {
24
+ component.input.classList.remove(`${PREFIX}-${NAME}-input--with-leading-icon`);
25
+ }
26
+ };
27
+ if (config.leadingIcon) {
28
+ setHTML(ensureIcon(), config.leadingIcon);
13
29
  }
14
30
  if ('lifecycle' in component && component.lifecycle?.destroy) {
15
31
  const originalDestroy = component.lifecycle.destroy;
16
32
  component.lifecycle.destroy = () => {
17
- clearTimeout(initialPosition);
18
- iconElement.remove();
33
+ iconElement?.remove();
34
+ iconElement = null;
19
35
  originalDestroy.call(component.lifecycle);
20
36
  };
21
37
  }
22
- const initialPosition = setTimeout(() => {
23
- const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
24
- if (labelEl) {
25
- if (!component.element.classList.contains(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`)) {
26
- labelEl.style.left = '44px';
27
- }
28
- }
29
- }, 10);
30
38
  return {
31
39
  ...component,
32
40
  leadingIcon: iconElement,
33
41
  setLeadingIcon(html) {
34
- iconElement.innerHTML = html;
42
+ this.leadingIcon = ensureIcon();
43
+ setHTML(this.leadingIcon, html);
35
44
  return this;
36
45
  },
37
46
  removeLeadingIcon() {
38
- if (iconElement.parentNode) {
39
- iconElement.remove();
40
- component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
41
- if (component.input) {
42
- component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
43
- }
44
- if (!component.element.classList.contains(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`)) {
45
- const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
46
- if (labelEl) {
47
- labelEl.style.left = '';
48
- }
49
- }
47
+ if (iconElement) {
48
+ detachIcon();
50
49
  this.leadingIcon = null;
51
50
  }
52
51
  return this;