mtrl 0.8.0-next.36 → 0.8.0-next.38

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 (76) hide show
  1. package/README.md +11 -0
  2. package/dist/README.md +11 -0
  3. package/dist/components/slider/features/controller.js +10 -49
  4. package/dist/components/slider/features/index.d.ts +1 -1
  5. package/dist/components/slider/features/index.js +1 -1
  6. package/dist/components/slider/features/states.js +19 -19
  7. package/dist/components/slider/features/tracks.d.ts +13 -0
  8. package/dist/components/slider/features/tracks.js +174 -0
  9. package/dist/components/slider/slider.d.ts +1 -1
  10. package/dist/components/slider/slider.js +2 -2
  11. package/dist/components/textfield/api.d.ts +1 -0
  12. package/dist/components/textfield/api.js +10 -10
  13. package/dist/components/textfield/features/density.js +1 -1
  14. package/dist/components/textfield/features/leading-icon.js +2 -1
  15. package/dist/components/textfield/features/placement.d.ts +2 -0
  16. package/dist/components/textfield/features/placement.js +25 -3
  17. package/dist/components/textfield/textfield.js +1 -1
  18. package/dist/core/compose/features/textinput.js +2 -1
  19. package/dist/index.cjs +12 -12
  20. package/dist/package.json +1 -1
  21. package/dist/styles/badge.css +1 -1
  22. package/dist/styles/base.css +1 -1
  23. package/dist/styles/bottom-app-bar.css +1 -1
  24. package/dist/styles/bottom-sheet.css +1 -1
  25. package/dist/styles/button-group.css +1 -1
  26. package/dist/styles/button.css +1 -1
  27. package/dist/styles/card.css +1 -1
  28. package/dist/styles/carousel.css +1 -1
  29. package/dist/styles/checkbox.css +1 -1
  30. package/dist/styles/chips.css +1 -1
  31. package/dist/styles/dialog.css +1 -1
  32. package/dist/styles/divider.css +1 -1
  33. package/dist/styles/drawer.css +1 -1
  34. package/dist/styles/extended-fab.css +1 -1
  35. package/dist/styles/fab.css +1 -1
  36. package/dist/styles/icon-button.css +1 -1
  37. package/dist/styles/list.css +1 -1
  38. package/dist/styles/loading-indicator.css +1 -1
  39. package/dist/styles/menu.css +1 -1
  40. package/dist/styles/navigation.css +1 -1
  41. package/dist/styles/progress.css +1 -1
  42. package/dist/styles/radios.css +1 -1
  43. package/dist/styles/search.css +1 -1
  44. package/dist/styles/segmented-button.css +1 -1
  45. package/dist/styles/select.css +1 -1
  46. package/dist/styles/side-sheet.css +1 -1
  47. package/dist/styles/slider.css +2 -2
  48. package/dist/styles/snackbar.css +1 -1
  49. package/dist/styles/split-button.css +1 -1
  50. package/dist/styles/switch.css +1 -1
  51. package/dist/styles/tabs.css +1 -1
  52. package/dist/styles/textfield.css +1 -1
  53. package/dist/styles/timepicker.css +1 -1
  54. package/dist/styles/top-app-bar.css +1 -1
  55. package/dist/styles/utilities.css +1 -1
  56. package/dist/styles.css +2 -2
  57. package/dist/themes/autumn.css +1 -1
  58. package/dist/themes/baseline.css +1 -1
  59. package/dist/themes/brownbeige.css +1 -1
  60. package/dist/themes/browngreen.css +1 -1
  61. package/dist/themes/desert.css +1 -1
  62. package/dist/themes/forest.css +1 -1
  63. package/dist/themes/highcontrast.css +1 -1
  64. package/dist/themes/legacy.css +1 -1
  65. package/dist/themes/material.css +1 -1
  66. package/dist/themes/ocean.css +1 -1
  67. package/dist/themes/sageivory.css +1 -1
  68. package/dist/themes/spring.css +1 -1
  69. package/dist/themes/summer.css +1 -1
  70. package/dist/themes/sunset.css +1 -1
  71. package/dist/themes/tealcaramel.css +1 -1
  72. package/dist/themes/winter.css +1 -1
  73. package/package.json +2 -1
  74. package/src/styles/components/_slider.scss +39 -10
  75. package/dist/components/slider/features/canvas.d.ts +0 -47
  76. package/dist/components/slider/features/canvas.js +0 -570
package/README.md CHANGED
@@ -137,6 +137,17 @@ Button progress and card action buttons retain their dynamic imports in ESM.
137
137
  Enable code splitting in your application build to load those features on demand.
138
138
  The CommonJS compatibility bundle includes them eagerly.
139
139
 
