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
@@ -1,35 +1,47 @@
1
1
  export const withPrefixText = (config) => (component) => {
2
- if (!config.prefixText) {
3
- return component;
4
- }
5
2
  const PREFIX = config.prefix || 'mtrl';
6
- const prefixElement = document.createElement('span');
7
- prefixElement.className = `${PREFIX}-${config.componentName || 'textfield'}-prefix`;
8
- prefixElement.textContent = config.prefixText;
9
- component.element.appendChild(prefixElement);
10
- component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`);
3
+ const NAME = config.componentName || 'textfield';
4
+ let slot = null;
5
+ const ensureSlot = () => {
6
+ if (slot && slot.parentNode)
7
+ return slot;
8
+ const element = document.createElement('span');
9
+ element.className = `${PREFIX}-${NAME}-prefix`;
10
+ component.element.appendChild(element);
11
+ component.element.classList.add(`${PREFIX}-${NAME}--with-prefix`);
12
+ slot = element;
13
+ return element;
14
+ };
15
+ const detachSlot = () => {
16
+ slot?.remove();
17
+ slot = null;
18
+ component.element.classList.remove(`${PREFIX}-${NAME}--with-prefix`);
19
+ };
20
+ const write = (element, value) => {
21
+ element.textContent = value;
22
+ };
23
+ if (config.prefixText) {
24
+ write(ensureSlot(), config.prefixText);
25
+ }
11
26
  if ('lifecycle' in component && component.lifecycle?.destroy) {
12
27
  const originalDestroy = component.lifecycle.destroy;
13
28
  component.lifecycle.destroy = () => {
14
- prefixElement.remove();
29
+ slot?.remove();
30
+ slot = null;
15
31
  originalDestroy.call(component.lifecycle);
16
32
  };
17
33
  }
18
34
  return {
19
35
  ...component,
20
- prefixTextElement: prefixElement,
36
+ prefixTextElement: slot,
21
37
  setPrefixText(text) {
22
- prefixElement.textContent = text;
38
+ this.prefixTextElement = ensureSlot();
39
+ write(this.prefixTextElement, text);
23
40
  return this;
24
41
  },
25
42
  removePrefixText() {
26
- if (prefixElement.parentNode) {
27
- prefixElement.remove();
28
- component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`);
29
- const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
30
- if (labelEl) {
31
- labelEl.style.left = '';
32
- }
43
+ if (slot) {
44
+ detachSlot();
33
45
  this.prefixTextElement = null;
34
46
  }
35
47
  return this;
@@ -1,31 +1,47 @@
1
1
  export const withSuffixText = (config) => (component) => {
2
- if (!config.suffixText) {
3
- return component;
4
- }
5
2
  const PREFIX = config.prefix || 'mtrl';
6
- const suffixElement = document.createElement('span');
7
- suffixElement.className = `${PREFIX}-${config.componentName || 'textfield'}-suffix`;
8
- suffixElement.textContent = config.suffixText;
9
- component.element.appendChild(suffixElement);
10
- component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-suffix`);
3
+ const NAME = config.componentName || 'textfield';
4
+ let slot = null;
5
+ const ensureSlot = () => {
6
+ if (slot && slot.parentNode)
7
+ return slot;
8
+ const element = document.createElement('span');
9
+ element.className = `${PREFIX}-${NAME}-suffix`;
10
+ component.element.appendChild(element);
11
+ component.element.classList.add(`${PREFIX}-${NAME}--with-suffix`);
12
+ slot = element;
13
+ return element;
14
+ };
15
+ const detachSlot = () => {
16
+ slot?.remove();
17
+ slot = null;
18
+ component.element.classList.remove(`${PREFIX}-${NAME}--with-suffix`);
19
+ };
20
+ const write = (element, value) => {
21
+ element.textContent = value;
22
+ };
23
+ if (config.suffixText) {
24
+ write(ensureSlot(), config.suffixText);
25
+ }
11
26
  if ('lifecycle' in component && component.lifecycle?.destroy) {
12
27
  const originalDestroy = component.lifecycle.destroy;
13
28
  component.lifecycle.destroy = () => {
14
- suffixElement.remove();
29
+ slot?.remove();
30
+ slot = null;
15
31
  originalDestroy.call(component.lifecycle);
16
32
  };
17
33
  }
18
34
  return {
19
35
  ...component,
20
- suffixTextElement: suffixElement,
36
+ suffixTextElement: slot,
21
37
  setSuffixText(text) {
22
- suffixElement.textContent = text;
38
+ this.suffixTextElement = ensureSlot();
39
+ write(this.suffixTextElement, text);
23
40
  return this;
24
41
  },
25
42
  removeSuffixText() {
26
- if (suffixElement.parentNode) {
27
- suffixElement.remove();
28
- component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-suffix`);
43
+ if (slot) {
44
+ detachSlot();
29
45
  this.suffixTextElement = null;
30
46
  }
