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
@@ -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";
@@ -7,7 +7,7 @@ import { createBaseConfig, getElementConfig, getApiConfig } from "./config.js";
7
7
  const createSwitch = (config = {}) => {
8
8
  const baseConfig = createBaseConfig(config);
9
9
  try {
10
- const switchComponent = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)), withTextLabel(baseConfig), withInput(baseConfig), withTrack(baseConfig), withSupportingText(baseConfig), withCheckable(baseConfig), withDisabled(baseConfig), withLifecycle(), (comp) => withAPI(getApiConfig(comp))(comp))(baseConfig);
10
+ const switchComponent = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)), withInput(baseConfig), withTextLabel(baseConfig), withTrack(baseConfig), withSupportingText(baseConfig), withCheckable(baseConfig), withDisabled(baseConfig), withLifecycle(), (comp) => withAPI(getApiConfig(comp))(comp))(baseConfig);
11
11
  return switchComponent;
12
12
  }
13
13
  catch (error) {
@@ -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() {
@@ -5,7 +5,7 @@ import { withAPI, getApiConfig } from "./api.js";
5
5
  import { withTabsManagement, withScrollable, withDivider, withIndicator } from "./features.js";
6
6
  import { createTabsConfig, getTabsElementConfig } from "./config.js";
7
7
  import { addTabStateStyles } from "./state.js";
8
- import { setupKeyboardNavigation } from "./utils.js";
8
+ import { setupKeyboardNavigation, syncTabStops } from "./utils.js";
9
9
  const createTabs = (config = {}) => {
10
10
  const baseConfig = createTabsConfig(config);
11
11
  addTabStateStyles();
@@ -18,6 +18,15 @@ const createTabs = (config = {}) => {
18
18
  });
19
19
  }
20
20
  setupKeyboardNavigation(component);
21
+ component.on('change', () => syncTabStops(component));
22
+ for (const method of ['addTab', 'add', 'removeTab', 'setActiveTab']) {
23
+ const original = component[method];
24
+ component[method] = (...args) => {
25
+ const result = original.apply(component, args);
26
+ syncTabStops(component);
27
+ return result;
28
+ };
29
+ }
21
30
  return component;
22
31
  }
