mtrl 0.8.0-next.38 → 0.8.0-next.39

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 (62) hide show
  1. package/dist/components/drawer/config.js +2 -3
  2. package/dist/components/drawer/features/headline.js +3 -0
  3. package/dist/components/drawer/features/items.js +19 -42
  4. package/dist/components/drawer/features/state.d.ts +1 -9
  5. package/dist/components/drawer/features/state.js +143 -68
  6. package/dist/index.cjs +12 -12
  7. package/dist/package.json +1 -1
  8. package/dist/styles/badge.css +1 -1
  9. package/dist/styles/base.css +1 -1
  10. package/dist/styles/bottom-app-bar.css +1 -1
  11. package/dist/styles/bottom-sheet.css +1 -1
  12. package/dist/styles/button-group.css +1 -1
  13. package/dist/styles/button.css +1 -1
  14. package/dist/styles/card.css +1 -1
  15. package/dist/styles/carousel.css +1 -1
  16. package/dist/styles/checkbox.css +1 -1
  17. package/dist/styles/chips.css +1 -1
  18. package/dist/styles/dialog.css +1 -1
  19. package/dist/styles/divider.css +1 -1
  20. package/dist/styles/drawer.css +2 -2
  21. package/dist/styles/extended-fab.css +1 -1
  22. package/dist/styles/fab.css +1 -1
  23. package/dist/styles/icon-button.css +1 -1
  24. package/dist/styles/list.css +1 -1
  25. package/dist/styles/loading-indicator.css +1 -1
  26. package/dist/styles/menu.css +1 -1
  27. package/dist/styles/navigation.css +1 -1
  28. package/dist/styles/progress.css +1 -1
  29. package/dist/styles/radios.css +1 -1
  30. package/dist/styles/search.css +1 -1
  31. package/dist/styles/segmented-button.css +1 -1
  32. package/dist/styles/select.css +1 -1
  33. package/dist/styles/side-sheet.css +1 -1
  34. package/dist/styles/slider.css +1 -1
  35. package/dist/styles/snackbar.css +1 -1
  36. package/dist/styles/split-button.css +1 -1
  37. package/dist/styles/switch.css +1 -1
  38. package/dist/styles/tabs.css +1 -1
  39. package/dist/styles/textfield.css +1 -1
  40. package/dist/styles/timepicker.css +1 -1
  41. package/dist/styles/top-app-bar.css +1 -1
  42. package/dist/styles/utilities.css +1 -1
  43. package/dist/styles.css +2 -2
  44. package/dist/themes/autumn.css +1 -1
  45. package/dist/themes/baseline.css +1 -1
  46. package/dist/themes/brownbeige.css +1 -1
  47. package/dist/themes/browngreen.css +1 -1
  48. package/dist/themes/desert.css +1 -1
  49. package/dist/themes/forest.css +1 -1
  50. package/dist/themes/highcontrast.css +1 -1
  51. package/dist/themes/legacy.css +1 -1
  52. package/dist/themes/material.css +1 -1
  53. package/dist/themes/ocean.css +1 -1
  54. package/dist/themes/sageivory.css +1 -1
  55. package/dist/themes/spring.css +1 -1
  56. package/dist/themes/summer.css +1 -1
  57. package/dist/themes/sunset.css +1 -1
  58. package/dist/themes/tealcaramel.css +1 -1
  59. package/dist/themes/winter.css +1 -1
  60. package/package.json +2 -1
  61. package/src/styles/components/_drawer.scss +5 -1
  62. package/src/styles/components/_slider.scss +7 -6
