material 3.3.3 → 3.4.0
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/README.md +2 -2
- package/dist/components/badge/api.js +7 -5
- package/dist/components/badge/config.js +3 -15
- package/dist/components/badge/features.js +6 -10
- package/dist/components/chips/chip/chip.js +3 -0
- package/dist/components/chips/types.d.ts +1 -0
- package/dist/components/menu/features/controller.js +23 -75
- package/dist/components/menu/features/opener.js +3 -15
- package/dist/components/menu/types.d.ts +1 -0
- package/dist/components/search/features/input.js +1 -1
- package/dist/components/search/features/structure.js +1 -1
- package/dist/components/search/types.d.ts +1 -0
- package/dist/components/select/features.js +2 -0
- package/dist/components/select/types.d.ts +1 -0
- package/dist/components/slider/features/dom.js +27 -9
- package/dist/components/split-button/features/menu-on-demand.js +141 -0
- package/dist/components/split-button/features/menu.js +3 -4
- package/dist/components/tabs/tab.js +2 -1
- package/dist/components/text-field/config.js +21 -1
- package/dist/components/text-field/types.d.ts +1 -0
- package/dist/core/compose/features/badge.js +1 -1
- package/dist/core/compose/features/textinput.d.ts +1 -0
- package/dist/core/compose/features/textinput.js +1 -0
- package/dist/elements/css/switch.css +1 -1
- package/dist/elements/css/switch.js +1 -1
- package/dist/elements/preupgrade/badge.css +1 -1
- package/dist/elements/preupgrade/bottom-app-bar.css +1 -1
- package/dist/elements/preupgrade/bottom-sheet.css +1 -1
- package/dist/elements/preupgrade/button-group.css +1 -1
- package/dist/elements/preupgrade/button.css +1 -1
- package/dist/elements/preupgrade/card.css +1 -1
- package/dist/elements/preupgrade/carousel.css +1 -1
- package/dist/elements/preupgrade/checkbox.css +1 -1
- package/dist/elements/preupgrade/chips.css +1 -1
- package/dist/elements/preupgrade/datepicker.css +1 -1
- package/dist/elements/preupgrade/dialog.css +1 -1
- package/dist/elements/preupgrade/divider.css +1 -1
- package/dist/elements/preupgrade/drawer.css +1 -1
- package/dist/elements/preupgrade/extended-fab.css +1 -1
- package/dist/elements/preupgrade/fab-menu.css +1 -1
- package/dist/elements/preupgrade/fab.css +1 -1
- package/dist/elements/preupgrade/icon-button.css +1 -1
- package/dist/elements/preupgrade/list.css +1 -1
- package/dist/elements/preupgrade/loading-indicator.css +1 -1
- package/dist/elements/preupgrade/menu.css +1 -1
- package/dist/elements/preupgrade/navigation-bar.css +1 -1
- package/dist/elements/preupgrade/navigation-rail.css +1 -1
- package/dist/elements/preupgrade/progress.css +1 -1
- package/dist/elements/preupgrade/radios.css +1 -1
- package/dist/elements/preupgrade/search.css +1 -1
- package/dist/elements/preupgrade/select.css +1 -1
- package/dist/elements/preupgrade/side-sheet.css +1 -1
- package/dist/elements/preupgrade/slider.css +1 -1
- package/dist/elements/preupgrade/snackbar.css +1 -1
- package/dist/elements/preupgrade/split-button.css +1 -1
- package/dist/elements/preupgrade/switch.css +1 -1
- package/dist/elements/preupgrade/tabs.css +1 -1
- package/dist/elements/preupgrade/text-field.css +1 -1
- package/dist/elements/preupgrade/timepicker.css +1 -1
- package/dist/elements/preupgrade/toolbar.css +1 -1
- package/dist/elements/preupgrade/tooltip.css +1 -1
- package/dist/elements/preupgrade/top-app-bar.css +1 -1
- package/dist/elements/preupgrade.css +1 -1
- 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/contrast.css +1 -1
- package/dist/styles/datepicker.css +1 -1
- package/dist/styles/dialog.css +1 -1
- package/dist/styles/divider.css +1 -1
- package/dist/styles/drawer.css +1 -1
- package/dist/styles/extended-fab.css +1 -1
- package/dist/styles/fab-menu.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-bar.css +1 -1
- package/dist/styles/navigation-rail.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/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 +2 -2
- package/dist/styles/tabs.css +1 -1
- package/dist/styles/text-field.css +1 -1
- package/dist/styles/timepicker.css +1 -1
- package/dist/styles/toolbar.css +1 -1
- package/dist/styles/tooltip.css +1 -1
- package/dist/styles/top-app-bar.css +1 -1
- package/dist/styles/typography.css +1 -1
- package/dist/styles/utilities.css +1 -1
- package/dist/styles.css +2 -2
- package/dist/themes/autumn-contrast.css +1 -1
- package/dist/themes/autumn.css +1 -1
- package/dist/themes/baseline-contrast.css +1 -1
- package/dist/themes/baseline.css +1 -1
- package/dist/themes/brownbeige-contrast.css +1 -1
- package/dist/themes/brownbeige.css +1 -1
- package/dist/themes/content-contrast.css +1 -1
- package/dist/themes/content.css +1 -1
- package/dist/themes/desert-contrast.css +1 -1
- package/dist/themes/desert.css +1 -1
- package/dist/themes/expressive-contrast.css +1 -1
- package/dist/themes/expressive.css +1 -1
- package/dist/themes/fidelity-contrast.css +1 -1
- package/dist/themes/fidelity.css +1 -1
- package/dist/themes/forest-contrast.css +1 -1
- package/dist/themes/forest.css +1 -1
- package/dist/themes/fruit-salad-contrast.css +1 -1
- package/dist/themes/fruit-salad.css +1 -1
- package/dist/themes/highcontrast-contrast.css +1 -1
- package/dist/themes/highcontrast.css +1 -1
- package/dist/themes/monochrome-contrast.css +1 -1
- package/dist/themes/monochrome.css +1 -1
- package/dist/themes/neutral-contrast.css +1 -1
- package/dist/themes/neutral.css +1 -1
- package/dist/themes/ocean-contrast.css +1 -1
- package/dist/themes/ocean.css +1 -1
- package/dist/themes/rainbow-contrast.css +1 -1
- package/dist/themes/rainbow.css +1 -1
- package/dist/themes/sageivory-contrast.css +1 -1
- package/dist/themes/sageivory.css +1 -1
- package/dist/themes/spring-contrast.css +1 -1
- package/dist/themes/spring.css +1 -1
- package/dist/themes/summer-contrast.css +1 -1
- package/dist/themes/summer.css +1 -1
- package/dist/themes/sunset-contrast.css +1 -1
- package/dist/themes/sunset.css +1 -1
- package/dist/themes/tealcaramel-contrast.css +1 -1
- package/dist/themes/tealcaramel.css +1 -1
- package/dist/themes/vibrant-contrast.css +1 -1
- package/dist/themes/vibrant.css +1 -1
- package/package.json +1 -1
- package/src/styles/components/_switch.scss +15 -5
- package/dist/components/badge/types.js +0 -1
- package/dist/components/bottom-app-bar/types.js +0 -1
- package/dist/components/bottom-sheet/types.js +0 -1
- package/dist/components/button/types.js +0 -1
- package/dist/components/button-group/types.js +0 -1
- package/dist/components/card/types.js +0 -1
- package/dist/components/carousel/types.js +0 -1
- package/dist/components/checkbox/types.js +0 -1
- package/dist/components/chips/types.js +0 -1
- package/dist/components/dialog/types.js +0 -1
- package/dist/components/divider/types.js +0 -1
- package/dist/components/drawer/types.js +0 -1
- package/dist/components/extended-fab/types.js +0 -1
- package/dist/components/fab/types.js +0 -1
- package/dist/components/fab-menu/types.js +0 -1
- package/dist/components/icon-button/types.js +0 -1
- package/dist/components/list/types.js +0 -1
- package/dist/components/loading-indicator/types.js +0 -1
- package/dist/components/menu/types.js +0 -14
- package/dist/components/navigation-bar/types.js +0 -1
- package/dist/components/navigation-rail/types.js +0 -1
- package/dist/components/progress/types.js +0 -1
- package/dist/components/radios/types.js +0 -1
- package/dist/components/search/types.js +0 -1
- package/dist/components/select/types.js +0 -4
- package/dist/components/side-sheet/types.js +0 -1
- package/dist/components/snackbar/types.js +0 -1
- package/dist/components/split-button/types.js +0 -1
- package/dist/components/switch/types.js +0 -1
- package/dist/components/tabs/types.js +0 -1
- package/dist/components/text-field/types.js +0 -18
- package/dist/components/toolbar/types.js +0 -1
- package/dist/components/top-app-bar/types.js +0 -1
- package/dist/react/jsx.js +0 -2
- package/dist/solid/jsx.js +0 -1
package/README.md
CHANGED
|
@@ -93,8 +93,8 @@ What a bundler keeps, minified and gzipped (kB is 1,000 bytes), measured from th
|
|
|
93
93
|
|--------|-----:|
|
|
94
94
|
| `createButton` from `material`, with code splitting: the initial chunks (the progress indicator loads on demand) | 7.6 kB |
|
|
95
95
|
| `createButton`, without code splitting (the progress indicator included) | 13.9 kB |
|
|
96
|
-
| `createButton`, `createTextField` and `createCheckbox`, without code splitting | 20.
|
|
97
|
-
| Everything the root exports (`export * from 'material'`), without code splitting |
|
|
96
|
+
| `createButton`, `createTextField` and `createCheckbox`, without code splitting | 20.4 kB |
|
|
97
|
+
| Everything the root exports (`export * from 'material'`), without code splitting | 127.4 kB |
|
|
98
98
|
| `material/styles/base` | 2.9 kB |
|
|
99
99
|
| `material/styles/base` and `material/styles/button` | 5.2 kB |
|
|
100
100
|
| `material/styles`, the full stylesheet | 63.7 kB |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isBadgeOverflow, isEmptyBadgeLabel } from "./config.js";
|
|
1
|
+
import { isBadgeOverflow, isEmptyBadgeLabel, smallBadgeLabel } from "./config.js";
|
|
2
2
|
import { formatBadgeLabel } from "./config.js";
|
|
3
3
|
const VARIANT_SMALL = "small";
|
|
4
4
|
const ALL_COLORS = [
|
|
@@ -21,10 +21,12 @@ export const withAPI = ({ visibility, lifecycle }) => (component) => {
|
|
|
21
21
|
component.config.label = label;
|
|
22
22
|
if (component.config.variant === VARIANT_SMALL) {
|
|
23
23
|
component.element.textContent = "";
|
|
24
|
+
component.element.setAttribute("aria-label", smallBadgeLabel(label));
|
|
24
25
|
return badgeComponent;
|
|
25
26
|
}
|
|
26
27
|
const formattedLabel = formatBadgeLabel(label, component.config.max);
|
|
27
28
|
component.element.textContent = formattedLabel;
|
|
29
|
+
component.element.removeAttribute("aria-label");
|
|
28
30
|
component.element.classList.remove(`${component.getClass("badge")}--overflow`);
|
|
29
31
|
if (isBadgeOverflow(label, component.config.max)) {
|
|
30
32
|
component.element.classList.add(`${component.getClass("badge")}--overflow`);
|
|
@@ -81,14 +83,14 @@ export const withAPI = ({ visibility, lifecycle }) => (component) => {
|
|
|
81
83
|
});
|
|
82
84
|
component.element.classList.add(`${component.getClass("badge")}--${variant}`);
|
|
83
85
|
component.config.variant = variant;
|
|
86
|
+
component.element.removeAttribute("aria-hidden");
|
|
87
|
+
component.element.setAttribute("role", "status");
|
|
84
88
|
if (variant === VARIANT_SMALL) {
|
|
85
89
|
component.element.textContent = "";
|
|
86
|
-
component.element.
|
|
87
|
-
component.element.setAttribute("aria-hidden", "true");
|
|
90
|
+
component.element.setAttribute("aria-label", smallBadgeLabel(component.config.label));
|
|
88
91
|
}
|
|
89
92
|
else {
|
|
90
|
-
component.element.removeAttribute("aria-
|
|
91
|
-
component.element.setAttribute("role", "status");
|
|
93
|
+
component.element.removeAttribute("aria-label");
|
|
92
94
|
if (component.config.label !== undefined) {
|
|
93
95
|
badgeComponent.setLabel(component.config.label);
|
|
94
96
|
}
|
|
@@ -7,22 +7,10 @@ export const defaultConfig = {
|
|
|
7
7
|
label: "",
|
|
8
8
|
visible: true,
|
|
9
9
|
};
|
|
10
|
+
export const smallBadgeLabel = (label) => typeof label === "string" && isNaN(+label)
|
|
11
|
+
? label.trim() : "New notification";
|
|
10
12
|
export const createBaseConfig = (config = {}) => createComponentConfig(defaultConfig, config, "badge");
|
|
11
|
-
export const getElementConfig = (config) => {
|
|
12
|
-
const attributes = {};
|
|
13
|
-
if (config.variant !== "small") {
|
|
14
|
-
attributes.role = "status";
|
|
15
|
-
}
|
|
16
|
-
const formattedLabel = config.variant === "small"
|
|
17
|
-
? ""
|
|
18
|
-
: formatBadgeLabel(config.label || "", config.max);
|
|
19
|
-
return createElementConfig(config, {
|
|
20
|
-
tag: "span",
|
|
21
|
-
attributes,
|
|
22
|
-
className: config.class,
|
|
23
|
-
text: formattedLabel,
|
|
24
|
-
});
|
|
25
|
-
};
|
|
13
|
+
export const getElementConfig = (config) => createElementConfig(config, { tag: "span" });
|
|
26
14
|
export const getApiConfig = (comp) => ({
|
|
27
15
|
visibility: {
|
|
28
16
|
show: () => comp.visibility.show(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatBadgeLabel, isBadgeOverflow, isEmptyBadgeLabel } from "./config.js";
|
|
1
|
+
import { formatBadgeLabel, isBadgeOverflow, isEmptyBadgeLabel, smallBadgeLabel } from "./config.js";
|
|
2
2
|
const VARIANT_SMALL = 'small';
|
|
3
3
|
const VARIANT_LARGE = 'large';
|
|
4
4
|
const COLOR_ERROR = 'error';
|
|
@@ -38,16 +38,14 @@ export const withVisibility = () => (component) => {
|
|
|
38
38
|
export const withVariant = (config) => (component) => {
|
|
39
39
|
const variant = config.variant || VARIANT_LARGE;
|
|
40
40
|
component.element.classList.add(`${component.getClass('badge')}--${variant}`);
|
|
41
|
+
component.element.setAttribute('role', 'status');
|
|
41
42
|
if (variant === VARIANT_SMALL) {
|
|
42
|
-
component.element.
|
|
43
|
-
component.element.
|
|
43
|
+
component.element.setAttribute('aria-label', smallBadgeLabel(config.label));
|
|
44
|
+
component.element.removeAttribute('aria-hidden');
|
|
44
45
|
}
|
|
45
46
|
else {
|
|
46
|
-
component.element.
|
|
47
|
-
|
|
48
|
-
const formattedLabel = formatBadgeLabel(config.label, config.max);
|
|
49
|
-
component.element.textContent = formattedLabel;
|
|
50
|
-
}
|
|
47
|
+
component.element.removeAttribute('aria-label');
|
|
48
|
+
component.element.textContent = formatBadgeLabel(config.label ?? '', config.max);
|
|
51
49
|
}
|
|
52
50
|
return component;
|
|
53
51
|
};
|
|
@@ -70,8 +68,6 @@ export const withMax = (config) => (component) => {
|
|
|
70
68
|
}
|
|
71
69
|
component.config.max = config.max;
|
|
72
70
|
if (config.label !== undefined) {
|
|
73
|
-
const formattedLabel = formatBadgeLabel(config.label, config.max);
|
|
74
|
-
component.element.textContent = formattedLabel;
|
|
75
71
|
if (isBadgeOverflow(config.label, config.max)) {
|
|
76
72
|
component.element.classList.add(`${component.getClass('badge')}--overflow`);
|
|
77
73
|
}
|
|
@@ -90,6 +90,7 @@ const createChip = (config = {}) => {
|
|
|
90
90
|
root.classList.toggle(base.getClass("chip--avatar"), !!avatar);
|
|
91
91
|
action.disabled = disabled;
|
|
92
92
|
if (oneActionCell) {
|
|
93
|
+
root.setAttribute("aria-label", label.textContent ?? "");
|
|
93
94
|
if (selectable)
|
|
94
95
|
root.setAttribute("aria-selected", String(selected));
|
|
95
96
|
root.setAttribute("aria-disabled", String(disabled));
|
|
@@ -114,6 +115,8 @@ const createChip = (config = {}) => {
|
|
|
114
115
|
if (!options.cell)
|
|
115
116
|
root.setAttribute("aria-label", text);
|
|
116
117
|
}
|
|
118
|
+
if (options.ariaLabel)
|
|
119
|
+
(oneActionCell ? root : action).setAttribute("aria-label", options.ariaLabel);
|
|
117
120
|
};
|
|
118
121
|
let made = false;
|
|
119
122
|
const animateChanges = () => {
|
|
@@ -45,23 +45,11 @@ const withController = (config, getComponent) => (component) => {
|
|
|
45
45
|
return eventData;
|
|
46
46
|
},
|
|
47
47
|
};
|
|
48
|
-
const getOpenerElement = () =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (typeof opener === "string") {
|
|
54
|
-
const element = document.querySelector(opener);
|
|
55
|
-
if (!element) {
|
|
56
|
-
console.warn(`Menu opener not found: ${opener}`);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
return element;
|
|
60
|
-
}
|
|
61
|
-
if (typeof opener === "object" && opener !== null && "element" in opener) {
|
|
62
|
-
return opener.element;
|
|
63
|
-
}
|
|
64
|
-
return opener;
|
|
48
|
+
const getOpenerElement = () => getComponent().getOpener() ?? null;
|
|
49
|
+
const reposition = () => {
|
|
50
|
+
const opener = getOpenerElement();
|
|
51
|
+
if (opener)
|
|
52
|
+
component.position?.positionMenu(opener);
|
|
65
53
|
};
|
|
66
54
|
const createMenuItem = (item, index) => {
|
|
67
55
|
const itemElement = document.createElement("li");
|
|
@@ -287,10 +275,7 @@ const withController = (config, getComponent) => (component) => {
|
|
|
287
275
|
if (topLayer)
|
|
288
276
|
showInTopLayer(component.element, { kind: "popover-manual" });
|
|
289
277
|
showTimer = tasks.setTimeout(() => {
|
|
290
|
-
|
|
291
|
-
if (openerElement && component.position) {
|
|
292
|
-
component.position.positionMenu(openerElement);
|
|
293
|
-
}
|
|
278
|
+
reposition();
|
|
294
279
|
component.element.setAttribute("aria-hidden", "false");
|
|
295
280
|
component.element.style.transform = "";
|
|
296
281
|
component.element.style.opacity = "";
|
|
@@ -298,24 +283,7 @@ const withController = (config, getComponent) => (component) => {
|
|
|
298
283
|
component.element.classList.add(`${component.getClass("menu--visible")}`);
|
|
299
284
|
if (!listbox)
|
|
300
285
|
focusTimer = tasks.setTimeout(() => {
|
|
301
|
-
|
|
302
|
-
component.keyboard.handleInitialFocus(component.element, interactionType);
|
|
303
|
-
}
|
|
304
|
-
else {
|
|
305
|
-
const items = Array.from(component.element.querySelectorAll(`.${component.getClass("menu__item")}`));
|
|
306
|
-
items.forEach((item) => {
|
|
307
|
-
item.tabIndex = -1;
|
|
308
|
-
});
|
|
309
|
-
if (items.length > 0)
|
|
310
|
-
items[0].tabIndex = 0;
|
|
311
|
-
if (interactionType === "keyboard" && items.length > 0) {
|
|
312
|
-
items[0].focus();
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
component.element.tabIndex = -1;
|
|
316
|
-
component.element.focus();
|
|
317
|
-
}
|
|
318
|
-
}
|
|
286
|
+
component.keyboard?.handleInitialFocus(component.element, interactionType);
|
|
319
287
|
}, 100);
|
|
320
288
|
window.addEventListener("resize", handleWindowResize, {
|
|
321
289
|
passive: true,
|
|
@@ -406,26 +374,18 @@ const withController = (config, getComponent) => (component) => {
|
|
|
406
374
|
}
|
|
407
375
|
}
|
|
408
376
|
else if (isTargetInsideSubmenu) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
else if (e.key === "Escape") {
|
|
417
|
-
e.preventDefault();
|
|
418
|
-
closeMenu(e, true);
|
|
419
|
-
}
|
|
377
|
+
component.keyboard?.handleMenuKeydown(e, state, {
|
|
378
|
+
closeMenu,
|
|
379
|
+
findItemById,
|
|
380
|
+
...submenuActions,
|
|
381
|
+
});
|
|
420
382
|
}
|
|
421
383
|
else {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
});
|
|
428
|
-
}
|
|
384
|
+
component.keyboard?.handleMenuKeydown(e, state, {
|
|
385
|
+
closeMenu,
|
|
386
|
+
findItemById,
|
|
387
|
+
...submenuActions,
|
|
388
|
+
});
|
|
429
389
|
}
|
|
430
390
|
}
|
|
431
391
|
};
|
|
@@ -435,10 +395,7 @@ const withController = (config, getComponent) => (component) => {
|
|
|
435
395
|
closeMenu(undefined, true);
|
|
436
396
|
}
|
|
437
397
|
else {
|
|
438
|
-
|
|
439
|
-
if (openerElement && component.position) {
|
|
440
|
-
component.position.positionMenu(openerElement);
|
|
441
|
-
}
|
|
398
|
+
reposition();
|
|
442
399
|
}
|
|
443
400
|
}
|
|
444
401
|
};
|
|
@@ -447,19 +404,13 @@ const withController = (config, getComponent) => (component) => {
|
|
|
447
404
|
return;
|
|
448
405
|
if (state.visible) {
|
|
449
406
|
tasks.requestAnimationFrame(() => {
|
|
450
|
-
|
|
451
|
-
if (openerElement && component.position) {
|
|
452
|
-
component.position.positionMenu(openerElement);
|
|
453
|
-
}
|
|
407
|
+
reposition();
|
|
454
408
|
});
|
|
455
409
|
}
|
|
456
410
|
};
|
|
457
411
|
const initMenu = () => {
|
|
458
412
|
if (state.visible) {
|
|
459
|
-
|
|
460
|
-
if (openerElement && component.position) {
|
|
461
|
-
component.position.positionMenu(openerElement);
|
|
462
|
-
}
|
|
413
|
+
reposition();
|
|
463
414
|
component.element.classList.add(`${component.getClass("menu--visible")}`);
|
|
464
415
|
if (config.closeOnClickOutside) {
|
|
465
416
|
document.addEventListener("click", handleDocumentClick);
|
|
@@ -493,8 +444,8 @@ const withController = (config, getComponent) => (component) => {
|
|
|
493
444
|
});
|
|
494
445
|
});
|
|
495
446
|
renderMenuItems();
|
|
496
|
-
if (!listbox
|
|
497
|
-
component.keyboard
|
|
447
|
+
if (!listbox) {
|
|
448
|
+
component.keyboard?.setupKeyboardHandlers(component.element, state, {
|
|
498
449
|
closeMenu,
|
|
499
450
|
findItemById,
|
|
500
451
|
...submenuActions,
|
|
@@ -550,10 +501,7 @@ const withController = (config, getComponent) => (component) => {
|
|
|
550
501
|
setPosition: (position) => {
|
|
551
502
|
state.position = position;
|
|
552
503
|
if (state.visible) {
|
|
553
|
-
|
|
554
|
-
if (openerElement && component.position) {
|
|
555
|
-
component.position.positionMenu(openerElement);
|
|
556
|
-
}
|
|
504
|
+
reposition();
|
|
557
505
|
}
|
|
558
506
|
return component;
|
|
559
507
|
},
|
|
@@ -2,16 +2,7 @@ import { createMenuTasks } from "./tasks.js";
|
|
|
2
2
|
import { focusWithin } from "./layer.js";
|
|
3
3
|
const withOpener = (config) => (component) => {
|
|
4
4
|
const tasks = createMenuTasks();
|
|
5
|
-
|
|
6
|
-
let tabTimeout = null;
|
|
7
|
-
const handleTabKey = (e) => {
|
|
8
|
-
isTabNavigation = e.key === "Tab";
|
|
9
|
-
tasks.clearTimeout(tabTimeout);
|
|
10
|
-
tabTimeout = tasks.setTimeout(() => {
|
|
11
|
-
isTabNavigation = false;
|
|
12
|
-
}, 100);
|
|
13
|
-
};
|
|
14
|
-
document.addEventListener("keydown", handleTabKey);
|
|
5
|
+
const isTabNavigationActive = () => component.keyboard?.isTabNavigationActive?.() ?? false;
|
|
15
6
|
const state = {
|
|
16
7
|
openerElement: null,
|
|
17
8
|
openerComponent: null,
|
|
@@ -203,7 +194,7 @@ const withOpener = (config) => (component) => {
|
|
|
203
194
|
if (!component.menu || !component.menu.isOpen())
|
|
204
195
|
return;
|
|
205
196
|
const relatedTarget = e.relatedTarget;
|
|
206
|
-
if (
|
|
197
|
+
if (isTabNavigationActive()) {
|
|
207
198
|
tasks.setTimeout(() => {
|
|
208
199
|
if (component.menu && component.menu.isOpen()) {
|
|
209
200
|
component.menu.close(e, false);
|
|
@@ -228,8 +219,6 @@ const withOpener = (config) => (component) => {
|
|
|
228
219
|
};
|
|
229
220
|
const cleanup = () => {
|
|
230
221
|
tasks.cancelAll();
|
|
231
|
-
tabTimeout = null;
|
|
232
|
-
isTabNavigation = false;
|
|
233
222
|
if (state.openerElement) {
|
|
234
223
|
if (!config.manualOpen) {
|
|
235
224
|
state.openerElement.removeEventListener("click", handleOpenerClick);
|
|
@@ -254,7 +243,6 @@ const withOpener = (config) => (component) => {
|
|
|
254
243
|
if (tasks.destroyed)
|
|
255
244
|
return;
|
|
256
245
|
tasks.destroy();
|
|
257
|
-
document.removeEventListener("keydown", handleTabKey);
|
|
258
246
|
cleanup();
|
|
259
247
|
originalDestroy.call(component.lifecycle);
|
|
260
248
|
};
|
|
@@ -269,7 +257,7 @@ const withOpener = (config) => (component) => {
|
|
|
269
257
|
if (state.openerElement && !config.listbox) {
|
|
270
258
|
state.openerElement.setAttribute("aria-expanded", "false");
|
|
271
259
|
setOpenerActive(false);
|
|
272
|
-
if (event.restoreFocus && !
|
|
260
|
+
if (event.restoreFocus && !isTabNavigationActive()) {
|
|
273
261
|
restoreFocusToOpener();
|
|
274
262
|
}
|
|
275
263
|
}
|
|
@@ -165,6 +165,7 @@ export interface MenuFeatureHost {
|
|
|
165
165
|
removeKeyboardHandlers: (element: HTMLElement) => void;
|
|
166
166
|
handleMenuKeydown: (e: KeyboardEvent, state: KeyboardMenuState, actions: KeyboardActions) => void;
|
|
167
167
|
handleInitialFocus: (menuElement: HTMLElement, interactionType: "keyboard" | "mouse") => void;
|
|
168
|
+
isTabNavigationActive: () => boolean;
|
|
168
169
|
};
|
|
169
170
|
submenu?: {
|
|
170
171
|
handleSubmenuClick: (item: MenuItem, index: number, itemElement: HTMLElement) => void;
|
|
@@ -76,7 +76,7 @@ export const withInput = (config, getComponent) => (component) => {
|
|
|
76
76
|
const input = component.structure?.input;
|
|
77
77
|
if (input) {
|
|
78
78
|
input.placeholder = text;
|
|
79
|
-
input.setAttribute("aria-label", text);
|
|
79
|
+
input.setAttribute("aria-label", config.ariaLabel || text);
|
|
80
80
|
}
|
|
81
81
|
};
|
|
82
82
|
const getPlaceholder = () => {
|
|
@@ -77,7 +77,7 @@ export const withStructure = (config) => (component) => {
|
|
|
77
77
|
const inputAttributes = {
|
|
78
78
|
type: "text",
|
|
79
79
|
placeholder,
|
|
80
|
-
"aria-label": placeholder,
|
|
80
|
+
"aria-label": config.ariaLabel || placeholder,
|
|
81
81
|
role: "combobox",
|
|
82
82
|
"aria-autocomplete": "list",
|
|
83
83
|
"aria-expanded": String(isViewState),
|
|
@@ -29,6 +29,8 @@ export const withTextField = (config) => (component) => {
|
|
|
29
29
|
});
|
|
30
30
|
textField.element.classList.add(`${config.prefix || "mtrl"}-select`);
|
|
31
31
|
if (textField.input) {
|
|
32
|
+
if (config.ariaLabel)
|
|
33
|
+
textField.input.setAttribute("aria-label", config.ariaLabel);
|
|
32
34
|
const input = textField.input;
|
|
33
35
|
input.addEventListener("keydown", (e) => {
|
|
34
36
|
const allowedKeys = [
|
|
@@ -16,10 +16,8 @@ export const withDom = (config) => (component) => {
|
|
|
16
16
|
setStyles(container, {
|
|
17
17
|
position: "relative",
|
|
18
18
|
});
|
|
19
|
-
const label = config.label || "";
|
|
20
|
-
const
|
|
21
|
-
const secondHandleLabel = label ? `${label} maximum` : "";
|
|
22
|
-
const handleElement = (index, handleValue, name) => {
|
|
19
|
+
const label = config.ariaLabel || config.label || "";
|
|
20
|
+
const handleElement = (index, handleValue) => {
|
|
23
21
|
const attributes = {
|
|
24
22
|
role: "slider",
|
|
25
23
|
"aria-valuemin": String(min),
|
|
@@ -31,9 +29,6 @@ export const withDom = (config) => (component) => {
|
|
|
31
29
|
"data-value": String(handleValue),
|
|
32
30
|
"data-handle-index": String(index),
|
|
33
31
|
};
|
|
34
|
-
if (name) {
|
|
35
|
-
attributes["aria-label"] = name;
|
|
36
|
-
}
|
|
37
32
|
const element = createElement({
|
|
38
33
|
tag: "div",
|
|
39
34
|
className: getClass("slider__handle"),
|
|
@@ -61,15 +56,38 @@ export const withDom = (config) => (component) => {
|
|
|
61
56
|
});
|
|
62
57
|
return element;
|
|
63
58
|
};
|
|
64
|
-
const handle = handleElement(0, value
|
|
59
|
+
const handle = handleElement(0, value);
|
|
65
60
|
const valueBubble = bubbleElement(0, value);
|
|
66
61
|
let secondHandle;
|
|
67
62
|
let secondValueBubble;
|
|
68
63
|
if (config.range) {
|
|
69
64
|
const secondValue = config.secondValue !== undefined ? config.secondValue : max;
|
|
70
|
-
secondHandle = handleElement(1, secondValue
|
|
65
|
+
secondHandle = handleElement(1, secondValue);
|
|
71
66
|
secondValueBubble = bubbleElement(1, secondValue);
|
|
72
67
|
}
|
|
68
|
+
[handle, secondHandle].forEach((target, index) => {
|
|
69
|
+
if (!target)
|
|
70
|
+
return;
|
|
71
|
+
const endpoint = config.range ? index ? "maximum" : "minimum" : "";
|
|
72
|
+
if (label)
|
|
73
|
+
target.setAttribute("aria-label", endpoint ? `${label} ${endpoint}` : label);
|
|
74
|
+
if (config.ariaLabelledBy) {
|
|
75
|
+
let labelledBy = config.ariaLabelledBy;
|
|
76
|
+
if (endpoint) {
|
|
77
|
+
const suffix = createElement({
|
|
78
|
+
tag: "span",
|
|
79
|
+
text: endpoint,
|
|
80
|
+
attributes: {
|
|
81
|
+
id: `${getClass("slider")}-${Math.random().toString(36).slice(2, 9)}`,
|
|
82
|
+
hidden: true,
|
|
83
|
+
},
|
|
84
|
+
container: target,
|
|
85
|
+
});
|
|
86
|
+
labelledBy += ` ${suffix.id}`;
|
|
87
|
+
}
|
|
88
|
+
target.setAttribute("aria-labelledby", labelledBy);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
73
91
|
const name = config.name;
|
|
74
92
|
const formFields = name
|
|
75
93
|
? [
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export const createOnDemandMenu = (settings, items) => {
|
|
2
|
+
let menu = null;
|
|
3
|
+
let loading = false;
|
|
4
|
+
let destroyed = false;
|
|
5
|
+
let opened = false;
|
|
6
|
+
let asked = [undefined, undefined];
|
|
7
|
+
let current = items;
|
|
8
|
+
let selected;
|
|
9
|
+
let position = settings.position ?? "bottom-end";
|
|
10
|
+
let opener = settings.opener;
|
|
11
|
+
const openerElement = typeof settings.opener === "string"
|
|
12
|
+
? null
|
|
13
|
+
: settings.opener.element ?? settings.opener;
|
|
14
|
+
const placeholder = document.createElement("div");
|
|
15
|
+
placeholder.id = `menu-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
|
16
|
+
openerElement?.setAttribute("aria-controls", placeholder.id);
|
|
17
|
+
const listeners = new Map();
|
|
18
|
+
const notify = (event, payload) => {
|
|
19
|
+
for (const listener of listeners.get(event) ?? [])
|
|
20
|
+
listener(payload);
|
|
21
|
+
};
|
|
22
|
+
const attach = (created) => {
|
|
23
|
+
menu = created;
|
|
24
|
+
if (selected !== undefined)
|
|
25
|
+
created.setSelected(selected);
|
|
26
|
+
if (position !== settings.position)
|
|
27
|
+
created.setPosition(position);
|
|
28
|
+
if (opener !== settings.opener)
|
|
29
|
+
created.setOpener(opener);
|
|
30
|
+
if (opened)
|
|
31
|
+
created.open(asked[0], asked[1]);
|
|
32
|
+
for (const [event, set] of listeners)
|
|
33
|
+
for (const listener of set)
|
|
34
|
+
created.on(event, listener);
|
|
35
|
+
listeners.clear();
|
|
36
|
+
};
|
|
37
|
+
const load = () => {
|
|
38
|
+
if (menu || loading || destroyed)
|
|
39
|
+
return;
|
|
40
|
+
loading = true;
|
|
41
|
+
import("../../menu/index.js").then(({ default: createMenu }) => {
|
|
42
|
+
loading = false;
|
|
43
|
+
if (destroyed)
|
|
44
|
+
return;
|
|
45
|
+
attach(createMenu({ ...settings, items: current }));
|
|
46
|
+
}, (error) => {
|
|
47
|
+
loading = false;
|
|
48
|
+
console.error("Split button menu failed to load:", error);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const api = {
|
|
52
|
+
get element() {
|
|
53
|
+
return menu?.element ?? placeholder;
|
|
54
|
+
},
|
|
55
|
+
open(event, interactionType) {
|
|
56
|
+
if (menu) {
|
|
57
|
+
menu.open(event, interactionType);
|
|
58
|
+
return api;
|
|
59
|
+
}
|
|
60
|
+
if (!opened) {
|
|
61
|
+
opened = true;
|
|
62
|
+
notify("open", { menu: api, originalEvent: event });
|
|
63
|
+
}
|
|
64
|
+
asked = [event, interactionType];
|
|
65
|
+
load();
|
|
66
|
+
return api;
|
|
67
|
+
},
|
|
68
|
+
close(event, restoreFocus = true, skipAnimation = false) {
|
|
69
|
+
if (menu) {
|
|
70
|
+
menu.close(event, restoreFocus, skipAnimation);
|
|
71
|
+
return api;
|
|
72
|
+
}
|
|
73
|
+
if (opened) {
|
|
74
|
+
opened = false;
|
|
75
|
+
notify("close", { menu: api, originalEvent: event });
|
|
76
|
+
}
|
|
77
|
+
return api;
|
|
78
|
+
},
|
|
79
|
+
toggle(event) {
|
|
80
|
+
if (api.isOpen())
|
|
81
|
+
api.close(event);
|
|
82
|
+
else
|
|
83
|
+
api.open(event);
|
|
84
|
+
return api;
|
|
85
|
+
},
|
|
86
|
+
isOpen: () => (menu ? menu.isOpen() : opened),
|
|
87
|
+
setItems(items) {
|
|
88
|
+
current = items;
|
|
89
|
+
menu?.setItems(items);
|
|
90
|
+
return api;
|
|
91
|
+
},
|
|
92
|
+
getItems: () => (menu ? menu.getItems() : current),
|
|
93
|
+
setOpener(value) {
|
|
94
|
+
if (menu)
|
|
95
|
+
menu.setOpener(value);
|
|
96
|
+
else
|
|
97
|
+
opener = value;
|
|
98
|
+
return api;
|
|
99
|
+
},
|
|
100
|
+
getOpener: () => (menu ? menu.getOpener() : opener),
|
|
101
|
+
setPosition(value) {
|
|
102
|
+
if (menu)
|
|
103
|
+
menu.setPosition(value);
|
|
104
|
+
else
|
|
105
|
+
position = value;
|
|
106
|
+
return api;
|
|
107
|
+
},
|
|
108
|
+
getPosition: () => (menu ? menu.getPosition() : position),
|
|
109
|
+
setSelected(value) {
|
|
110
|
+
if (menu)
|
|
111
|
+
menu.setSelected(value);
|
|
112
|
+
else
|
|
113
|
+
selected = value;
|
|
114
|
+
return api;
|
|
115
|
+
},
|
|
116
|
+
getSelected: () => (menu ? menu.getSelected() : (selected ?? null)),
|
|
117
|
+
on(event, handler) {
|
|
118
|
+
if (menu)
|
|
119
|
+
menu.on(event, handler);
|
|
120
|
+
else {
|
|
121
|
+
const set = listeners.get(event) ?? new Set();
|
|
122
|
+
set.add(handler);
|
|
123
|
+
listeners.set(event, set);
|
|
124
|
+
}
|
|
125
|
+
return api;
|
|
126
|
+
},
|
|
127
|
+
off(event, handler) {
|
|
128
|
+
if (menu)
|
|
129
|
+
menu.off(event, handler);
|
|
130
|
+
else
|
|
131
|
+
listeners.get(event)?.delete(handler);
|
|
132
|
+
return api;
|
|
133
|
+
},
|
|
134
|
+
destroy() {
|
|
135
|
+
destroyed = true;
|
|
136
|
+
listeners.clear();
|
|
137
|
+
menu?.destroy();
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
return api;
|
|
141
|
+
};
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import createMenu from "../../menu/index.js";
|
|
2
1
|
import { MENU } from "../../menu/inner.js";
|
|
3
2
|
import { SPLIT_BUTTON_DEFAULTS } from "../constants.js";
|
|
4
|
-
|
|
3
|
+
import { createOnDemandMenu } from "./menu-on-demand.js";
|
|
4
|
+
export const makeMenu = (config, trailing, items) => createOnDemandMenu({
|
|
5
5
|
opener: trailing,
|
|
6
|
-
items,
|
|
7
6
|
position: "bottom-end",
|
|
8
7
|
offset: SPLIT_BUTTON_DEFAULTS.MENU_OFFSET,
|
|
9
8
|
manualOpen: true,
|
|
10
9
|
...(config.layer ? { layer: config.layer } : {}),
|
|
11
10
|
prefix: config.prefix,
|
|
12
|
-
});
|
|
11
|
+
}, items);
|
|
13
12
|
export const withMenu = (config) => (component) => {
|
|
14
13
|
if (!config.items || config.items.length === 0)
|
|
15
14
|
return component;
|
|
@@ -203,7 +203,8 @@ export const createTab = (config = {}) => {
|
|
|
203
203
|
},
|
|
204
204
|
};
|
|
205
205
|
const announceBadge = () => {
|
|
206
|
-
const count = tab.badge?.isVisible()
|
|
206
|
+
const count = tab.badge?.isVisible()
|
|
207
|
+
? tab.badge.element.getAttribute("aria-label") || tab.badge.getContent() : "";
|
|
207
208
|
const label = baseConfig.ariaLabel || button.getText();
|
|
208
209
|
const name = [label, count].filter(Boolean).join(", ");
|
|
209
210
|
if (name !== label) {
|