140
+ Slider decoration uses DOM tracks and CSS backgrounds for ticks; it no longer
141
+ creates a canvas or subscribes to theme changes. Its public slider API and
142
+ accessible handles remain. Themes and per-slider colour overrides apply through
143
+ CSS. Custom styling that targets the former `.mtrl-slider-canvas` element must be
144
+ updated. Run `bun run slider:check` after building for slider geometry, keyboard,
145
+ pointer, resize, and lifecycle checks. For comparison with a previous canvas
146
+ build, pass `--reference=/path/to/reference` to `scripts/check-slider.ts`; that
147
+ directory must contain an ESM `slider.js` exporting `createSlider` and its full
148
+ `styles.css`. Comparison screenshots and measurements go to `analysis/slider-dom`.
149
+ CSS rounded corners differ slightly from the previous canvas curves.
150
+
140
151
  ### Checking distribution size
141
152
 
142
153
  ```bash
package/dist/README.md CHANGED
@@ -137,6 +137,17 @@ Button progress and card action buttons retain their dynamic imports in ESM.
137
137
  Enable code splitting in your application build to load those features on demand.
138
138
  The CommonJS compatibility bundle includes them eagerly.
139
139
 
140
+ Slider decoration uses DOM tracks and CSS backgrounds for ticks; it no longer
141
+ creates a canvas or subscribes to theme changes. Its public slider API and
142
+ accessible handles remain. Themes and per-slider colour overrides apply through
143
+ CSS. Custom styling that targets the former `.mtrl-slider-canvas` element must be
144
+ updated. Run `bun run slider:check` after building for slider geometry, keyboard,
145
+ pointer, resize, and lifecycle checks. For comparison with a previous canvas
146
+ build, pass `--reference=/path/to/reference` to `scripts/check-slider.ts`; that
147
+ directory must contain an ESM `slider.js` exporting `createSlider` and its full
148
+ `styles.css`. Comparison screenshots and measurements go to `analysis/slider-dom`.
149
+ CSS rounded corners differ slightly from the previous canvas curves.
150
+
140
151
  ### Checking distribution size
141
152
 
142
153
  ```bash
@@ -54,41 +54,9 @@ export const withController = (config) => (component) => {
54
54
  const range = state.max - state.min;
55
55
  return range === 0 ? 0 : ((value - state.min) / range) * 100;
56
56
  };
