mtrl 0.8.0-next.42 → 0.8.0-next.44
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/navigation-rail/config.js +2 -1
- package/dist/components/navigation-rail/constants.d.ts +5 -0
- package/dist/components/navigation-rail/constants.js +4 -0
- package/dist/components/navigation-rail/navigation-rail.js +19 -1
- package/dist/components/navigation-rail/types.d.ts +4 -0
- package/dist/components/select/features.js +1 -0
- package/dist/components/select/types.d.ts +21 -0
- package/dist/core/compose/cleanup.d.ts +14 -0
- package/dist/core/compose/cleanup.js +51 -0
- package/dist/core/compose/component.d.ts +2 -0
- package/dist/core/compose/component.js +38 -25
- package/dist/core/compose/features/debounce.js +13 -22
- package/dist/core/compose/features/disabled.js +1 -3
- package/dist/core/compose/features/events.js +5 -1
- package/dist/core/compose/features/lifecycle.js +15 -4
- package/dist/core/compose/features/ripple.d.ts +0 -6
- package/dist/core/compose/features/ripple.js +61 -111
- package/dist/core/compose/features/throttle.js +13 -16
- package/dist/core/compose/features/withEvents.js +3 -8
- package/dist/core/dom/create.js +30 -27
- package/dist/core/dom/events.js +29 -15
- package/dist/core/state/events.js +13 -5
- package/dist/core/state/lifecycle.js +6 -1
- package/dist/core/utils/performance.d.ts +5 -2
- package/dist/core/utils/performance.js +16 -2
- package/dist/index.cjs +12 -12
- package/dist/package.json +1 -1
- 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 +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 +1 -1
- 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-rail.css +2 -2
- 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/_navigation-rail.scss +193 -65
- package/src/styles/utilities/_ripple.scss +12 -3
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createComponentConfig } from "../../core/config/component.js";
|
|
2
|
-
import { NAVIGATION_RAIL_DEFAULTS as defaults } from "./constants.js";
|
|
2
|
+
import { NAVIGATION_RAIL_DEFAULTS as defaults, NAVIGATION_RAIL_ICONS as icons } from "./constants.js";
|
|
3
3
|
export const createBaseConfig = (config) => {
|
|
4
4
|
const result = createComponentConfig({
|
|
5
5
|
expanded: defaults.EXPANDED, expandedWidth: defaults.EXPANDED_WIDTH,
|
|
6
6
|
showToggle: defaults.SHOW_TOGGLE, ripple: true, items: [],
|
|
7
7
|
expandLabel: 'Expand navigation', collapseLabel: 'Collapse navigation',
|
|
8
|
+
expandIcon: icons.EXPAND, collapseIcon: icons.COLLAPSE,
|
|
8
9
|
}, config, 'navigation-rail');
|
|
9
10
|
const width = result.expandedWidth;
|
|
10
11
|
result.expandedWidth = Number.isFinite(width)
|
|
@@ -7,3 +7,8 @@ export declare const NAVIGATION_RAIL_DEFAULTS: {
|
|
|
7
7
|
readonly MAX_EXPANDED_WIDTH: 360;
|
|
8
8
|
readonly SHOW_TOGGLE: true;
|
|
9
9
|
};
|
|
10
|
+
/** Material Symbols `menu` and `menu_open`, the M3 rail menu button in each state. */
|
|
11
|
+
export declare const NAVIGATION_RAIL_ICONS: {
|
|
12
|
+
readonly EXPAND: "<svg viewBox=\"0 -960 960 960\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z\"/></svg>";
|
|
13
|
+
readonly COLLAPSE: "<svg viewBox=\"0 -960 960 960\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M120-240v-80h520v80H120Zm664-40L584-480l200-200 56 56-144 144 144 144-56 56ZM120-440v-80h400v80H120Zm0-200v-80h520v80H120Z\"/></svg>";
|
|
14
|
+
};
|
|
@@ -6,3 +6,7 @@ export const NAVIGATION_RAIL_DEFAULTS = {
|
|
|
6
6
|
MAX_EXPANDED_WIDTH: 360,
|
|
7
7
|
SHOW_TOGGLE: true,
|
|
8
8
|
};
|
|
9
|
+
export const NAVIGATION_RAIL_ICONS = {
|
|
10
|
+
EXPAND: '<svg viewBox="0 -960 960 960" aria-hidden="true" focusable="false"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg>',
|
|
11
|
+
COLLAPSE: '<svg viewBox="0 -960 960 960" aria-hidden="true" focusable="false"><path d="M120-240v-80h520v80H120Zm664-40L584-480l200-200 56 56-144 144 144 144-56 56ZM120-440v-80h400v80H120Zm0-200v-80h520v80H120Z"/></svg>',
|
|
12
|
+
};
|
|
@@ -27,6 +27,19 @@ export default function createNavigationRail(config = {}) {
|
|
|
27
27
|
const emitter = createEmitter();
|
|
28
28
|
let expanded = !!options.expanded;
|
|
29
29
|
let destroyed = false;
|
|
30
|
+
let switching = null;
|
|
31
|
+
const SWITCH_DURATION = 450;
|
|
32
|
+
const endSwitch = () => {
|
|
33
|
+
if (switching !== null)
|
|
34
|
+
clearTimeout(switching);
|
|
35
|
+
switching = null;
|
|
36
|
+
root.classList.remove(cls('--switching'));
|
|
37
|
+
};
|
|
38
|
+
const startSwitch = () => {
|
|
39
|
+
endSwitch();
|
|
40
|
+
root.classList.add(cls('--switching'));
|
|
41
|
+
switching = setTimeout(endSwitch, SWITCH_DURATION);
|
|
42
|
+
};
|
|
30
43
|
let connectionObserver = null;
|
|
31
44
|
const dialog = options.layout === 'modal' ? root : null;
|
|
32
45
|
if (dialog)
|
|
@@ -43,7 +56,6 @@ export default function createNavigationRail(config = {}) {
|
|
|
43
56
|
toggle = document.createElement('button');
|
|
44
57
|
toggle.type = 'button';
|
|
45
58
|
toggle.className = cls('__toggle');
|
|
46
|
-
toggle.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>';
|
|
47
59
|
header.append(toggle);
|
|
48
60
|
}
|
|
49
61
|
if (options.header)
|
|
@@ -57,6 +69,7 @@ export default function createNavigationRail(config = {}) {
|
|
|
57
69
|
const visible = item.badge !== undefined && item.badge !== false && item.badge !== '';
|
|
58
70
|
if (!visible) {
|
|
59
71
|
badge?.remove();
|
|
72
|
+
element.classList.remove(cls('__item--badged'));
|
|
60
73
|
element.setAttribute('aria-label', item.label);
|
|
61
74
|
return;
|
|
62
75
|
}
|
|
@@ -68,6 +81,7 @@ export default function createNavigationRail(config = {}) {
|
|
|
68
81
|
}
|
|
69
82
|
badge.classList.toggle(cls('__badge--dot'), item.badge === true);
|
|
70
83
|
badge.textContent = item.badge === true ? '' : String(item.badge);
|
|
84
|
+
element.classList.toggle(cls('__item--badged'), item.badge !== true);
|
|
71
85
|
element.setAttribute('aria-label', `${item.label}, ${item.badgeLabel || (item.badge === true ? 'New activity' : String(item.badge))}`);
|
|
72
86
|
};
|
|
73
87
|
const updateSelection = () => {
|
|
@@ -147,6 +161,8 @@ export default function createNavigationRail(config = {}) {
|
|
|
147
161
|
root.classList.toggle(cls('--expanded'), expanded);
|
|
148
162
|
toggle?.setAttribute('aria-expanded', String(expanded));
|
|
149
163
|
toggle?.setAttribute('aria-label', (expanded ? options.collapseLabel : options.expandLabel));
|
|
164
|
+
if (toggle)
|
|
165
|
+
toggle.innerHTML = (expanded ? options.collapseIcon : options.expandIcon);
|
|
150
166
|
if (dialog) {
|
|
151
167
|
if (expanded)
|
|
152
168
|
showModal();
|
|
@@ -166,6 +182,7 @@ export default function createNavigationRail(config = {}) {
|
|
|
166
182
|
if (destroyed || expanded === value)
|
|
167
183
|
return api;
|
|
168
184
|
expanded = value;
|
|
185
|
+
startSwitch();
|
|
169
186
|
synchronize();
|
|
170
187
|
emitter.emit(value ? 'expand' : 'collapse', { expanded: value });
|
|
171
188
|
if (!destroyed) {
|
|
@@ -264,6 +281,7 @@ export default function createNavigationRail(config = {}) {
|
|
|
264
281
|
if (destroyed)
|
|
265
282
|
return;
|
|
266
283
|
destroyed = true;
|
|
284
|
+
endSwitch();
|
|
267
285
|
connectionObserver?.disconnect();
|
|
268
286
|
connectionObserver = null;
|
|
269
287
|
dialog?.removeEventListener('keydown', modalTab);
|
|
@@ -40,6 +40,10 @@ export interface NavigationRailConfig extends BaseComponentConfig {
|
|
|
40
40
|
showToggle?: boolean;
|
|
41
41
|
expandLabel?: string;
|
|
42
42
|
collapseLabel?: string;
|
|
43
|
+
/** Menu button icon while collapsed. Default: Material Symbols `menu`. */
|
|
44
|
+
expandIcon?: string;
|
|
45
|
+
/** Menu button icon while expanded. Default: Material Symbols `menu_open`. */
|
|
46
|
+
collapseIcon?: string;
|
|
43
47
|
/** Optional application-owned header, such as a FAB. Its lifecycle stays with its owner. */
|
|
44
48
|
header?: HTMLElement;
|
|
45
49
|
ripple?: boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MenuColor, MenuVariant } from "../menu/types.js";
|
|
1
2
|
/**
|
|
2
3
|
* Available Select variants
|
|
3
4
|
*/
|
|
@@ -75,6 +76,26 @@ export interface SelectConfig {
|
|
|
75
76
|
* Whether select is required
|
|
76
77
|
*/
|
|
77
78
|
required?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Options handed to the select's menu. By default the menu lives inside
|
|
81
|
+
* the select's own element with no height of its own, which suits a short
|
|
82
|
+
* list in a still surface; a long list, or a select inside a scrolling or
|
|
83
|
+
* clipping container (a drawer, a sheet), wants the menu in the body with a
|
|
84
|
+
* max height, so it is neither cropped nor taller than the screen.
|
|
85
|
+
* @example { container: document.body, maxHeight: "320px" }
|
|
86
|
+
*/
|
|
87
|
+
menu?: {
|
|
88
|
+
/** Where the menu is mounted and positioned against; document.body for a menu that escapes a clipping container */
|
|
89
|
+
container?: HTMLElement | null;
|
|
90
|
+
/** The menu's max height (a CSS length); the list scrolls inside it */
|
|
91
|
+
maxHeight?: string;
|
|
92
|
+
/** Whether the menu flips above the field when there is no room below */
|
|
93
|
+
autoFlip?: boolean;
|
|
94
|
+
/** The menu's variant: 'vertical' is the M3 expressive menu, items apart in a rounded container */
|
|
95
|
+
variant?: MenuVariant;
|
|
96
|
+
/** The vertical variant's colour mapping */
|
|
97
|
+
color?: MenuColor;
|
|
98
|
+
};
|
|
78
99
|
/**
|
|
79
100
|
* Whether select is disabled
|
|
80
101
|
*/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A resource scope shared by every stage of a component's composition. */
|
|
2
|
+
export interface CleanupScope {
|
|
3
|
+
readonly destroyed: boolean;
|
|
4
|
+
add(cleanup: () => void): void;
|
|
5
|
+
destroy(): void;
|
|
6
|
+
}
|
|
7
|
+
export declare const createCleanup: () => CleanupScope;
|
|
8
|
+
/** Also support custom component bases that already provide a lifecycle. */
|
|
9
|
+
export declare const getCleanup: (component: {
|
|
10
|
+
resources?: CleanupScope;
|
|
11
|
+
lifecycle?: {
|
|
12
|
+
destroy(): void;
|
|
13
|
+
};
|
|
14
|
+
}) => CleanupScope;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const createCleanup = () => {
|
|
2
|
+
let callbacks;
|
|
3
|
+
let destroyed = false;
|
|
4
|
+
return {
|
|
5
|
+
get destroyed() { return destroyed; },
|
|
6
|
+
add(cleanup) {
|
|
7
|
+
if (destroyed)
|
|
8
|
+
cleanup();
|
|
9
|
+
else
|
|
10
|
+
(callbacks ?? (callbacks = [])).push(cleanup);
|
|
11
|
+
},
|
|
12
|
+
destroy() {
|
|
13
|
+
if (destroyed)
|
|
14
|
+
return;
|
|
15
|
+
destroyed = true;
|
|
16
|
+
const pending = callbacks;
|
|
17
|
+
callbacks = undefined;
|
|
18
|
+
let failure;
|
|
19
|
+
let failed = false;
|
|
20
|
+
for (const cleanup of pending ?? []) {
|
|
21
|
+
try {
|
|
22
|
+
cleanup();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (!failed)
|
|
26
|
+
failure = error;
|
|
27
|
+
failed = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (failed)
|
|
31
|
+
throw failure;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export const getCleanup = (component) => {
|
|
36
|
+
if (component.resources)
|
|
37
|
+
return component.resources;
|
|
38
|
+
const resources = component.resources = createCleanup();
|
|
39
|
+
if (component.lifecycle) {
|
|
40
|
+
const destroy = component.lifecycle.destroy.bind(component.lifecycle);
|
|
41
|
+
component.lifecycle.destroy = () => {
|
|
42
|
+
try {
|
|
43
|
+
resources.destroy();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
destroy();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return resources;
|
|
51
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @module core/compose/component
|
|
3
3
|
* @description Core utilities for component composition and creation with built-in mobile support
|
|
4
4
|
*/
|
|
5
|
+
import { type CleanupScope } from "./cleanup.js";
|
|
5
6
|
/**
|
|
6
7
|
* Touch state interface to track touch interactions
|
|
7
8
|
*/
|
|
@@ -18,6 +19,7 @@ export interface TouchState {
|
|
|
18
19
|
* Base component interface with prefix utilities
|
|
19
20
|
*/
|
|
20
21
|
export interface BaseComponent {
|
|
22
|
+
resources?: CleanupScope;
|
|
21
23
|
config: Record<string, any>;
|
|
22
24
|
componentName?: string;
|
|
23
25
|
getClass: (name: string) => string;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { getCleanup } from "./cleanup.js";
|
|
1
2
|
import { createElement, removeEventHandlers, } from "../dom/create.js";
|
|
2
3
|
import { normalizeEvent, hasTouchSupport, TOUCH_CONFIG, PASSIVE_EVENTS, } from "../utils/mobile.js";
|
|
4
|
+
const getModifierClass = (base, modifier) => `${base}--${modifier}`;
|
|
5
|
+
const getElementClass = (base, element) => `${base}-${element}`;
|
|
3
6
|
const withPrefix = (prefix) => ({
|
|
4
7
|
getClass: (name) => `${prefix}-${name}`,
|
|
5
|
-
getModifierClass
|
|
6
|
-
getElementClass
|
|
8
|
+
getModifierClass,
|
|
9
|
+
getElementClass,
|
|
7
10
|
});
|
|
8
11
|
export const createBase = (config = {}) => ({
|
|
9
12
|
config,
|
|
@@ -34,12 +37,12 @@ export const createBase = (config = {}) => ({
|
|
|
34
37
|
}
|
|
35
38
|
},
|
|
36
39
|
});
|
|
37
|
-
|
|
40
|
+
const setupTouch = (element, base, options) => {
|
|
38
41
|
const handleTouchStart = (event) => {
|
|
39
42
|
base.updateTouchState(event, "start");
|
|
40
43
|
element.classList.add(`${base.getClass("touch-active")}`);
|
|
41
|
-
if (options.forwardEvents?.touchstart && "emit" in
|
|
42
|
-
|
|
44
|
+
if (options.forwardEvents?.touchstart && "emit" in base && typeof base.emit === "function") {
|
|
45
|
+
base.emit("touchstart", normalizeEvent(event));
|
|
43
46
|
}
|
|
44
47
|
};
|
|
45
48
|
const handleTouchEnd = (event) => {
|
|
@@ -48,11 +51,11 @@ export const withElement = (options = {}) => (component) => {
|
|
|
48
51
|
const touchDuration = Date.now() - base.touchState.startTime;
|
|
49
52
|
element.classList.remove(`${base.getClass("touch-active")}`);
|
|
50
53
|
base.updateTouchState(event, "end");
|
|
51
|
-
if (touchDuration < TOUCH_CONFIG.TAP_THRESHOLD && "emit" in
|
|
52
|
-
|
|
54
|
+
if (touchDuration < TOUCH_CONFIG.TAP_THRESHOLD && "emit" in base && typeof base.emit === "function") {
|
|
55
|
+
base.emit("tap", normalizeEvent(event));
|
|
53
56
|
}
|
|
54
|
-
if (options.forwardEvents?.touchend && "emit" in
|
|
55
|
-
|
|
57
|
+
if (options.forwardEvents?.touchend && "emit" in base && typeof base.emit === "function") {
|
|
58
|
+
base.emit("touchend", normalizeEvent(event));
|
|
56
59
|
}
|
|
57
60
|
};
|
|
58
61
|
const handleTouchMove = (event) => {
|
|
@@ -62,17 +65,28 @@ export const withElement = (options = {}) => (component) => {
|
|
|
62
65
|
const deltaX = normalized.clientX - base.touchState.startPosition.x;
|
|
63
66
|
const deltaY = normalized.clientY - base.touchState.startPosition.y;
|
|
64
67
|
if (Math.abs(deltaX) > TOUCH_CONFIG.SWIPE_THRESHOLD &&
|
|
65
|
-
"emit" in
|
|
66
|
-
|
|
68
|
+
"emit" in base && typeof base.emit === "function") {
|
|
69
|
+
base.emit("swipe", {
|
|
67
70
|
direction: deltaX > 0 ? "right" : "left",
|
|
68
71
|
deltaX,
|
|
69
72
|
deltaY,
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
|
-
if (options.forwardEvents?.touchmove && "emit" in
|
|
73
|
-
|
|
75
|
+
if (options.forwardEvents?.touchmove && "emit" in base && typeof base.emit === "function") {
|
|
76
|
+
base.emit("touchmove", { ...normalized, deltaX, deltaY });
|
|
74
77
|
}
|
|
75
78
|
};
|
|
79
|
+
element.addEventListener("touchstart", handleTouchStart, PASSIVE_EVENTS);
|
|
80
|
+
element.addEventListener("touchend", handleTouchEnd);
|
|
81
|
+
element.addEventListener("touchmove", handleTouchMove, PASSIVE_EVENTS);
|
|
82
|
+
return () => {
|
|
83
|
+
element.removeEventListener("touchstart", handleTouchStart);
|
|
84
|
+
element.removeEventListener("touchend", handleTouchEnd);
|
|
85
|
+
element.removeEventListener("touchmove", handleTouchMove);
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export const withElement = (options = {}) => (component) => {
|
|
89
|
+
const resources = getCleanup(component);
|
|
76
90
|
const base = component;
|
|
77
91
|
let parent = component.config.parent || options.parent;
|
|
78
92
|
if (typeof parent === "string") {
|
|
@@ -106,11 +120,16 @@ export const withElement = (options = {}) => (component) => {
|
|
|
106
120
|
container: parent,
|
|
107
121
|
};
|
|
108
122
|
const element = createElement(elementOptions);
|
|
109
|
-
|
|
110
|
-
element
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
const cleanupTouch = hasTouchSupport() && options.interactive
|
|
124
|
+
? setupTouch(element, base, options)
|
|
125
|
+
: undefined;
|
|
126
|
+
resources.add(() => {
|
|
127
|
+
cleanupTouch?.();
|
|
128
|
+
base.touchState.activeTarget = null;
|
|
129
|
+
base.touchState.isTouching = false;
|
|
130
|
+
removeEventHandlers(element);
|
|
131
|
+
element.remove();
|
|
132
|
+
});
|
|
114
133
|
return {
|
|
115
134
|
...component,
|
|
116
135
|
element,
|
|
@@ -119,13 +138,7 @@ export const withElement = (options = {}) => (component) => {
|
|
|
119
138
|
return this;
|
|
120
139
|
},
|
|
121
140
|
destroy() {
|
|
122
|
-
|
|
123
|
-
element.removeEventListener("touchstart", handleTouchStart);
|
|
124
|
-
element.removeEventListener("touchend", handleTouchEnd);
|
|
125
|
-
element.removeEventListener("touchmove", handleTouchMove);
|
|
126
|
-
}
|
|
127
|
-
removeEventHandlers(element);
|
|
128
|
-
element.remove();
|
|
141
|
+
resources.destroy();
|
|
129
142
|
},
|
|
130
143
|
};
|
|
131
144
|
};
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
+
import { getCleanup } from "../cleanup.js";
|
|
1
2
|
import { debounce } from "../../utils/performance.js";
|
|
2
|
-
function hasLifecycle(component) {
|
|
3
|
-
return 'lifecycle' in component &&
|
|
4
|
-
component.lifecycle &&
|
|
5
|
-
typeof component.lifecycle === 'object' &&
|
|
6
|
-
'destroy' in component.lifecycle &&
|
|
7
|
-
typeof component.lifecycle.destroy === 'function';
|
|
8
|
-
}
|
|
9
3
|
export const withDebounce = (config = {}) => (component) => {
|
|
10
4
|
const debouncedHandlers = {};
|
|
11
5
|
const addDebouncedEvent = (event, handler, wait, options = {}) => {
|
|
6
|
+
if (resources.destroyed)
|
|
7
|
+
return enhancedComponent;
|
|
12
8
|
if (debouncedHandlers[event]) {
|
|
13
9
|
removeDebouncedEvent(event);
|
|
14
10
|
}
|
|
@@ -23,31 +19,26 @@ export const withDebounce = (config = {}) => (component) => {
|
|
|
23
19
|
const removeDebouncedEvent = (event) => {
|
|
24
20
|
const handler = debouncedHandlers[event];
|
|
25
21
|
if (handler) {
|
|
22
|
+
handler.debounced.cancel();
|
|
26
23
|
component.element.removeEventListener(event, handler.debounced);
|
|
27
24
|
delete debouncedHandlers[event];
|
|
28
25
|
}
|
|
29
26
|
return enhancedComponent;
|
|
30
27
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const handler = debouncedHandlers[event];
|
|
36
|
-
component.element.removeEventListener(event, handler.debounced);
|
|
37
|
-
});
|
|
38
|
-
originalDestroy.call(component.lifecycle);
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
if (config.debouncedEvents) {
|
|
42
|
-
Object.entries(config.debouncedEvents).forEach(([event, settings]) => {
|
|
43
|
-
addDebouncedEvent(event, settings.handler, settings.wait, settings.options);
|
|
44
|
-
});
|
|
45
|
-
}
|
|
28
|
+
const resources = getCleanup(component);
|
|
29
|
+
resources.add(() => {
|
|
30
|
+
Object.keys(debouncedHandlers).forEach(event => removeDebouncedEvent(event));
|
|
31
|
+
});
|
|
46
32
|
const enhancedComponent = {
|
|
47
33
|
...component,
|
|
48
34
|
addDebouncedEvent,
|
|
49
35
|
removeDebouncedEvent
|
|
50
36
|
};
|
|
37
|
+
if (config.debouncedEvents) {
|
|
38
|
+
Object.entries(config.debouncedEvents).forEach(([event, settings]) => {
|
|
39
|
+
addDebouncedEvent(event, settings.handler, settings.wait, settings.options);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
51
42
|
return enhancedComponent;
|
|
52
43
|
};
|
|
53
44
|
export default withDebounce;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { getCleanup } from "../cleanup.js";
|
|
1
2
|
import { createEmitter } from "../../state/emitter.js";
|
|
2
3
|
export const withEvents = () => (component) => {
|
|
3
4
|
const emitter = createEmitter();
|
|
5
|
+
const resources = getCleanup(component);
|
|
6
|
+
resources.add(() => emitter.clear());
|
|
4
7
|
return {
|
|
5
8
|
...component,
|
|
6
9
|
on(event, handler) {
|
|
7
|
-
|
|
10
|
+
if (!resources.destroyed)
|
|
11
|
+
emitter.on(event, handler);
|
|
8
12
|
return this;
|
|
9
13
|
},
|
|
10
14
|
off(event, handler) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getCleanup } from "../cleanup.js";
|
|
1
2
|
import { createEmitter } from "../../state/emitter.js";
|
|
2
3
|
function hasEvents(component) {
|
|
3
4
|
return 'events' in component &&
|
|
@@ -21,13 +22,17 @@ function hasIcon(component) {
|
|
|
21
22
|
typeof component.icon.getElement === 'function';
|
|
22
23
|
}
|
|
23
24
|
export const withLifecycle = () => (component) => {
|
|
25
|
+
const resources = getCleanup(component);
|
|
24
26
|
let mounted = false;
|
|
27
|
+
let destroyed = false;
|
|
28
|
+
const destroyElement = component.destroy?.bind(component);
|
|
25
29
|
const emitter = createEmitter();
|
|
30
|
+
resources.add(() => emitter.clear());
|
|
26
31
|
const lifecycle = {
|
|
27
32
|
onMount: (handler) => emitter.on('mount', handler),
|
|
28
33
|
onUnmount: (handler) => emitter.on('unmount', handler),
|
|
29
34
|
mount: () => {
|
|
30
|
-
if (!mounted) {
|
|
35
|
+
if (!mounted && !destroyed) {
|
|
31
36
|
mounted = true;
|
|
32
37
|
emitter.emit('mount');
|
|
33
38
|
}
|
|
@@ -41,9 +46,14 @@ export const withLifecycle = () => (component) => {
|
|
|
41
46
|
},
|
|
42
47
|
isMounted: () => mounted,
|
|
43
48
|
destroy() {
|
|
49
|
+
if (destroyed)
|
|
50
|
+
return;
|
|
51
|
+
destroyed = true;
|
|
44
52
|
if (mounted) {
|
|
45
53
|
this.unmount();
|
|
46
54
|
}
|
|
55
|
+
emitter.clear();
|
|
56
|
+
resources.destroy();
|
|
47
57
|
if (hasEvents(component)) {
|
|
48
58
|
component.events.destroy();
|
|
49
59
|
}
|
|
@@ -59,9 +69,10 @@ export const withLifecycle = () => (component) => {
|
|
|
59
69
|
iconElement.remove();
|
|
60
70
|
}
|
|
61
71
|
}
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
if (destroyElement)
|
|
73
|
+
destroyElement();
|
|
74
|
+
else
|
|
75
|
+
component.element?.remove();
|
|
65
76
|
}
|
|
66
77
|
};
|
|
67
78
|
return {
|
|
@@ -52,12 +52,6 @@ export interface RippleComponent extends BaseComponent {
|
|
|
52
52
|
* @returns Ripple controller
|
|
53
53
|
*/
|
|
54
54
|
export declare const createRipple: (config?: RippleConfig) => RippleController;
|
|
55
|
-
declare global {
|
|
56
|
-
interface HTMLElement {
|
|
57
|
-
__rippleHandlers?: Array<(e: MouseEvent) => void>;
|
|
58
|
-
__rippleContainer?: HTMLElement;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
55
|
/**
|
|
62
56
|
* Adds ripple effect functionality to a component
|
|
63
57
|
*
|