mtrl 0.9.5 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +20 -0
  2. package/dist/README.md +20 -0
  3. package/dist/components/bottom-app-bar/bottom-app-bar.js +2 -2
  4. package/dist/components/bottom-sheet/features/structure.js +4 -3
  5. package/dist/components/button/api.js +3 -2
  6. package/dist/components/button/features/progress.js +26 -21
  7. package/dist/components/button-group/config.d.ts +11 -3
  8. package/dist/components/button-group/config.js +1 -1
  9. package/dist/components/card/content.js +5 -4
  10. package/dist/components/carousel/features/slides.js +2 -1
  11. package/dist/components/checkbox/checkbox.js +22 -0
  12. package/dist/components/checkbox/config.d.ts +3 -3
  13. package/dist/components/checkbox/config.js +10 -9
  14. package/dist/components/checkbox/types.d.ts +19 -4
  15. package/dist/components/chips/api.js +3 -1
  16. package/dist/components/chips/chip/api.js +3 -2
  17. package/dist/components/chips/chip/chip.js +6 -4
  18. package/dist/components/chips/config.js +1 -1
  19. package/dist/components/datepicker/api.d.ts +8 -2
  20. package/dist/components/datepicker/api.js +1 -0
  21. package/dist/components/datepicker/config.d.ts +14 -4
  22. package/dist/components/datepicker/datepicker.js +5 -2
  23. package/dist/components/datepicker/types.d.ts +14 -0
  24. package/dist/components/datepicker/utils.js +4 -6
  25. package/dist/components/dialog/config.js +0 -1
  26. package/dist/components/dialog/features.js +10 -8
  27. package/dist/components/dialog/types.d.ts +3 -2
  28. package/dist/components/drawer/features/items.js +3 -2
  29. package/dist/components/list/features/renderer.js +4 -3
  30. package/dist/components/menu/api.d.ts +1 -1
  31. package/dist/components/menu/config.js +2 -2
  32. package/dist/components/menu/features/controller.js +41 -26
  33. package/dist/components/menu/features/keyboard.js +7 -5
  34. package/dist/components/menu/features/opener.js +19 -12
  35. package/dist/components/menu/features/position.js +1 -1
  36. package/dist/components/menu/features/submenu.js +7 -1
  37. package/dist/components/menu/types.d.ts +13 -2
  38. package/dist/components/navigation/nav-item.js +4 -3
  39. package/dist/components/navigation/system/mobile.js +3 -2
  40. package/dist/components/navigation-rail/navigation-rail.js +3 -2
  41. package/dist/components/progress/features/canvas.js +0 -2
  42. package/dist/components/search/features/states.js +3 -3
  43. package/dist/components/search/features/structure.js +3 -0
  44. package/dist/components/search/features/suggestions.js +2 -2
  45. package/dist/components/search/types.d.ts +6 -0
  46. package/dist/components/segmented-button/config.d.ts +4 -1
  47. package/dist/components/segmented-button/config.js +1 -1
  48. package/dist/components/segmented-button/segment.d.ts +2 -5
  49. package/dist/components/segmented-button/segment.js +7 -3
  50. package/dist/components/select/features.js +267 -44
  51. package/dist/components/side-sheet/features/structure.js +5 -4
  52. package/dist/components/slider/features/controller.js +7 -0
  53. package/dist/components/slider/features/dom.d.ts +1 -0
  54. package/dist/components/slider/features/dom.js +11 -0
  55. package/dist/components/slider/features/handlers.js +8 -3
  56. package/dist/components/slider/features/states.js +8 -0
  57. package/dist/components/slider/types.d.ts +7 -0
  58. package/dist/components/switch/config.d.ts +3 -3
  59. package/dist/components/switch/config.js +7 -7
  60. package/dist/components/switch/features.js +5 -5
  61. package/dist/components/switch/switch.js +1 -1
  62. package/dist/components/switch/types.d.ts +10 -0
  63. package/dist/components/tabs/features.d.ts +1 -0
  64. package/dist/components/tabs/scroll-indicators.js +3 -2
  65. package/dist/components/tabs/tab.d.ts +0 -5
  66. package/dist/components/tabs/tab.js +12 -3
  67. package/dist/components/tabs/tabs.js +10 -1
  68. package/dist/components/tabs/utils.d.ts +8 -0
  69. package/dist/components/tabs/utils.js +63 -18
  70. package/dist/components/textfield/api.js +12 -4
  71. package/dist/components/textfield/features/error.d.ts +1 -0
  72. package/dist/components/textfield/features/error.js +10 -0
  73. package/dist/components/textfield/features/leading-icon.js +32 -33
  74. package/dist/components/textfield/features/prefix-text.js +30 -18
  75. package/dist/components/textfield/features/suffix-text.js +30 -14
  76. package/dist/components/textfield/features/supporting-text.d.ts +1 -0
  77. package/dist/components/textfield/features/supporting-text.js +25 -0
  78. package/dist/components/textfield/features/trailing-icon.js +36 -19
  79. package/dist/components/timepicker/api.d.ts +1 -1
  80. package/dist/components/timepicker/api.js +16 -11
  81. package/dist/components/timepicker/render.js +27 -24
  82. package/dist/components/timepicker/timepicker.js +4 -2
  83. package/dist/components/timepicker/types.d.ts +7 -0
  84. package/dist/components/timepicker/utils.d.ts +14 -0
  85. package/dist/components/timepicker/utils.js +4 -0
  86. package/dist/components/tooltip/api.js +27 -2
  87. package/dist/components/top-app-bar/top-app-bar.js +3 -2
  88. package/dist/core/compose/component.d.ts +1 -1
  89. package/dist/core/compose/component.js +1 -1
  90. package/dist/core/compose/features/icon.js +3 -2
  91. package/dist/core/compose/features/input.js +1 -1
  92. package/dist/core/compose/features/lifecycle.js +10 -11
  93. package/dist/core/compose/features/textlabel.js +3 -0
  94. package/dist/core/compose/features/track.js +2 -1
  95. package/dist/core/config/component.d.ts +1 -1
  96. package/dist/core/config/component.js +1 -1
  97. package/dist/core/dom/create.js +5 -4
  98. package/dist/core/dom/form-value.d.ts +24 -0
  99. package/dist/core/dom/form-value.js +14 -0
  100. package/dist/core/dom/html.d.ts +34 -0
  101. package/dist/core/dom/html.js +13 -0
  102. package/dist/core/dom/index.d.ts +2 -0
  103. package/dist/core/dom/index.js +1 -0
  104. package/dist/core/dom/utils.js +1 -1
  105. package/dist/core/state/emitter.js +8 -1
  106. package/dist/core/state/index.d.ts +1 -1
  107. package/dist/core/state/store.d.ts +10 -2
  108. package/dist/core/state/store.js +2 -1
  109. package/dist/index.cjs +0 -0
  110. package/dist/package.json +5 -2
  111. package/dist/styles/badge.css +1 -1
  112. package/dist/styles/base.css +2 -2
  113. package/dist/styles/bottom-app-bar.css +1 -1
  114. package/dist/styles/bottom-sheet.css +2 -2
  115. package/dist/styles/button-group.css +1 -1
  116. package/dist/styles/button.css +2 -2
  117. package/dist/styles/card.css +2 -2
  118. package/dist/styles/carousel.css +1 -1
  119. package/dist/styles/checkbox.css +2 -2
  120. package/dist/styles/chips.css +2 -2
  121. package/dist/styles/dialog.css +2 -2
  122. package/dist/styles/divider.css +1 -1
  123. package/dist/styles/drawer.css +2 -2
  124. package/dist/styles/extended-fab.css +2 -2
  125. package/dist/styles/fab.css +2 -2
  126. package/dist/styles/icon-button.css +2 -2
  127. package/dist/styles/list.css +2 -2
  128. package/dist/styles/loading-indicator.css +1 -1
  129. package/dist/styles/menu.css +2 -2
  130. package/dist/styles/navigation-rail.css +1 -1
  131. package/dist/styles/navigation.css +2 -2
  132. package/dist/styles/progress.css +2 -2
  133. package/dist/styles/radios.css +1 -1
  134. package/dist/styles/search.css +1 -1
  135. package/dist/styles/segmented-button.css +2 -2
  136. package/dist/styles/select.css +2 -2
  137. package/dist/styles/side-sheet.css +2 -2
  138. package/dist/styles/slider.css +1 -1
  139. package/dist/styles/snackbar.css +1 -1
  140. package/dist/styles/split-button.css +2 -2
  141. package/dist/styles/switch.css +2 -2
  142. package/dist/styles/tabs.css +1 -1
  143. package/dist/styles/textfield.css +2 -2
  144. package/dist/styles/timepicker.css +1 -1
  145. package/dist/styles/tooltip.css +2 -2
  146. package/dist/styles/top-app-bar.css +1 -1
  147. package/dist/styles/utilities.css +1 -1
  148. package/dist/styles.css +2 -2
  149. package/dist/styles.d.ts +1 -0
  150. package/dist/themes/autumn.css +1 -1
  151. package/dist/themes/baseline.css +1 -1
  152. package/dist/themes/brownbeige.css +1 -1
  153. package/dist/themes/browngreen.css +1 -1
  154. package/dist/themes/desert.css +1 -1
  155. package/dist/themes/forest.css +1 -1
  156. package/dist/themes/highcontrast.css +1 -1
  157. package/dist/themes/legacy.css +1 -1
  158. package/dist/themes/material.css +1 -1
  159. package/dist/themes/ocean.css +1 -1
  160. package/dist/themes/sageivory.css +1 -1
  161. package/dist/themes/spring.css +1 -1
  162. package/dist/themes/summer.css +1 -1
  163. package/dist/themes/sunset.css +1 -1
  164. package/dist/themes/tealcaramel.css +1 -1
  165. package/dist/themes/winter.css +1 -1
  166. package/package.json +7 -3
  167. package/src/styles/abstract/_mixins.scss +26 -0
  168. package/src/styles/abstract/_variables.scss +25 -11
  169. package/src/styles/base/_reset.scss +9 -2
  170. package/src/styles/components/_bottom-sheet.scss +23 -7
  171. package/src/styles/components/_button.scss +1 -12
  172. package/src/styles/components/_dialog.scss +59 -26
  173. package/src/styles/components/_drawer.scss +60 -7
  174. package/src/styles/components/_menu.scss +12 -7
  175. package/src/styles/components/_side-sheet.scss +33 -4
  176. package/src/styles/components/_tooltip.scss +3 -0