23
32
  catch (error) {
@@ -8,6 +8,7 @@ interface TabsHost {
8
8
  getTabs?: () => TabComponent[];
9
9
  getActiveTab?: () => TabComponent | null;
10
10
  setActiveTab?: (tabOrValue: TabComponent | string) => unknown;
11
+ handleTabClick?: (event: unknown, tab: TabComponent) => void;
11
12
  }
12
13
  /**
13
14
  * Gets the active tab from a component
@@ -20,6 +21,13 @@ export declare function getActiveTab(component: TabsHost): TabComponent | null;
20
21
  * @param component - Component with tabs
21
22
  */
22
23
  export declare function updateTabPanels(component: TabsHost): void;
24
+ /**
25
+ * Gives the tablist a single tab stop: the active tab, or the first enabled
26
+ * one when none is active. Every other tab takes tabindex -1 and is reached
27
+ * with the arrow keys (WAI-ARIA tabs pattern).
28
+ * @param component - Tabs component
29
+ */
30
+ export declare function syncTabStops(component: TabsHost): void;
23
31
  /**
24
32
  * Sets up keyboard navigation for tabs
25
33
  * @param component - Tabs component
@@ -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,42 +29,84 @@ 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
+ }
49
+ }
50
+ export function syncTabStops(component) {
51
+ if (typeof component.getTabs !== "function")
52
+ return;
53
+ const tabs = component.getTabs();
54
+ const enabled = tabs.filter((tab) => !tab.element.disabled);
55
+ const active = getActiveTab(component);
56
+ const stop = active && enabled.includes(active) ? active : enabled[0];
57
+ tabs.forEach((tab) => tab.element.setAttribute("tabindex", tab === stop ? "0" : "-1"));
29
58
  }
30
59
  export function setupKeyboardNavigation(component) {
31
60
  if (!component.element)
32
61
  return;
33
- component.element.addEventListener("keydown", (event) => {
34
- if (event.target !== event.currentTarget)
62
+ const tablist = component.element;
63
+ syncTabStops(component);
64
+ tablist.addEventListener("keydown", (event) => {
65
+ const tabElement = event.target?.closest?.('[role="tab"]');
66
+ if (!tabElement || !tablist.contains(tabElement))
35
67
  return;
36
- if (typeof component.getTabs !== "function" ||
37
- typeof component.setActiveTab !== "function")
68
+ if (typeof component.getTabs !== "function")
38
69
  return;
39
- const tabs = component.getTabs();
40
- const currentTab = getActiveTab(component);
41
- const currentIndex = currentTab ? tabs.indexOf(currentTab) : -1;
42
- let newIndex = currentIndex;
70
+ const tabs = component
71
+ .getTabs()
72
+ .filter((tab) => !tab.element.disabled);
73
+ const currentIndex = tabs.findIndex((tab) => tab.element === tabElement);
74
+ if (currentIndex === -1)
75
+ return;
76
+ const rtl = getComputedStyle(tablist).direction === "rtl";
77
+ const last = tabs.length - 1;
78
+ let newIndex;
43
79
  switch (event.key) {
44
80
  case "ArrowRight":
45
- case "ArrowDown":
46
- newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
81
+ newIndex = rtl
82
+ ? (currentIndex > 0 ? currentIndex - 1 : last)
83
+ : (currentIndex < last ? currentIndex + 1 : 0);
47
84
  break;
48
85
  case "ArrowLeft":
49
- case "ArrowUp":
50
- newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
86
+ newIndex = rtl
87
+ ? (currentIndex < last ? currentIndex + 1 : 0)
88
+ : (currentIndex > 0 ? currentIndex - 1 : last);
51
89
  break;
52
90
  case "Home":
53
91
  newIndex = 0;
54
92
  break;
55
93
  case "End":
56
- newIndex = tabs.length - 1;
94
+ newIndex = last;
57
95
  break;
58
96
  default:
59
97
  return;
60
98
  }
61
- if (newIndex !== currentIndex && tabs[newIndex]) {
62
- event.preventDefault();
63
- tabs[newIndex].element.focus();
64
- component.setActiveTab(tabs[newIndex]);
99
+ event.preventDefault();
100
+ const target = tabs[newIndex];
101
+ if (target === tabs[currentIndex])
102
+ return;
103
+ target.element.focus();
104
+ if (typeof component.handleTabClick === "function") {
105
+ component.handleTabClick(null, target);
106
+ }
107
+ else if (typeof component.setActiveTab === "function") {
108
+ component.setActiveTab(target);
65
109
  }
110
+ syncTabStops(component);
66
111
  });
67
112
  }
@@ -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);
@@ -20,6 +20,7 @@ export interface ErrorConfig {
20
20
  * Component with supporting text capabilities (if available)
21
21
  */
22
22
  interface ComponentWithSupportingText extends ElementComponent {
23
+ input?: HTMLInputElement | HTMLTextAreaElement;
23
24
  setSupportingText?: (text: string, isError?: boolean) => void;
24
25
  removeSupportingText?: () => void;
25
26
  supportingTextElement?: HTMLElement | null;
@@ -3,15 +3,25 @@ export const withError = (config) => (component) => {
3
3
  const COMPONENT = config.componentName || "textfield";
4
4
  let errorState = config.error || false;
5
5
  let displacedText = null;
6
+ const markInvalid = (invalid) => {
7
+ if (!component.input)
8
+ return;
9
+ if (invalid)
10
+ component.input.setAttribute("aria-invalid", "true");
11
+ else
12
+ component.input.removeAttribute("aria-invalid");
13
+ };
6
14
  if (errorState) {
7
15
  component.element.classList.add(`${PREFIX}-${COMPONENT}--error`);
8
16
  }
17
+ markInvalid(errorState);
9
18
  return {
10
19
  ...component,
11
20
  errorState,
12
21
  setError(error, message) {
13
22
  errorState = error;
14
23
  component.element.classList.toggle(`${PREFIX}-${COMPONENT}--error`, error);
24
+ markInvalid(error);
15
25
  if (message !== undefined && component.setSupportingText) {
16
26
  if (message) {
17
27
  if (error && displacedText === null) {
@@ -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;