mtrl 0.8.0-next.43 → 0.8.0-next.45

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 (78) hide show
  1. package/dist/components/menu/features/position.js +2 -0
  2. package/dist/components/select/features.js +1 -0
  3. package/dist/components/select/types.d.ts +21 -0
  4. package/dist/core/compose/cleanup.d.ts +14 -0
  5. package/dist/core/compose/cleanup.js +51 -0
  6. package/dist/core/compose/component.d.ts +2 -0
  7. package/dist/core/compose/component.js +38 -25
  8. package/dist/core/compose/features/debounce.js +13 -22
  9. package/dist/core/compose/features/disabled.js +1 -3
  10. package/dist/core/compose/features/events.js +5 -1
  11. package/dist/core/compose/features/lifecycle.js +15 -4
  12. package/dist/core/compose/features/ripple.d.ts +0 -6
  13. package/dist/core/compose/features/ripple.js +61 -111
  14. package/dist/core/compose/features/throttle.js +13 -16
  15. package/dist/core/compose/features/withEvents.js +3 -8
  16. package/dist/core/dom/create.js +30 -27
  17. package/dist/core/dom/events.js +29 -15
  18. package/dist/core/state/events.js +13 -5
  19. package/dist/core/state/lifecycle.js +6 -1
  20. package/dist/core/utils/performance.d.ts +5 -2
  21. package/dist/core/utils/performance.js +16 -2
  22. package/dist/index.cjs +11 -11
  23. package/dist/package.json +1 -1
  24. package/dist/styles/badge.css +1 -1
  25. package/dist/styles/base.css +2 -2
  26. package/dist/styles/bottom-app-bar.css +1 -1
  27. package/dist/styles/bottom-sheet.css +1 -1
  28. package/dist/styles/button-group.css +1 -1
  29. package/dist/styles/button.css +1 -1
  30. package/dist/styles/card.css +1 -1
  31. package/dist/styles/carousel.css +1 -1
  32. package/dist/styles/checkbox.css +1 -1
  33. package/dist/styles/chips.css +1 -1
  34. package/dist/styles/dialog.css +1 -1
  35. package/dist/styles/divider.css +1 -1
  36. package/dist/styles/drawer.css +1 -1
  37. package/dist/styles/extended-fab.css +1 -1
  38. package/dist/styles/fab.css +1 -1
  39. package/dist/styles/icon-button.css +1 -1
  40. package/dist/styles/list.css +1 -1
  41. package/dist/styles/loading-indicator.css +1 -1
  42. package/dist/styles/menu.css +1 -1
  43. package/dist/styles/navigation-rail.css +1 -1
  44. package/dist/styles/navigation.css +1 -1
  45. package/dist/styles/progress.css +1 -1
  46. package/dist/styles/radios.css +1 -1
  47. package/dist/styles/search.css +1 -1
  48. package/dist/styles/segmented-button.css +1 -1
  49. package/dist/styles/select.css +1 -1
  50. package/dist/styles/side-sheet.css +1 -1
  51. package/dist/styles/slider.css +1 -1
  52. package/dist/styles/snackbar.css +1 -1
  53. package/dist/styles/split-button.css +1 -1
  54. package/dist/styles/switch.css +1 -1
  55. package/dist/styles/tabs.css +1 -1
  56. package/dist/styles/textfield.css +1 -1
  57. package/dist/styles/timepicker.css +1 -1
  58. package/dist/styles/top-app-bar.css +1 -1
  59. package/dist/styles/utilities.css +1 -1
  60. package/dist/styles.css +2 -2
  61. package/dist/themes/autumn.css +1 -1
  62. package/dist/themes/baseline.css +1 -1
  63. package/dist/themes/brownbeige.css +1 -1
  64. package/dist/themes/browngreen.css +1 -1
  65. package/dist/themes/desert.css +1 -1
  66. package/dist/themes/forest.css +1 -1
  67. package/dist/themes/highcontrast.css +1 -1
  68. package/dist/themes/legacy.css +1 -1
  69. package/dist/themes/material.css +1 -1
  70. package/dist/themes/ocean.css +1 -1
  71. package/dist/themes/sageivory.css +1 -1
  72. package/dist/themes/spring.css +1 -1
  73. package/dist/themes/summer.css +1 -1
  74. package/dist/themes/sunset.css +1 -1
  75. package/dist/themes/tealcaramel.css +1 -1
  76. package/dist/themes/winter.css +1 -1
  77. package/package.json +2 -1
  78. package/src/styles/utilities/_ripple.scss +12 -3
