mtrl 0.9.5 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/README.md +20 -0
- package/dist/components/bottom-app-bar/bottom-app-bar.js +2 -2
- package/dist/components/bottom-sheet/features/structure.js +4 -3
- package/dist/components/button/api.js +3 -2
- package/dist/components/button/features/progress.js +26 -21
- package/dist/components/button-group/config.d.ts +11 -3
- package/dist/components/button-group/config.js +1 -1
- package/dist/components/card/content.js +5 -4
- package/dist/components/carousel/features/slides.js +2 -1
- package/dist/components/checkbox/checkbox.js +22 -0
- package/dist/components/checkbox/config.d.ts +3 -3
- package/dist/components/checkbox/config.js +10 -9
- package/dist/components/checkbox/types.d.ts +19 -4
- package/dist/components/chips/api.js +3 -1
- package/dist/components/chips/chip/api.js +3 -2
- package/dist/components/chips/chip/chip.js +6 -4
- package/dist/components/chips/config.js +1 -1
- package/dist/components/datepicker/api.d.ts +8 -2
- package/dist/components/datepicker/api.js +1 -0
- package/dist/components/datepicker/config.d.ts +14 -4
- package/dist/components/datepicker/datepicker.js +5 -2
- package/dist/components/datepicker/types.d.ts +14 -0
- package/dist/components/datepicker/utils.js +4 -6
- package/dist/components/dialog/config.js +0 -1
- package/dist/components/dialog/features.js +10 -8
- package/dist/components/dialog/types.d.ts +3 -2
- package/dist/components/drawer/features/items.js +3 -2
- package/dist/components/list/features/renderer.js +4 -3
- package/dist/components/menu/api.d.ts +1 -1
- package/dist/components/menu/config.js +2 -2
- package/dist/components/menu/features/controller.js +41 -26
- package/dist/components/menu/features/keyboard.js +7 -5
- package/dist/components/menu/features/opener.js +19 -12
- package/dist/components/menu/features/position.js +1 -1
- package/dist/components/menu/features/submenu.js +7 -1
- package/dist/components/menu/types.d.ts +13 -2
- package/dist/components/navigation/nav-item.js +4 -3
- package/dist/components/navigation/system/mobile.js +3 -2
- package/dist/components/navigation-rail/navigation-rail.js +3 -2
- package/dist/components/progress/features/canvas.js +0 -2
- package/dist/components/search/features/states.js +3 -3
- package/dist/components/search/features/structure.js +3 -0
- package/dist/components/search/features/suggestions.js +2 -2
- package/dist/components/search/types.d.ts +6 -0
- package/dist/components/segmented-button/config.d.ts +4 -1
- package/dist/components/segmented-button/config.js +1 -1
- package/dist/components/segmented-button/segment.d.ts +2 -5
- package/dist/components/segmented-button/segment.js +7 -3
- package/dist/components/select/features.js +267 -44
- package/dist/components/side-sheet/features/structure.js +5 -4
- package/dist/components/slider/features/controller.js +7 -0
- package/dist/components/slider/features/dom.d.ts +1 -0
- package/dist/components/slider/features/dom.js +11 -0
- package/dist/components/slider/features/handlers.js +8 -3
- package/dist/components/slider/features/states.js +8 -0
- package/dist/components/slider/types.d.ts +7 -0
- package/dist/components/switch/config.d.ts +3 -3
- package/dist/components/switch/config.js +7 -7
- package/dist/components/switch/features.js +5 -5
- package/dist/components/switch/switch.js +1 -1
- package/dist/components/switch/types.d.ts +10 -0
- package/dist/components/tabs/features.d.ts +1 -0
- package/dist/components/tabs/scroll-indicators.js +3 -2
- package/dist/components/tabs/tab.d.ts +0 -5
- package/dist/components/tabs/tab.js +12 -3
- package/dist/components/tabs/tabs.js +10 -1
- package/dist/components/tabs/utils.d.ts +8 -0
- package/dist/components/tabs/utils.js +63 -18
- package/dist/components/textfield/api.js +12 -4
- package/dist/components/textfield/features/error.d.ts +1 -0
- package/dist/components/textfield/features/error.js +10 -0
- package/dist/components/textfield/features/leading-icon.js +32 -33
- package/dist/components/textfield/features/prefix-text.js +30 -18
- package/dist/components/textfield/features/suffix-text.js +30 -14
- package/dist/components/textfield/features/supporting-text.d.ts +1 -0
- package/dist/components/textfield/features/supporting-text.js +25 -0
- package/dist/components/textfield/features/trailing-icon.js +36 -19
- package/dist/components/timepicker/api.d.ts +1 -1
- package/dist/components/timepicker/api.js +16 -11
- package/dist/components/timepicker/render.js +27 -24
- package/dist/components/timepicker/timepicker.js +4 -2
- package/dist/components/timepicker/types.d.ts +7 -0
- package/dist/components/timepicker/utils.d.ts +14 -0
- package/dist/components/timepicker/utils.js +4 -0
- package/dist/components/tooltip/api.js +27 -2
- package/dist/components/top-app-bar/top-app-bar.js +3 -2
- package/dist/core/compose/component.d.ts +1 -1
- package/dist/core/compose/component.js +1 -1
- package/dist/core/compose/features/icon.js +3 -2
- package/dist/core/compose/features/input.js +1 -1
- package/dist/core/compose/features/lifecycle.js +10 -11
- package/dist/core/compose/features/textlabel.js +3 -0
- package/dist/core/compose/features/track.js +2 -1
- package/dist/core/config/component.d.ts +1 -1
- package/dist/core/config/component.js +1 -1
- package/dist/core/dom/create.js +5 -4
- package/dist/core/dom/form-value.d.ts +24 -0
- package/dist/core/dom/form-value.js +14 -0
- package/dist/core/dom/html.d.ts +34 -0
- package/dist/core/dom/html.js +13 -0
- package/dist/core/dom/index.d.ts +2 -0
- package/dist/core/dom/index.js +1 -0
- package/dist/core/dom/utils.js +1 -1
- package/dist/core/state/emitter.js +8 -1
- package/dist/core/state/index.d.ts +1 -1
- package/dist/core/state/store.d.ts +10 -2
- package/dist/core/state/store.js +2 -1
- package/dist/index.cjs +0 -0
- package/dist/package.json +5 -2
- package/dist/styles/badge.css +1 -1
- package/dist/styles/base.css +2 -2
- package/dist/styles/bottom-app-bar.css +1 -1
- package/dist/styles/bottom-sheet.css +2 -2
- package/dist/styles/button-group.css +1 -1
- package/dist/styles/button.css +2 -2
- package/dist/styles/card.css +2 -2
- package/dist/styles/carousel.css +1 -1
- package/dist/styles/checkbox.css +2 -2
- package/dist/styles/chips.css +2 -2
- package/dist/styles/dialog.css +2 -2
- package/dist/styles/divider.css +1 -1
- package/dist/styles/drawer.css +2 -2
- package/dist/styles/extended-fab.css +2 -2
- package/dist/styles/fab.css +2 -2
- package/dist/styles/icon-button.css +2 -2
- package/dist/styles/list.css +2 -2
- package/dist/styles/loading-indicator.css +1 -1
- package/dist/styles/menu.css +2 -2
- package/dist/styles/navigation-rail.css +1 -1
- package/dist/styles/navigation.css +2 -2
- package/dist/styles/progress.css +2 -2
- package/dist/styles/radios.css +1 -1
- package/dist/styles/search.css +1 -1
- package/dist/styles/segmented-button.css +2 -2
- package/dist/styles/select.css +2 -2
- package/dist/styles/side-sheet.css +2 -2
- package/dist/styles/slider.css +1 -1
- package/dist/styles/snackbar.css +1 -1
- package/dist/styles/split-button.css +2 -2
- package/dist/styles/switch.css +2 -2
- package/dist/styles/tabs.css +1 -1
- package/dist/styles/textfield.css +2 -2
- package/dist/styles/timepicker.css +1 -1
- package/dist/styles/tooltip.css +2 -2
- package/dist/styles/top-app-bar.css +1 -1
- package/dist/styles/utilities.css +1 -1
- package/dist/styles.css +2 -2
- package/dist/styles.d.ts +1 -0
- 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 +7 -3
- package/src/styles/abstract/_mixins.scss +26 -0
- package/src/styles/abstract/_variables.scss +25 -11
- package/src/styles/base/_reset.scss +9 -2
- package/src/styles/components/_bottom-sheet.scss +23 -7
- package/src/styles/components/_button.scss +1 -12
- package/src/styles/components/_dialog.scss +59 -26
- package/src/styles/components/_drawer.scss +60 -7
- package/src/styles/components/_menu.scss +12 -7
- package/src/styles/components/_side-sheet.scss +33 -4
- package/src/styles/components/_tooltip.scss +3 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createElement } from "../../../core/dom/create.js";
|
|
2
2
|
import { setStyles } from "../../../core/dom/utils.js";
|
|
3
|
+
import { createFormValue } from "../../../core/dom/form-value.js";
|
|
3
4
|
export const withDom = (config) => (component) => {
|
|
4
5
|
const getClass = (className) => component.getClass(className);
|
|
5
6
|
const min = config.min || 0;
|
|
@@ -97,6 +98,15 @@ export const withDom = (config) => (component) => {
|
|
|
97
98
|
left: `${secondValuePercent}%`,
|
|
98
99
|
});
|
|
99
100
|
}
|
|
101
|
+
const name = config.name;
|
|
102
|
+
const formFields = name
|
|
103
|
+
? [
|
|
104
|
+
createFormValue(component.element, name, String(value), isDisabled),
|
|
105
|
+
config.range
|
|
106
|
+
? createFormValue(component.element, `${name}-end`, String(config.secondValue ?? max), isDisabled)
|
|
107
|
+
: null,
|
|
108
|
+
]
|
|
109
|
+
: null;
|
|
100
110
|
return {
|
|
101
111
|
...component,
|
|
102
112
|
container,
|
|
@@ -104,6 +114,7 @@ export const withDom = (config) => (component) => {
|
|
|
104
114
|
valueBubble,
|
|
105
115
|
secondHandle,
|
|
106
116
|
secondValueBubble,
|
|
117
|
+
formFields,
|
|
107
118
|
getContainer: () => container,
|
|
108
119
|
getHandle: () => handle,
|
|
109
120
|
getValueBubble: () => valueBubble,
|
|
@@ -51,11 +51,16 @@ export const createHandlers = (config, state, uiRenderer, eventHelpers) => {
|
|
|
51
51
|
secondHandle.classList.remove(`${state.component.getClass("slider-handle")}--focused`);
|
|
52
52
|
try {
|
|
53
53
|
const focusClass = state.component.getClass("slider-handle--focused");
|
|
54
|
-
|
|
54
|
+
state.component.element
|
|
55
|
+
.querySelectorAll(`.${focusClass}`)
|
|
56
|
+
.forEach((el) => {
|
|
55
57
|
el.classList.remove(focusClass);
|
|
56
58
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const active = document.activeElement;
|
|
60
|
+
if (active &&
|
|
61
|
+
state.component.element.contains(active) &&
|
|
62
|
+
active.classList.contains(state.component.getClass("slider-handle"))) {
|
|
63
|
+
active.blur();
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
catch (error) {
|
|
@@ -10,6 +10,12 @@ export const withStates = (config) => (component) => {
|
|
|
10
10
|
disableComponent();
|
|
11
11
|
}, 0);
|
|
12
12
|
}
|
|
13
|
+
const setFieldsDisabled = (disabled) => {
|
|
14
|
+
for (const field of component.formFields ?? []) {
|
|
15
|
+
if (field)
|
|
16
|
+
field.disabled = disabled;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
13
19
|
function disableComponent() {
|
|
14
20
|
component.element.classList.add(`${component.getClass("slider")}--disabled`);
|
|
15
21
|
component.element.setAttribute("aria-disabled", "true");
|
|
@@ -21,6 +27,7 @@ export const withStates = (config) => (component) => {
|
|
|
21
27
|
secondHandle.tabIndex = -1;
|
|
22
28
|
secondHandle.setAttribute("aria-disabled", "true");
|
|
23
29
|
}
|
|
30
|
+
setFieldsDisabled(true);
|
|
24
31
|
if (component.renderTracks) {
|
|
25
32
|
component.renderTracks();
|
|
26
33
|
}
|
|
@@ -36,6 +43,7 @@ export const withStates = (config) => (component) => {
|
|
|
36
43
|
secondHandle.tabIndex = 0;
|
|
37
44
|
secondHandle.setAttribute("aria-disabled", "false");
|
|
38
45
|
}
|
|
46
|
+
setFieldsDisabled(false);
|
|
39
47
|
if (component.renderTracks) {
|
|
40
48
|
component.renderTracks();
|
|
41
49
|
}
|
|
@@ -26,6 +26,13 @@ export interface SliderConfig extends BaseComponentConfig {
|
|
|
26
26
|
step?: number;
|
|
27
27
|
/** Whether the slider is disabled */
|
|
28
28
|
disabled?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Form field name. A slider renders no form control of its own, so it
|
|
31
|
+
* submits nothing unless this is set; with it, a hidden input inside the
|
|
32
|
+
* slider carries the current value. A range slider submits two fields,
|
|
33
|
+
* `name` and `name-end`. A disabled slider submits neither.
|
|
34
|
+
*/
|
|
35
|
+
name?: string;
|
|
29
36
|
/** Color variant of the slider *
|
|
30
37
|
* A stylesheet can override the drawn colours per slider with the custom
|
|
31
38
|
* properties `--<prefix>-slider-color` and `--<prefix>-slider-on-color`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SwitchConfig,
|
|
1
|
+
import { SwitchConfig, ApiComponent, ApiOptions } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Default configuration for the Switch component
|
|
4
4
|
*/
|
|
@@ -38,8 +38,8 @@ export declare const getElementConfig: (config: SwitchConfig) => {
|
|
|
38
38
|
};
|
|
39
39
|
/**
|
|
40
40
|
* Creates API configuration for the Switch component
|
|
41
|
-
* @param {
|
|
41
|
+
* @param {ApiComponent} comp - Component with disabled, lifecycle, and checkable features
|
|
42
42
|
* @returns {ApiOptions} API configuration object
|
|
43
43
|
*/
|
|
44
|
-
export declare const getApiConfig: (comp:
|
|
44
|
+
export declare const getApiConfig: (comp: ApiComponent) => ApiOptions;
|
|
45
45
|
export default defaultConfig;
|
|
@@ -13,17 +13,17 @@ export const getElementConfig = (config) => createElementConfig(config, {
|
|
|
13
13
|
});
|
|
14
14
|
export const getApiConfig = (comp) => ({
|
|
15
15
|
disabled: {
|
|
16
|
-
enable: comp.disabled
|
|
17
|
-
disable: comp.disabled
|
|
16
|
+
enable: comp.disabled.enable,
|
|
17
|
+
disable: comp.disabled.disable,
|
|
18
18
|
},
|
|
19
19
|
lifecycle: {
|
|
20
|
-
destroy: comp.lifecycle
|
|
20
|
+
destroy: comp.lifecycle.destroy,
|
|
21
21
|
},
|
|
22
22
|
checkable: {
|
|
23
|
-
check: comp.checkable
|
|
24
|
-
uncheck: comp.checkable
|
|
25
|
-
toggle: comp.checkable
|
|
26
|
-
isChecked: comp.checkable
|
|
23
|
+
check: comp.checkable.check,
|
|
24
|
+
uncheck: comp.checkable.uncheck,
|
|
25
|
+
toggle: comp.checkable.toggle,
|
|
26
|
+
isChecked: comp.checkable.isChecked,
|
|
27
27
|
},
|
|
28
28
|
});
|
|
29
29
|
export default defaultConfig;
|
|
@@ -2,9 +2,9 @@ import { withTrack as withTrackCore, } from "../../core/compose/features/index.j
|
|
|
2
2
|
const ensureSwitchStructure = (component, prefix, componentName) => {
|
|
3
3
|
const PREFIX = prefix || "mtrl";
|
|
4
4
|
const COMPONENT = componentName || "switch";
|
|
5
|
-
|
|
6
|
-
if (!
|
|
7
|
-
container = document.createElement("div");
|
|
5
|
+
const existingContainer = component.element.querySelector(`.${PREFIX}-${COMPONENT}-container`);
|
|
6
|
+
if (!existingContainer) {
|
|
7
|
+
const container = document.createElement("div");
|
|
8
8
|
container.className = `${PREFIX}-${COMPONENT}-container`;
|
|
9
9
|
const input = component.element.querySelector(`.${PREFIX}-${COMPONENT}-input`);
|
|
10
10
|
const track = component.element.querySelector(`.${PREFIX}-${COMPONENT}-track`);
|
|
@@ -32,9 +32,9 @@ const ensureSwitchStructure = (component, prefix, componentName) => {
|
|
|
32
32
|
if (label) {
|
|
33
33
|
contentWrapper.appendChild(label);
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
existingContainer.insertBefore(contentWrapper, existingContainer.firstChild);
|
|
36
36
|
}
|
|
37
|
-
return { container, contentWrapper };
|
|
37
|
+
return { container: existingContainer, contentWrapper };
|
|
38
38
|
};
|
|
39
39
|
export const withSupportingText = (config) => (component) => {
|
|
40
40
|
const PREFIX = config.prefix || "mtrl";
|
|
@@ -7,7 +7,7 @@ import { createBaseConfig, getElementConfig, getApiConfig } from "./config.js";
|
|
|
7
7
|
const createSwitch = (config = {}) => {
|
|
8
8
|
const baseConfig = createBaseConfig(config);
|
|
9
9
|
try {
|
|
10
|
-
const switchComponent = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)),
|
|
10
|
+
const switchComponent = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)), withInput(baseConfig), withTextLabel(baseConfig), withTrack(baseConfig), withSupportingText(baseConfig), withCheckable(baseConfig), withDisabled(baseConfig), withLifecycle(), (comp) => withAPI(getApiConfig(comp))(comp))(baseConfig);
|
|
11
11
|
return switchComponent;
|
|
12
12
|
}
|
|
13
13
|
catch (error) {
|
|
@@ -130,3 +130,13 @@ export interface BaseComponent {
|
|
|
130
130
|
setSupportingText?: (text: string, isError?: boolean) => void;
|
|
131
131
|
removeSupportingText?: () => void;
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Component as it reaches the API step of the pipeline.
|
|
135
|
+
*
|
|
136
|
+
* `BaseComponent` marks the feature controllers optional because a component
|
|
137
|
+
* part-way through the pipeline has not got them yet. By the time
|
|
138
|
+
* `getApiConfig` runs, withCheckable, withDisabled and withLifecycle have all
|
|
139
|
+
* been applied, so they are there.
|
|
140
|
+
* @internal
|
|
141
|
+
*/
|
|
142
|
+
export type ApiComponent = BaseComponent & Required<Pick<BaseComponent, "checkable" | "disabled" | "lifecycle">>;
|
|
@@ -125,5 +125,6 @@ export interface IndicatorComponent {
|
|
|
125
125
|
*/
|
|
126
126
|
export declare const withIndicator: <T extends IndicatorFeatureConfig>(config: T) => <C extends ComponentBase & {
|
|
127
127
|
tabs: TabComponent[];
|
|
128
|
+
handleTabClick: (event: unknown, tab: TabComponent) => void;
|
|
128
129
|
}>(component: C) => C & IndicatorComponent;
|
|
129
130
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setHTML } from "../../core/dom/html.js";
|
|
1
2
|
export const addScrollIndicators = (tabs, config = {}) => {
|
|
2
3
|
const { enabled = true, showButtons = false, appearance = 'fade' } = config;
|
|
3
4
|
if (!enabled)
|
|
@@ -17,11 +18,11 @@ export const addScrollIndicators = (tabs, config = {}) => {
|
|
|
17
18
|
const leftButton = document.createElement('button');
|
|
18
19
|
leftButton.className = `${tabs.getClass('tabs')}-scroll-button ${tabs.getClass('tabs')}-scroll-button--left`;
|
|
19
20
|
leftButton.setAttribute('aria-label', 'Scroll tabs left');
|
|
20
|
-
leftButton
|
|
21
|
+
setHTML(leftButton, '<svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>');
|
|
21
22
|
const rightButton = document.createElement('button');
|
|
22
23
|
rightButton.className = `${tabs.getClass('tabs')}-scroll-button ${tabs.getClass('tabs')}-scroll-button--right`;
|
|
23
24
|
rightButton.setAttribute('aria-label', 'Scroll tabs right');
|
|
24
|
-
rightButton
|
|
25
|
+
setHTML(rightButton, '<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>');
|
|
25
26
|
tabs.element.appendChild(leftButton);
|
|
26
27
|
tabs.element.appendChild(rightButton);
|
|
27
28
|
leftButton.addEventListener('click', () => {
|
|
@@ -1,7 +1,2 @@
|
|
|
1
1
|
import { TabConfig, TabComponent } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Creates a new Tab component following MD3 guidelines
|
|
4
|
-
* @param {TabConfig} config - Tab configuration object
|
|
5
|
-
* @returns {TabComponent} Tab component instance
|
|
6
|
-
*/
|
|
7
2
|
export declare const createTab: (config?: TabConfig) => TabComponent;
|
|
@@ -5,6 +5,15 @@ import { TAB_LAYOUT } from "./constants.js";
|
|
|
5
5
|
import { createTabConfig } from "./config.js";
|
|
6
6
|
import createButton from "../button/index.js";
|
|
7
7
|
import createBadge from "../badge/index.js";
|
|
8
|
+
function linkPanel(element, value) {
|
|
9
|
+
const panelId = `tabpanel-${value}`;
|
|
10
|
+
if (typeof document !== "undefined" && document.getElementById(panelId)) {
|
|
11
|
+
element.setAttribute("aria-controls", panelId);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
element.removeAttribute("aria-controls");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
8
17
|
export const createTab = (config = {}) => {
|
|
9
18
|
const baseConfig = createTabConfig(config);
|
|
10
19
|
try {
|
|
@@ -31,7 +40,7 @@ export const createTab = (config = {}) => {
|
|
|
31
40
|
baseComponent.element.setAttribute("aria-selected", baseConfig.state === "active" ? "true" : "false");
|
|
32
41
|
if (baseConfig.value) {
|
|
33
42
|
baseComponent.element.setAttribute("id", `tab-${baseConfig.value}`);
|
|
34
|
-
baseComponent.element
|
|
43
|
+
linkPanel(baseComponent.element, baseConfig.value);
|
|
35
44
|
}
|
|
36
45
|
if (baseConfig.state === "active") {
|
|
37
46
|
baseComponent.element.classList.add(`${baseComponent.getClass("tab")}--active`);
|
|
@@ -53,7 +62,7 @@ export const createTab = (config = {}) => {
|
|
|
53
62
|
baseComponent.off(event, handler);
|
|
54
63
|
return this;
|
|
55
64
|
},
|
|
56
|
-
badge:
|
|
65
|
+
badge: undefined,
|
|
57
66
|
getValue() {
|
|
58
67
|
return button.getValue();
|
|
59
68
|
},
|
|
@@ -61,7 +70,7 @@ export const createTab = (config = {}) => {
|
|
|
61
70
|
const safeValue = value || "";
|
|
62
71
|
button.setValue(safeValue);
|
|
63
72
|
this.element.setAttribute("id", `tab-${safeValue}`);
|
|
64
|
-
this.element
|
|
73
|
+
linkPanel(this.element, safeValue);
|
|
65
74
|
return this;
|
|
66
75
|
},
|
|
67
76
|
activate() {
|
|
@@ -5,7 +5,7 @@ import { withAPI, getApiConfig } from "./api.js";
|
|
|
5
5
|
import { withTabsManagement, withScrollable, withDivider, withIndicator } from "./features.js";
|
|
6
6
|
import { createTabsConfig, getTabsElementConfig } from "./config.js";
|
|
7
7
|
import { addTabStateStyles } from "./state.js";
|
|
8
|
-
import { setupKeyboardNavigation } from "./utils.js";
|
|
8
|
+
import { setupKeyboardNavigation, syncTabStops } from "./utils.js";
|
|
9
9
|
const createTabs = (config = {}) => {
|
|
10
10
|
const baseConfig = createTabsConfig(config);
|
|
11
11
|
addTabStateStyles();
|
|
@@ -18,6 +18,15 @@ const createTabs = (config = {}) => {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
setupKeyboardNavigation(component);
|
|
21
|
+
component.on('change', () => syncTabStops(component));
|
|
22
|
+
for (const method of ['addTab', 'add', 'removeTab', 'setActiveTab']) {
|
|
23
|
+
const original = component[method];
|
|
24
|
+
component[method] = (...args) => {
|
|
25
|
+
const result = original.apply(component, args);
|
|
26
|
+
syncTabStops(component);
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
21
30
|
return component;
|
|
22
31
|
}
|
|
23
32
|
catch (error) {
|
|
@@ -8,6 +8,7 @@ interface TabsHost {
|
|
|
8
8
|
getTabs?: () => TabComponent[];
|
|
9
9
|
getActiveTab?: () => TabComponent | null;
|
|
10
10
|
setActiveTab?: (tabOrValue: TabComponent | string) => unknown;
|
|
11
|
+
handleTabClick?: (event: unknown, tab: TabComponent) => void;
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
14
|
* Gets the active tab from a component
|
|
@@ -20,6 +21,13 @@ export declare function getActiveTab(component: TabsHost): TabComponent | null;
|
|
|
20
21
|
* @param component - Component with tabs
|
|
21
22
|
*/
|
|
22
23
|
export declare function updateTabPanels(component: TabsHost): void;
|
|
24
|
+
/**
|
|
25
|
+
* Gives the tablist a single tab stop: the active tab, or the first enabled
|
|
26
|
+
* one when none is active. Every other tab takes tabindex -1 and is reached
|
|
27
|
+
* with the arrow keys (WAI-ARIA tabs pattern).
|
|
28
|
+
* @param component - Tabs component
|
|
29
|
+
*/
|
|
30
|
+
export declare function syncTabStops(component: TabsHost): void;
|
|
23
31
|
/**
|
|
24
32
|
* Sets up keyboard navigation for tabs
|
|
25
33
|
* @param component - Tabs component
|
|
@@ -3,7 +3,7 @@ export function getActiveTab(component) {
|
|
|
3
3
|
return component.getActiveTab();
|
|
4
4
|
}
|
|
5
5
|
if (Array.isArray(component.tabs)) {
|
|
6
|
-
return component.tabs.find((tab) => tab.isActive && tab.isActive());
|
|
6
|
+
return component.tabs.find((tab) => tab.isActive && tab.isActive()) ?? null;
|
|
7
7
|
}
|
|
8
8
|
return null;
|
|
9
9
|
}
|
|
@@ -15,8 +15,11 @@ export function updateTabPanels(component) {
|
|
|
15
15
|
return;
|
|
16
16
|
const activeValue = activeTab.getValue();
|
|
17
17
|
const tabPanels = document.querySelectorAll(`[role="tabpanel"]`);
|
|
18
|
+
const linked = new Set();
|
|
18
19
|
tabPanels.forEach((panel) => {
|
|
19
20
|
const forTab = panel.getAttribute("aria-labelledby")?.replace("tab-", "");
|
|
21
|
+
if (forTab)
|
|
22
|
+
linked.add(forTab);
|
|
20
23
|
if (forTab === activeValue) {
|
|
21
24
|
panel.removeAttribute("hidden");
|
|
22
25
|
panel.setAttribute("tabindex", "0");
|
|
@@ -26,42 +29,84 @@ export function updateTabPanels(component) {
|
|
|
26
29
|
panel.setAttribute("tabindex", "-1");
|
|
27
30
|
}
|
|
28
31
|
});
|
|
32
|
+
const allTabs = typeof component.getTabs === "function"
|
|
33
|
+
? component.getTabs()
|
|
34
|
+
: component.tabs ?? [];
|
|
35
|
+
{
|
|
36
|
+
for (const tab of allTabs) {
|
|
37
|
+
if (typeof tab?.getValue !== "function" || !tab.element)
|
|
38
|
+
continue;
|
|
39
|
+
const value = tab.getValue();
|
|
40
|
+
const panelId = `tabpanel-${value}`;
|
|
41
|
+
if (linked.has(value) && document.getElementById(panelId)) {
|
|
42
|
+
tab.element.setAttribute("aria-controls", panelId);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
tab.element.removeAttribute("aria-controls");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function syncTabStops(component) {
|
|
51
|
+
if (typeof component.getTabs !== "function")
|
|
52
|
+
return;
|
|
53
|
+
const tabs = component.getTabs();
|
|
54
|
+
const enabled = tabs.filter((tab) => !tab.element.disabled);
|
|
55
|
+
const active = getActiveTab(component);
|
|
56
|
+
const stop = active && enabled.includes(active) ? active : enabled[0];
|
|
57
|
+
tabs.forEach((tab) => tab.element.setAttribute("tabindex", tab === stop ? "0" : "-1"));
|
|
29
58
|
}
|
|
30
59
|
export function setupKeyboardNavigation(component) {
|
|
31
60
|
if (!component.element)
|
|
32
61
|
return;
|
|
33
|
-
component.element
|
|
34
|
-
|
|
62
|
+
const tablist = component.element;
|
|
63
|
+
syncTabStops(component);
|
|
64
|
+
tablist.addEventListener("keydown", (event) => {
|
|
65
|
+
const tabElement = event.target?.closest?.('[role="tab"]');
|
|
66
|
+
if (!tabElement || !tablist.contains(tabElement))
|
|
35
67
|
return;
|
|
36
|
-
if (typeof component.getTabs !== "function"
|
|
37
|
-
typeof component.setActiveTab !== "function")
|
|
68
|
+
if (typeof component.getTabs !== "function")
|
|
38
69
|
return;
|
|
39
|
-
const tabs = component
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
70
|
+
const tabs = component
|
|
71
|
+
.getTabs()
|
|
72
|
+
.filter((tab) => !tab.element.disabled);
|
|
73
|
+
const currentIndex = tabs.findIndex((tab) => tab.element === tabElement);
|
|
74
|
+
if (currentIndex === -1)
|
|
75
|
+
return;
|
|
76
|
+
const rtl = getComputedStyle(tablist).direction === "rtl";
|
|
77
|
+
const last = tabs.length - 1;
|
|
78
|
+
let newIndex;
|
|
43
79
|
switch (event.key) {
|
|
44
80
|
case "ArrowRight":
|
|
45
|
-
|
|
46
|
-
|
|
81
|
+
newIndex = rtl
|
|
82
|
+
? (currentIndex > 0 ? currentIndex - 1 : last)
|
|
83
|
+
: (currentIndex < last ? currentIndex + 1 : 0);
|
|
47
84
|
break;
|
|
48
85
|
case "ArrowLeft":
|
|
49
|
-
|
|
50
|
-
|
|
86
|
+
newIndex = rtl
|
|
87
|
+
? (currentIndex < last ? currentIndex + 1 : 0)
|
|
88
|
+
: (currentIndex > 0 ? currentIndex - 1 : last);
|
|
51
89
|
break;
|
|
52
90
|
case "Home":
|
|
53
91
|
newIndex = 0;
|
|
54
92
|
break;
|
|
55
93
|
case "End":
|
|
56
|
-
newIndex =
|
|
94
|
+
newIndex = last;
|
|
57
95
|
break;
|
|
58
96
|
default:
|
|
59
97
|
return;
|
|
60
98
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
const target = tabs[newIndex];
|
|
101
|
+
if (target === tabs[currentIndex])
|
|
102
|
+
return;
|
|
103
|
+
target.element.focus();
|
|
104
|
+
if (typeof component.handleTabClick === "function") {
|
|
105
|
+
component.handleTabClick(null, target);
|
|
106
|
+
}
|
|
107
|
+
else if (typeof component.setActiveTab === "function") {
|
|
108
|
+
component.setActiveTab(target);
|
|
65
109
|
}
|
|
110
|
+
syncTabStops(component);
|
|
66
111
|
});
|
|
67
112
|
}
|
|
@@ -45,7 +45,9 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
45
45
|
getLabel() {
|
|
46
46
|
return component.label?.getText() || "";
|
|
47
47
|
},
|
|
48
|
-
|
|
48
|
+
get leadingIcon() {
|
|
49
|
+
return component.leadingIcon ?? null;
|
|
50
|
+
},
|
|
49
51
|
setLeadingIcon(html) {
|
|
50
52
|
if (component.setLeadingIcon) {
|
|
51
53
|
component.setLeadingIcon(html);
|
|
@@ -64,7 +66,9 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
64
66
|
}
|
|
65
67
|
return this;
|
|
66
68
|
},
|
|
67
|
-
|
|
69
|
+
get trailingIcon() {
|
|
70
|
+
return component.trailingIcon ?? null;
|
|
71
|
+
},
|
|
68
72
|
setTrailingIcon(html) {
|
|
69
73
|
if (component.setTrailingIcon) {
|
|
70
74
|
component.setTrailingIcon(html);
|
|
@@ -96,7 +100,9 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
96
100
|
}
|
|
97
101
|
return this;
|
|
98
102
|
},
|
|
99
|
-
|
|
103
|
+
get prefixTextElement() {
|
|
104
|
+
return component.prefixTextElement ?? null;
|
|
105
|
+
},
|
|
100
106
|
setPrefixText(text) {
|
|
101
107
|
if (component.setPrefixText) {
|
|
102
108
|
component.setPrefixText(text);
|
|
@@ -115,7 +121,9 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
|
|
|
115
121
|
}
|
|
116
122
|
return this;
|
|
117
123
|
},
|
|
118
|
-
|
|
124
|
+
get suffixTextElement() {
|
|
125
|
+
return component.suffixTextElement ?? null;
|
|
126
|
+
},
|
|
119
127
|
setSuffixText(text) {
|
|
120
128
|
if (component.setSuffixText) {
|
|
121
129
|
component.setSuffixText(text);
|
|
@@ -20,6 +20,7 @@ export interface ErrorConfig {
|
|
|
20
20
|
* Component with supporting text capabilities (if available)
|
|
21
21
|
*/
|
|
22
22
|
interface ComponentWithSupportingText extends ElementComponent {
|
|
23
|
+
input?: HTMLInputElement | HTMLTextAreaElement;
|
|
23
24
|
setSupportingText?: (text: string, isError?: boolean) => void;
|
|
24
25
|
removeSupportingText?: () => void;
|
|
25
26
|
supportingTextElement?: HTMLElement | null;
|
|
@@ -3,15 +3,25 @@ export const withError = (config) => (component) => {
|
|
|
3
3
|
const COMPONENT = config.componentName || "textfield";
|
|
4
4
|
let errorState = config.error || false;
|
|
5
5
|
let displacedText = null;
|
|
6
|
+
const markInvalid = (invalid) => {
|
|
7
|
+
if (!component.input)
|
|
8
|
+
return;
|
|
9
|
+
if (invalid)
|
|
10
|
+
component.input.setAttribute("aria-invalid", "true");
|
|
11
|
+
else
|
|
12
|
+
component.input.removeAttribute("aria-invalid");
|
|
13
|
+
};
|
|
6
14
|
if (errorState) {
|
|
7
15
|
component.element.classList.add(`${PREFIX}-${COMPONENT}--error`);
|
|
8
16
|
}
|
|
17
|
+
markInvalid(errorState);
|
|
9
18
|
return {
|
|
10
19
|
...component,
|
|
11
20
|
errorState,
|
|
12
21
|
setError(error, message) {
|
|
13
22
|
errorState = error;
|
|
14
23
|
component.element.classList.toggle(`${PREFIX}-${COMPONENT}--error`, error);
|
|
24
|
+
markInvalid(error);
|
|
15
25
|
if (message !== undefined && component.setSupportingText) {
|
|
16
26
|
if (message) {
|
|
17
27
|
if (error && displacedText === null) {
|
|
@@ -1,52 +1,51 @@
|
|
|
1
|
+
import { setHTML } from "../../../core/dom/html.js";
|
|
1
2
|
export const withLeadingIcon = (config) => (component) => {
|
|
2
|
-
if (!config.leadingIcon) {
|
|
3
|
-
return component;
|
|
4
|
-
}
|
|
5
3
|
const PREFIX = config.prefix || 'mtrl';
|
|
6
|
-
const
|
|
7
|
-
iconElement
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
const NAME = config.componentName || 'textfield';
|
|
5
|
+
let iconElement = null;
|
|
6
|
+
const ensureIcon = () => {
|
|
7
|
+
if (iconElement && iconElement.parentNode)
|
|
8
|
+
return iconElement;
|
|
9
|
+
const element = document.createElement('span');
|
|
10
|
+
element.className = `${PREFIX}-${NAME}-leading-icon`;
|
|
11
|
+
component.element.appendChild(element);
|
|
12
|
+
component.element.classList.add(`${PREFIX}-${NAME}--with-leading-icon`);
|
|
13
|
+
if (component.input) {
|
|
14
|
+
component.input.classList.add(`${PREFIX}-${NAME}-input--with-leading-icon`);
|
|
15
|
+
}
|
|
16
|
+
iconElement = element;
|
|
17
|
+
return element;
|
|
18
|
+
};
|
|
19
|
+
const detachIcon = () => {
|
|
20
|
+
iconElement?.remove();
|
|
21
|
+
iconElement = null;
|
|
22
|
+
component.element.classList.remove(`${PREFIX}-${NAME}--with-leading-icon`);
|
|
23
|
+
if (component.input) {
|
|
24
|
+
component.input.classList.remove(`${PREFIX}-${NAME}-input--with-leading-icon`);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
if (config.leadingIcon) {
|
|
28
|
+
setHTML(ensureIcon(), config.leadingIcon);
|
|
13
29
|
}
|
|
14
30
|
if ('lifecycle' in component && component.lifecycle?.destroy) {
|
|
15
31
|
const originalDestroy = component.lifecycle.destroy;
|
|
16
32
|
component.lifecycle.destroy = () => {
|
|
17
|
-
|
|
18
|
-
iconElement
|
|
33
|
+
iconElement?.remove();
|
|
34
|
+
iconElement = null;
|
|
19
35
|
originalDestroy.call(component.lifecycle);
|
|
20
36
|
};
|
|
21
37
|
}
|
|
22
|
-
const initialPosition = setTimeout(() => {
|
|
23
|
-
const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
|
|
24
|
-
if (labelEl) {
|
|
25
|
-
if (!component.element.classList.contains(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`)) {
|
|
26
|
-
labelEl.style.left = '44px';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}, 10);
|
|
30
38
|
return {
|
|
31
39
|
...component,
|
|
32
40
|
leadingIcon: iconElement,
|
|
33
41
|
setLeadingIcon(html) {
|
|
34
|
-
|
|
42
|
+
this.leadingIcon = ensureIcon();
|
|
43
|
+
setHTML(this.leadingIcon, html);
|
|
35
44
|
return this;
|
|
36
45
|
},
|
|
37
46
|
removeLeadingIcon() {
|
|
38
|
-
if (iconElement
|
|
39
|
-
|
|
40
|
-
component.element.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}--with-leading-icon`);
|
|
41
|
-
if (component.input) {
|
|
42
|
-
component.input.classList.remove(`${PREFIX}-${config.componentName || 'textfield'}-input--with-leading-icon`);
|
|
43
|
-
}
|
|
44
|
-
if (!component.element.classList.contains(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`)) {
|
|
45
|
-
const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
|
|
46
|
-
if (labelEl) {
|
|
47
|
-
labelEl.style.left = '';
|
|
48
|
-
}
|
|
49
|
-
}
|
|
47
|
+
if (iconElement) {
|
|
48
|
+
detachIcon();
|
|
50
49
|
this.leadingIcon = null;
|
|
51
50
|
}
|
|
52
51
|
return this;
|