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.
Files changed (83) hide show
  1. package/dist/components/navigation-rail/config.js +2 -1
  2. package/dist/components/navigation-rail/constants.d.ts +5 -0
  3. package/dist/components/navigation-rail/constants.js +4 -0
  4. package/dist/components/navigation-rail/navigation-rail.js +19 -1
  5. package/dist/components/navigation-rail/types.d.ts +4 -0
  6. package/dist/components/select/features.js +1 -0
  7. package/dist/components/select/types.d.ts +21 -0
  8. package/dist/core/compose/cleanup.d.ts +14 -0
  9. package/dist/core/compose/cleanup.js +51 -0
  10. package/dist/core/compose/component.d.ts +2 -0
  11. package/dist/core/compose/component.js +38 -25
  12. package/dist/core/compose/features/debounce.js +13 -22
  13. package/dist/core/compose/features/disabled.js +1 -3
  14. package/dist/core/compose/features/events.js +5 -1
  15. package/dist/core/compose/features/lifecycle.js +15 -4
  16. package/dist/core/compose/features/ripple.d.ts +0 -6
  17. package/dist/core/compose/features/ripple.js +61 -111
  18. package/dist/core/compose/features/throttle.js +13 -16
  19. package/dist/core/compose/features/withEvents.js +3 -8
  20. package/dist/core/dom/create.js +30 -27
  21. package/dist/core/dom/events.js +29 -15
  22. package/dist/core/state/events.js +13 -5
  23. package/dist/core/state/lifecycle.js +6 -1
  24. package/dist/core/utils/performance.d.ts +5 -2
  25. package/dist/core/utils/performance.js +16 -2
  26. package/dist/index.cjs +12 -12
  27. package/dist/package.json +1 -1
  28. package/dist/styles/badge.css +1 -1
  29. package/dist/styles/base.css +2 -2
  30. package/dist/styles/bottom-app-bar.css +1 -1
  31. package/dist/styles/bottom-sheet.css +1 -1
  32. package/dist/styles/button-group.css +1 -1
  33. package/dist/styles/button.css +1 -1
  34. package/dist/styles/card.css +1 -1
  35. package/dist/styles/carousel.css +1 -1
  36. package/dist/styles/checkbox.css +1 -1
  37. package/dist/styles/chips.css +1 -1
  38. package/dist/styles/dialog.css +1 -1
  39. package/dist/styles/divider.css +1 -1
  40. package/dist/styles/drawer.css +1 -1
  41. package/dist/styles/extended-fab.css +1 -1
  42. package/dist/styles/fab.css +1 -1
  43. package/dist/styles/icon-button.css +1 -1
  44. package/dist/styles/list.css +1 -1
  45. package/dist/styles/loading-indicator.css +1 -1
  46. package/dist/styles/menu.css +1 -1
  47. package/dist/styles/navigation-rail.css +2 -2
  48. package/dist/styles/navigation.css +1 -1
  49. package/dist/styles/progress.css +1 -1
  50. package/dist/styles/radios.css +1 -1
  51. package/dist/styles/search.css +1 -1
  52. package/dist/styles/segmented-button.css +1 -1
  53. package/dist/styles/select.css +1 -1
  54. package/dist/styles/side-sheet.css +1 -1
  55. package/dist/styles/slider.css +1 -1
  56. package/dist/styles/snackbar.css +1 -1
  57. package/dist/styles/split-button.css +1 -1
  58. package/dist/styles/switch.css +1 -1
  59. package/dist/styles/tabs.css +1 -1
  60. package/dist/styles/textfield.css +1 -1
  61. package/dist/styles/timepicker.css +1 -1
  62. package/dist/styles/top-app-bar.css +1 -1
  63. package/dist/styles/utilities.css +1 -1
  64. package/dist/styles.css +2 -2
  65. package/dist/themes/autumn.css +1 -1
  66. package/dist/themes/baseline.css +1 -1
  67. package/dist/themes/brownbeige.css +1 -1
  68. package/dist/themes/browngreen.css +1 -1
  69. package/dist/themes/desert.css +1 -1
  70. package/dist/themes/forest.css +1 -1
  71. package/dist/themes/highcontrast.css +1 -1
  72. package/dist/themes/legacy.css +1 -1
  73. package/dist/themes/material.css +1 -1
  74. package/dist/themes/ocean.css +1 -1
  75. package/dist/themes/sageivory.css +1 -1
  76. package/dist/themes/spring.css +1 -1
  77. package/dist/themes/summer.css +1 -1
  78. package/dist/themes/sunset.css +1 -1
  79. package/dist/themes/tealcaramel.css +1 -1
  80. package/dist/themes/winter.css +1 -1
  81. package/package.json +2 -1
  82. package/src/styles/components/_navigation-rail.scss +193 -65
  83. 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;
@@ -99,6 +99,7 @@ export const withMenu = (config) => (component) => {
99
99
  closeOnResize: true,
100
100
  offset: 0,
101
101
  container: component.element,
102
+ ...(config.menu ?? {}),
102
103
  });
103
104
  menu.on("select", (event) => {
104
105
  if (!event.item || event.item.hasSubmenu) {
@@ -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: (base, modifier) => `${base}--${modifier}`,
6
- getElementClass: (base, element) => `${base}-${element}`,
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
- export const withElement = (options = {}) => (component) => {
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 component) {
42
- component.emit("touchstart", normalizeEvent(event));
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 component) {
52
- component.emit("tap", normalizeEvent(event));
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 component) {
55
- component.emit("touchend", normalizeEvent(event));
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 component) {
66
- component.emit("swipe", {
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 component) {
73
- component.emit("touchmove", { ...normalized, deltaX, deltaY });
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
- if (hasTouchSupport() && options.interactive) {
110
- element.addEventListener("touchstart", handleTouchStart, PASSIVE_EVENTS);
111
- element.addEventListener("touchend", handleTouchEnd);
112
- element.addEventListener("touchmove", handleTouchMove, PASSIVE_EVENTS);
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
- if (hasTouchSupport() && options.interactive) {
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
- if (hasLifecycle(component)) {
32
- const originalDestroy = component.lifecycle.destroy;
33
- component.lifecycle.destroy = () => {
34
- Object.keys(debouncedHandlers).forEach(event => {
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;
@@ -54,9 +54,7 @@ export const withDisabled = (config) => (component) => {
54
54
  }
55
55
  };
56
56
  if (config.disabled) {
57
- requestAnimationFrame(() => {
58
- disabled.disable();
59
- });
57
+ disabled.disable();
60
58
  }
61
59
  return {
62
60
  ...component,
@@ -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
- emitter.on(event, handler);
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 (component.element) {
63
- component.element.remove();
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
  *