mtrl 0.9.4 → 0.9.6

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 (106) hide show
  1. package/dist/components/badge/api.d.ts +0 -1
  2. package/dist/components/badge/api.js +3 -1
  3. package/dist/components/bottom-app-bar/bottom-app-bar.js +9 -2
  4. package/dist/components/button/features/progress.d.ts +5 -0
  5. package/dist/components/button/features/progress.js +16 -12
  6. package/dist/components/card/api.js +17 -14
  7. package/dist/components/card/config.js +1 -1
  8. package/dist/components/card/content.js +2 -5
  9. package/dist/components/dialog/config.js +0 -1
  10. package/dist/components/dialog/features.js +5 -4
  11. package/dist/components/dialog/types.d.ts +3 -2
  12. package/dist/components/divider/features.d.ts +2 -46
  13. package/dist/components/divider/features.js +54 -115
  14. package/dist/components/menu/config.js +2 -2
  15. package/dist/components/menu/features/controller.js +37 -23
  16. package/dist/components/menu/features/opener.js +4 -2
  17. package/dist/components/menu/types.d.ts +12 -1
  18. package/dist/components/radios/api.d.ts +4 -6
  19. package/dist/components/radios/api.js +5 -25
  20. package/dist/components/radios/radio.js +8 -33
  21. package/dist/components/search/features/states.js +2 -0
  22. package/dist/components/search/features/suggestions.js +3 -0
  23. package/dist/components/select/api.js +2 -0
  24. package/dist/components/select/features.js +252 -31
  25. package/dist/components/slider/features/controller.js +11 -2
  26. package/dist/components/switch/switch.js +1 -1
  27. package/dist/components/tabs/api.js +12 -6
  28. package/dist/components/tabs/features.js +5 -7
  29. package/dist/components/tabs/tabs.js +16 -1
  30. package/dist/components/tabs/utils.d.ts +8 -0
  31. package/dist/components/tabs/utils.js +42 -17
  32. package/dist/components/textfield/features/error.d.ts +2 -0
  33. package/dist/components/textfield/features/error.js +22 -0
  34. package/dist/components/textfield/features/supporting-text.d.ts +1 -0
  35. package/dist/components/textfield/features/supporting-text.js +25 -0
  36. package/dist/components/tooltip/api.js +42 -7
  37. package/dist/components/top-app-bar/top-app-bar.js +3 -0
  38. package/dist/core/compose/features/input.js +1 -1
  39. package/dist/core/compose/features/textinput.d.ts +4 -0
  40. package/dist/core/compose/features/textinput.js +1 -0
  41. package/dist/core/compose/features/textlabel.js +3 -0
  42. package/dist/index.cjs +0 -0
  43. package/dist/package.json +1 -1
  44. package/dist/styles/badge.css +1 -1
  45. package/dist/styles/base.css +2 -2
  46. package/dist/styles/bottom-app-bar.css +1 -1
  47. package/dist/styles/bottom-sheet.css +2 -2
  48. package/dist/styles/button-group.css +1 -1
  49. package/dist/styles/button.css +2 -2
  50. package/dist/styles/card.css +2 -2
  51. package/dist/styles/carousel.css +1 -1
  52. package/dist/styles/checkbox.css +2 -2
  53. package/dist/styles/chips.css +2 -2
  54. package/dist/styles/dialog.css +2 -2
  55. package/dist/styles/divider.css +1 -1
  56. package/dist/styles/drawer.css +2 -2
  57. package/dist/styles/extended-fab.css +2 -2
  58. package/dist/styles/fab.css +2 -2
  59. package/dist/styles/icon-button.css +2 -2
  60. package/dist/styles/list.css +2 -2
  61. package/dist/styles/loading-indicator.css +1 -1
  62. package/dist/styles/menu.css +2 -2
  63. package/dist/styles/navigation-rail.css +1 -1
  64. package/dist/styles/navigation.css +2 -2
  65. package/dist/styles/progress.css +2 -2
  66. package/dist/styles/radios.css +1 -1
  67. package/dist/styles/search.css +1 -1
  68. package/dist/styles/segmented-button.css +2 -2
  69. package/dist/styles/select.css +2 -2
  70. package/dist/styles/side-sheet.css +2 -2
  71. package/dist/styles/slider.css +1 -1
  72. package/dist/styles/snackbar.css +1 -1
  73. package/dist/styles/split-button.css +2 -2
  74. package/dist/styles/switch.css +2 -2
  75. package/dist/styles/tabs.css +1 -1
  76. package/dist/styles/textfield.css +2 -2
  77. package/dist/styles/timepicker.css +1 -1
  78. package/dist/styles/tooltip.css +2 -2
  79. package/dist/styles/top-app-bar.css +1 -1
  80. package/dist/styles/utilities.css +1 -1
  81. package/dist/styles.css +2 -2
  82. package/dist/themes/autumn.css +1 -1
  83. package/dist/themes/baseline.css +1 -1
  84. package/dist/themes/brownbeige.css +1 -1
  85. package/dist/themes/browngreen.css +1 -1
  86. package/dist/themes/desert.css +1 -1
  87. package/dist/themes/forest.css +1 -1
  88. package/dist/themes/highcontrast.css +1 -1
  89. package/dist/themes/legacy.css +1 -1
  90. package/dist/themes/material.css +1 -1
  91. package/dist/themes/ocean.css +1 -1
  92. package/dist/themes/sageivory.css +1 -1
  93. package/dist/themes/spring.css +1 -1
  94. package/dist/themes/summer.css +1 -1
  95. package/dist/themes/sunset.css +1 -1
  96. package/dist/themes/tealcaramel.css +1 -1
  97. package/dist/themes/winter.css +1 -1
  98. package/package.json +1 -1
  99. package/src/styles/abstract/_mixins.scss +26 -0
  100. package/src/styles/abstract/_variables.scss +25 -11
  101. package/src/styles/components/_bottom-sheet.scss +23 -7
  102. package/src/styles/components/_dialog.scss +59 -26
  103. package/src/styles/components/_drawer.scss +60 -7
  104. package/src/styles/components/_menu.scss +12 -7
  105. package/src/styles/components/_side-sheet.scss +33 -4
  106. package/src/styles/components/_tooltip.scss +3 -0