@@ -20,6 +20,8 @@ export const createPositioner = (component, config) => {
20
20
  tempMenu.style.transform = "none";
21
21
  tempMenu.style.opacity = "0";
22
22
  tempMenu.style.pointerEvents = "none";
23
+ if (config.maxHeight)
24
+ tempMenu.style.maxHeight = config.maxHeight;
23
25
  tempMenu.classList.add(`${component.getClass("menu--visible")}`);
24
26
  if (config.width === "100%" && !isSubmenu) {
25
27
  tempMenu.style.width = `${openerRect.width}px`;
@@ -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
  *
@@ -1,4 +1,5 @@
1
1
  import { RIPPLE_CONFIG } from "./constants.js";
2
+ import { getCleanup } from "../cleanup.js";
2
3
  import { PREFIX } from "../../config.js";
3
4
  export const createRipple = (config = {}) => {
4
5
  const options = {
@@ -6,111 +7,69 @@ export const createRipple = (config = {}) => {
6
7
  ...config,
7
8
  opacity: config.opacity || RIPPLE_CONFIG.opacity,
8
9
  };
9
- const activeRipples = new WeakMap();
10
- let documentListeners = [];
11
- const createRippleElement = () => {
12
- const ripple = document.createElement("div");
13
- ripple.className = `${PREFIX}-ripple-wave`;
14
- return ripple;
15
- };
16
- const addDocumentListener = (event, handler) => {
17
- if (typeof document.addEventListener === "function") {
18
- document.addEventListener(event, handler);
19
- documentListeners.push({ event, handler });
20
- }
21
- };
22
- const removeDocumentListener = (event, handler) => {
23
- if (typeof document.removeEventListener === "function") {
24
- document.removeEventListener(event, handler);
25
- documentListeners = documentListeners.filter((listener) => !(listener.event === event && listener.handler === handler));
26
- }
27
- };
28
- const animate = (event, container) => {
29
- if (!container || !container.__rippleContainer)
30
- return;
31
- const rippleContainer = container.__rippleContainer;
32
- const bounds = container.getBoundingClientRect();
33
- const ripple = createRippleElement();
34
- const size = Math.max(bounds.width, bounds.height) * 2;
35
- const x = event.clientX - bounds.left - size / 2;
36
- const y = event.clientY - bounds.top - size / 2;
37
- Object.assign(ripple.style, {
38
- width: `${size}px`,
39
- height: `${size}px`,
40
- left: `${x}px`,
41
- top: `${y}px`,
42
- });
43
- rippleContainer.appendChild(ripple);
44
- ripple.offsetHeight;
45
- requestAnimationFrame(() => {
46
- ripple.classList.add("active");
47
- });
48
- const cleanup = () => {
49
- removeDocumentListener("mouseup", cleanup);
50
- removeDocumentListener("mouseleave", cleanup);
51
- ripple.classList.remove("active");
52
- ripple.classList.add("fade-out");
53
- setTimeout(() => {
54
- if (ripple.parentNode) {
55
- ripple.parentNode.removeChild(ripple);
56
- }
57
- activeRipples.get(container)?.delete(ripple);
58
- }, options.duration);
59
- };
60
- addDocumentListener("mouseup", cleanup);
61
- addDocumentListener("mouseleave", cleanup);
62
- };
10
+ const mounts = new WeakMap();
63
11
  return {
64
- mount: (element) => {
65
- if (!element)
12
+ mount(element) {
13
+ if (!element || mounts.has(element))
66
14
  return;
67
- const currentPosition = window.getComputedStyle(element).position;
68
- if (currentPosition === "static") {
15
+ const doc = element.ownerDocument;
16
+ const view = doc.defaultView;
17
+ if (!view)
18
+ return;
19
+ if (view.getComputedStyle(element).position === "static") {
69
20
  element.style.position = "relative";
70
21
  }
71
- let rippleContainer = element.querySelector(`.${PREFIX}-ripple`);
72
- if (!rippleContainer) {
73
- rippleContainer = document.createElement("div");
74
- rippleContainer.className = `${PREFIX}-ripple`;
75
- element.appendChild(rippleContainer);
76
- }
77
- element.__rippleContainer = rippleContainer;
78
- activeRipples.set(element, new Set());
79
- const mousedownHandler = (e) => animate(e, element);
80
- if (!element.__rippleHandlers) {
81
- element.__rippleHandlers = [];
82
- }
83
- element.__rippleHandlers.push(mousedownHandler);
84
- element.addEventListener("mousedown", mousedownHandler);
85
- },
86
- unmount: (element) => {
87
- if (!element)
88
- return;
89
- documentListeners.forEach(({ event, handler }) => {
90
- removeDocumentListener(event, handler);
91
- });
92
- documentListeners = [];
93
- if (element.__rippleHandlers) {
94
- element.__rippleHandlers.forEach((handler) => {
95
- element.removeEventListener("mousedown", handler);
22
+ const container = doc.createElement("div");
23
+ container.className = `${PREFIX}-ripple`;
24
+ element.appendChild(container);
25
+ const waves = new Set();
26
+ const press = (event) => {
27
+ const bounds = element.getBoundingClientRect();
28
+ const size = Math.max(bounds.width, bounds.height) * 2;
29
+ const wave = doc.createElement("div");
30
+ wave.className = `${PREFIX}-ripple-wave active`;
31
+ Object.assign(wave.style, {
32
+ width: `${size}px`, height: `${size}px`,
33
+ left: `${event.clientX - bounds.left - size / 2}px`,
34
+ top: `${event.clientY - bounds.top - size / 2}px`,
96
35
  });
97
- element.__rippleHandlers = [];
98
- }
99
- if (activeRipples.has(element)) {
100
- const ripples = activeRipples.get(element);
101
- if (ripples) {
102
- ripples.forEach((ripple) => {
103
- if (ripple.parentNode) {
104
- ripple.parentNode.removeChild(ripple);
105
- }
106
- });
107
- }
108
- activeRipples.delete(element);
109
- }
110
- if (element.__rippleContainer && element.__rippleContainer.parentNode) {
111
- element.__rippleContainer.parentNode.removeChild(element.__rippleContainer);
112
- delete element.__rippleContainer;
113
- }
36
+ let timer;
37
+ let released = false;
38
+ const removeListeners = () => {
39
+ doc.removeEventListener("mouseup", release);
40
+ doc.removeEventListener("mouseleave", release);
41
+ };
42
+ const dispose = () => {
43
+ removeListeners();
44
+ if (timer !== undefined)
45
+ view.clearTimeout(timer);
46
+ wave.remove();
47
+ waves.delete(dispose);
48
+ };
49
+ const release = () => {
50
+ if (released)
51
+ return;
52
+ released = true;
53
+ removeListeners();
54
+ wave.classList.add("fade-out");
55
+ timer = view.setTimeout(dispose, options.duration);
56
+ };
57
+ waves.add(dispose);
58
+ doc.addEventListener("mouseup", release);
59
+ doc.addEventListener("mouseleave", release);
60
+ container.appendChild(wave);
61
+ };
62
+ element.addEventListener("mousedown", press);
63
+ mounts.set(element, () => {
64
+ element.removeEventListener("mousedown", press);
65
+ for (const dispose of waves)
66
+ dispose();
67
+ container.remove();
68
+ });
69
+ },
70
+ unmount(element) {
71
+ mounts.get(element)?.();
72
+ mounts.delete(element);
114
73
  },
115
74
  };
116
75
  };
@@ -119,17 +78,8 @@ export const withRipple = (config) => (component) => {
119
78
  return component;
120
79
  const rippleInstance = createRipple(config.rippleConfig);
121
80
  rippleInstance.mount(component.element);
122
- if (component.lifecycle) {
123
- const originalMount = component.lifecycle.mount;
124
- const originalDestroy = component.lifecycle.destroy;
125
- component.lifecycle.mount = () => {
126
- originalMount.call(component.lifecycle);
127
- };
128
- component.lifecycle.destroy = () => {
129
- rippleInstance.unmount(component.element);
130
- originalDestroy.call(component.lifecycle);
131
- };
132
- }
81
+ const resources = getCleanup(component);
82
+ resources.add(() => rippleInstance.unmount(component.element));
133
83
  return {
134
84
  ...component,
135
85
  ripple: rippleInstance,
@@ -1,8 +1,10 @@
1
+ import { getCleanup } from "../cleanup.js";
1
2
  import { throttle } from "../../utils/performance.js";
2
- import { hasLifecycle } from "../utils/type-guards.js";
3
3
  export const withThrottle = (config = {}) => (component) => {
4
4
  const throttledHandlers = {};
5
5
  const addThrottledEvent = (event, handler, wait, options = {}) => {
6
+ if (resources.destroyed)
7
+ return enhancedComponent;
6
8
  if (throttledHandlers[event]) {
7
9
  removeThrottledEvent(event);
8
10
  }
@@ -17,31 +19,26 @@ export const withThrottle = (config = {}) => (component) => {
17
19
  const removeThrottledEvent = (event) => {
18
20
  const handler = throttledHandlers[event];
19
21
  if (handler) {
22
+ handler.throttled.cancel();
20
23
  component.element.removeEventListener(event, handler.throttled);
21
24
  delete throttledHandlers[event];
22
25
  }
23
26
  return enhancedComponent;
24
27
  };
25
- if (hasLifecycle(component)) {
26
- const originalDestroy = component.lifecycle.destroy;
27
- component.lifecycle.destroy = () => {
28
- Object.keys(throttledHandlers).forEach((event) => {
29
- const handler = throttledHandlers[event];
30
- component.element.removeEventListener(event, handler.throttled);
31
- });
32
- originalDestroy.call(component.lifecycle);
33
- };
34
- }
35
- if (config.throttledEvents) {
36
- Object.entries(config.throttledEvents).forEach(([event, settings]) => {
37
- addThrottledEvent(event, settings.handler, settings.wait, settings.options);
38
- });
39
- }
28
+ const resources = getCleanup(component);
29
+ resources.add(() => {
30
+ Object.keys(throttledHandlers).forEach(event => removeThrottledEvent(event));
31
+ });
40
32
  const enhancedComponent = {
41
33
  ...component,
42
34
  addThrottledEvent,
43
35
  removeThrottledEvent,
44
36
  };
37
+ if (config.throttledEvents) {
38
+ Object.entries(config.throttledEvents).forEach(([event, settings]) => {
39
+ addThrottledEvent(event, settings.handler, settings.wait, settings.options);
40
+ });
41
+ }
45
42
  return enhancedComponent;
46
43
  };
47
44
  export default withThrottle;