57
- const getTrackDimensions = () => {
58
- const components = getComponents();
59
- const { handle, container } = components;
60
- if (!handle || !container)
61
- return null;
62
- try {
63
- const handleRect = handle.getBoundingClientRect();
64
- const containerRect = container.getBoundingClientRect();
65
- const handleSize = handleRect.width || SLIDER_MEASUREMENTS.HANDLE_SIZE;
66
- const trackSize = containerRect.width;
67
- const edgeConstraint = (SLIDER_MEASUREMENTS.EDGE_PADDING / trackSize) * 100;
68
- const paddingPixels = state.activeHandle ? 6 : 8;
69
- const paddingPercent = (paddingPixels / trackSize) * 100;
70
- return { handleSize, trackSize, edgeConstraint, paddingPercent };
71
- }
72
- catch (error) {
73
- console.warn("Error calculating track dimensions:", error);
74
- return {
75
- handleSize: SLIDER_MEASUREMENTS.HANDLE_SIZE,
76
- trackSize: 200,
77
- edgeConstraint: 3,
78
- paddingPercent: 4,
79
- };
80
- }
81
- };
82
- const mapValueToVisualPercent = (valuePercent, trackSize = 200) => {
83
- const edgeConstraint = (SLIDER_MEASUREMENTS.EDGE_PADDING / trackSize) * 100;
84
- const minEdge = edgeConstraint;
85
- const maxEdge = 100 - edgeConstraint;
86
- const visualRange = maxEdge - minEdge;
87
- if (valuePercent <= 0)
88
- return minEdge;
89
- if (valuePercent >= 100)
90
- return maxEdge;
91
- return minEdge + (valuePercent / 100) * visualRange;
57
+ const visualPosition = (percent) => {
58
+ const ratio = Math.min(1, Math.max(0, percent / 100));
59
+ return `calc(${ratio * 100}% + ${SLIDER_MEASUREMENTS.EDGE_PADDING * (1 - 2 * ratio)}px)`;
92
60
  };
93
61
  const getValueFromPosition = (position) => {
94
62
  const components = getComponents();
@@ -123,25 +91,17 @@ export const withController = (config) => (component) => {
123
91
  const { handle, container, valueBubble, secondHandle, secondValueBubble } = components;
124
92
  if (!handle || !container)
125
93
  return;
126
- const dims = getTrackDimensions();
127
- if (!dims)
128
- return;
129
- const { trackSize } = dims;
130
- const percent = getPercentage(state.value);
131
- const adjustedPercent = mapValueToVisualPercent(percent, trackSize);
132
- handle.style.left = `${adjustedPercent}%`;
94
+ handle.style.left = visualPosition(getPercentage(state.value));
133
95
  handle.style.transform = "translate(-50%, -50%)";
134
96
  if (valueBubble) {
135
- valueBubble.style.left = `${adjustedPercent}%`;
97
+ valueBubble.style.left = visualPosition(getPercentage(state.value));
136
98
  valueBubble.style.transform = "translateX(-50%)";
137
99
  }
138
100
  if (config.range && secondHandle && state.secondValue !== null) {
139
- const secondPercent = getPercentage(state.secondValue);
140
- const adjustedSecondPercent = mapValueToVisualPercent(secondPercent, trackSize);
141
- secondHandle.style.left = `${adjustedSecondPercent}%`;
101
+ secondHandle.style.left = visualPosition(getPercentage(state.secondValue));
142
102
  secondHandle.style.transform = "translate(-50%, -50%)";
143
103
  if (secondValueBubble) {
144
- secondValueBubble.style.left = `${adjustedSecondPercent}%`;
104
+ secondValueBubble.style.left = visualPosition(getPercentage(state.secondValue));
145
105
  secondValueBubble.style.transform = "translateX(-50%)";
146
106
  }
147
107
  }
@@ -174,8 +134,8 @@ export const withController = (config) => (component) => {
174
134
  try {
175
135
  updateHandlePositions();
176
136
  updateValueBubbles();
177
- if (component.drawCanvas) {
178
- component.drawCanvas(state);
137
+ if (component.renderTracks) {
138
+ component.renderTracks(state);
179
139
  }
180
140
  }
181
141
  catch (error) {
@@ -307,6 +267,7 @@ export const withController = (config) => (component) => {
307
267
  },
308
268
  setStep(step) {
309
269
  state.step = step;
270
+ render();
310
271
  component.element.classList[step > 0 ? "add" : "remove"](`${component.getClass("slider")}--discrete`);
311
272
  if (config.ticks || config.tickLabels) {
312
273
  generateTicks();
@@ -1,5 +1,5 @@
1
1
  export { withRange } from "./range.js";
2
2
  export { withStates } from "./states.js";
3
3
  export { withController } from "./controller.js";
4
- export { withCanvas } from "./canvas.js";
4
+ export { withTracks } from "./tracks.js";
5
5
  export { withDom } from "./dom.js";
@@ -1,5 +1,5 @@
1
1
  export { withRange } from "./range.js";
2
2
  export { withStates } from "./states.js";
3
3
  export { withController } from "./controller.js";
4
- export { withCanvas } from "./canvas.js";
4
+ export { withTracks } from "./tracks.js";
5
5
  export { withDom } from "./dom.js";
@@ -1,6 +1,8 @@
1
1
  import { SLIDER_COLORS } from "../types.js";
2
2
  import { SLIDER_SIZES } from "../constants.js";
3
3
  export const withStates = (config) => (component) => {
4
+ const handle = component.handle ?? component.structure?.handle;
5
+ const secondHandle = component.secondHandle ?? component.structure?.secondHandle;
4
6
  const isDisabled = config.disabled === true;
5
7
  let initialization = null;
6
8
  if (isDisabled) {
@@ -11,31 +13,31 @@ export const withStates = (config) => (component) => {
11
13
  function disableComponent() {
12
14
  component.element.classList.add(`${component.getClass("slider")}--disabled`);
13
15
  component.element.setAttribute("aria-disabled", "true");
14
- if (component.structure?.handle) {
15
- component.structure.handle.tabIndex = -1;
16
- component.structure.handle.setAttribute("aria-disabled", "true");
16
+ if (handle) {
17
+ handle.tabIndex = -1;
18
+ handle.setAttribute("aria-disabled", "true");
17
19
  }
18
- if (config.range && component.structure?.secondHandle) {
19
- component.structure.secondHandle.tabIndex = -1;
20
- component.structure.secondHandle.setAttribute("aria-disabled", "true");
20
+ if (config.range && secondHandle) {
21
+ secondHandle.tabIndex = -1;
22
+ secondHandle.setAttribute("aria-disabled", "true");
21
23
  }
22
- if (component.drawCanvas) {
23
- component.drawCanvas();
24
+ if (component.renderTracks) {
25
+ component.renderTracks();
24
26
  }
25
27
  }
26
28
  function enableComponent() {
27
29
  component.element.classList.remove(`${component.getClass("slider")}--disabled`);
28
30
  component.element.setAttribute("aria-disabled", "false");
29
- if (component.structure?.handle) {
30
- component.structure.handle.tabIndex = 0;
31
- component.structure.handle.setAttribute("aria-disabled", "false");
31
+ if (handle) {
32
+ handle.tabIndex = 0;
33
+ handle.setAttribute("aria-disabled", "false");
32
34
  }
33
- if (config.range && component.structure?.secondHandle) {
34
- component.structure.secondHandle.tabIndex = 0;
35
- component.structure.secondHandle.setAttribute("aria-disabled", "false");
35
+ if (config.range && secondHandle) {
36
+ secondHandle.tabIndex = 0;
37
+ secondHandle.setAttribute("aria-disabled", "false");
36
38
  }
37
- if (component.drawCanvas) {
38
- component.drawCanvas();
39
+ if (component.renderTracks) {
40
+ component.renderTracks();
39
41
  }
40
42
  }
41
43
  function getActiveColor() {
@@ -105,9 +107,7 @@ export const withStates = (config) => (component) => {
105
107
  },
106
108
  showTicks(show) {
107
109
  config.ticks = show;
108
- if (component.slider) {
109
- component.slider.regenerateTicks();
110
- }
110
+ component.renderTracks?.();
111
111
  return this;
112
112
  },
113
113
  showCurrentValue(show) {
@@ -0,0 +1,13 @@
1
+ import { SliderConfig } from "../types.js";
2
+ import { SliderSize } from "../constants.js";
3
+ export declare const getTrackHeight: (size?: SliderSize) => number;
4
+ /**
5
+ * Gets the handle height based on the slider size
6
+ */
7
+ export declare const getHandleHeight: (size?: SliderSize) => number;
8
+ /**
9
+ * Gets the external track radius based on the slider size
10
+ */
11
+ export declare const getExternalTrackRadius: (size?: SliderSize) => number;
12
+ /** Decorative tracks and ticks. Handles and controller retain interaction ownership. */
13
+ export declare const withTracks: (config: SliderConfig) => (component: any) => any;
@@ -0,0 +1,174 @@
1
+ import { SLIDER_SIZES, SLIDER_MEASUREMENTS } from "../constants.js";
2
+ export const getTrackHeight = (size) => {
3
+ if (typeof size === "number") {
4
+ return Math.max(size, SLIDER_SIZES.XS);
5
+ }
6
+ if (typeof size === "string" && size in SLIDER_SIZES) {
7
+ return SLIDER_SIZES[size];
8
+ }
9
+ return SLIDER_SIZES.XS;
10
+ };
11
+ export const getHandleHeight = (size) => {
12
+ const trackHeight = getTrackHeight(size);
13
+ if (trackHeight <= SLIDER_SIZES.S) {
14
+ return SLIDER_MEASUREMENTS.SMALL_HANDLE_HEIGHT;
15
+ }
16
+ return trackHeight + SLIDER_MEASUREMENTS.HANDLE_HEIGHT_OFFSET;
17
+ };
18
+ export const getExternalTrackRadius = (size) => {
19
+ const trackHeight = getTrackHeight(size);
20
+ if (trackHeight <= SLIDER_SIZES.S) {
21
+ return SLIDER_MEASUREMENTS.SMALL_TRACK_EXTERNAL_RADIUS;
22
+ }
23
+ return trackHeight * SLIDER_MEASUREMENTS.LARGE_TRACK_RADIUS_RATIO;
24
+ };
25
+ export const withTracks = (config) => (component) => {
26
+ const container = component.container;
27
+ const element = (name, parent) => {
28
+ const node = document.createElement("div");
29
+ node.className = component.getClass(`slider-${name}`);
30
+ parent.append(node);
31
+ return node;
32
+ };
33
+ const visual = element("visual", container);
34
+ visual.setAttribute("aria-hidden", "true");
35
+ const track = element("track", visual);
36
+ const segments = Array.from({ length: 3 }, () => element("segment", track));
37
+ const ticks = [element("ticks", visual), element("ticks", visual)];
38
+ ticks[1].classList.add(component.getClass("slider-ticks--active"));
39
+ const dot = element("dot", visual);
40
+ let destroyed = false;
41
+ let size = config.size ?? "XS";
42
+ let state = {
43
+ value: config.value ?? 0, secondValue: config.secondValue ?? null,
44
+ min: config.min ?? 0, max: config.max ?? 100, step: config.step ?? 1,
45
+ };
46
+ const render = (next) => {
47
+ if (destroyed)
48
+ return;
49
+ if (next)
50
+ state = { ...next };
51
+ const width = container.getBoundingClientRect().width || 200;
52
+ const height = getTrackHeight(size);
53
+ const edge = SLIDER_MEASUREMENTS.EDGE_PADDING;
54
+ const position = (value) => edge + Math.min(1, Math.max(0, state.max === state.min ? 0 : (value - state.min) / (state.max - state.min))) * Math.max(0, width - 2 * edge);
55
+ const first = position(state.value);
56
+ const second = position(state.secondValue ?? state.max);
57
+ const zero = position(0);
58
+ const gap = SLIDER_MEASUREMENTS.HANDLE_GAP;
59
+ const reduction = SLIDER_MEASUREMENTS.HANDLE_GAP_PRESSED_REDUCTION;
60
+ const firstGap = gap - (state.pressed && (!config.range || state.activeHandle === "first") ? reduction : 0);
61
+ const secondGap = gap - (state.pressed && state.activeHandle === "second" ? reduction : 0);
62
+ const parts = [];
63
+ let activeStart = 0, activeEnd = first;
64
+ if (config.centered) {
65
+ activeStart = Math.min(zero, first);
66
+ activeEnd = Math.max(zero, first);
67
+ const centerGap = SLIDER_MEASUREMENTS.CENTER_GAP / 2;
68
+ if (Math.abs(first - zero) < firstGap) {
69
+ parts.push([0, first - firstGap, false], [zero, zero, true], [first + firstGap, width, false]);
70
+ }
71
+ else if (state.value >= 0) {
72
+ parts.push([0, zero - centerGap, false], [zero + centerGap, first - firstGap, true], [first + firstGap, width, false]);
73
+ }
74
+ else {
75
+ parts.push([0, first - firstGap, false], [first + firstGap, zero - centerGap, true], [zero + centerGap, width, false]);
76
+ }
77
+ }
78
+ else if (config.range && state.secondValue !== null) {
79
+ activeStart = Math.min(first, second);
80
+ activeEnd = Math.max(first, second);
81
+ const lowGap = first <= second ? firstGap : secondGap;
82
+ const highGap = first <= second ? secondGap : firstGap;
83
+ const start = activeStart + lowGap, end = activeEnd - highGap;
84
+ parts.push([0, activeStart - lowGap, false], [start, end - start > height ? end : start, true], [activeEnd + highGap, width, false]);
85
+ }
86
+ else {
87
+ parts.push([0, 0, false], [0, first - firstGap, true], [first + firstGap, width, false]);
88
+ }
89
+ segments.forEach((segment, index) => {
90
+ const [start, end, active] = parts[index] ?? [0, 0, false];
91
+ segment.style.left = `${start}px`;
92
+ segment.style.width = `${Math.max(0, end - start)}px`;
93
+ segment.classList.toggle(component.getClass("slider-segment--active"), active);
94
+ });
95
+ const discrete = !!config.ticks && state.step > 0 && state.max > state.min;
96
+ ticks.forEach(tick => { tick.hidden = !discrete; });
97
+ dot.hidden = !!config.ticks;
98
+ if (discrete) {
99
+ const spacing = (width - 2 * edge) * state.step / (state.max - state.min);
100
+ const last = position(state.min + Math.floor((state.max - state.min) / state.step) * state.step);
101
+ const holes = [state.value, ...(config.range && state.secondValue !== null ? [state.secondValue] : [])]
102
+ .filter(value => Math.abs((value - state.min) / state.step - Math.round((value - state.min) / state.step)) < 0.1)
103
+ .map(value => position(state.min + Math.round((value - state.min) / state.step) * state.step))
104
+ .sort((a, b) => a - b);
105
+ const mask = (intervals) => {
106
+ const merged = [];
107
+ for (const [start, end] of intervals.sort((a, b) => a[0] - b[0])) {
108
+ const previous = merged[merged.length - 1];
109
+ if (previous && start <= previous[1])
110
+ previous[1] = Math.max(previous[1], end);
111
+ else
112
+ merged.push([start, end]);
113
+ }
114
+ const stops = merged.flatMap(([start, end]) => [`black ${start}px`, `transparent ${start}px`, `transparent ${end}px`, `black ${end}px`]);
115
+ return stops.length ? `linear-gradient(to right, black 0px, ${stops.join(",")}, black 100%)` : "none";
116
+ };
117
+ ticks.forEach((tick, index) => {
118
+ tick.style.backgroundSize = `${Math.max(spacing, 0.01)}px 100%`;
119
+ tick.style.backgroundPosition = `${edge - spacing / 2}px center`;
120
+ tick.style.maskImage = mask([
121
+ ...holes.map(x => [x - 2, x + 2]),
122
+ ...(index === 0 ? [[activeStart - 2, activeEnd + 2]] : []),
123
+ ]);
124
+ });
125
+ ticks[0].style.clipPath = `inset(0 ${Math.max(0, width - last - 2)}px 0 0)`;
126
+ ticks[1].style.clipPath = `inset(0 ${Math.max(0, width - Math.min(last + 2, activeEnd + 2))}px 0 ${Math.max(0, activeStart - 2)}px)`;
127
+ }
128
+ };
129
+ const setSize = (next) => {
130
+ if (destroyed)
131
+ return;
132
+ size = next;
133
+ const handleHeight = getHandleHeight(size);
134
+ container.style.height = `${Math.max(handleHeight, SLIDER_MEASUREMENTS.MIN_HEIGHT)}px`;
135
+ track.style.height = `${getTrackHeight(size)}px`;
136
+ track.style.borderRadius = `${getExternalTrackRadius(size)}px`;
137
+ for (const handle of [component.handle, component.secondHandle]) {
138
+ if (handle)
139
+ handle.style.height = `${handleHeight}px`;
140
+ }
141
+ render();
142
+ };
143
+ component.renderTracks = render;
144
+ component.setSize = setSize;
145
+ component.getSize = () => size;
146
+ const setColor = component.appearance.setColor;
147
+ component.appearance.setColor = (color) => {
148
+ if (destroyed)
149
+ return;
150
+ setColor.call(component.appearance, color);
151
+ config.color = color;
152
+ };
153
+ setSize(size);
154
+ const resize = () => {
155
+ if (!destroyed)
156
+ render();
157
+ };
158
+ const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(resize) : null;
159
+ if (observer)
160
+ observer.observe(container);
161
+ else
162
+ window.addEventListener("resize", resize);
163
+ const destroy = component.lifecycle.destroy;
164
+ component.lifecycle.destroy = () => {
165
+ if (destroyed)
166
+ return;
167
+ destroyed = true;
168
+ observer?.disconnect();
169
+ if (!observer)
170
+ window.removeEventListener("resize", resize);
171
+ destroy.call(component.lifecycle);
172
+ };
173
+ return component;
174
+ };
@@ -6,7 +6,7 @@ import { SliderConfig, SliderComponent } from "./types.js";
6
6
  * 1. Base component - Creates the foundation with event system
7
7
  * 2. Feature enhancement - Adds specific capabilities (range, icons)
8
8
  * 3. DOM creation - Creates DOM elements directly using optimized createElement
9
- * 4. Canvas rendering - Uses canvas for visual elements, keeping handles as DOM
9
+ * 4. Track rendering - Uses decorative DOM and CSS alongside accessible handles
10
10
  * 5. State management - Handles visual states and appearance
11
11
  * 6. Controller - Manages behavior, events, and UI rendering
12
12
  * 7. Lifecycle - Handles component lifecycle events
@@ -1,13 +1,13 @@
1
1
  import { pipe } from "../../core/compose/pipe.js";
2
2
  import { createBase, withElement } from "../../core/compose/component.js";
3
3
  import { withEvents, withLifecycle, withIcon, withTextLabel, } from "../../core/compose/features/index.js";
4
- import { withRange, withStates, withController, withCanvas, withDom, } from "./features/index.js";
4
+ import { withRange, withStates, withController, withTracks, withDom, } from "./features/index.js";
5
5
  import { withAPI } from "./api.js";
6
6
  import { createBaseConfig, getElementConfig, getApiConfig } from "./config.js";
7
7
  const createSlider = (config = {}) => {
8
8
  const baseConfig = createBaseConfig(config);
9
9
  try {
10
- const component = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)), withRange(baseConfig), withDom(baseConfig), withTextLabel(baseConfig), withIcon(baseConfig), withLifecycle(), withStates(baseConfig), withCanvas(baseConfig), withController(baseConfig))(baseConfig);
10
+ const component = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)), withRange(baseConfig), withDom(baseConfig), withTextLabel(baseConfig), withIcon(baseConfig), withLifecycle(), withStates(baseConfig), withTracks(baseConfig), withController(baseConfig))(baseConfig);
11
11
  const apiOptions = getApiConfig(component);
12
12
  const slider = withAPI(apiOptions)(component);
13
13
  if (baseConfig.on && typeof slider.on === "function") {
@@ -8,6 +8,7 @@ type ComponentWithDensity = BaseComponent & {
8
8
  set: (density: string) => void;
9
9
  };
10
10
  updateElementPositions?: () => void;
11
+ schedulePositionUpdate?: () => void;
11
12
  setError?: (error: boolean, message?: string) => any;
12
13
  isError?: () => boolean;
13
14
  };
@@ -23,7 +23,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
23
23
  component.element.classList.remove(`${PREFIX}-${COMPONENT}--filled`, `${PREFIX}-${COMPONENT}--outlined`);
24
24
  component.element.classList.add(`${PREFIX}-${COMPONENT}--${variant}`);
25
25
  if (component.updateElementPositions) {
26
- setTimeout(() => component.updateElementPositions(), 10);
26
+ component.schedulePositionUpdate?.();
27
27
  }
28
28
  return this;
29
29
  },
@@ -38,7 +38,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
38
38
  setLabel(text) {
39
39
  component.label?.setText(text);
40
40
  if (component.updateElementPositions) {
41
- setTimeout(() => component.updateElementPositions(), 10);
41
+ component.schedulePositionUpdate?.();
42
42
  }
43
43
  return this;
44
44
  },
@@ -50,7 +50,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
50
50
  if (component.setLeadingIcon) {
51
51
  component.setLeadingIcon(html);
52
52
  if (component.updateElementPositions) {
53
- setTimeout(() => component.updateElementPositions(), 10);
53
+ component.schedulePositionUpdate?.();
54
54
  }
55
55
  }
56
56
  return this;
@@ -59,7 +59,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
59
59
  if (component.removeLeadingIcon) {
60
60
  component.removeLeadingIcon();
61
61
  if (component.updateElementPositions) {
62
- setTimeout(() => component.updateElementPositions(), 10);
62
+ component.schedulePositionUpdate?.();
63
63
  }
64
64
  }
65
65
  return this;
@@ -69,7 +69,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
69
69
  if (component.setTrailingIcon) {
70
70
  component.setTrailingIcon(html);
71
71
  if (component.updateElementPositions) {
72
- setTimeout(() => component.updateElementPositions(), 10);
72
+ component.schedulePositionUpdate?.();
73
73
  }
74
74
  }
75
75
  return this;
@@ -78,7 +78,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
78
78
  if (component.removeTrailingIcon) {
79
79
  component.removeTrailingIcon();
80
80
  if (component.updateElementPositions) {
81
- setTimeout(() => component.updateElementPositions(), 10);
81
+ component.schedulePositionUpdate?.();
82
82
  }
83
83
  }
84
84
  return this;
@@ -101,7 +101,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
101
101
  if (component.setPrefixText) {
102
102
  component.setPrefixText(text);
103
103
  if (component.updateElementPositions) {
104
- setTimeout(() => component.updateElementPositions(), 10);
104
+ component.schedulePositionUpdate?.();
105
105
  }
106
106
  }
107
107
  return this;
@@ -110,7 +110,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
110
110
  if (component.removePrefixText) {
111
111
  component.removePrefixText();
112
112
  if (component.updateElementPositions) {
113
- setTimeout(() => component.updateElementPositions(), 10);
113
+ component.schedulePositionUpdate?.();
114
114
  }
115
115
  }
116
116
  return this;
@@ -120,7 +120,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
120
120
  if (component.setSuffixText) {
121
121
  component.setSuffixText(text);
122
122
  if (component.updateElementPositions) {
123
- setTimeout(() => component.updateElementPositions(), 10);
123
+ component.schedulePositionUpdate?.();
124
124
  }
125
125
  }
126
126
  return this;
@@ -129,7 +129,7 @@ export const withAPI = ({ disabled, lifecycle }) => (component) => ({
129
129
  if (component.removeSuffixText) {
130
130
  component.removeSuffixText();
131
131
  if (component.updateElementPositions) {
132
- setTimeout(() => component.updateElementPositions(), 10);
132
+ component.schedulePositionUpdate?.();
133
133
  }
134
134
  }
135
135
  return this;
@@ -27,7 +27,7 @@ export const withDensity = (config) => (component) => {
27
27
  component.density.current = newDensity;
28
28
  }
29
29
  if (component.updateElementPositions) {
30
- setTimeout(() => component.updateElementPositions(), 10);
30
+ component.updateElementPositions();
31
31
  }
32
32
  };
33
33
  component.density = {
@@ -14,11 +14,12 @@ export const withLeadingIcon = (config) => (component) => {
14
14
  if ('lifecycle' in component && component.lifecycle?.destroy) {
15
15
  const originalDestroy = component.lifecycle.destroy;
16
16
  component.lifecycle.destroy = () => {
17
+ clearTimeout(initialPosition);
17
18
  iconElement.remove();
18
19
  originalDestroy.call(component.lifecycle);
19
20
  };
20
21
  }
21
- setTimeout(() => {
22
+ const initialPosition = setTimeout(() => {
22
23
  const labelEl = component.element.querySelector(`.${PREFIX}-${config.componentName || 'textfield'}-label`);
23
24
  if (labelEl) {
24
25
  if (!component.element.classList.contains(`${PREFIX}-${config.componentName || 'textfield'}--with-prefix`)) {
@@ -17,6 +17,8 @@ export interface PlacementComponent extends BaseComponent {
17
17
  * @returns The component instance for chaining
18
18
  */
19
19
  updateElementPositions: () => PlacementComponent;
20
+ /** Queue a placement update owned by this component lifecycle. */
21
+ schedulePositionUpdate: () => void;
20
22
  }
21
23
  /**
22
24
  * Handles dynamic positioning of textfield elements (label, prefix, suffix)
@@ -2,12 +2,24 @@ import { getInheritedBackground } from "../../../core/utils/background.js";
2
2
  export const withPlacement = () => (component) => {
3
3
  const PREFIX = component.config.prefix || "mtrl";
4
4
  const COMPONENT = component.config.componentName || "textfield";
5
+ let destroyed = false;
6
+ let updateTimer = null;
7
+ const schedulePositionUpdate = () => {
8
+ if (destroyed)
9
+ return;
10
+ if (updateTimer !== null)
11
+ clearTimeout(updateTimer);
12
+ updateTimer = setTimeout(() => {
13
+ updateTimer = null;
14
+ updateElementPositions();
15
+ }, 10);
16
+ };
5
17
  const bgSourceObservers = new WeakMap();
6
18
  const parentObservers = new WeakMap();
7
19
  const themeChangeHandlers = new WeakMap();
8
20
  const classObservers = new WeakMap();
9
21
  const updateElementPositions = () => {
10
- if (!component.element || !component.element.isConnected)
22
+ if (destroyed || !component.element || !component.element.isConnected)
11
23
  return component;
12
24
  const labelEl = component.element.querySelector(`.${PREFIX}-${COMPONENT}-label`);
13
25
  const prefixEl = component.element.querySelector(`.${PREFIX}-${COMPONENT}-prefix`);
@@ -110,7 +122,7 @@ export const withPlacement = () => (component) => {
110
122
  mutations.forEach((mutation) => {
111
123
  if (mutation.type === "attributes" &&
112
124
  mutation.attributeName === "class") {
113
- setTimeout(updateElementPositions, 10);
125
+ schedulePositionUpdate();
114
126
  }
115
127
  });
116
128
  });
@@ -121,13 +133,22 @@ export const withPlacement = () => (component) => {
121
133
  window.addEventListener("resize", updateElementPositions);
122
134
  classObservers.set(component.element, classObserver);
123
135
  };
124
- setTimeout(() => {
136
+ const initialization = setTimeout(() => {
137
+ if (destroyed)
138
+ return;
125
139
  setupEventListeners();
126
140
  updateElementPositions();
127
141
  }, 0);
128
142
  if ("lifecycle" in component && component.lifecycle?.destroy) {
129
143
  const originalDestroy = component.lifecycle.destroy;
130
144
  component.lifecycle.destroy = () => {
145
+ if (destroyed)
146
+ return;
147
+ destroyed = true;
148
+ clearTimeout(initialization);
149
+ if (updateTimer !== null)
150
+ clearTimeout(updateTimer);
151
+ updateTimer = null;
131
152
  window.removeEventListener("resize", updateElementPositions);
132
153
  const classObserver = classObservers.get(component.element);
133
154
  if (classObserver) {
@@ -154,6 +175,7 @@ export const withPlacement = () => (component) => {
154
175
  }
155
176
  return {
156
177
  ...component,
178
+ schedulePositionUpdate,
157
179
  updateElementPositions: () => {
158
180
  updateElementPositions();
159
181
  return component;
@@ -7,7 +7,7 @@ import { createBaseConfig, getElementConfig, getApiConfig } from "./config.js";
7
7
  const createTextfield = (config = {}) => {
8
8
  const baseConfig = createBaseConfig(config);
9
9
  try {
10
- const textfield = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)), withVariant(baseConfig), withDensity(baseConfig), withTextInput(baseConfig), withTextLabel(baseConfig), withLeadingIcon(baseConfig), withTrailingIcon(baseConfig), withPrefixText(baseConfig), withSuffixText(baseConfig), withSupportingText(baseConfig), withError(baseConfig), withDisabled(baseConfig), withLifecycle(), withPlacement(), (comp) => withAPI(getApiConfig(comp))(comp))(baseConfig);
10
+ const textfield = pipe(createBase, withEvents(), withElement(getElementConfig(baseConfig)), withLifecycle(), withVariant(baseConfig), withDensity(baseConfig), withTextInput(baseConfig), withTextLabel(baseConfig), withLeadingIcon(baseConfig), withTrailingIcon(baseConfig), withPrefixText(baseConfig), withSuffixText(baseConfig), withSupportingText(baseConfig), withError(baseConfig), withDisabled(baseConfig), withPlacement(), (comp) => withAPI(getApiConfig(comp))(comp))(baseConfig);
11
11
  return textfield;
12
12
  }
13
13
  catch (error) {