package/README.md CHANGED
@@ -167,6 +167,26 @@ const createNote = (config: NoteConfig): NoteComponent =>
167
167
 
168
168
  The element gets the `mtrl-note` class, `withEvents` adds `on`, `off` and `emit`, and `destroy()` removes the element. Add `withLifecycle()` from `mtrl/core/compose/features` when features need to register cleanup.
169
169
 
170
+ ## Markup and sanitizing
171
+
172
+ Icons and `content` options are markup strings, written with `innerHTML`. Every such write goes through one sink, so you can decide once how markup is treated. Set a policy when the strings can come from users or a CMS, or when your page enforces Trusted Types:
173
+
174
+ ```typescript
175
+ import { configureHTML } from 'mtrl';
176
+
177
+ // A sanitizer
178
+ configureHTML({ sanitize: (html) => DOMPurify.sanitize(html) });
179
+
180
+ // Trusted Types: under `require-trusted-types-for 'script'` a plain string
181
+ // assignment throws, so return a TrustedHTML from a policy your CSP allows
182
+ const policy = window.trustedTypes.createPolicy('mtrl', {
183
+ createHTML: (html) => DOMPurify.sanitize(html),
184
+ });
185
+ configureHTML({ sanitize: (html) => policy.createHTML(html) });
186
+ ```
187
+
188
+ The policy sees every string, the library's own icons included; a `TrustedHTML` value passed as an icon or content skips it. With no policy set, markup is written as it is. Text options (`text`, a card's `text`) never go through `innerHTML`.
189
+
170
190
  ## Upgrading from 0.7