@@ -19,9 +19,8 @@ export const getElementConfig = (config) => {
19
19
  else {
20
20
  attributes.role = "navigation";
21
21
  }
22
- if (config.ariaLabel) {
23
- attributes["aria-label"] = config.ariaLabel;
24
- }
22
+ attributes["aria-label"] = config.ariaLabel || config.headline || "Navigation";
23
+ attributes.tabindex = "-1";
25
24
  return createElementConfig(config, {
26
25
  tag: "aside",
27
26
  attributes,
@@ -8,12 +8,15 @@ export const withHeadline = (config) => (component) => {
8
8
  }
9
9
  const setHeadline = (text) => {
10
10
  headlineText = text;
11
+ if (!config.ariaLabel)
12
+ component.element.setAttribute("aria-label", text || "Navigation");
11
13
  if (text) {
12
14
  if (!headlineElement) {
13
15
  headlineElement = document.createElement("div");
14
16
  headlineElement.className = component.getClass("drawer__headline");
15
17
  }
16
18
  headlineElement.textContent = text;
19
+ component.element.querySelector(`.${component.getClass("drawer__sheet")}`)?.prepend(headlineElement);
17
20
  }
18
21
  else if (headlineElement && headlineElement.parentNode) {
19
22
  headlineElement.parentNode.removeChild(headlineElement);
@@ -2,8 +2,8 @@ import { DRAWER_EVENTS } from "../constants.js";
2
2
  const createItemElement = (item, getClass) => {
3
3
  const el = document.createElement("button");
4
4
  el.className = getClass("drawer__item");
5
- el.setAttribute("role", "tab");
6
- el.setAttribute("tabindex", "-1");
5
+ el.type = "button";
6
+ el.setAttribute("tabindex", item.disabled ? "-1" : "0");
7
7
  if (item.id) {
8
8
  el.dataset.id = item.id;
9
9
  }
@@ -18,6 +18,7 @@ const createItemElement = (item, getClass) => {
18
18
  const iconEl = document.createElement("span");
19
19
  iconEl.className = getClass("drawer__item-icon");
20
20
  iconEl.innerHTML = item.icon;
21
+ iconEl.setAttribute("aria-hidden", "true");
21
22
  el.appendChild(iconEl);
22
23
  }
23
24
  if (item.label) {
@@ -34,11 +35,7 @@ const createItemElement = (item, getClass) => {
34
35
  }
35
36
  if (item.active) {
36
37
  el.classList.add(`${getClass("drawer__item")}--active`);
37
- el.setAttribute("aria-selected", "true");
38
- el.setAttribute("tabindex", "0");
39
- }
40
- else {
41
- el.setAttribute("aria-selected", "false");
38
+ el.setAttribute("aria-current", "page");
42
39
  }
43
40
  return el;
44
41
  };
@@ -58,7 +55,7 @@ export const withItems = (config) => (component) => {
58
55
  let currentItems = config.items || [];
59
56
  let activeId = null;
60
57
  let itemsContainer = null;
61
- const initialActive = currentItems.find((item) => item.type !== "divider" && item.type !== "section" && item.active);
58
+ const initialActive = currentItems.find((item) => item.type !== "divider" && item.type !== "section" && item.active && !item.disabled);
62
59
  if (initialActive?.id) {
63
60
  activeId = initialActive.id;
64
61
  }
@@ -77,7 +74,7 @@ export const withItems = (config) => (component) => {
77
74
  itemsContainer.appendChild(createSectionElement(item, component.getClass));
78
75
  return;
79
76
  }
80
- const itemEl = createItemElement(item, component.getClass);
77
+ const itemEl = createItemElement({ ...item, active: !!activeId && item.id === activeId }, component.getClass);
81
78
  itemEl.dataset.navIndex = String(navIndex);
82
79
  navIndex++;
83
80
  itemEl.addEventListener("click", (e) => {
@@ -97,39 +94,23 @@ export const withItems = (config) => (component) => {
97
94
  });
98
95
  itemsContainer.appendChild(itemEl);
99
96
  });
100
- if (!activeId) {
101
- const firstItem = itemsContainer.querySelector(`.${component.getClass("drawer__item")}`);
102
- if (firstItem) {
103
- firstItem.setAttribute("tabindex", "0");
104
- }
105
- }
106
97
  };
107
98
  const setActive = (id) => {
108
99
  if (!itemsContainer)
109
100
  return;
110
101
  const activeClass = `${component.getClass("drawer__item")}--active`;
111
- const allItems = itemsContainer.querySelectorAll(`.${component.getClass("drawer__item")}`);
112
- allItems.forEach((el) => {
113
- el.classList.remove(activeClass);
114
- el.setAttribute("aria-selected", "false");
115
- el.setAttribute("tabindex", "-1");
102
+ const allItems = [...itemsContainer.querySelectorAll(`.${component.getClass("drawer__item")}`)];
103
+ const target = allItems.find(element => element.dataset.id === id && !element.hasAttribute("disabled"));
104
+ if (id && !target)
105
+ return;
106
+ allItems.forEach(element => {
107
+ element.classList.toggle(activeClass, element === target);
108
+ if (element === target)
109
+ element.setAttribute("aria-current", "page");
110
+ else
111
+ element.removeAttribute("aria-current");
116
112
  });
117
- if (!id) {
118
- activeId = null;
119
- const firstItem = itemsContainer.querySelector(`.${component.getClass("drawer__item")}`);
120
- if (firstItem) {
121
- firstItem.setAttribute("tabindex", "0");
122
- }
123
- }
124
- else {
125
- const targetItem = itemsContainer.querySelector(`[data-id="${id}"]`);
126
- if (targetItem) {
127
- targetItem.classList.add(activeClass);
128
- targetItem.setAttribute("aria-selected", "true");
129
- targetItem.setAttribute("tabindex", "0");
130
- activeId = id;
131
- }
132
- }
113
+ activeId = target ? id : null;
133
114
  currentItems = currentItems.map((item) => ({
134
115
  ...item,
135
116
  active: item.id === id,
@@ -138,7 +119,7 @@ export const withItems = (config) => (component) => {
138
119
  const getActive = () => activeId;
139
120
  const setItems = (items) => {
140
121
  currentItems = items;
141
- const active = items.find((item) => item.type !== "divider" && item.type !== "section" && item.active);
122
+ const active = items.find((item) => item.type !== "divider" && item.type !== "section" && item.active && !item.disabled);
142
123
  activeId = active?.id || null;
143
124
  renderItems();
144
125
  };
@@ -146,7 +127,7 @@ export const withItems = (config) => (component) => {
146
127
  const setBadge = (id, badge) => {
147
128
  if (!itemsContainer)
148
129
  return;
149
- const itemEl = itemsContainer.querySelector(`[data-id="${id}"]`);
130
+ const itemEl = [...itemsContainer.querySelectorAll("[data-id]")].find(element => element.dataset.id === id);
150
131
  if (!itemEl)
151
132
  return;
152
133
  let badgeEl = itemEl.querySelector(`.${component.getClass("drawer__item-badge")}`);
@@ -199,15 +180,11 @@ export const withItems = (config) => (component) => {
199
180
  return;
200
181
  }
201
182
  if (nextIndex >= 0 && nextIndex < items.length) {
202
- items.forEach((item) => item.setAttribute("tabindex", "-1"));
203
- items[nextIndex].setAttribute("tabindex", "0");
204
183
  items[nextIndex].focus();
205
184
  }
206
185
  };
207
186
  itemsContainer = document.createElement("div");
208
187
  itemsContainer.className = component.getClass("drawer__items");
209
- itemsContainer.setAttribute("role", "tablist");
210
- itemsContainer.setAttribute("aria-orientation", "vertical");
211
188
  itemsContainer.addEventListener("keydown", handleKeydown);
212
189
  renderItems();
213
190
  return {
@@ -1,18 +1,10 @@
1
1
  import { DrawerConfig } from "../types.js";
2
- /**
3
- * Component shape expected by withState
4
- */
5
2
  interface StateBaseComponent {
6
3
  element: HTMLElement;
7
4
  getClass: (name: string) => string;
8
5
  emit: (event: string, data?: unknown) => void;
9
6
  [key: string]: unknown;
10
7
  }
11
- /**
12
- * Adds open/close state management, scrim, and keyboard dismiss to the drawer
13
- *
14
- * @param config - Drawer configuration
15
- * @returns Component enhancer function
16
- */
8
+ /** Standard drawers remain in the page; modal drawers own focus and background interaction. */
17
9
  export declare const withState: (config: DrawerConfig) => (component: StateBaseComponent) => StateBaseComponent;
18
10
  export {};
@@ -1,95 +1,170 @@
1
1
  import { DRAWER_EVENTS } from "../constants.js";
2
+ const modals = new WeakMap();
3
+ function updateBackground(doc, state) {
4
+ state.inerted.forEach(element => element.removeAttribute("inert"));
5
+ state.inerted = [];
6
+ let branch = state.roots.at(-1);
7
+ while (branch?.parentElement) {
8
+ for (const sibling of branch.parentElement.children) {
9
+ if (sibling !== branch && !sibling.hasAttribute("inert")) {
10
+ sibling.setAttribute("inert", "");
11
+ state.inerted.push(sibling);
12
+ }
13
+ }
14
+ branch = branch.parentElement;
15
+ if (branch === doc.body)
16
+ break;
17
+ }
18
+ }
19
+ function acquireModal(root) {
20
+ const doc = root.ownerDocument;
21
+ let state = modals.get(doc);
22
+ if (!state) {
23
+ state = { roots: [], inerted: [], overflow: doc.body.style.overflow, priority: doc.body.style.getPropertyPriority("overflow") };
24
+ modals.set(doc, state);
25
+ doc.body.style.setProperty("overflow", "hidden");
26
+ }
27
+ if (!state.roots.includes(root))
28
+ state.roots.push(root);
29
+ updateBackground(doc, state);
30
+ }
31
+ function releaseModal(root) {
32
+ const doc = root.ownerDocument;
33
+ const state = modals.get(doc);
34
+ if (!state || !state.roots.includes(root))
35
+ return;
36
+ state.roots = state.roots.filter(element => element !== root);
37
+ updateBackground(doc, state);
38
+ if (!state.roots.length) {
39
+ doc.body.style.setProperty("overflow", state.overflow, state.priority);
40
+ modals.delete(doc);
41
+ }
42
+ }
2
43
  export const withState = (config) => (component) => {
44
+ const root = component.element;
45
+ const doc = root.ownerDocument;
3
46
  const isModal = config.variant === "modal";
4
47
  const dismissible = config.dismissible !== false;
5
48
  let isOpen = config.open === true;
6
- let scrimElement = null;
7
- if (isModal) {
8
- scrimElement = document.createElement("div");
49
+ let destroyed = false;
50
+ let frame = null;
51
+ let previousFocus = null;
52
+ const scrimElement = isModal ? doc.createElement("div") : null;
53
+ if (scrimElement) {
9
54
  scrimElement.className = component.getClass("drawer__scrim");
10
- if (dismissible) {
11
- scrimElement.addEventListener("click", () => {
12
- close();
13
- });
14
- }
55
+ scrimElement.setAttribute("aria-hidden", "true");
56
+ if (dismissible)
57
+ scrimElement.addEventListener("click", close);
15
58
  }
16
- const handleKeydown = (e) => {
17
- if (e.key === "Escape" && isOpen && isModal && dismissible) {
18
- e.preventDefault();
19
- close();
20
- }
59
+ const cancelFocus = () => {
60
+ if (frame !== null)
61
+ cancelAnimationFrame(frame);
62
+ frame = null;
21
63
  };
22
- if (isModal) {
23
- document.addEventListener("keydown", handleKeydown);
24
- }
25
- function open() {
26
- if (isOpen)
64
+ const focusable = () => [...root.querySelectorAll('button, a[href], input, select, textarea, [tabindex]')].filter(element => {
65
+ const style = doc.defaultView.getComputedStyle(element);
66
+ return element.tabIndex >= 0 && !element.matches(':disabled, [aria-disabled="true"]') && !element.closest('[inert], [hidden]') && style.display !== 'none' && style.visibility !== 'hidden';
67
+ });
68
+ const focusInside = () => {
69
+ const items = focusable();
70
+ (items.find(element => element.getAttribute("aria-current") === "page") || items[0] || root).focus();
71
+ };
72
+ const isTopModal = () => modals.get(doc)?.roots.at(-1) === root;
73
+ function handleKeydown(event) {
74
+ if (!isOpen || !isTopModal() || event.defaultPrevented)
27
75
  return;
28
- isOpen = true;
29
- component.element.classList.add(`${component.getClass("drawer")}--open`);
30
- if (isModal && scrimElement) {
31
- scrimElement.classList.add(`${component.getClass("drawer__scrim")}--visible`);
32
- document.body.style.overflow = "hidden";
76
+ if (event.key === "Escape" && dismissible) {
77
+ event.preventDefault();
78
+ close();
33
79
  }
34
- requestAnimationFrame(() => {
35
- const firstItem = component.element.querySelector(`.${component.getClass("drawer__item")}[tabindex="0"]`);
36
- if (firstItem) {
37
- firstItem.focus();
80
+ else if (event.key === "Tab") {
81
+ const items = focusable();
82
+ const first = items[0], last = items.at(-1);
83
+ if (!first) {
84
+ event.preventDefault();
85
+ root.focus();
86
+ }
87
+ else if (event.shiftKey && (doc.activeElement === first || doc.activeElement === root || !root.contains(doc.activeElement))) {
88
+ event.preventDefault();
89
+ last.focus();
38
90
  }
39
- else {
40
- component.element.focus();
91
+ else if (!event.shiftKey && (doc.activeElement === last || doc.activeElement === root || !root.contains(doc.activeElement))) {
92
+ event.preventDefault();
93
+ first.focus();
41
94
  }
95
+ }
96
+ }
97
+ function handleFocus(event) {
98
+ if (isOpen && isTopModal() && !root.contains(event.target))
99
+ focusInside();
100
+ }
101
+ const synchronize = () => {
102
+ root.classList.toggle(`${component.getClass("drawer")}--open`, isOpen);
103
+ root.toggleAttribute("inert", !isOpen);
104
+ root.setAttribute("aria-hidden", String(!isOpen));
105
+ scrimElement?.classList.toggle(`${component.getClass("drawer__scrim")}--visible`, isOpen);
106
+ };
107
+ const activate = () => {
108
+ if (!isModal)
109
+ return;
110
+ previousFocus = doc.activeElement;
111
+ if (root.isConnected)
112
+ acquireModal(root);
113
+ doc.addEventListener("keydown", handleKeydown);
114
+ doc.addEventListener("focusin", handleFocus);
115
+ frame = requestAnimationFrame(() => {
116
+ frame = null;
117
+ if (destroyed || !isOpen || !root.isConnected)
118
+ return;
119
+ acquireModal(root);
120
+ if (isTopModal())
121
+ focusInside();
42
122
  });
123
+ };
124
+ const deactivate = () => {
125
+ cancelFocus();
126
+ doc.removeEventListener("keydown", handleKeydown);
127
+ doc.removeEventListener("focusin", handleFocus);
128
+ const restore = isTopModal();
129
+ releaseModal(root);
130
+ if (restore && previousFocus?.isConnected && !previousFocus.closest('[inert]'))
131
+ previousFocus.focus();
132
+ previousFocus = null;
133
+ };
134
+ function open() {
135
+ if (destroyed || isOpen)
136
+ return;
137
+ isOpen = true;
138
+ synchronize();
139
+ activate();
43
140
  component.emit(DRAWER_EVENTS.OPEN);
44
- if (config.onOpen) {
45
- config.onOpen();
46
- }
141
+ config.onOpen?.();
47
142
  }
48
143
  function close() {
49
- if (!isOpen)
144
+ if (destroyed || !isOpen)
50
145
  return;
51
146
  isOpen = false;
52
- component.element.classList.remove(`${component.getClass("drawer")}--open`);
53
- if (isModal && scrimElement) {
54
- scrimElement.classList.remove(`${component.getClass("drawer__scrim")}--visible`);
55
- document.body.style.overflow = "";
56
- }
147
+ deactivate();
148
+ synchronize();
57
149
  component.emit(DRAWER_EVENTS.CLOSE);
58
- if (config.onClose) {
59
- config.onClose();
60
- }
61
- }
62
- function toggle() {
63
- if (isOpen) {
64
- close();
65
- }
66
- else {
67
- open();
68
- }
69
- }
70
- if (isOpen) {
71
- component.element.classList.add(`${component.getClass("drawer")}--open`);
72
- if (isModal && scrimElement) {
73
- scrimElement.classList.add(`${component.getClass("drawer__scrim")}--visible`);
74
- }
150
+ config.onClose?.();
75
151
  }
152
+ synchronize();
153
+ if (isOpen)
154
+ activate();
76
155
  return {
77
156
  ...component,
78
157
  scrimElement,
79
- drawerState: {
80
- open,
81
- close,
82
- toggle,
83
- isOpen: () => isOpen,
84
- },
158
+ drawerState: { open, close, toggle: () => isOpen ? close() : open(), isOpen: () => isOpen },
85
159
  _stateCleanup: () => {
86
- if (isModal) {
87
- document.removeEventListener("keydown", handleKeydown);
88
- if (scrimElement && scrimElement.parentNode) {
89
- scrimElement.parentNode.removeChild(scrimElement);
90
- }
91
- document.body.style.overflow = "";
92
- }
160
+ if (destroyed)
161
+ return;
162
+ destroyed = true;
163
+ isOpen = false;
164
+ deactivate();
165
+ synchronize();
166
+ scrimElement?.removeEventListener("click", close);
167
+ scrimElement?.remove();
93
168
  },
94
169
  };
95
170
  };