@@ -30,7 +30,6 @@ interface ComponentWithElements {
30
30
  };
31
31
  getClass: (name: string) => string;
32
32
  addClass: (...classes: string[]) => ComponentWithElements;
33
- removeClass: (...classes: string[]) => ComponentWithElements;
34
33
  on: (event: string, handler: Function) => ComponentWithElements;
35
34
  off: (event: string, handler: Function) => ComponentWithElements;
36
35
  }
@@ -84,9 +84,11 @@ export const withAPI = ({ visibility, lifecycle }) => (component) => {
84
84
  component.config.variant = variant;
85
85
  if (variant === VARIANT_SMALL) {
86
86
  component.element.textContent = "";
87
+ component.element.removeAttribute("role");
87
88
  component.element.setAttribute("aria-hidden", "true");
88
89
  }
89
90
  else {
91
+ component.element.removeAttribute("aria-hidden");
90
92
  component.element.setAttribute("role", "status");
91
93
  if (component.config.label !== undefined) {
92
94
  badgeComponent.setLabel(component.config.label);
@@ -146,7 +148,7 @@ export const withAPI = ({ visibility, lifecycle }) => (component) => {
146
148
  return badgeComponent;
147
149
  },
148
150
  removeClass: (...classes) => {
149
- component.removeClass(...classes);
151
+ component.element.classList.remove(...classes.filter(Boolean));
150
152
  return badgeComponent;
151
153
  },
152
154
  on: (event, handler) => {
@@ -37,13 +37,11 @@ export const createBottomAppBar = (config = {}) => {
37
37
  if (currentScrollY > prevScrollY + 10) {
38
38
  if (isVisible) {
39
39
  bottomBar.hide();
40
- componentConfig.onVisibilityChange?.(false);
41
40
  }
42
41
  }
43
42
  else if (currentScrollY < prevScrollY - 10) {
44
43
  if (!isVisible) {
45
44
  bottomBar.show();
46
- componentConfig.onVisibilityChange?.(true);
47
45
  }
48
46
  }
49
47
  prevScrollY = currentScrollY;
@@ -57,6 +55,9 @@ export const createBottomAppBar = (config = {}) => {
57
55
  }
58
56
  const bottomBar = {
59
57
  ...withLifecycleComponent,
58
+ destroy() {
59
+ withLifecycleComponent.lifecycle.destroy();
60
+ },
60
61
  addAction(button) {
61
62
  actionsContainer.appendChild(button);
62
63
  return this;
@@ -69,12 +70,18 @@ export const createBottomAppBar = (config = {}) => {
69
70
  },
70
71
  show() {
71
72
  this.element.classList.remove(`${component.getClass("bottom-app-bar")}--hidden`);
73
+ const changed = !isVisible;
72
74
  isVisible = true;
75
+ if (changed)
76
+ componentConfig.onVisibilityChange?.(true);
73
77
  return this;
74
78
  },
75
79
  hide() {
76
80
  this.element.classList.add(`${component.getClass("bottom-app-bar")}--hidden`);
81
+ const changed = isVisible;
77
82
  isVisible = false;
83
+ if (changed)
84
+ componentConfig.onVisibilityChange?.(false);
78
85
  return this;
79
86
  },
80
87
  isVisible() {
@@ -17,6 +17,11 @@ interface ProgressEnhancedComponent {
17
17
  };
18
18
  setText?: (text: string) => unknown;
19
19
  getText?: () => string;
20
+ /** The label manager withText installs; the setText/getText above only exist after withAPI. */
21
+ text?: {
22
+ setText: (text: string) => unknown;
23
+ getText: () => string;
24
+ };
20
25
  showProgress?: () => Promise<ProgressEnhancedComponent>;
21
26
  showProgressSync?: () => ProgressEnhancedComponent;
22
27
  hideProgress?: () => Promise<ProgressEnhancedComponent>;
@@ -109,32 +109,36 @@ export const withProgress = (config) => (component) => {
109
109
  component.setIndeterminate(indeterminate);
110
110
  return component;
111
111
  };
112
+ const readLabel = () => component.text ? component.text.getText() : component.getText?.();
113
+ const writeLabel = (value) => {
114
+ if (component.text)
115
+ component.text.setText(value);
116
+ else
117
+ component.setText?.(value);
118
+ };
112
119
  component.setLoading = async function (loading, text) {
113
120
  if (loading && !isLoading) {
114
- if (component.setText && component.getText) {
115
- originalText = component.getText();
116
- }
121
+ originalText = readLabel() ?? "";
117
122
  isLoading = true;
123
+ component.element.setAttribute("aria-busy", "true");
118
124
  await component.showProgress();
119
125
  if (component.disabled?.disable) {
120
126
  component.disabled.disable();
121
127
  }
122
- if (text && component.setText) {
123
- component.setText(text);
124
- }
128
+ if (text)
129
+ writeLabel(text);
125
130
  }
126
131
  else if (!loading && isLoading) {
127
132
  isLoading = false;
133
+ component.element.removeAttribute("aria-busy");
128
134
  await component.hideProgress();
129
135
  if (component.disabled?.enable) {
130
136
  component.disabled.enable();
131
137
  }
132
- if (text && component.setText) {
133
- component.setText(text);
134
- }
135
- else if (originalText && component.setText) {
136
- component.setText(originalText);
137
- }
138
+ if (text)
139
+ writeLabel(text);
140
+ else if (originalText)
141
+ writeLabel(originalText);
138
142
  }
139
143
  return component;
140
144
  };
@@ -9,24 +9,27 @@ export const withAPI = ({ lifecycle, config }) => (component) => ({
9
9
  return this;
10
10
  },
11
11
  setHeader(headerElement) {
12
- if (headerElement && headerElement.classList.contains(`${component.getClass('card')}-header`)) {
13
- const existingHeader = component.element.querySelector(`.${component.getClass('card')}-header`);
12
+ const card = component.getClass('card');
13
+ if (headerElement && headerElement.classList.contains(`${card}-header`)) {
14
+ const title = (header) => header?.querySelector(`.${card}-header-title`)?.id || null;
15
+ const existingHeader = component.element.querySelector(`.${card}-header`);
14
16
  if (existingHeader) {
17
+ const previous = title(existingHeader);
18
+ if (previous && component.element.getAttribute('aria-labelledby') === previous) {
19
+ component.element.removeAttribute('aria-labelledby');
20
+ }
15
21
  existingHeader.remove();
16
22
  }
17
- const mediaElement = component.element.querySelector(`.${component.getClass('card')}-media`);
18
- if (mediaElement) {
19
- const mediaElements = component.element.querySelectorAll(`.${component.getClass('card')}-media`);
20
- const lastMedia = mediaElements[mediaElements.length - 1];
21
- if (lastMedia.nextSibling) {
22
- component.element.insertBefore(headerElement, lastMedia.nextSibling);
23
- }
24
- else {
25
- component.element.appendChild(headerElement);
26
- }
23
+ let before = component.element.firstElementChild;
24
+ while (before && before.classList.contains(`${card}-media`)) {
25
+ before = before.nextElementSibling;
27
26
  }
28
- else {
29
- component.element.insertBefore(headerElement, component.element.firstChild);
27
+ component.element.insertBefore(headerElement, before);
28
+ const titleId = title(headerElement);
29
+ if (titleId &&
30
+ !component.element.hasAttribute('aria-labelledby') &&
31
+ !component.element.hasAttribute('aria-label')) {
32
+ component.element.setAttribute('aria-labelledby', titleId);
30
33
  }
31
34
  }
32
35
  return this;
@@ -75,7 +75,7 @@ export const getElementConfig = (config) => {
75
75
  if (config.aria) {
76
76
  Object.entries(config.aria).forEach(([key, value]) => {
77
77
  if (value !== undefined) {
78
- const attrName = key.startsWith("aria-") ? key : `aria-${key}`;
78
+ const attrName = key === "role" || key.startsWith("aria-") ? key : `aria-${key}`;
79
79
  ariaAttributes[attrName] = value;
80
80
  }
81
81
  });
@@ -4,6 +4,7 @@ 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
6
  export const CARD_CONTENT_PADDING = true;
7
+ let headerCount = 0;
7
8
  export const createCardContent = (config = {}) => {
8
9
  const baseConfig = {
9
10
  ...config,
@@ -73,13 +74,9 @@ export const createCardHeader = (config = {}) => {
73
74
  text: config.title,
74
75
  container: textContainer,
75
76
  attributes: {
76
- id: `${header.element.id || 'card-header'}-title`
77
+ id: `${header.element.id || `card-header-${++headerCount}`}-title`
77
78
  }
78
79
  });
79
- const parentCard = header.element.closest(`.${PREFIX}-card`);
80
- if (parentCard && !parentCard.hasAttribute('aria-labelledby')) {
81
- parentCard.setAttribute('aria-labelledby', `${header.element.id || 'card-header'}-title`);
82
- }
83
80
  }
84
81
  if (config.subtitle) {
85
82
  createElement({
@@ -10,7 +10,6 @@ export const defaultConfig = {
10
10
  autofocus: true,
11
11
  trapFocus: true,
12
12
  divider: false,
13
- animationDuration: 150,
14
13
  buttons: []
15
14
  };
16
15
  export const createBaseConfig = (config = {}) => createComponentConfig(defaultConfig, config, 'dialog');
@@ -236,7 +236,8 @@ const addButton = (footer, buttonConfig, component) => {
236
236
  };
237
237
  export const withVisibility = () => (component) => {
238
238
  const isOpen = component.config.open === true;
239
- const animationDuration = component.config.animationDuration || 150;
239
+ const openDuration = component.config.animationDuration ?? 500;
240
+ const closeDuration = component.config.animationDuration ?? 150;
240
241
  const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
241
242
  let previouslyFocusedElement = null;
242
243
  let mouseDownOnOverlay = false;
@@ -370,8 +371,8 @@ export const withVisibility = () => (component) => {
370
371
  const container = component.structure.container || document.body;
371
372
  container.appendChild(component.overlay);
372
373
  }
373
- addClass(component.overlay, `${component.getClass("dialog-overlay")}--visible`);
374
374
  setTimeout(() => {
375
+ addClass(component.overlay, `${component.getClass("dialog-overlay")}--visible`);
375
376
  addClass(component.element, `${component.getClass("dialog")}--visible`);
376
377
  trapFocus();
377
378
  setupEvents();
@@ -379,7 +380,7 @@ export const withVisibility = () => (component) => {
379
380
  component.emit(DIALOG_EVENTS.OPEN, { dialog: component });
380
381
  setTimeout(() => {
381
382
  component.emit(DIALOG_EVENTS.AFTER_OPEN, { dialog: component });
382
- }, animationDuration);
383
+ }, openDuration);
383
384
  }
384
385
  }, 10);
385
386
  },
@@ -414,7 +415,7 @@ export const withVisibility = () => (component) => {
414
415
  if (typeof component.emit === "function") {
415
416
  component.emit(DIALOG_EVENTS.AFTER_CLOSE, { dialog: component });
416
417
  }
417
- }, animationDuration);
418
+ }, closeDuration);
418
419
  },
419
420
  toggle(open) {
420
421
  if (open === undefined) {
@@ -164,8 +164,9 @@ export interface DialogConfig {
164
164
  */
165
165
  zIndex?: number;
166
166
  /**
167
- * Duration of open/close animations in milliseconds
168
- * @default 150
167
+ * How long `afteropen` and `afterclose` wait after opening and closing, in
168
+ * milliseconds, and when a closed dialog leaves the DOM. By default they
169
+ * follow the stylesheet: 450 after opening, 200 after closing.
169
170
  */
170
171
  animationDuration?: number;
171
172
  /**
@@ -1,50 +1,6 @@
1
1
  import { BaseComponent, ElementComponent } from "../../core/compose/index.js";
2
2
  import { DividerConfig } from "./config.js";
3
3
  import { DividerComponent } from "./types.js";
4
- /**
5
- * Adds orientation functionality to divider
6
- *
7
- * Higher-order function that enhances a component with the ability to be
8
- * oriented horizontally (default) or vertically. Controls the appropriate
9
- * CSS classes and dimensional styling based on orientation.
10
- *
11
- * @param config - Divider configuration
12
- * @returns Function that enhances a component with orientation capabilities
13
- *
14
- * @internal
15
- */
16
4
  export declare const withOrientation: (config: DividerConfig) => <C extends ElementComponent & BaseComponent>(component: C) => C & Partial<DividerComponent>;
17
- /**
18
- * Adds inset functionality to divider
19
- *
20
- * Higher-order function that enhances a component with the ability to have
21
- * customized inset spacing. Supports three variants (full-width, inset, middle-inset)
22
- * and allows fine-grained control over start and end insets.
23
- *
24
- * Insets are applied as margins in the appropriate direction based on orientation:
25
- * - For horizontal dividers: left/right margins
26
- * - For vertical dividers: top/bottom margins
27
- *
28
- * @param config - Divider configuration
29
- * @returns Function that enhances a component with inset capabilities
30
- *
31
- * @internal
32
- */
33
- export declare const withInset: (config: DividerConfig) => <C extends ElementComponent & Partial<DividerComponent>>(component: C) => C & Partial<DividerComponent>;
34
- /**
35
- * Adds style customization to divider
36
- *
37
- * Higher-order function that enhances a component with visual customization
38
- * capabilities, including:
39
- * - Custom thickness (height for horizontal, width for vertical dividers)
40
- * - Custom colors (background-color CSS property)
41
- *
42
- * These styling options allow dividers to be visually adapted to different
43
- * design requirements while maintaining Material Design principles.
44
- *
45
- * @param config - Divider configuration
46
- * @returns Function that enhances a component with style capabilities
47
- *
48
- * @internal
49
- */
50
- export declare const withStyle: (config: DividerConfig) => <C extends ElementComponent & Partial<DividerComponent>>(component: C) => C & Partial<DividerComponent>;
5
+ export declare const withInset: (config: DividerConfig) => <C extends ElementComponent & BaseComponent & Partial<DividerComponent>>(component: C) => C & Partial<DividerComponent>;
6
+ export declare const withStyle: (config: DividerConfig) => <C extends ElementComponent & BaseComponent & Partial<DividerComponent>>(component: C) => C & Partial<DividerComponent>;
@@ -1,138 +1,77 @@
1
- export const withOrientation = (config) => (component) => {
1
+ const ORIENTATIONS = ['horizontal', 'vertical'];
2
+ const VARIANTS = ['full-width', 'inset', 'middle-inset'];
3
+ const DEFAULT_INSET = 16;
4
+ const applyLayout = (component, config) => {
5
+ const { element } = component;
6
+ const base = component.getClass('divider');
2
7
  const orientation = config.orientation || 'horizontal';
3
- component.element.classList.add(`${component.getClass('divider')}--${orientation}`);
8
+ const variant = config.variant || 'full-width';
4
9
  const thickness = config.thickness || 1;
5
- if (orientation === 'horizontal') {
6
- component.element.style.height = `${thickness}px`;
7
- component.element.style.width = '100%';
8
- }
9
- else {
10
- component.element.style.width = `${thickness}px`;
11
- component.element.style.height = '100%';
10
+ const horizontal = orientation === 'horizontal';
11
+ ORIENTATIONS.forEach(name => element.classList.toggle(`${base}--${name}`, name === orientation));
12
+ VARIANTS.forEach(name => element.classList.toggle(`${base}--${name}`, name === variant));
13
+ if (horizontal)
14
+ element.removeAttribute('aria-orientation');
15
+ else
16
+ element.setAttribute('aria-orientation', 'vertical');
17
+ const style = element.style;
18
+ style.marginLeft = style.marginRight = style.marginTop = style.marginBottom = '';
19
+ const cross = horizontal ? 'height' : 'width';
20
+ const main = horizontal ? 'width' : 'height';
21
+ style[cross] = `${thickness}px`;
22
+ style[main] = '100%';
23
+ if (variant !== 'full-width') {
24
+ const insetStart = config.insetStart !== undefined ? config.insetStart : DEFAULT_INSET;
25
+ const insetEnd = config.insetEnd !== undefined
26
+ ? config.insetEnd
27
+ : (variant === 'middle-inset' ? DEFAULT_INSET : 0);
28
+ style[horizontal ? 'marginLeft' : 'marginTop'] = `${insetStart}px`;
29
+ style[horizontal ? 'marginRight' : 'marginBottom'] = `${insetEnd}px`;
30
+ style[main] = 'auto';
12
31
  }
32
+ };
33
+ export const withOrientation = (config) => (component) => {
34
+ applyLayout(component, config);
13
35
  return {
14
36
  ...component,
15
37
  getOrientation() {
16
- return orientation;
38
+ return config.orientation || 'horizontal';
17
39
  },
18
40
  setOrientation(newOrientation) {
19
- component.element.classList.remove(`${component.getClass('divider')}--${orientation}`);
20
- component.element.classList.add(`${component.getClass('divider')}--${newOrientation}`);
21
- if (newOrientation === 'horizontal') {
22
- component.element.style.height = `${thickness}px`;
23
- component.element.style.width = '100%';
24
- component.element.style.marginTop = '';
25
- component.element.style.marginBottom = '';
26
- }
27
- else {
28
- component.element.style.width = `${thickness}px`;
29
- component.element.style.height = '100%';
30
- component.element.style.marginLeft = '';
31
- component.element.style.marginRight = '';
32
- }
41
+ config.orientation = newOrientation;
42
+ applyLayout(component, config);
33
43
  return this;
34
44
  }
35
45
  };
36
46
  };
37
- export const withInset = (config) => (component) => {
38
- const variant = config.variant || 'full-width';
39
- const orientation = config.orientation || 'horizontal';
40
- if (variant === 'inset' || variant === 'middle-inset') {
41
- if (orientation === 'horizontal') {
42
- const insetStart = config.insetStart !== undefined ? config.insetStart : 16;
43
- const insetEnd = config.insetEnd !== undefined ? config.insetEnd : (variant === 'middle-inset' ? 16 : 0);
44
- component.element.style.marginLeft = `${insetStart}px`;
45
- component.element.style.marginRight = `${insetEnd}px`;
46
- component.element.style.width = "auto";
47
- }
48
- else {
49
- const insetStart = config.insetStart !== undefined ? config.insetStart : 16;
50
- const insetEnd = config.insetEnd !== undefined ? config.insetEnd : (variant === 'middle-inset' ? 16 : 0);
51
- component.element.style.marginTop = `${insetStart}px`;
52
- component.element.style.marginBottom = `${insetEnd}px`;
53
- component.element.style.height = "auto";
54
- }
47
+ export const withInset = (config) => (component) => ({
48
+ ...component,
49
+ getVariant() {
50
+ return config.variant || 'full-width';
51
+ },
52
+ setVariant(newVariant) {
53
+ config.variant = newVariant;
54
+ applyLayout(component, config);
55
+ return this;
56
+ },
57
+ setInset(insetStart, insetEnd) {
58
+ if (insetStart !== undefined)
59
+ config.insetStart = insetStart;
60
+ if (insetEnd !== undefined)
61
+ config.insetEnd = insetEnd;
62
+ applyLayout(component, config);
63
+ return this;
55
64
  }
56
- return {
57
- ...component,
58
- getVariant() {
59
- return variant;
60
- },
61
- setVariant(newVariant) {
62
- component.element.classList.remove(`${component.getClass('divider')}--${variant}`);
63
- component.element.classList.add(`${component.getClass('divider')}--${newVariant}`);
64
- const currentOrientation = component.getOrientation ? component.getOrientation() : orientation;
65
- if (newVariant === 'full-width') {
66
- if (currentOrientation === 'horizontal') {
67
- component.element.style.marginLeft = '';
68
- component.element.style.marginRight = '';
69
- }
70
- else {
71
- component.element.style.marginTop = '';
72
- component.element.style.marginBottom = '';
73
- }
74
- }
75
- else {
76
- const insetStart = config.insetStart !== undefined ? config.insetStart : 16;
77
- const insetEnd = config.insetEnd !== undefined ? config.insetEnd : (newVariant === 'middle-inset' ? 16 : 0);
78
- if (currentOrientation === 'horizontal') {
79
- component.element.style.marginLeft = `${insetStart}px`;
80
- component.element.style.marginRight = `${insetEnd}px`;
81
- }
82
- else {
83
- component.element.style.marginTop = `${insetStart}px`;
84
- component.element.style.marginBottom = `${insetEnd}px`;
85
- }
86
- }
87
- return this;
88
- },
89
- setInset(insetStart, insetEnd) {
90
- const currentOrientation = component.getOrientation ? component.getOrientation() : orientation;
91
- const currentVariant = component.getVariant ? component.getVariant() : variant;
92
- if (currentVariant !== 'full-width') {
93
- if (currentOrientation === 'horizontal') {
94
- if (insetStart !== undefined) {
95
- component.element.style.marginLeft = `${insetStart}px`;
96
- }
97
- if (insetEnd !== undefined) {
98
- component.element.style.marginRight = `${insetEnd}px`;
99
- }
100
- }
101
- else {
102
- if (insetStart !== undefined) {
103
- component.element.style.marginTop = `${insetStart}px`;
104
- }
105
- if (insetEnd !== undefined) {
106
- component.element.style.marginBottom = `${insetEnd}px`;
107
- }
108
- }
109
- }
110
- return this;
111
- }
112
- };
113
- };
65
+ });
114
66
  export const withStyle = (config) => (component) => {
115
67
  if (config.color) {
116
68
  component.element.style.backgroundColor = config.color;
117
69
  }
118
- const thickness = config.thickness || 1;
119
- const orientation = config.orientation || 'horizontal';
120
- if (orientation === 'horizontal') {
121
- component.element.style.height = `${thickness}px`;
122
- }
123
- else {
124
- component.element.style.width = `${thickness}px`;
125
- }
126
70
  return {
127
71
  ...component,
128
72
  setThickness(newThickness) {
129
- const currentOrientation = component.getOrientation ? component.getOrientation() : orientation;
130
- if (currentOrientation === 'horizontal') {
131
- component.element.style.height = `${newThickness}px`;
132
- }
133
- else {
134
- component.element.style.width = `${newThickness}px`;
135
- }
73
+ config.thickness = newThickness;
74
+ applyLayout(component, config);
136
75
  return this;
137
76
  },
138
77
  setColor(color) {
@@ -26,8 +26,8 @@ export const getElementConfig = (config) => {
26
26
  styles.maxHeight = config.maxHeight;
27
27
  }
28
28
  const attributes = {
29
- role: "menu",
30
- tabindex: "-1",
29
+ role: config.listbox ? "presentation" : "menu",
30
+ ...(config.listbox ? {} : { tabindex: "-1" }),
31
31
  "aria-hidden": (!config.visible).toString(),
32
32
  style: Object.entries(styles)
33
33
  .map(([key, value]) => `${key}: ${value}`)
@@ -6,6 +6,11 @@ const withController = (config) => (component) => {
6
6
  return component;
7
7
  }
8
8
  const tasks = createMenuTasks();
9
+ const listbox = config.listbox === true;
10
+ const optionIdPrefix = `${component.getClass("menu")}-${Math.random().toString(36).slice(2, 9)}`;
11
+ if (listbox) {
12
+ component.element.addEventListener("mousedown", (e) => e.preventDefault());
13
+ }
9
14
  const state = {
10
15
  visible: config.visible || false,
11
16
  items: config.items || [],
@@ -51,8 +56,14 @@ const withController = (config) => (component) => {
51
56
  const itemElement = document.createElement("li");
52
57
  const itemClass = `${component.getClass("menu-item")}`;
53
58
  itemElement.className = itemClass;
54
- itemElement.setAttribute("role", "menuitem");
55
- itemElement.setAttribute("tabindex", "-1");
59
+ if (listbox) {
60
+ itemElement.setAttribute("role", "option");
61
+ itemElement.id = `${optionIdPrefix}-option-${index}`;
62
+ }
63
+ else {
64
+ itemElement.setAttribute("role", "menuitem");
65
+ itemElement.setAttribute("tabindex", "-1");
66
+ }
56
67
  itemElement.setAttribute("data-id", item.id);
57
68
  itemElement.setAttribute("data-index", index.toString());
58
69
  if (item.disabled) {
@@ -144,7 +155,9 @@ const withController = (config) => (component) => {
144
155
  const renderMenuItems = () => {
145
156
  const menuList = document.createElement("ul");
146
157
  menuList.className = `${component.getClass("menu-list")}`;
147
- menuList.setAttribute("role", "menu");
158
+ menuList.setAttribute("role", listbox ? "listbox" : "menu");
159
+ if (listbox)
160
+ menuList.id = `${optionIdPrefix}-listbox`;
148
161
  let target = menuList;
149
162
  const startGroup = () => {
150
163
  const group = document.createElement("li");
@@ -256,31 +269,32 @@ const withController = (config) => (component) => {
256
269
  component.element.style.opacity = "";
257
270
  void component.element.getBoundingClientRect();
258
271
  component.element.classList.add(`${component.getClass("menu--visible")}`);
259
- tasks.setTimeout(() => {
260
- if (component.keyboard && component.keyboard.handleInitialFocus) {
261
- component.keyboard.handleInitialFocus(component.element, interactionType);
262
- }
263
- else {
264
- const items = Array.from(component.element.querySelectorAll(`.${component.getClass("menu-item")}`));
265
- items.forEach((item) => {
266
- item.tabIndex = -1;
267
- });
268
- if (items.length > 0)
269
- items[0].tabIndex = 0;
270
- if (interactionType === "keyboard" && items.length > 0) {
271
- items[0].focus();
272
+ if (!listbox)
273
+ tasks.setTimeout(() => {
274
+ if (component.keyboard && component.keyboard.handleInitialFocus) {
275
+ component.keyboard.handleInitialFocus(component.element, interactionType);
272
276
  }
273
277
  else {
274
- component.element.tabIndex = -1;
275
- component.element.focus();
278
+ const items = Array.from(component.element.querySelectorAll(`.${component.getClass("menu-item")}`));
279
+ items.forEach((item) => {
280
+ item.tabIndex = -1;
281
+ });
282
+ if (items.length > 0)
283
+ items[0].tabIndex = 0;
284
+ if (interactionType === "keyboard" && items.length > 0) {
285
+ items[0].focus();
286
+ }
287
+ else {
288
+ component.element.tabIndex = -1;
289
+ component.element.focus();
290
+ }
276
291
  }
277
- }
278
- }, 100);
292
+ }, 100);
279
293
  tasks.setTimeout(() => {
280
294
  if (config.closeOnClickOutside && state.visible) {
281
295
  document.addEventListener("click", handleDocumentClick);
282
296
  }
283
- if (config.closeOnEscape) {
297
+ if (config.closeOnEscape && !listbox) {
284
298
  document.addEventListener("keydown", handleDocumentKeydown);
285
299
  }
286
300
  window.addEventListener("resize", handleWindowResize, {
@@ -432,7 +446,7 @@ const withController = (config) => (component) => {
432
446
  };
433
447
  const initMenu = () => {
434
448
  renderMenuItems();
435
- if (component.keyboard && component.keyboard.setupKeyboardHandlers) {
449
+ if (!listbox && component.keyboard && component.keyboard.setupKeyboardHandlers) {
436
450
  component.keyboard.setupKeyboardHandlers(component.element, state, {
437
451
  closeMenu,
438
452
  closeSubmenu: component.submenu ? component.submenu.closeSubmenu : null,
@@ -454,7 +468,7 @@ const withController = (config) => (component) => {
454
468
  if (config.closeOnClickOutside) {
455
469
  document.addEventListener("click", handleDocumentClick);
456
470
  }
457
- if (config.closeOnEscape) {
471
+ if (config.closeOnEscape && !listbox) {
458
472
  document.addEventListener("keydown", handleDocumentKeydown);
459
473
  }
460
474
  window.addEventListener("resize", handleWindowResize);