171
191
 
172
192
  0.8.0 aligns the components with Material 3 expressive, and some of that changes the API or the styles:
package/dist/README.md CHANGED
@@ -167,6 +167,26 @@ const createNote = (config: NoteConfig): NoteComponent =>
167
167
 
168
168
  The element gets the `mtrl-note` class, `withEvents` adds `on`, `off` and `emit`, and `destroy()` removes the element. Add `withLifecycle()` from `mtrl/core/compose/features` when features need to register cleanup.
169
169
 
170
+ ## Markup and sanitizing
171
+
172
+ Icons and `content` options are markup strings, written with `innerHTML`. Every such write goes through one sink, so you can decide once how markup is treated. Set a policy when the strings can come from users or a CMS, or when your page enforces Trusted Types:
173
+
174
+ ```typescript
175
+ import { configureHTML } from 'mtrl';
176
+
177
+ // A sanitizer
178
+ configureHTML({ sanitize: (html) => DOMPurify.sanitize(html) });
179
+
180
+ // Trusted Types: under `require-trusted-types-for 'script'` a plain string
181
+ // assignment throws, so return a TrustedHTML from a policy your CSP allows
182
+ const policy = window.trustedTypes.createPolicy('mtrl', {
183
+ createHTML: (html) => DOMPurify.sanitize(html),
184
+ });
185
+ configureHTML({ sanitize: (html) => policy.createHTML(html) });
186
+ ```
187
+
188
+ The policy sees every string, the library's own icons included; a `TrustedHTML` value passed as an icon or content skips it. With no policy set, markup is written as it is. Text options (`text`, a card's `text`) never go through `innerHTML`.
189
+
170
190
  ## Upgrading from 0.7
171
191
 
172
192
  0.8.0 aligns the components with Material 3 expressive, and some of that changes the API or the styles:
@@ -18,7 +18,7 @@ export const createBottomAppBar = (config = {}) => {
18
18
  ? `${component.getClass("bottom-app-bar")}--fab-center`
19
19
  : "",
20
20
  componentConfig.class,
21
- ],
21
+ ].filter((name) => Boolean(name)),
22
22
  attributes: {
23
23
  role: "toolbar",
24
24
  "aria-label": "Bottom app bar",
@@ -63,7 +63,7 @@ export const createBottomAppBar = (config = {}) => {
63
63
  return this;
64
64
  },
65
65
  addFab(fab) {
66
- fabContainer.innerHTML = "";
66
+ fabContainer.replaceChildren();
67
67
  fabContainer.appendChild(fab);
68
68
  this.element.classList.add(`${component.getClass("bottom-app-bar")}--with-fab`);
69
69
  return this;
@@ -1,4 +1,5 @@
1
1
  import { BOTTOM_SHEET_CLASSES, BOTTOM_SHEET_VARIANTS } from "../constants.js";
2
+ import { setHTML } from "../../../core/dom/html.js";
2
3
  export const withStructure = (config) => (component) => {
3
4
  const { element, getClass } = component;
4
5
  const isModal = config.variant === BOTTOM_SHEET_VARIANTS.MODAL;
@@ -46,7 +47,7 @@ export const withStructure = (config) => (component) => {
46
47
  content.appendChild(config.content);
47
48
  }
48
49
  else if (typeof config.content === "string") {
49
- content.innerHTML = config.content;
50
+ setHTML(content, config.content);
50
51
  }
51
52
  container.appendChild(content);
52
53
  element.appendChild(container);
@@ -60,11 +61,11 @@ export const withStructure = (config) => (component) => {
60
61
  title,
61
62
  content,
62
63
  setContent(next) {
63
- content.innerHTML = "";
64
+ content.replaceChildren();
64
65
  if (next instanceof HTMLElement)
65
66
  content.appendChild(next);
66
67
  else
67
- content.innerHTML = next;
68
+ setHTML(content, next);
68
69
  },
69
70
  setTitle(next) {
70
71
  if (title) {
@@ -1,4 +1,5 @@
1
1
  import { addClass } from "../../core/index.js";
2
+ import { setHTML } from "../../core/dom/html.js";
2
3
  export const withAPI = ({ disabled, lifecycle }) => (component) => {
3
4
  const textAPI = {
4
5
  setText: (content) => {
@@ -85,7 +86,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => {
85
86
  else if (!buttonComponent.hasIcon()) {
86
87
  const newIconElement = document.createElement("span");
87
88
  newIconElement.className = `${component.getClass("button-icon")}`;
88
- newIconElement.innerHTML = icon;
89
+ setHTML(newIconElement, icon);
89
90
  if (component.element.firstChild) {
90
91
  component.element.insertBefore(newIconElement, component.element.firstChild);
91
92
  }
@@ -99,7 +100,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => {
99
100
  else {
100
101
  const iconElement = component.icon.getElement();
101
102
  if (iconElement) {
102
- iconElement.innerHTML = icon;
103
+ setHTML(iconElement, icon);
103
104
  }
104
105
  }
105
106
  return buttonComponent;
@@ -36,7 +36,7 @@ export const withProgress = (config) => (component) => {
36
36
  addClass(progress.element, component.getClass("button-progress"));
37
37
  progress.element.style.display = "none";
38
38
  Object.entries(progressEventHandlers).forEach(([event, handler]) => {
39
- progress.on(event, handler);
39
+ p.on(event, handler);
40
40
  });
41
41
  component.progress = progress;
42
42
  return progress;
@@ -51,7 +51,7 @@ export const withProgress = (config) => (component) => {
51
51
  }
52
52
  return component.element.firstChild;
53
53
  };
54
- component.showProgress = async function () {
54
+ const showProgress = async () => {
55
55
  const p = await ensureProgress();
56
56
  if (p.element) {
57
57
  if (!component.element.contains(p.element)) {
@@ -72,11 +72,12 @@ export const withProgress = (config) => (component) => {
72
72
  }
73
73
  return component;
74
74
  };
75
- component.showProgressSync = function () {
76
- component.showProgress();
75
+ component.showProgress = showProgress;
76
+ component.showProgressSync = () => {
77
+ void showProgress();
77
78
  return component;
78
79
  };
79
- component.hideProgress = async function () {
80
+ const hideProgress = async () => {
80
81
  if (!progress)
81
82
  return component;
82
83
  progress.element.style.display = "none";
@@ -87,26 +88,29 @@ export const withProgress = (config) => (component) => {
87
88
  }
88
89
  return component;
89
90
  };
90
- component.hideProgressSync = function () {
91
- component.hideProgress();
91
+ component.hideProgress = hideProgress;
92
+ component.hideProgressSync = () => {
93
+ void hideProgress();
92
94
  return component;
93
95
  };
94
- component.setProgress = async function (value) {
96
+ const setProgress = async (value) => {
95
97
  const p = await ensureProgress();
96
98
  p.setValue(value);
97
99
  return component;
98
100
  };
99
- component.setProgressSync = function (value) {
100
- component.setProgress(value);
101
+ component.setProgress = setProgress;
102
+ component.setProgressSync = (value) => {
103
+ void setProgress(value);
101
104
  return component;
102
105
  };
103
- component.setIndeterminate = async function (indeterminate) {
106
+ const setIndeterminate = async (indeterminate) => {
104
107
  const p = await ensureProgress();
105
108
  p.setIndeterminate(indeterminate);
106
109
  return component;
107
110
  };
108
- component.setIndeterminateSync = function (indeterminate) {
109
- component.setIndeterminate(indeterminate);
111
+ component.setIndeterminate = setIndeterminate;
112
+ component.setIndeterminateSync = (indeterminate) => {
113
+ void setIndeterminate(indeterminate);
110
114
  return component;
111
115
  };
112
116
  const readLabel = () => component.text ? component.text.getText() : component.getText?.();
@@ -116,11 +120,12 @@ export const withProgress = (config) => (component) => {
116
120
  else
117
121
  component.setText?.(value);
118
122
  };
119
- component.setLoading = async function (loading, text) {
123
+ const setLoading = async (loading, text) => {
120
124
  if (loading && !isLoading) {
121
125
  originalText = readLabel() ?? "";
122
126
  isLoading = true;
123
- await component.showProgress();
127
+ component.element.setAttribute("aria-busy", "true");
128
+ await showProgress();
124
129
  if (component.disabled?.disable) {
125
130
  component.disabled.disable();
126
131
  }
@@ -129,7 +134,8 @@ export const withProgress = (config) => (component) => {
129
134
  }
130
135
  else if (!loading && isLoading) {
131
136
  isLoading = false;
132
- await component.hideProgress();
137
+ component.element.removeAttribute("aria-busy");
138
+ await hideProgress();
133
139
  if (component.disabled?.enable) {
134
140
  component.disabled.enable();
135
141
  }
@@ -140,8 +146,9 @@ export const withProgress = (config) => (component) => {
140
146
  }
141
147
  return component;
142
148
  };
143
- component.setLoadingSync = function (loading, text) {
144
- component.setLoading(loading, text);
149
+ component.setLoading = setLoading;
150
+ component.setLoadingSync = (loading, text) => {
151
+ void setLoading(loading, text);
145
152
  return component;
146
153
  };
147
154
  if (component.lifecycle) {
@@ -157,9 +164,7 @@ export const withProgress = (config) => (component) => {
157
164
  };
158
165
  }
159
166
  if (config.showProgress) {
160
- ensureProgress().then(() => {
161
- component.showProgress();
162
- });
167
+ void ensureProgress().then(() => showProgress());
163
168
  }
164
169
  return component;
165
170
  };
@@ -6,11 +6,19 @@ import { ButtonGroupConfig, ButtonGroupVariant, ButtonGroupDensity } from "./typ
6
6
  export declare const DEFAULT_CONFIG: Partial<ButtonGroupConfig>;
7
7
  /**
8
8
  * Creates the base configuration for Button Group component
9
+ *
10
+ * The return type keeps what createComponentConfig guarantees: componentName
11
+ * and prefix are always filled in, so callers building class names from them
12
+ * do not have to treat them as missing.
13
+ *
9
14
  * @param {ButtonGroupConfig} config - User provided configuration
10
- * @returns {ButtonGroupConfig} Complete configuration with defaults applied
15
+ * @returns Complete configuration with defaults applied
11
16
  * @internal
12
17
  */
13
- export declare const createBaseConfig: (config?: ButtonGroupConfig) => ButtonGroupConfig;
18
+ export declare const createBaseConfig: (config?: ButtonGroupConfig) => Partial<ButtonGroupConfig> & {
19
+ componentName: string;
20
+ prefix: string;
21
+ };
14
22
  /**
15
23
  * Generates element configuration for the Button Group container
16
24
  * @param {ButtonGroupConfig} config - Button Group configuration
@@ -50,7 +58,7 @@ export declare const getSizeStyles: (prefix: string, size: ButtonGroupConfig["si
50
58
  * @returns {Object} Button configuration with group-specific settings
51
59
  * @internal
52
60
  */
53
- export declare const getButtonConfig: (buttonConfig: ButtonGroupConfig["buttons"][number], index: number, total: number, groupConfig: ButtonGroupConfig) => {
61
+ export declare const getButtonConfig: (buttonConfig: NonNullable<ButtonGroupConfig["buttons"]>[number], index: number, total: number, groupConfig: ButtonGroupConfig) => {
54
62
  variant: ButtonGroupVariant;
55
63
  size: import("./types.js").ButtonGroupSize;
56
64
  shape: import("./types.js").ButtonGroupShape;
@@ -53,7 +53,7 @@ export const getContainerConfig = (config) => {
53
53
  ? `${config.prefix}-button-group--density-${density}`
54
54
  : null,
55
55
  config.equalWidth ? `${config.prefix}-button-group--equal-width` : null
56
- ],
56
+ ].filter((name) => Boolean(name)),
57
57
  interactive: true
58
58
  };
59
59
  };
@@ -3,6 +3,7 @@ import { pipe } from "../../core/compose/index.js";
3
3
  import { createBase, withElement } from "../../core/compose/component.js";
4
4
  import { createElement } from "../../core/dom/create.js";
5
5
  import { safeUrl } from "../../core/utils/url.js";
6
+ import { setHTML } from "../../core/dom/html.js";
6
7
  export const CARD_CONTENT_PADDING = true;
7
8
  let headerCount = 0;
8
9
  export const createCardContent = (config = {}) => {
@@ -18,14 +19,14 @@ export const createCardContent = (config = {}) => {
18
19
  className: [
19
20
  config.class,
20
21
  config.padding === false ? `${PREFIX}-card-content--no-padding` : null
21
- ],
22
+ ].filter((name) => Boolean(name)),
22
23
  attributes: {
23
24
  'role': 'region',
24
25
  'style': 'display: block; color: inherit;'
25
26
  }
26
27
  }))(baseConfig);
27
28
  if (config.html) {
28
- content.element.innerHTML = config.html;
29
+ setHTML(content.element, config.html);
29
30
  }
30
31
  else if (config.text) {
31
32
  const paragraph = document.createElement("p");
@@ -133,7 +134,7 @@ export const createCardActions = (config = {}) => {
133
134
  config.fullBleed ? `${PREFIX}-card-actions--full-bleed` : null,
134
135
  config.vertical ? `${PREFIX}-card-actions--vertical` : null,
135
136
  config.align ? `${PREFIX}-card-actions--${config.align}` : null
136
- ],
137
+ ].filter((name) => Boolean(name)),
137
138
  attributes: {
138
139
  'role': 'group'
139
140
  }
@@ -171,7 +172,7 @@ export const createCardMedia = (config = {}) => {
171
172
  config.class,
172
173
  config.aspectRatio ? `${PREFIX}-card-media--${config.aspectRatio.replace(':', '-')}` : null,
173
174
  config.contain ? `${PREFIX}-card-media--contain` : null
174
- ]
175
+ ].filter((name) => Boolean(name))
175
176
  }))(baseConfig);
176
177
  if (config.element instanceof HTMLElement) {
177
178
  media.element.appendChild(config.element);
@@ -1,4 +1,5 @@
1
1
  import { safeUrl } from "../../../core/utils/url.js";
2
+ import { setHTML } from "../../../core/dom/html.js";
2
3
  export const withSlides = (config) => (component) => {
3
4
  const prefix = component.getClass("carousel");
4
5
  const scroller = document.createElement("div");
@@ -22,7 +23,7 @@ export const withSlides = (config) => (component) => {
22
23
  el.replaceChildren();
23
24
  if (slide.content !== undefined) {
24
25
  if (typeof slide.content === "string")
25
- el.innerHTML = slide.content;
26
+ setHTML(el, slide.content);
26
27
  else
27
28
  el.appendChild(slide.content);
28
29
  return;
@@ -18,6 +18,28 @@ const enhanceWithCheckable = (component, config) => {
18
18
  syncIndeterminate();
19
19
  return enhanced;
20
20
  };
21
+ const clearIndeterminate = () => {
22
+ if (enhanced.input.indeterminate) {
23
+ enhanced.input.indeterminate = false;
24
+ syncIndeterminate();
25
+ }
26
+ };
27
+ const { checkable } = enhanced;
28
+ if (checkable) {
29
+ const { check, uncheck, toggle } = checkable;
30
+ checkable.check = () => {
31
+ clearIndeterminate();
32
+ return check.call(checkable);
33
+ };
34
+ checkable.uncheck = () => {
35
+ clearIndeterminate();
36
+ return uncheck.call(checkable);
37
+ };
38
+ checkable.toggle = () => {
39
+ clearIndeterminate();
40
+ return toggle.call(checkable);
41
+ };
42
+ }
21
43
  return enhanced;
22
44
  };
23
45
  const createCheckbox = (config = {}) => {
@@ -1,4 +1,4 @@
1
- import { CheckboxConfig, BaseComponent, ApiOptions } from "./types.js";
1
+ import { CheckboxConfig, BaseComponent, ApiComponent, ApiOptions } from "./types.js";
2
2
  /**
3
3
  * Default configuration for the Checkbox component
4
4
  */
@@ -48,8 +48,8 @@ export declare const withCheckIcon: (config: CheckboxConfig) => (component: Base
48
48
  export declare const withLabelPosition: (config: CheckboxConfig) => (component: BaseComponent) => BaseComponent;
49
49
  /**
50
50
  * Creates API configuration for the Checkbox component
51
- * @param {BaseComponent} comp - Component with disabled, lifecycle, and checkable features
51
+ * @param {ApiComponent} comp - Component with disabled, lifecycle, and checkable features
52
52
  * @returns {ApiOptions} API configuration object
53
53
  */
54
- export declare const getApiConfig: (comp: BaseComponent) => ApiOptions;
54
+ export declare const getApiConfig: (comp: ApiComponent) => ApiOptions;
55
55
  export default defaultConfig;
@@ -1,4 +1,5 @@
1
1
  import { createComponentConfig, createElementConfig, } from "../../core/config/component.js";
2
+ import { setHTML } from "../../core/dom/html.js";
2
3
  export const defaultConfig = {
3
4
  variant: "filled",
4
5
  labelPosition: "end",
@@ -12,11 +13,11 @@ export const getElementConfig = (config) => createElementConfig(config, {
12
13
  export const withCheckIcon = (config) => (component) => {
13
14
  const icon = document.createElement("span");
14
15
  icon.className = `${config.prefix}-checkbox-icon`;
15
- icon.innerHTML = `
16
+ setHTML(icon, `
16
17
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
17
18
  <path d="M9.55 14.6L6.35 11.4l-1.9 1.9L9.55 18.4l10.9-10.9-1.9-1.9z"/>
18
19
  </svg>
19
- `;
20
+ `);
20
21
  component.element.appendChild(icon);
21
22
  return component;
22
23
  };
@@ -28,17 +29,17 @@ export const withLabelPosition = (config) => (component) => {
28
29
  };
29
30
  export const getApiConfig = (comp) => ({
30
31
  disabled: {
31
- enable: comp.disabled?.enable,
32
- disable: comp.disabled?.disable,
32
+ enable: comp.disabled.enable,
33
+ disable: comp.disabled.disable,
33
34
  },
34
35
  lifecycle: {
35
- destroy: comp.lifecycle?.destroy,
36
+ destroy: comp.lifecycle.destroy,
36
37
  },
37
38
  checkable: {
38
- check: comp.checkable?.check,
39
- uncheck: comp.checkable?.uncheck,
40
- toggle: comp.checkable?.toggle,
41
- isChecked: comp.checkable?.isChecked,
39
+ check: comp.checkable.check,
40
+ uncheck: comp.checkable.uncheck,
41
+ toggle: comp.checkable.toggle,
42
+ isChecked: comp.checkable.isChecked,
42
43
  },
43
44
  });
44
45
  export default defaultConfig;
@@ -111,7 +111,8 @@ export interface CheckboxComponent {
111
111
  */
112
112
  getValue: () => boolean;
113
113
  /**
114
- * Sets the checkbox's checked state
114
+ * Sets the checkbox's checked state.
115
+ * Clears indeterminate, as a user click does.
115
116
  * @param value - Boolean or string ("true"/"false"/"1"/"0") value
116
117
  * @returns Checkbox component for method chaining
117
118
  */
@@ -128,17 +129,20 @@ export interface CheckboxComponent {
128
129
  */
129
130
  setValueAttribute: (value: string) => CheckboxComponent;
130
131
  /**
131
- * Checks the checkbox (sets checked=true)
132
+ * Checks the checkbox (sets checked=true).
133
+ * Clears indeterminate, as a user click does.
132
134
  * @returns Checkbox component for method chaining
133
135
  */
134
136
  check: () => CheckboxComponent;
135
137
  /**
136
- * Unchecks the checkbox (sets checked=false)
138
+ * Unchecks the checkbox (sets checked=false).
139
+ * Clears indeterminate, as a user click does.
137
140
  * @returns Checkbox component for method chaining
138
141
  */
139
142
  uncheck: () => CheckboxComponent;
140
143
  /**
141
- * Toggles the checkbox's checked state
144
+ * Toggles the checkbox's checked state.
145
+ * Clears indeterminate, as a user click does.
142
146
  * @returns Checkbox component for method chaining
143
147
  */
144
148
  toggle: () => CheckboxComponent;
@@ -250,3 +254,14 @@ export interface BaseComponent {
250
254
  isChecked: () => boolean;
251
255
  };
252
256
  }
257
+ /**
258
+ * Component as it reaches the API step of the pipeline.
259
+ *
260
+ * `BaseComponent` marks the feature controllers optional because a component
261
+ * part-way through the pipeline has not got them yet. By the time
262
+ * `getApiConfig` runs, the checkable, disabled and lifecycle features have all
263
+ * been applied, so they are there.
264
+ * @category Components
265
+ * @internal
266
+ */
267
+ export type ApiComponent = BaseComponent & Required<Pick<BaseComponent, "checkable" | "disabled" | "lifecycle">>;
@@ -37,7 +37,9 @@ export const withAPI = (options) => (component) => {
37
37
  getValue() {
38
38
  if (options.chips &&
39
39
  typeof options.chips.getSelectedValues === "function") {
40
- const values = options.chips.getSelectedValues();
40
+ const values = options.chips
41
+ .getSelectedValues()
42
+ .filter((value) => value !== null);
41
43
  if (!options.config?.multiSelect) {
42
44
  return values.length > 0 ? values[0] : null;
43
45
  }
@@ -1,3 +1,4 @@
1
+ import { setHTML } from "../../../core/dom/html.js";
1
2
  export const withAPI = ({ disabled, lifecycle }) => (component) => {
2
3
  let isSelected = component.element.classList.contains(`${component.getClass("chip")}--selected`);
3
4
  let chipValue = component.element.getAttribute("data-value") || null;
@@ -118,7 +119,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => {
118
119
  contentContainer.insertBefore(leadingIconElement, contentContainer.firstChild);
119
120
  }
120
121
  if (leadingIconElement) {
121
- leadingIconElement.innerHTML = icon || "";
122
+ setHTML(leadingIconElement, icon || "");
122
123
  if (!icon && leadingIconElement.parentNode) {
123
124
  leadingIconElement.parentNode.removeChild(leadingIconElement);
124
125
  }
@@ -141,7 +142,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => {
141
142
  }
142
143
  }
143
144
  if (trailingIconElement) {
144
- trailingIconElement.innerHTML = icon || "";
145
+ setHTML(trailingIconElement, icon || "");
145
146
  if (!icon && trailingIconElement.parentNode) {
146
147
  trailingIconElement.parentNode.removeChild(trailingIconElement);
147
148
  }
@@ -3,6 +3,7 @@ import { createBase, withElement } from "../../../core/compose/component.js";
3
3
  import { withEvents, withVariant, withRipple, withDisabled, withLifecycle, } from "../../../core/compose/features/index.js";
4
4
  import { withAPI } from "./api.js";
5
5
  import { createBaseConfig, getElementConfig, getApiConfig } from "./config.js";
6
+ import { setHTML } from "../../../core/dom/html.js";
6
7
  const createChip = (config = {}) => {
7
8
  const baseConfig = createBaseConfig(config);
8
9
  try {
@@ -17,7 +18,7 @@ const createChip = (config = {}) => {
17
18
  if (config.leadingIcon || config.icon) {
18
19
  const leadingIconElement = document.createElement("span");
19
20
  leadingIconElement.className = `${chip.getClass("chip")}-leading-icon`;
20
- leadingIconElement.innerHTML = config.leadingIcon || config.icon || "";
21
+ setHTML(leadingIconElement, config.leadingIcon || config.icon || "");
21
22
  contentContainer.appendChild(leadingIconElement);
22
23
  }
23
24
  if (config.text) {
@@ -29,11 +30,12 @@ const createChip = (config = {}) => {
29
30
  if (config.trailingIcon) {
30
31
  const trailingIconElement = document.createElement("span");
31
32
  trailingIconElement.className = `${chip.getClass("chip")}-trailing-icon`;
32
- trailingIconElement.innerHTML = config.trailingIcon;
33
- if (config.onTrailingIconClick) {
33
+ setHTML(trailingIconElement, config.trailingIcon);
34
+ const onTrailingIconClick = config.onTrailingIconClick;
35
+ if (onTrailingIconClick) {
34
36
  trailingIconElement.addEventListener("click", (e) => {
35
37
  e.stopPropagation();
36
- config.onTrailingIconClick(chip);
38
+ onTrailingIconClick(chip);
37
39
  });
38
40
  }
39
41
  contentContainer.appendChild(trailingIconElement);
@@ -4,7 +4,7 @@ export const defaultConfig = {
4
4
  scrollable: false,
5
5
  vertical: false,
6
6
  multiSelect: false,
7
- onChange: null,
7
+ onChange: undefined,
8
8
  selector: null,
9
9
  labelPosition: "start",
10
10
  };
@@ -1,7 +1,7 @@
1
1
  import type { ElementComponent } from "../../core/compose/component.js";
2
2
  import type { DisabledComponent } from "../../core/compose/features/disabled.js";
3
3
  import type { LifecycleComponent } from "../../core/compose/features/lifecycle.js";
4
- import { DatePickerComponent, DatePickerState, ApiOptions } from "./types.js";
4
+ import { DatePickerComponent, DatePickerState, DatePickerApiState, ApiOptions } from "./types.js";
5
5
  /**
6
6
  * Enhances a datepicker component with API methods
7
7
  * @param {object} state - Current state of the datepicker
@@ -9,4 +9,10 @@ import { DatePickerComponent, DatePickerState, ApiOptions } from "./types.js";
9
9
  * @returns {Function} Higher-order function that adds API methods to component
10
10
  * @internal This is an internal utility for the DatePicker component
11
11
  */
12
- export declare const withAPI: (state: DatePickerState, { disabled, lifecycle, events }: ApiOptions) => (component: ElementComponent & DisabledComponent & LifecycleComponent) => DatePickerComponent;
12
+ /**
13
+ * The API's precondition, checked rather than asserted: the factory creates the
14
+ * input and installs the outside-click handler before it builds the API, and
15
+ * this verifies that at runtime instead of casting past it.
16
+ */
17
+ export declare const isReadyForApi: (state: DatePickerState) => state is DatePickerApiState;
18
+ export declare const withAPI: (state: DatePickerApiState, { disabled, lifecycle, events }: ApiOptions) => (component: ElementComponent & DisabledComponent & LifecycleComponent) => DatePickerComponent;
@@ -1,4 +1,5 @@
1
1
  import { formatDate, parseDate, addMonths } from "./utils.js";
2
+ export const isReadyForApi = (state) => state.input !== null && state.outsideClickHandler !== undefined;
2
3
  export const withAPI = (state, { disabled, lifecycle, events }) => (component) => {
3
4
  const calendar = {
4
5
  goToDate(date) {
@@ -6,12 +6,22 @@ import { DatePickerConfig } from "./types.js";
6
6
  * Default configuration for the DatePicker component
7
7
  */
8
8
  export declare const defaultConfig: DatePickerConfig;
9
+ /**
10
+ * Configuration as it leaves `createBaseConfig`.
11
+ *
12
+ * `DatePickerConfig` marks these optional because a caller may omit them.
13
+ * Once the defaults have been merged and `closeOnSelect` derived from the
14
+ * variant, they are all present — and the state the component builds from
15
+ * this declares them non-optional. Saying so here is what lets that hold
16
+ * without a cast.
17
+ */
18
+ export type ResolvedDatePickerConfig = DatePickerConfig & Required<Pick<DatePickerConfig, "variant" | "initialView" | "selectionMode" | "dateFormat" | "closeOnSelect">>;
9
19
  /**
10
20
  * Creates the base configuration for DatePicker component
11
21
  * @param {DatePickerConfig} config - User provided configuration
12
- * @returns {DatePickerConfig} Complete configuration with defaults applied
22
+ * @returns {ResolvedDatePickerConfig} Complete configuration with defaults applied
13
23
  */
14
- export declare const createBaseConfig: (config?: DatePickerConfig) => DatePickerConfig;
24
+ export declare const createBaseConfig: (config?: DatePickerConfig) => ResolvedDatePickerConfig;
15
25
  /**
16
26
  * Generates element configuration for the DatePicker container
17
27
  * @param {DatePickerConfig} config - DatePicker configuration
@@ -44,7 +54,7 @@ export declare const getContainerConfig: (config: DatePickerConfig) => {
44
54
  * @param {DatePickerConfig} config - DatePicker configuration
45
55
  * @returns {Object} Element configuration object for input field
46
56
  */
47
- export declare const getInputConfig: (config: DatePickerConfig) => {
57
+ export declare const getInputConfig: (config: ResolvedDatePickerConfig) => {
48
58
  tag: string;
49
59
  componentName: string;
50
60
  attributes: Record<string, unknown>;
@@ -71,7 +81,7 @@ export declare const getInputConfig: (config: DatePickerConfig) => {
71
81
  * @param {DatePickerConfig} config - DatePicker configuration
72
82
  * @returns {Object} Element configuration object for calendar container
73
83
  */
74
- export declare const getCalendarConfig: (config: DatePickerConfig) => {
84
+ export declare const getCalendarConfig: (config: ResolvedDatePickerConfig) => {
75
85
  tag: string;
76
86
  componentName: string;
77
87
  attributes: Record<string, unknown>;