mtrl 0.8.0-next.37 → 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.
- package/dist/components/drawer/config.js +2 -3
- package/dist/components/drawer/features/headline.js +3 -0
- package/dist/components/drawer/features/items.js +19 -42
- package/dist/components/drawer/features/state.d.ts +1 -9
- package/dist/components/drawer/features/state.js +143 -68
- package/dist/components/textfield/api.d.ts +1 -0
- package/dist/components/textfield/api.js +10 -10
- package/dist/components/textfield/features/density.js +1 -1
- package/dist/components/textfield/features/leading-icon.js +2 -1
- package/dist/components/textfield/features/placement.d.ts +2 -0
- package/dist/components/textfield/features/placement.js +25 -3
- package/dist/components/textfield/textfield.js +1 -1
- package/dist/core/compose/features/textinput.js +2 -1
- package/dist/index.cjs +12 -12
- package/dist/package.json +1 -1
- package/dist/styles/badge.css +1 -1
- package/dist/styles/base.css +1 -1
- package/dist/styles/bottom-app-bar.css +1 -1
- package/dist/styles/bottom-sheet.css +1 -1
- package/dist/styles/button-group.css +1 -1
- package/dist/styles/button.css +1 -1
- package/dist/styles/card.css +1 -1
- package/dist/styles/carousel.css +1 -1
- package/dist/styles/checkbox.css +1 -1
- package/dist/styles/chips.css +1 -1
- package/dist/styles/dialog.css +1 -1
- package/dist/styles/divider.css +1 -1
- package/dist/styles/drawer.css +2 -2
- package/dist/styles/extended-fab.css +1 -1
- package/dist/styles/fab.css +1 -1
- package/dist/styles/icon-button.css +1 -1
- package/dist/styles/list.css +1 -1
- package/dist/styles/loading-indicator.css +1 -1
- package/dist/styles/menu.css +1 -1
- package/dist/styles/navigation.css +1 -1
- package/dist/styles/progress.css +1 -1
- package/dist/styles/radios.css +1 -1
- package/dist/styles/search.css +1 -1
- package/dist/styles/segmented-button.css +1 -1
- package/dist/styles/select.css +1 -1
- package/dist/styles/side-sheet.css +1 -1
- package/dist/styles/slider.css +1 -1
- package/dist/styles/snackbar.css +1 -1
- package/dist/styles/split-button.css +1 -1
- package/dist/styles/switch.css +1 -1
- package/dist/styles/tabs.css +1 -1
- package/dist/styles/textfield.css +1 -1
- package/dist/styles/timepicker.css +1 -1
- package/dist/styles/top-app-bar.css +1 -1
- package/dist/styles/utilities.css +1 -1
- package/dist/styles.css +2 -2
- package/dist/themes/autumn.css +1 -1
- package/dist/themes/baseline.css +1 -1
- package/dist/themes/brownbeige.css +1 -1
- package/dist/themes/browngreen.css +1 -1
- package/dist/themes/desert.css +1 -1
- package/dist/themes/forest.css +1 -1
- package/dist/themes/highcontrast.css +1 -1
- package/dist/themes/legacy.css +1 -1
- package/dist/themes/material.css +1 -1
- package/dist/themes/ocean.css +1 -1
- package/dist/themes/sageivory.css +1 -1
- package/dist/themes/spring.css +1 -1
- package/dist/themes/summer.css +1 -1
- package/dist/themes/sunset.css +1 -1
- package/dist/themes/tealcaramel.css +1 -1
- package/dist/themes/winter.css +1 -1
- package/package.json +2 -1
- package/src/styles/components/_drawer.scss +5 -1
- 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
|
-
|
|
23
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
14
|
-
}
|
|
55
|
+
scrimElement.setAttribute("aria-hidden", "true");
|
|
56
|
+
if (dismissible)
|
|
57
|
+
scrimElement.addEventListener("click", close);
|
|
15
58
|
}
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
59
|
+
const cancelFocus = () => {
|
|
60
|
+
if (frame !== null)
|
|
61
|
+
cancelAnimationFrame(frame);
|
|
62
|
+
frame = null;
|
|
21
63
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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 (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
};
|
|
@@ -23,7 +23,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
23
23
|
component.element.classList.remove(`${PREFIX}-${COMPONENT}--filled`, `${PREFIX}-${COMPONENT}--outlined`);
|
|
24
24
|
component.element.classList.add(`${PREFIX}-${COMPONENT}--${variant}`);
|
|
25
25
|
if (component.updateElementPositions) {
|
|
26
|
-
|
|
26
|
+
component.schedulePositionUpdate?.();
|
|
27
27
|
}
|
|
28
28
|
return this;
|
|
29
29
|
},
|
|
@@ -38,7 +38,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
38
38
|
setLabel(text) {
|
|
39
39
|
component.label?.setText(text);
|
|
40
40
|
if (component.updateElementPositions) {
|
|
41
|
-
|
|
41
|
+
component.schedulePositionUpdate?.();
|
|
42
42
|
}
|
|
43
43
|
return this;
|
|
44
44
|
},
|
|
@@ -50,7 +50,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
50
50
|
if (component.setLeadingIcon) {
|
|
51
51
|
component.setLeadingIcon(html);
|
|
52
52
|
if (component.updateElementPositions) {
|
|
53
|
-
|
|
53
|
+
component.schedulePositionUpdate?.();
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
return this;
|
|
@@ -59,7 +59,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
59
59
|
if (component.removeLeadingIcon) {
|
|
60
60
|
component.removeLeadingIcon();
|
|
61
61
|
if (component.updateElementPositions) {
|
|
62
|
-
|
|
62
|
+
component.schedulePositionUpdate?.();
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
return this;
|
|
@@ -69,7 +69,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
69
69
|
if (component.setTrailingIcon) {
|
|
70
70
|
component.setTrailingIcon(html);
|
|
71
71
|
if (component.updateElementPositions) {
|
|
72
|
-
|
|
72
|
+
component.schedulePositionUpdate?.();
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
return this;
|
|
@@ -78,7 +78,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
78
78
|
if (component.removeTrailingIcon) {
|
|
79
79
|
component.removeTrailingIcon();
|
|
80
80
|
if (component.updateElementPositions) {
|
|
81
|
-
|
|
81
|
+
component.schedulePositionUpdate?.();
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
return this;
|
|
@@ -101,7 +101,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
101
101
|
if (component.setPrefixText) {
|
|
102
102
|
component.setPrefixText(text);
|
|
103
103
|
if (component.updateElementPositions) {
|
|
104
|
-
|
|
104
|
+
component.schedulePositionUpdate?.();
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
return this;
|
|
@@ -110,7 +110,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
110
110
|
if (component.removePrefixText) {
|
|
111
111
|
component.removePrefixText();
|
|
112
112
|
if (component.updateElementPositions) {
|
|
113
|
-
|
|
113
|
+
component.schedulePositionUpdate?.();
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
return this;
|
|
@@ -120,7 +120,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
120
120
|
if (component.setSuffixText) {
|
|
121
121
|
component.setSuffixText(text);
|
|
122
122
|
if (component.updateElementPositions) {
|
|
123
|
-
|
|
123
|
+
component.schedulePositionUpdate?.();
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
return this;
|
|
@@ -129,7 +129,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
129
129
|
if (component.removeSuffixText) {
|
|
130
130
|
component.removeSuffixText();
|
|
131
131
|
if (component.updateElementPositions) {
|
|
132
|
-
|
|
132
|
+
component.schedulePositionUpdate?.();
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
return this;
|
|
@@ -27,7 +27,7 @@ export const withDensity = (config) => (component) => {
|
|
|
27
27
|
component.density.current = newDensity;
|
|
28
28
|
}
|
|
29
29
|
if (component.updateElementPositions) {
|
|
30
|
-
|
|
30
|
+
component.updateElementPositions();
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
component.density = {
|
|
@@ -14,11 +14,12 @@ export const withLeadingIcon = (config) => (component) => {
|
|
|
14
14
|
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
15
15
|
const originalDestroy = component.lifecycle.destroy;
|
|
16
16
|
component.lifecycle.destroy = () => {
|
|
17
|
+
clearTimeout(initialPosition);
|
|
17
18
|
iconElement.remove();
|
|
18
19
|
originalDestroy.call(component.lifecycle);
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
setTimeout(() => {
|
|
22
|
+
const initialPosition = setTimeout(() => {
|
|
22
23
|
const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
|
|
23
24
|
if (labelEl) {
|
|
24
25
|
if (!component.element.classList.contains(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`)) {
|
|
@@ -17,6 +17,8 @@ export interface PlacementComponent extends BaseComponent {
|
|
|
17
17
|
* @returns The component instance for chaining
|
|
18
18
|
*/
|
|
19
19
|
updateElementPositions: () => PlacementComponent;
|
|
20
|
+
/** Queue a placement update owned by this component lifecycle. */
|
|
21
|
+
schedulePositionUpdate: () => void;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Handles dynamic positioning of textfield elements (label, prefix, suffix)
|
|
@@ -2,12 +2,24 @@ import { getInheritedBackground } from "../../../core/utils/background.js";
|
|
|
2
2
|
export const withPlacement = () => (component) => {
|
|
3
3
|
const PREFIX = component.config.prefix || "mtrl";
|
|
4
4
|
const COMPONENT = component.config.componentName || "textfield";
|
|
5
|
+
let destroyed = false;
|
|
6
|
+
let updateTimer = null;
|
|
7
|
+
const schedulePositionUpdate = () => {
|
|
8
|
+
if (destroyed)
|
|
9
|
+
return;
|
|
10
|
+
if (updateTimer !== null)
|
|
11
|
+
clearTimeout(updateTimer);
|
|
12
|
+
updateTimer = setTimeout(() => {
|
|
13
|
+
updateTimer = null;
|
|
14
|
+
updateElementPositions();
|
|
15
|
+
}, 10);
|
|
16
|
+
};
|
|
5
17
|
const bgSourceObservers = new WeakMap();
|
|
6
18
|
const parentObservers = new WeakMap();
|
|
7
19
|
const themeChangeHandlers = new WeakMap();
|
|
8
20
|
const classObservers = new WeakMap();
|
|
9
21
|
const updateElementPositions = () => {
|
|
10
|
-
if (!component.element || !component.element.isConnected)
|
|
22
|
+
if (destroyed || !component.element || !component.element.isConnected)
|
|
11
23
|
return component;
|
|
12
24
|
const labelEl = component.element.querySelector(`.${PREFIX}-${COMPONENT}-label`);
|
|
13
25
|
const prefixEl = component.element.querySelector(`.${PREFIX}-${COMPONENT}-prefix`);
|
|
@@ -110,7 +122,7 @@ export const withPlacement = () => (component) => {
|
|
|
110
122
|
mutations.forEach((mutation) => {
|
|
111
123
|
if (mutation.type === "attributes" &&
|
|
112
124
|
mutation.attributeName === "class") {
|
|
113
|
-
|
|
125
|
+
schedulePositionUpdate();
|
|
114
126
|
}
|
|
115
127
|
});
|
|
116
128
|
});
|
|
@@ -121,13 +133,22 @@ export const withPlacement = () => (component) => {
|
|
|
121
133
|
window.addEventListener("resize", updateElementPositions);
|
|
122
134
|
classObservers.set(component.element, classObserver);
|
|
123
135
|
};
|
|
124
|
-
setTimeout(() => {
|
|
136
|
+
const initialization = setTimeout(() => {
|
|
137
|
+
if (destroyed)
|
|
138
|
+
return;
|
|
125
139
|
setupEventListeners();
|
|
126
140
|
updateElementPositions();
|
|
127
141
|
}, 0);
|
|
128
142
|
if ("lifecycle" in component && component.lifecycle?.destroy) {
|
|
129
143
|
const originalDestroy = component.lifecycle.destroy;
|
|
130
144
|
component.lifecycle.destroy = () => {
|
|
145
|
+
if (destroyed)
|
|
146
|
+
return;
|
|
147
|
+
destroyed = true;
|
|
148
|
+
clearTimeout(initialization);
|
|
149
|
+
if (updateTimer !== null)
|
|
150
|
+
clearTimeout(updateTimer);
|
|
151
|
+
updateTimer = null;
|
|
131
152
|
window.removeEventListener("resize", updateElementPositions);
|
|
132
153
|
const classObserver = classObservers.get(component.element);
|
|
133
154
|
if (classObserver) {
|
|
@@ -154,6 +175,7 @@ export const withPlacement = () => (component) => {
|
|
|
154
175
|
}
|
|
155
176
|
return {
|
|
156
177
|
...component,
|
|
178
|
+
schedulePositionUpdate,
|
|
157
179
|
updateElementPositions: () => {
|
|
158
180
|
updateElementPositions();
|
|
159
181
|
return component;
|
|
@@ -7,7 +7,7 @@ import { createBaseConfig, getElementConfig, getApiConfig } from "./config.js";
|
|
|
7
7
|
const createTextfield = (config = {}) => {
|
|
8
8
|
const baseConfig = createBaseConfig(config);
|
|
9
9
|
try {
|
|
10
|
-
const textfield = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)), withVariant(baseConfig), withDensity(baseConfig), withTextInput(baseConfig), withTextLabel(baseConfig), withLeadingIcon(baseConfig), withTrailingIcon(baseConfig), withPrefixText(baseConfig), withSuffixText(baseConfig), withSupportingText(baseConfig), withError(baseConfig), withDisabled(baseConfig),
|
|
10
|
+
const textfield = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)), withLifecycle(), withVariant(baseConfig), withDensity(baseConfig), withTextInput(baseConfig), withTextLabel(baseConfig), withLeadingIcon(baseConfig), withTrailingIcon(baseConfig), withPrefixText(baseConfig), withSuffixText(baseConfig), withSupportingText(baseConfig), withError(baseConfig), withDisabled(baseConfig), withPlacement(), (comp) => withAPI(getApiConfig(comp))(comp))(baseConfig);
|
|
11
11
|
return textfield;
|
|
12
12
|
}
|
|
13
13
|
catch (error) {
|
|
@@ -118,7 +118,7 @@ export const withTextInput = (config = {}) => (component) => {
|
|
|
118
118
|
}
|
|
119
119
|
});
|
|
120
120
|
updateInputState();
|
|
121
|
-
setTimeout(() => {
|
|
121
|
+
const initialAutofillCheck = setTimeout(() => {
|
|
122
122
|
checkForAutofill(false);
|
|
123
123
|
}, 0);
|
|
124
124
|
if (isMultiline) {
|
|
@@ -128,6 +128,7 @@ export const withTextInput = (config = {}) => (component) => {
|
|
|
128
128
|
if (hasLifecycle(component)) {
|
|
129
129
|
const originalDestroy = component.lifecycle.destroy;
|
|
130
130
|
component.lifecycle.destroy = () => {
|
|
131
|
+
clearTimeout(initialAutofillCheck);
|
|
131
132
|
if (autofillObserver) {
|
|
132
133
|
autofillObserver.disconnect();
|
|
133
134
|
}
|