31
47
  return this;
@@ -1,37 +1,54 @@
1
+ import { setHTML } from "../../../core/dom/html.js";
1
2
  export const withTrailingIcon = (config) => (component) => {
2
- if (!config.trailingIcon) {
3
- return component;
4
- }
5
3
  const PREFIX = config.prefix || 'mtrl';
6
- const iconElement = document.createElement('span');
7
- iconElement.className = `${PREFIX}-${config.componentName || 'textfield'}-trailing-icon`;
8
- iconElement.innerHTML = config.trailingIcon;
9
- component.element.appendChild(iconElement);
10
- component.element.classList.add(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
11
- if (component.input) {
12
- component.input.classList.add(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
4
+ const NAME = config.componentName || 'textfield';
5
+ let slot = null;
6
+ const ensureSlot = () => {
7
+ if (slot && slot.parentNode)
8
+ return slot;
9
+ const element = document.createElement('span');
10
+ element.className = `${PREFIX}-${NAME}-trailing-icon`;
11
+ component.element.appendChild(element);
12
+ component.element.classList.add(`${PREFIX}-${NAME}--with-trailing-icon`);
13
+ if (component.input) {
14
+ component.input.classList.add(`${PREFIX}-${NAME}-input--with-trailing-icon`);
15
+ }
16
+ slot = element;
17
+ return element;
18
+ };
19
+ const detachSlot = () => {
20
+ slot?.remove();
21
+ slot = null;
22
+ component.element.classList.remove(`${PREFIX}-${NAME}--with-trailing-icon`);
23
+ if (component.input) {
24
+ component.input.classList.remove(`${PREFIX}-${NAME}-input--with-trailing-icon`);
25
+ }
26
+ };
27
+ const write = (element, value) => {
28
+ setHTML(element, value);
29
+ };
30
+ if (config.trailingIcon) {
31
+ write(ensureSlot(), config.trailingIcon);
13
32
  }
14
33
  if ('lifecycle' in component && component.lifecycle?.destroy) {
15
34
  const originalDestroy = component.lifecycle.destroy;
16
35
  component.lifecycle.destroy = () => {
17
- iconElement.remove();
36
+ slot?.remove();
37
+ slot = null;
18
38
  originalDestroy.call(component.lifecycle);
19
39
  };
20
40
  }
21
41
  return {
22
42
  ...component,
23
- trailingIcon: iconElement,
43
+ trailingIcon: slot,
24
44
  setTrailingIcon(html) {
25
- iconElement.innerHTML = html;
45
+ this.trailingIcon = ensureSlot();
46
+ write(this.trailingIcon, html);
26
47
  return this;
27
48
  },
28
49
  removeTrailingIcon() {
29
- if (iconElement.parentNode) {
30
- iconElement.remove();
31
- component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-trailing-icon`);
32
- if (component.input) {
33
- component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-trailing-icon`);
34
- }
50
+ if (slot) {
51
+ detachSlot();
35
52
  this.trailingIcon = null;
36
53
  }
37
54
  return this;
@@ -21,5 +21,5 @@ interface ApiOptions {
21
21
  * @param options - API options with events and lifecycle methods
22
22
  * @returns TimePicker component API
23
23
  */
24
- export declare const createTimePickerAPI: (baseComponent: ElementComponent, modalElement: HTMLElement, dialogElement: HTMLElement, timeValue: TimeValue, config: TimePickerConfig, options: ApiOptions) => TimePickerComponent;
24
+ export declare const createTimePickerAPI: (baseComponent: ElementComponent, modalElement: HTMLElement, dialogElement: HTMLElement, timeValue: TimeValue, config: TimePickerConfig, options: ApiOptions, formValue?: HTMLInputElement | null) => TimePickerComponent;
25
25
  export {};
@@ -1,9 +1,14 @@
1
1
  import { TIME_PICKER_TYPE, TIME_PICKER_ORIENTATION, TIME_FORMAT, TIME_PERIOD } from "./types.js";
2
2
  import { TIMEPICKER_EVENTS as EVENTS, TIMEPICKER_SELECTORS as SELECTORS } from "./constants.js";
3
- import { formatTime, padZero } from "./utils.js";
3
+ import { formatTime, padZero, formatFormValue } from "./utils.js";
4
4
  import { renderTimePicker } from "./render.js";
5
5
  import { renderClockDial, getTimeValueFromClick } from "./clockdial.js";
6
- export const createTimePickerAPI = (baseComponent, modalElement, dialogElement, timeValue, config, options) => {
6
+ import { setFormValue } from "../../core/dom/form-value.js";
7
+ export const createTimePickerAPI = (baseComponent, modalElement, dialogElement, timeValue, config, options, formValue = null) => {
8
+ const render = () => {
9
+ renderTimePicker(dialogElement, timeValue, config);
10
+ setFormValue(formValue, formatFormValue(timeValue, config.showSeconds === true));
11
+ };
7
12
  let isOpen = !!config.isOpen;
8
13
  const handleKeydown = (event) => {
9
14
  if (event.key === 'Escape') {
@@ -33,7 +38,7 @@ export const createTimePickerAPI = (baseComponent, modalElement, dialogElement,
33
38
  isOpen = true;
34
39
  baseComponent.element.classList.add(`${config.prefix}-time-picker--open`);
35
40
  setTimeout(() => {
36
- renderTimePicker(dialogElement, timeValue, config);
41
+ render();
37
42
  }, 50);
38
43
  options.events.emit(EVENTS.OPEN);
39
44
  if (config.onOpen) {
@@ -83,7 +88,7 @@ export const createTimePickerAPI = (baseComponent, modalElement, dialogElement,
83
88
  timeValue.minutes = minutes;
84
89
  timeValue.seconds = seconds;
85
90
  timeValue.period = hours >= 12 ? TIME_PERIOD.PM : TIME_PERIOD.AM;
86
- renderTimePicker(dialogElement, timeValue, config);
91
+ render();
87
92
  options.events.emit(EVENTS.CHANGE, this.getValue());
88
93
  if (config.onChange) {
89
94
  config.onChange(this.getValue());
@@ -100,7 +105,7 @@ export const createTimePickerAPI = (baseComponent, modalElement, dialogElement,
100
105
  config.type = type;
101
106
  dialogElement.classList.remove(`${config.prefix}-time-picker-dialog--${TIME_PICKER_TYPE.DIAL}`, `${config.prefix}-time-picker-dialog--${TIME_PICKER_TYPE.INPUT}`);
102
107
  dialogElement.classList.add(`${config.prefix}-time-picker-dialog--${type}`);
103
- renderTimePicker(dialogElement, timeValue, config);
108
+ render();
104
109
  return this;
105
110
  },
106
111
  getType() {
@@ -118,7 +123,7 @@ export const createTimePickerAPI = (baseComponent, modalElement, dialogElement,
118
123
  else {
119
124
  timeValue.period = timeValue.hours >= 12 ? TIME_PERIOD.PM : TIME_PERIOD.AM;
120
125
  }
121
- renderTimePicker(dialogElement, timeValue, config);
126
+ render();
122
127
  options.events.emit(EVENTS.CHANGE, this.getValue());
123
128
  return this;
124
129
  },
@@ -131,7 +136,7 @@ export const createTimePickerAPI = (baseComponent, modalElement, dialogElement,
131
136
  config.orientation = orientation;
132
137
  dialogElement.classList.remove(`${config.prefix}-time-picker-dialog--${TIME_PICKER_ORIENTATION.VERTICAL}`, `${config.prefix}-time-picker-dialog--${TIME_PICKER_ORIENTATION.HORIZONTAL}`);
133
138
  dialogElement.classList.add(`${config.prefix}-time-picker-dialog--${orientation}`);
134
- renderTimePicker(dialogElement, timeValue, config);
139
+ render();
135
140
  return this;
136
141
  },
137
142
  getOrientation() {
@@ -144,7 +149,7 @@ export const createTimePickerAPI = (baseComponent, modalElement, dialogElement,
144
149
  titleElement.textContent = title;
145
150
  }
146
151
  else {
147
- renderTimePicker(dialogElement, timeValue, config);
152
+ render();
148
153
  }
149
154
  return this;
150
155
  },
@@ -197,7 +202,7 @@ export const createTimePickerAPI = (baseComponent, modalElement, dialogElement,
197
202
  if (timeValue.hours >= 12) {
198
203
  timeValue.hours -= 12;
199
204
  }
200
- renderTimePicker(dialogElement, timeValue, config);
205
+ render();
201
206
  options.events.emit(EVENTS.CHANGE, timePickerAPI.getValue());
202
207
  if (config.onChange) {
203
208
  config.onChange(timePickerAPI.getValue());
@@ -210,7 +215,7 @@ export const createTimePickerAPI = (baseComponent, modalElement, dialogElement,
210
215
  if (timeValue.hours < 12) {
211
216
  timeValue.hours += 12;
212
217
  }
213
- renderTimePicker(dialogElement, timeValue, config);
218
+ render();
214
219
  options.events.emit(EVENTS.CHANGE, timePickerAPI.getValue());
215
220
  if (config.onChange) {
216
221
  config.onChange(timePickerAPI.getValue());
@@ -372,7 +377,7 @@ export const createTimePickerAPI = (baseComponent, modalElement, dialogElement,
372
377
  newSeconds = 59;
373
378
  timeValue.seconds = newSeconds;
374
379
  }
375
- renderTimePicker(dialogElement, timeValue, config);
380
+ render();
376
381
  options.events.emit(EVENTS.CHANGE, timePickerAPI.getValue());
377
382
  if (config.onChange) {
378
383
  config.onChange(timePickerAPI.getValue());
@@ -2,8 +2,9 @@ import { TIME_PICKER_TYPE, TIME_FORMAT, TIME_PERIOD, } from "./types.js";
2
2
  import { TIMEPICKER_DIAL, TIMEPICKER_ICONS } from "./constants.js";
3
3
  import { padZero, convertTo12Hour } from "./utils.js";
4
4
  import { renderClockDial, getTimeValueFromClick } from "./clockdial.js";
5
+ import { setHTML } from "../../core/dom/html.js";
5
6
  export const renderTimePicker = (container, timeValue, config, onTimeChange) => {
6
- container.innerHTML = "";
7
+ container.replaceChildren();
7
8
  if (config.title) {
8
9
  const title = document.createElement("div");
9
10
  title.className = `${config.prefix}-time-picker-title`;
@@ -117,10 +118,9 @@ export const renderTimePicker = (container, timeValue, config, onTimeChange) =>
117
118
  toggleTypeButton.setAttribute("aria-label", config.type === TIME_PICKER_TYPE.DIAL
118
119
  ? "Switch to keyboard input"
119
120
  : "Switch to dial selector");
120
- toggleTypeButton.innerHTML =
121
- config.type === TIME_PICKER_TYPE.DIAL
122
- ? config.keyboardIcon || TIMEPICKER_ICONS.KEYBOARD
123
- : config.clockIcon || TIMEPICKER_ICONS.CLOCK;
121
+ setHTML(toggleTypeButton, config.type === TIME_PICKER_TYPE.DIAL
122
+ ? config.keyboardIcon || TIMEPICKER_ICONS.KEYBOARD
123
+ : config.clockIcon || TIMEPICKER_ICONS.CLOCK);
124
124
  actions.appendChild(toggleTypeButton);
125
125
  const actionButtons = document.createElement("div");
126
126
  actionButtons.className = `${config.prefix}-time-picker-action-buttons`;
@@ -150,16 +150,15 @@ export const renderTimePicker = (container, timeValue, config, onTimeChange) =>
150
150
  toggleTypeButton.addEventListener("click", () => {
151
151
  if (dialContainer.style.display === "none") {
152
152
  dialContainer.style.display = "block";
153
- toggleTypeButton.innerHTML =
154
- config.keyboardIcon || TIMEPICKER_ICONS.KEYBOARD;
153
+ setHTML(toggleTypeButton, config.keyboardIcon || TIMEPICKER_ICONS.KEYBOARD);
155
154
  toggleTypeButton.setAttribute("aria-label", "Switch to keyboard input");
156
155
  setTimeout(() => {
157
156
  canvas.focus();
158
- const hourElements = document.querySelectorAll(`.${config.prefix}-time-picker-hours`);
157
+ const hourElements = container.querySelectorAll(`.${config.prefix}-time-picker-hours`);
159
158
  hourElements.forEach((el) => el.setAttribute("data-active", "true"));
160
- const minuteElements = document.querySelectorAll(`.${config.prefix}-time-picker-minutes`);
159
+ const minuteElements = container.querySelectorAll(`.${config.prefix}-time-picker-minutes`);
161
160
  minuteElements.forEach((el) => el.setAttribute("data-active", "false"));
162
- const secondElements = document.querySelectorAll(`.${config.prefix}-time-picker-seconds`);
161
+ const secondElements = container.querySelectorAll(`.${config.prefix}-time-picker-seconds`);
163
162
  secondElements.forEach((el) => el.setAttribute("data-active", "false"));
164
163
  renderClockDial(canvas, timeValue, {
165
164
  type: TIME_PICKER_TYPE.DIAL,
@@ -172,7 +171,7 @@ export const renderTimePicker = (container, timeValue, config, onTimeChange) =>
172
171
  }
173
172
  else {
174
173
  dialContainer.style.display = "none";
175
- toggleTypeButton.innerHTML = config.clockIcon || TIMEPICKER_ICONS.CLOCK;
174
+ setHTML(toggleTypeButton, config.clockIcon || TIMEPICKER_ICONS.CLOCK);
176
175
  toggleTypeButton.setAttribute("aria-label", "Switch to dial selector");
177
176
  setTimeout(() => {
178
177
  hoursInput.focus();
@@ -324,13 +323,13 @@ export const renderTimePicker = (container, timeValue, config, onTimeChange) =>
324
323
  : timeValue.hours;
325
324
  hoursInput.value = padZero(displayHours);
326
325
  }
327
- document
326
+ container
328
327
  .querySelectorAll(`.${config.prefix}-time-picker-period-am, .${config.prefix}-time-picker-period-pm`)
329
328
  .forEach((el) => {
330
329
  el.classList.remove(`${config.prefix}-time-picker-period--selected`);
331
330
  el.setAttribute("aria-pressed", "false");
332
331
  });
333
- const selectedPeriod = document.querySelector(`.${config.prefix}-time-picker-period-${period.toLowerCase()}`);
332
+ const selectedPeriod = container.querySelector(`.${config.prefix}-time-picker-period-${period.toLowerCase()}`);
334
333
  if (selectedPeriod) {
335
334
  selectedPeriod.classList.add(`${config.prefix}-time-picker-period--selected`);
336
335
  selectedPeriod.setAttribute("aria-pressed", "true");
@@ -350,18 +349,22 @@ export const renderTimePicker = (container, timeValue, config, onTimeChange) =>
350
349
  }
351
350
  };
352
351
  if (config.format === TIME_FORMAT.AMPM) {
353
- const amPeriodElement = document.querySelector(`.${config.prefix}-time-picker-period-am`);
354
- const pmPeriodElement = document.querySelector(`.${config.prefix}-time-picker-period-pm`);
355
- if (amPeriodElement) {
356
- amPeriodElement.addEventListener("click", () => {
357
- handlePeriodChange(TIME_PERIOD.AM);
352
+ const amPeriodElement = container.querySelector(`.${config.prefix}-time-picker-period-am`);
353
+ const pmPeriodElement = container.querySelector(`.${config.prefix}-time-picker-period-pm`);
354
+ const activateOn = (element, period) => {
355
+ if (!element)
356
+ return;
357
+ element.addEventListener("click", () => handlePeriodChange(period));
358
+ element.addEventListener("keydown", (event) => {
359
+ const key = event.key;
360
+ if (key !== "Enter" && key !== " " && key !== "Spacebar")
361
+ return;
362
+ event.preventDefault();
363
+ handlePeriodChange(period);
358
364
  });
359
- }
360
- if (pmPeriodElement) {
361
- pmPeriodElement.addEventListener("click", () => {
362
- handlePeriodChange(TIME_PERIOD.PM);
363
- });
364
- }
365
+ };
366
+ activateOn(amPeriodElement, TIME_PERIOD.AM);
367
+ activateOn(pmPeriodElement, TIME_PERIOD.PM);
365
368
  }
366
369
  if (config.type === TIME_PICKER_TYPE.DIAL ||
367
370
  dialContainer.style.display === "block") {
@@ -7,7 +7,8 @@ import { createBaseConfig, getContainerConfig, getApiConfig } from "./config.js"
7
7
  import { TIMEPICKER_Z_INDEX as Z_INDEX } from "./constants.js";
8
8
  import { createTimePickerAPI } from "./api.js";
9
9
  import { renderTimePicker } from "./render.js";
10
- import { parseTime } from "./utils.js";
10
+ import { parseTime, formatFormValue } from "./utils.js";
11
+ import { createFormValue } from "../../core/dom/form-value.js";
11
12
  const createTimePicker = (config = {}) => {
12
13
  const baseConfig = createBaseConfig(config);
13
14
  try {
@@ -54,7 +55,8 @@ const createTimePicker = (config = {}) => {
54
55
  };
55
56
  }
56
57
  renderTimePicker(dialogElement, timeValue, baseConfig);
57
- const timePicker = createTimePickerAPI(baseComponent, modalElement, dialogElement, timeValue, baseConfig, getApiConfig(baseComponent));
58
+ const formValue = createFormValue(baseComponent.element, baseConfig.name, formatFormValue(timeValue, baseConfig.showSeconds === true));
59
+ const timePicker = createTimePickerAPI(baseComponent, modalElement, dialogElement, timeValue, baseConfig, getApiConfig(baseComponent), formValue);
58
60
  if (baseConfig.isOpen) {
59
61
  setTimeout(() => timePicker.open(), 0);
60
62
  }
@@ -48,6 +48,13 @@ export interface TimePickerConfig {
48
48
  * @example '14:30'
49
49
  */
50
50
  value?: string;
51
+ /**
52
+ * Form field name. A time picker renders no form control of its own, so it
53
+ * submits nothing unless this is set; with it, a hidden input on the
54
+ * picker's element carries the current time as 24-hour `HH:mm` — or
55
+ * `HH:mm:ss` when `showSeconds` is on — whatever format the picker displays.
56
+ */
57
+ name?: string;
51
58
  /**
52
59
  * Type of time picker to display
53
60
  * @default TIME_PICKER_TYPE.DIAL
@@ -72,3 +72,17 @@ export declare const getTimeFromPosition: (centerX: number, centerY: number, cli
72
72
  * @returns {boolean} Whether the time is within constraints
73
73
  */
74
74
  export declare const isTimeWithinConstraints: (time: TimeValue, minTime?: string, maxTime?: string) => boolean;
75
+ /**
76
+ * Formats a time for submission in a form: 24-hour `HH:mm`, or `HH:mm:ss`
77
+ * when the picker shows seconds.
78
+ *
79
+ * Separate from `formatTime` on purpose. What a form submits should not change
80
+ * because the picker happens to display 12-hour time, and `formatTime` appends
81
+ * seconds whenever the value carries them — which it always does, since the
82
+ * picker stores 0 when seconds are hidden.
83
+ *
84
+ * @param timeValue - Current time value, whose hours are 24-hour
85
+ * @param showSeconds - Whether the picker shows seconds
86
+ * @returns The value to submit
87
+ */
88
+ export declare const formatFormValue: (timeValue: TimeValue, showSeconds: boolean) => string;
@@ -129,3 +129,7 @@ export const isTimeWithinConstraints = (time, minTime, maxTime) => {
129
129
  }
130
130
  return true;
131
131
  };
132
+ export const formatFormValue = (timeValue, showSeconds) => {
133
+ const base = `${padZero(timeValue.hours)}:${padZero(timeValue.minutes)}`;
134
+ return showSeconds ? `${base}:${padZero(timeValue.seconds ?? 0)}` : base;
135
+ };
@@ -182,10 +182,16 @@ export const withAPI = ({ lifecycle }) => (component) => {
182
182
  getClass: component.getClass,
183
183
  setText(text) {
184
184
  const contentNode = document.createTextNode(text);
185
- while (component.element.firstChild !== arrowElement) {
185
+ while (component.element.firstChild &&
186
+ component.element.firstChild !== arrowElement) {
186
187
  component.element.removeChild(component.element.firstChild);
187
188
  }
188
- component.element.insertBefore(contentNode, arrowElement);
189
+ if (arrowElement.parentNode === component.element) {
190
+ component.element.insertBefore(contentNode, arrowElement);
191
+ }
192
+ else {
193
+ component.element.appendChild(contentNode);
194
+ }
189
195
  if (isVisible) {
190
196
  this.updatePosition();
191
197
  }
@@ -1,5 +1,6 @@
1
1
  import { createBase, withElement, withEvents, withLifecycle, } from "../../core/compose/index.js";
2
2
  import { createConfig } from "./config.js";
3
+ import { TOP_APP_BAR_DEFAULTS } from "./constants.js";
3
4
  export const createTopAppBar = (config = {}) => {
4
5
  const componentConfig = createConfig(config);
5
6
  const component = createBase(componentConfig);
@@ -25,7 +26,7 @@ export const createTopAppBar = (config = {}) => {
25
26
  (componentConfig.type === "medium" || componentConfig.type === "large")) {
26
27
  classes.push(`${component.getClass("top-app-bar")}--compressible`);
27
28
  }
28
- return classes.filter(Boolean);
29
+ return classes.filter((name) => Boolean(name));
29
30
  };
30
31
  const enhancedComponent = withElement({
31
32
  tag: componentConfig.tag,
@@ -61,7 +62,7 @@ export const createTopAppBar = (config = {}) => {
61
62
  let isScrolled = false;
62
63
  if (componentConfig.scrollable) {
63
64
  const handleScroll = () => {
64
- const shouldBeScrolled = window.scrollY > componentConfig.scrollThreshold;
65
+ const shouldBeScrolled = window.scrollY > (componentConfig.scrollThreshold ?? TOP_APP_BAR_DEFAULTS.SCROLL_THRESHOLD);
65
66
  if (isScrolled !== shouldBeScrolled) {
66
67
  isScrolled = shouldBeScrolled;
67
68
  if (isScrolled) {
@@ -22,7 +22,7 @@ export interface TouchState {
22
22
  export interface BaseConfig {
23
23
  componentName?: string;
24
24
  prefix?: string;
25
- parent?: HTMLElement | string;
25
+ parent?: HTMLElement | string | null;
26
26
  }
27
27
  /**
28
28
  * Base component interface with prefix utilities
@@ -102,7 +102,7 @@ export const withElement = (options = {}) => (component) => {
102
102
  ...(Array.isArray(options.className)
103
103
  ? options.className
104
104
  : [options.className]),
105
- ].filter(Boolean),
105
+ ].filter((name) => Boolean(name)),
106
106
  attributes: options.attributes || {},
107
107
  id: options.id,
108
108
  name: options.name,
@@ -1,3 +1,4 @@
1
+ import { setHTML } from "../../dom/html.js";
1
2
  const createIcon = (element, config = {}) => {
2
3
  let iconElement = null;
3
4
  const PREFIX = config.prefix || "mtrl";
@@ -12,7 +13,7 @@ const createIcon = (element, config = {}) => {
12
13
  if (config.iconSize) {
13
14
  iconEl.classList.add(`${PREFIX}-icon--${config.iconSize}`);
14
15
  }
15
- iconEl.innerHTML = html;
16
+ setHTML(iconEl, html);
16
17
  return iconEl;
17
18
  };
18
19
  return {
@@ -38,7 +39,7 @@ const createIcon = (element, config = {}) => {
38
39
  }
39
40
  }
40
41
  else if (iconElement && html) {
41
- iconElement.innerHTML = html;
42
+ setHTML(iconElement, html);
42
43
  }
43
44
  return this;
44
45
  },
@@ -28,6 +28,13 @@ export const withLifecycle = () => (component) => {
28
28
  const destroyElement = component.destroy?.bind(component);
29
29
  const emitter = createEmitter();
30
30
  resources.add(() => emitter.clear());
31
+ const unmount = () => {
32
+ if (mounted) {
33
+ mounted = false;
34
+ emitter.emit('unmount');
35
+ emitter.clear();
36
+ }
37
+ };
31
38
  const lifecycle = {
32
39
  onMount: (handler) => emitter.on('mount', handler),
33
40
  onUnmount: (handler) => emitter.on('unmount', handler),
@@ -37,21 +44,13 @@ export const withLifecycle = () => (component) => {
37
44
  emitter.emit('mount');
38
45
  }
39
46
  },
40
- unmount: () => {
41
- if (mounted) {
42
- mounted = false;
43
- emitter.emit('unmount');
44
- emitter.clear();
45
- }
46
- },
47
+ unmount,
47
48
  isMounted: () => mounted,
48
- destroy() {
49
+ destroy: () => {
49
50
  if (destroyed)
50
51
  return;
51
52
  destroyed = true;
52
- if (mounted) {
53
- this.unmount();
54
- }
53
+ unmount();
55
54
  emitter.clear();
56
55
  resources.destroy();
57
56
  if (hasEvents(component)) {
@@ -1,3 +1,4 @@
1
+ import { setHTML } from "../../dom/html.js";
1
2
  const DEFAULT_ICON = `
2
3
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3
4
  <polyline points="20 6 9 17 4 12"></polyline>
@@ -11,7 +12,7 @@ export const withTrack = (config) => (component) => {
11
12
  if (config.icon !== 'none') {
12
13
  const icon = document.createElement('span');
13
14
  icon.className = `${config.prefix}-${config.componentName}-thumb-icon`;
14
- icon.innerHTML = config.icon || DEFAULT_ICON;
15
+ setHTML(icon, config.icon || DEFAULT_ICON);
15
16
  thumb.appendChild(icon);
16
17
  }
17
18
  component.element.appendChild(track);
@@ -86,7 +86,7 @@ export declare const processClassNames: (classNames: string | string[] | null) =
86
86
  export declare const createElementConfig: (config: BaseComponentConfig, options: {
87
87
  tag: string;
88
88
  attributes?: Record<string, unknown>;
89
- className?: string | string[] | null;
89
+ className?: string | (string | null | undefined)[] | null;
90
90
  html?: string;
91
91
  text?: string;
92
92
  forwardEvents?: Record<string, EventCondition>;
@@ -44,7 +44,7 @@ export const createElementConfig = (config, options) => {
44
44
  const combinedClassNames = [
45
45
  ...(Array.isArray(configClasses) ? configClasses : [configClasses]),
46
46
  ...(Array.isArray(optionsClasses) ? optionsClasses : [optionsClasses]),
47
- ].filter(Boolean);
47
+ ].filter((name) => Boolean(name));
48
48
  const elementAttributes = options.attributes || {};
49
49
  return {
50
50
  tag: options.tag,