@recursica/mui-adapter 0.31.0 → 0.32.0
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/CHANGELOG.md +12 -0
- package/dist/index.d.ts +11 -3
- package/dist/mui-adapter.cjs +50 -50
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +3717 -3659
- package/dist/mui-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Slider/IMPLEMENTATION_NOTES.md +18 -2
- package/src/components/Slider/Slider.module.css +12 -3
- package/src/components/Slider/Slider.stories.tsx +65 -0
- package/src/components/Slider/Slider.tsx +130 -40
- package/src/components/Slider/USAGE.md +15 -1
- package/src/components/Tree/IMPLEMENTATION_NOTES.md +3 -0
- package/src/components/Tree/Tree.tsx +7 -1
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "git+https://github.com/borderux/recursica.git",
|
|
14
14
|
"directory": "packages/mui-adapter"
|
|
15
15
|
},
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.32.0",
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"vitest": "^3.2.4"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@recursica/adapter-common": "^0.
|
|
105
|
+
"@recursica/adapter-common": "^0.23.0",
|
|
106
106
|
"@recursica/official-release": "^2.8.0",
|
|
107
107
|
"dayjs": "^1.11.21"
|
|
108
108
|
},
|
|
@@ -34,6 +34,22 @@ This document contains specific design decisions, architectural constraints, and
|
|
|
34
34
|
|
|
35
35
|
**Fix:** `.currentValue` now runs `resolvedValue` through `tooltipLabel` when it's a function, reusing the same formatter passed to `valueLabelFormat`. Added `minLabel`/`maxLabel` (new `adapter-common` props) to override the `.minMaxGuide` text at either end of the track, and `trailingIcon` (new `adapter-common` prop) to render a second icon opposite the existing `icon`, reusing the same `.iconWrapper` styling.
|
|
36
36
|
|
|
37
|
-
## 6.
|
|
37
|
+
## 6. Dual-Thumb / Range Support
|
|
38
38
|
|
|
39
|
-
**Decision:**
|
|
39
|
+
**Decision:** Previously requested and declined (see git history for this section) as no use case needed it; reopened with a full spec and implemented. `value`/`defaultValue`/`onChange`/`onChangeEnd` are now typed `number | [number, number]` in `@recursica/adapter-common`'s `RecursicaSliderProps`.
|
|
40
|
+
**Implementation:**
|
|
41
|
+
|
|
42
|
+
- MUI's `Slider` already accepts `number[]` for `value`/`onChange` and renders two thumbs natively, so unlike `mantine-adapter` (which must swap to a separate `RangeSlider` component) this stays the same `<MuiSlider>` — arrays are no longer collapsed to `value[0]`.
|
|
43
|
+
- Internal state (`internalValue`, `resolvedValue`, `inputValue`) generalized from `number` to `number | [number, number]`; range-mode input handlers (`handleLowerInputChange`/`handleUpperInputChange`) clamp each thumb against the other's current value (not the shared `min`/`max`) so the two inputs can't cross.
|
|
44
|
+
- `SliderReadOnlyValue` and the floating `.currentValue` display both render `"lower – upper"` for a range value, running each side through `tooltipLabel` independently (`valueLabelFormat` already handled this per-thumb natively, unchanged).
|
|
45
|
+
- DOM order in range mode (input → leading icon → min label → track → max label → trailing icon → input) mirrors Forge's own Material/Carbon range layouts — see §7 below.
|
|
46
|
+
|
|
47
|
+
## 7. Trailing Icon Order Relative to the Input Field
|
|
48
|
+
|
|
49
|
+
**Decision:** `trailingIcon` previously rendered after the numeric input (`... max label, input, trailing icon`), reversed from Forge's Material/Carbon kits, which always render the trailing icon directly after the max label and before the input.
|
|
50
|
+
**Implementation:** Moved `{trailingIconEl}` before the `showInput` input block in the single-value layout; the range layout was built with this order from the start (input → icon → min label → track → max label → trailing icon → input).
|
|
51
|
+
|
|
52
|
+
## 8. Mark Vertically Off-Center
|
|
53
|
+
|
|
54
|
+
**Decision:** MUI's own mark is `top: 50%; transform: translate(-1px, -50%)` — the `-1px` assumes MUI's built-in 2px dot, the `-50%` is real vertical centering. `.sliderMark` overrode `transform` to `translateX(-50%)` (horizontal-only, meant to mirror the mantine-adapter) without noticing it dropped MUI's vertical `-50%`, leaving the dot hanging below the track's midpoint instead of centered.
|
|
55
|
+
**Implementation:** Changed `.sliderMark`'s transform to `translate(-50%, -50%)` — keeps MUI's vertical centering and swaps the horizontal term to properly center our (non-2px) `step-indicator-width` instead of MUI's hardcoded 1px.
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* HARDCODED VALUES:
|
|
3
3
|
* - display: flex; align-items: center; width: 100%; (Standard CSS flexbox layouts for bidirectional components)
|
|
4
4
|
* - flex-grow: 1; flex-shrink: 0; (Layout control structures)
|
|
5
|
-
* - transform:
|
|
6
|
-
*
|
|
5
|
+
* - transform: translate(-50%, -50%); on .sliderMark (preserves MUI's own vertical mark
|
|
6
|
+
* centering while re-deriving the horizontal offset for our step-indicator-width token — see
|
|
7
|
+
* .sliderMark below)
|
|
8
|
+
* - transform: translateX(-50%); on .sliderMarkLabel (horizontal-only; matches Mantine's offset
|
|
9
|
+
* mechanism)
|
|
7
10
|
* - outline: none; border-style: solid; (Standard focus reset and border outlines)
|
|
8
11
|
* - Focus ring on thumb/input: the token schema no longer provides per-component focus
|
|
9
12
|
* colors for these (only `active` covers track/step-indicator-color). We apply the generic
|
|
@@ -164,7 +167,13 @@
|
|
|
164
167
|
background-color: var(
|
|
165
168
|
--recursica_ui-kit_components_slider_properties_colors_step-indicator-color
|
|
166
169
|
);
|
|
167
|
-
transform:
|
|
170
|
+
/* MUI's own mark is `top: 50%; transform: translate(-1px, -50%)` — the -1px assumes its
|
|
171
|
+
built-in 2px-wide dot and the -50% is real vertical centering, not decoration. Overriding
|
|
172
|
+
width/height above without preserving that vertical -50% would leave the dot vertically
|
|
173
|
+
un-centered (it would hang below the track's midpoint); translateX(-50%) alone stomped it.
|
|
174
|
+
translate(-50%, -50%) keeps the vertical centering and swaps the horizontal offset to match
|
|
175
|
+
our (non-2px) step-indicator-width. */
|
|
176
|
+
transform: translate(-50%, -50%);
|
|
168
177
|
border: none;
|
|
169
178
|
}
|
|
170
179
|
|
|
@@ -190,6 +190,71 @@ export const WithIconsAndLabels: Story = {
|
|
|
190
190
|
},
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
+
export const RangeMode: Story = {
|
|
194
|
+
args: {
|
|
195
|
+
label: "Price Range",
|
|
196
|
+
assistiveText: "Pass a [number, number] tuple to render two thumbs.",
|
|
197
|
+
defaultValue: [20, 80],
|
|
198
|
+
min: 0,
|
|
199
|
+
max: 100,
|
|
200
|
+
showMinMaxLabels: true,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const RangeModeWithInputs: Story = {
|
|
205
|
+
args: {
|
|
206
|
+
...RangeMode.args,
|
|
207
|
+
showInput: true,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const RangeModeWithIconsAndInputs: Story = {
|
|
212
|
+
args: {
|
|
213
|
+
label: "Price Range",
|
|
214
|
+
assistiveText:
|
|
215
|
+
"Full range usage: leading/trailing icons, min/max label overrides, and both bound inputs.",
|
|
216
|
+
defaultValue: [20, 80],
|
|
217
|
+
min: 0,
|
|
218
|
+
max: 100,
|
|
219
|
+
showInput: true,
|
|
220
|
+
minLabel: "$0",
|
|
221
|
+
maxLabel: "$100",
|
|
222
|
+
tooltipLabel: (value: number) => `$${value}`,
|
|
223
|
+
icon: (
|
|
224
|
+
<svg
|
|
225
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
226
|
+
width="16"
|
|
227
|
+
height="16"
|
|
228
|
+
viewBox="0 0 24 24"
|
|
229
|
+
fill="none"
|
|
230
|
+
stroke="currentColor"
|
|
231
|
+
strokeWidth="2"
|
|
232
|
+
strokeLinecap="round"
|
|
233
|
+
strokeLinejoin="round"
|
|
234
|
+
>
|
|
235
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
|
236
|
+
</svg>
|
|
237
|
+
),
|
|
238
|
+
trailingIcon: (
|
|
239
|
+
<svg
|
|
240
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
241
|
+
width="16"
|
|
242
|
+
height="16"
|
|
243
|
+
viewBox="0 0 24 24"
|
|
244
|
+
fill="none"
|
|
245
|
+
stroke="currentColor"
|
|
246
|
+
strokeWidth="2"
|
|
247
|
+
strokeLinecap="round"
|
|
248
|
+
strokeLinejoin="round"
|
|
249
|
+
>
|
|
250
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
|
251
|
+
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
|
252
|
+
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
|
|
253
|
+
</svg>
|
|
254
|
+
),
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
193
258
|
export const FormLayouts: Story = {
|
|
194
259
|
render: () => (
|
|
195
260
|
<div
|
|
@@ -20,7 +20,13 @@ import { type RecursicaSliderProps as BaseRecursicaSliderProps } from "@recursic
|
|
|
20
20
|
export interface RecursicaSliderProps
|
|
21
21
|
extends Omit<
|
|
22
22
|
MuiSliderProps,
|
|
23
|
-
|
|
23
|
+
| "size"
|
|
24
|
+
| "color"
|
|
25
|
+
| "classes"
|
|
26
|
+
| "onChange"
|
|
27
|
+
| "onChangeCommitted"
|
|
28
|
+
| "value"
|
|
29
|
+
| "defaultValue"
|
|
24
30
|
>,
|
|
25
31
|
Omit<
|
|
26
32
|
RecursicaFormControlWrapperProps,
|
|
@@ -38,15 +44,23 @@ export type SliderProps = RecursicaOverStyled<RecursicaSliderProps>;
|
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
46
|
* Custom Read-Only visual representation of the Slider value.
|
|
41
|
-
* Utilizes component-specific read-only typography variables.
|
|
47
|
+
* Utilizes component-specific read-only typography variables. Renders a "lower – upper" pair
|
|
48
|
+
* when the value is a range tuple.
|
|
42
49
|
*/
|
|
43
|
-
const SliderReadOnlyValue: React.FC<{ value: number }> = ({
|
|
44
|
-
|
|
50
|
+
const SliderReadOnlyValue: React.FC<{ value: number | [number, number] }> = ({
|
|
51
|
+
value,
|
|
52
|
+
}) => {
|
|
53
|
+
const display = Array.isArray(value) ? `${value[0]} – ${value[1]}` : value;
|
|
54
|
+
return <div className={styles.readOnlyValue}>{display}</div>;
|
|
45
55
|
};
|
|
46
56
|
|
|
47
57
|
/**
|
|
48
58
|
* Recursica Slider component wrapping Mui's Slider.
|
|
49
59
|
*
|
|
60
|
+
* MUI's own `Slider` already renders two thumbs natively when given a tuple `value`, so range
|
|
61
|
+
* mode here is a typing/handler concern rather than a different underlying component (contrast
|
|
62
|
+
* with the mantine-adapter, which swaps in Mantine's separate `RangeSlider`).
|
|
63
|
+
*
|
|
50
64
|
* Implements a bidirectional text input field next to the slider track, responsive layouts,
|
|
51
65
|
* custom typography-bound min/max labels (optionally overridden via `minLabel`/`maxLabel`),
|
|
52
66
|
* optional leading/trailing icons, and an explicit read-only layout.
|
|
@@ -96,35 +110,45 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
96
110
|
...rest
|
|
97
111
|
} = props;
|
|
98
112
|
|
|
99
|
-
// Bidirectional state linking the slider track value to the input field string
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
// Bidirectional state linking the slider track value to the input field string
|
|
114
|
+
// representation. A `[number, number]` value/defaultValue switches the component into
|
|
115
|
+
// two-thumb range mode — MUI's own Slider already renders two thumbs for a tuple value.
|
|
116
|
+
type SliderValue = number | [number, number];
|
|
117
|
+
|
|
118
|
+
const [internalValue, setInternalValue] = useState<SliderValue>(() => {
|
|
119
|
+
if (value !== undefined) return value;
|
|
120
|
+
if (defaultValue !== undefined) return defaultValue;
|
|
104
121
|
return min;
|
|
105
122
|
});
|
|
106
123
|
|
|
107
|
-
const resolvedValue =
|
|
108
|
-
value !== undefined
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
const resolvedValue: SliderValue =
|
|
125
|
+
value !== undefined ? value : internalValue;
|
|
126
|
+
const isRange = Array.isArray(resolvedValue);
|
|
127
|
+
|
|
128
|
+
const [inputValue, setInputValue] = useState<string | [string, string]>(
|
|
129
|
+
() =>
|
|
130
|
+
Array.isArray(resolvedValue)
|
|
131
|
+
? [resolvedValue[0].toString(), resolvedValue[1].toString()]
|
|
132
|
+
: resolvedValue.toString(),
|
|
115
133
|
);
|
|
116
134
|
|
|
117
|
-
// Synchronize text input whenever the slider value changes
|
|
135
|
+
// Synchronize text input(s) whenever the slider value changes
|
|
118
136
|
useEffect(() => {
|
|
119
|
-
setInputValue(
|
|
137
|
+
setInputValue(
|
|
138
|
+
Array.isArray(resolvedValue)
|
|
139
|
+
? [resolvedValue[0].toString(), resolvedValue[1].toString()]
|
|
140
|
+
: resolvedValue.toString(),
|
|
141
|
+
);
|
|
120
142
|
}, [resolvedValue]);
|
|
121
143
|
|
|
122
144
|
const handleValueChange = (_e: Event, val: number | number[]) => {
|
|
123
|
-
const
|
|
145
|
+
const normalized: SliderValue = Array.isArray(val)
|
|
146
|
+
? [val[0], val[1]]
|
|
147
|
+
: val;
|
|
124
148
|
if (value === undefined) {
|
|
125
|
-
setInternalValue(
|
|
149
|
+
setInternalValue(normalized);
|
|
126
150
|
}
|
|
127
|
-
onChange?.(
|
|
151
|
+
onChange?.(normalized);
|
|
128
152
|
};
|
|
129
153
|
|
|
130
154
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -140,7 +164,38 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
140
164
|
};
|
|
141
165
|
|
|
142
166
|
const handleInputBlur = () => {
|
|
143
|
-
setInputValue(resolvedValue.toString());
|
|
167
|
+
setInputValue((resolvedValue as number).toString());
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Range-mode input handlers: each bound clamps against the other thumb rather than the
|
|
171
|
+
// shared min/max, so the lower thumb can never cross the upper one and vice versa.
|
|
172
|
+
const handleLowerInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
173
|
+
const current = resolvedValue as [number, number];
|
|
174
|
+
const valStr = e.target.value;
|
|
175
|
+
setInputValue([valStr, current[1].toString()]);
|
|
176
|
+
|
|
177
|
+
const parsed = parseFloat(valStr);
|
|
178
|
+
if (!isNaN(parsed)) {
|
|
179
|
+
const clamped = Math.max(min, Math.min(current[1], parsed));
|
|
180
|
+
handleValueChange(null as unknown as Event, [clamped, current[1]]);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const handleUpperInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
185
|
+
const current = resolvedValue as [number, number];
|
|
186
|
+
const valStr = e.target.value;
|
|
187
|
+
setInputValue([current[0].toString(), valStr]);
|
|
188
|
+
|
|
189
|
+
const parsed = parseFloat(valStr);
|
|
190
|
+
if (!isNaN(parsed)) {
|
|
191
|
+
const clamped = Math.max(current[0], Math.min(max, parsed));
|
|
192
|
+
handleValueChange(null as unknown as Event, [current[0], clamped]);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const handleRangeInputBlur = () => {
|
|
197
|
+
const current = resolvedValue as [number, number];
|
|
198
|
+
setInputValue([current[0].toString(), current[1].toString()]);
|
|
144
199
|
};
|
|
145
200
|
|
|
146
201
|
// Props this component intentionally doesn't support — deleted at runtime so they can't leak
|
|
@@ -232,8 +287,12 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
232
287
|
|
|
233
288
|
// Duplicates the raw numeric value next to the track by default; when `tooltipLabel` is a
|
|
234
289
|
// formatter, reuse it here too so both displays agree instead of one showing raw numbers.
|
|
235
|
-
|
|
236
|
-
|
|
290
|
+
// Range mode formats each thumb independently and joins them with an en dash.
|
|
291
|
+
const displayValue = Array.isArray(resolvedValue)
|
|
292
|
+
? typeof tooltipLabel === "function"
|
|
293
|
+
? `${tooltipLabel(resolvedValue[0])} – ${tooltipLabel(resolvedValue[1])}`
|
|
294
|
+
: `${resolvedValue[0]} – ${resolvedValue[1]}`
|
|
295
|
+
: typeof tooltipLabel === "function"
|
|
237
296
|
? tooltipLabel(resolvedValue)
|
|
238
297
|
: resolvedValue;
|
|
239
298
|
|
|
@@ -272,6 +331,22 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
272
331
|
data-error={error ? "true" : undefined}
|
|
273
332
|
data-suppress-focus-ring={suppressFocusRing ? "true" : undefined}
|
|
274
333
|
>
|
|
334
|
+
{isRange && showInput && (
|
|
335
|
+
<input
|
|
336
|
+
type="number"
|
|
337
|
+
className={styles.inputField}
|
|
338
|
+
value={(inputValue as [string, string])[0]}
|
|
339
|
+
onChange={handleLowerInputChange}
|
|
340
|
+
onBlur={handleRangeInputBlur}
|
|
341
|
+
min={min}
|
|
342
|
+
max={(resolvedValue as [number, number])[1]}
|
|
343
|
+
step={step ?? undefined}
|
|
344
|
+
disabled={disabled}
|
|
345
|
+
data-error={error ? "true" : undefined}
|
|
346
|
+
aria-label="Minimum value"
|
|
347
|
+
/>
|
|
348
|
+
)}
|
|
349
|
+
|
|
275
350
|
{leadingIcon}
|
|
276
351
|
|
|
277
352
|
{showMinMaxLabels && (
|
|
@@ -319,22 +394,37 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
|
319
394
|
)}
|
|
320
395
|
</div>
|
|
321
396
|
|
|
322
|
-
{showInput && (
|
|
323
|
-
<input
|
|
324
|
-
type="number"
|
|
325
|
-
className={styles.inputField}
|
|
326
|
-
value={inputValue}
|
|
327
|
-
onChange={handleInputChange}
|
|
328
|
-
onBlur={handleInputBlur}
|
|
329
|
-
min={min}
|
|
330
|
-
max={max}
|
|
331
|
-
step={step ?? undefined}
|
|
332
|
-
disabled={disabled}
|
|
333
|
-
data-error={error ? "true" : undefined}
|
|
334
|
-
/>
|
|
335
|
-
)}
|
|
336
|
-
|
|
337
397
|
{trailingIconEl}
|
|
398
|
+
|
|
399
|
+
{showInput &&
|
|
400
|
+
(isRange ? (
|
|
401
|
+
<input
|
|
402
|
+
type="number"
|
|
403
|
+
className={styles.inputField}
|
|
404
|
+
value={(inputValue as [string, string])[1]}
|
|
405
|
+
onChange={handleUpperInputChange}
|
|
406
|
+
onBlur={handleRangeInputBlur}
|
|
407
|
+
min={(resolvedValue as [number, number])[0]}
|
|
408
|
+
max={max}
|
|
409
|
+
step={step ?? undefined}
|
|
410
|
+
disabled={disabled}
|
|
411
|
+
data-error={error ? "true" : undefined}
|
|
412
|
+
aria-label="Maximum value"
|
|
413
|
+
/>
|
|
414
|
+
) : (
|
|
415
|
+
<input
|
|
416
|
+
type="number"
|
|
417
|
+
className={styles.inputField}
|
|
418
|
+
value={inputValue as string}
|
|
419
|
+
onChange={handleInputChange}
|
|
420
|
+
onBlur={handleInputBlur}
|
|
421
|
+
min={min}
|
|
422
|
+
max={max}
|
|
423
|
+
step={step ?? undefined}
|
|
424
|
+
disabled={disabled}
|
|
425
|
+
data-error={error ? "true" : undefined}
|
|
426
|
+
/>
|
|
427
|
+
))}
|
|
338
428
|
</div>
|
|
339
429
|
}
|
|
340
430
|
/>
|
|
@@ -39,4 +39,18 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
|
|
|
39
39
|
|
|
40
40
|
## 4. Key Integration Features & Constraints
|
|
41
41
|
|
|
42
|
-
The `label` prop is passed through to the surrounding form label rather than MUI's dragging tooltip; use `tooltipLabel` to set the label shown while dragging. When `showInput` is enabled, a numeric text input is rendered alongside the track and stays in sync with the slider's value. Set `showMinMaxLabels` to `false` to hide the min/max guides shown at either end of the track. Otherwise, the current value is displayed near the track instead — pass `tooltipLabel` as a formatter function (`(value) => ReactNode`) and that same formatter is reused for this display, instead of always showing the raw number. `minLabel`/`maxLabel` override the text shown at either end of the track (defaults to the numeric `min`/`max`). `icon` renders a leading icon next to the track; `trailingIcon` renders one on the opposite side.
|
|
42
|
+
The `label` prop is passed through to the surrounding form label rather than MUI's dragging tooltip; use `tooltipLabel` to set the label shown while dragging. When `showInput` is enabled, a numeric text input is rendered alongside the track and stays in sync with the slider's value. Set `showMinMaxLabels` to `false` to hide the min/max guides shown at either end of the track. Otherwise, the current value is displayed near the track instead — pass `tooltipLabel` as a formatter function (`(value) => ReactNode`) and that same formatter is reused for this display, instead of always showing the raw number. `minLabel`/`maxLabel` override the text shown at either end of the track (defaults to the numeric `min`/`max`). `icon` renders a leading icon next to the track; `trailingIcon` renders one on the opposite side, rendered right after the max label and before the numeric input.
|
|
43
|
+
|
|
44
|
+
### Range Mode
|
|
45
|
+
|
|
46
|
+
Pass a `[number, number]` tuple as `value`/`defaultValue` to render a two-thumb range slider — `onChange`/`onChangeEnd` are then called with a `[number, number]` tuple instead of a `number`. With `showInput` enabled, a second numeric input for the upper bound appears after the trailing icon, with the lower-bound input before the leading icon:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<Slider
|
|
50
|
+
label="Price Range"
|
|
51
|
+
defaultValue={[20, 80]}
|
|
52
|
+
min={0}
|
|
53
|
+
max={100}
|
|
54
|
+
onChange={(value) => console.log(value)} // [number, number]
|
|
55
|
+
/>
|
|
56
|
+
```
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
- **MUI-only: a leftover default selected background was still showing through.** `@mui/x-tree-view`'s own `TreeItemContent` ships a built-in selected tint (`rgba(25, 118, 210, 0.08)`, and a separate, higher-specificity `rgba(25, 118, 210, 0.2)` for the `[data-selected][data-focused]` combination specifically) that the earlier `.row:hover, .row[data-focused] { background-color: transparent; }` neutralizer never covered. Added `.row[data-selected]` to that neutralizer — but the `[data-selected][data-focused]` compound needed its _own_ explicit rule too: MUI's equivalent compound selector has higher specificity (two attribute selectors plus its own class) than a single-attribute `.row[data-selected]` rule, so it kept winning regardless of source order until matched with an equally-specific `.row[data-selected][data-focused]` rule on our side.
|
|
44
44
|
|
|
45
45
|
- **Whole-tree `disabled` (Matt Massey, 2026-08-10), added to support a `Disabled` story.** Unlike mantine-adapter (which has zero `disabled` concept anywhere in its `Tree` API — see its own note on this), `@mui/x-tree-view` already had per-item `disabled` plumbing sitting mostly dormant here: `CustomTreeItem` already destructured `disabled` off `UseTreeItemParameters`, and `.row[data-disabled] { opacity: ...; cursor: auto; }` already existed in `Tree.module.css` — just with no way for a caller to actually set it, since `RecursicaTreeNode` has no `disabled` field (and still doesn't; per-node disabling remains unexposed, same reasoning as the existing "Deliberately not implemented" entry above).
|
|
46
|
+
|
|
46
47
|
- **Implementation**: `isItemDisabled={disabled ? () => true : undefined}` passed to `<RichTreeView>` — marking every item disabled at once, reusing the library's own per-item mechanism rather than inventing a parallel one. `interactions.handleExpansion`/`handleSelection` (used by both the icon container's click handling and our own `Enter`-key override) already check `status.disabled` internally and no-op, so nothing in `CustomTreeItem` needed an extra guard.
|
|
47
48
|
- **Visual**: the pre-existing `.row[data-disabled]` rule now actually activates. Verified a pre-selected node's chip stays visible underneath the dimming (`.row[data-selected] .label` and `.row[data-disabled]` are independent, non-conflicting selectors — one styles `.label`, the other dims the whole `.row` via `opacity`), per Matt's ask to check that combination.
|
|
48
49
|
- **Found while verifying**: disabled rows still showed the hover tint (`.label::after` opacity) on mouse-over — MUI's own disabled checks block the actual select/expand _action_ on click, but `:hover` is pure CSS with nothing stopping it. Added `pointer-events: none` to `.row[data-disabled]`, matching mantine-adapter's `.root[data-disabled]` (tree-wide there; per-row here, since MUI's disabled state is inherently per-item even when every item is disabled at once by the same whole-tree prop).
|
|
50
|
+
|
|
51
|
+
- **`ExpandToggleButton` could still steal DOM focus despite `tabIndex={-1}` (Matt Massey, 2026-08-24), caught via a live browser console warning, in both adapters.** `tabIndex={-1}`/`aria-hidden` only removes the embedded chevron `Button` from the sequential tab order; it doesn't stop a native `<button>` from receiving DOM focus on a direct mouse click on the chevron, which browsers still do regardless of `tabIndex`. Since the button also has `aria-hidden="true"`, clicking it left an aria-hidden element holding focus — flagged by Chrome as "Blocked aria-hidden on an element because its descendant retained focus." Fixed with `onMouseDown={(event) => event.preventDefault()}` on the `Button` inside `ExpandToggleButton`: this suppresses the browser's default click-focuses-the-target behavior without affecting expand/collapse (still driven entirely by `TreeItemIconContainer`'s own click handling, untouched by this). Same fix applied to mantine-adapter's embedded chevron `Button`.
|
|
@@ -71,7 +71,12 @@ function ChevronGlyph({ rotated }: { rotated?: boolean }) {
|
|
|
71
71
|
* not by this Button itself, so the row stays the only focusable element and the button never
|
|
72
72
|
* shows its own focus state. Rendered for every row, including leaf items, so row alignment
|
|
73
73
|
* stays consistent; `hidden` (visibility, not display) is used for leaf rows instead of omitting
|
|
74
|
-
* the button, so the layout space is reserved without duplicating Button's own size tokens.
|
|
74
|
+
* the button, so the layout space is reserved without duplicating Button's own size tokens.
|
|
75
|
+
* `onMouseDown` prevents the browser's default click-focuses-the-button behavior (tabIndex={-1}
|
|
76
|
+
* only removes it from the tab order, it doesn't stop a direct mouse click on the chevron from
|
|
77
|
+
* focusing this button) — without this, clicking the chevron leaves the (aria-hidden) button
|
|
78
|
+
* focused, which browsers now flag as an accessibility violation ("focus must not be hidden from
|
|
79
|
+
* assistive technology"). */
|
|
75
80
|
function ExpandToggleButton({
|
|
76
81
|
rotated,
|
|
77
82
|
hidden,
|
|
@@ -88,6 +93,7 @@ function ExpandToggleButton({
|
|
|
88
93
|
aria-label="Toggle subtree"
|
|
89
94
|
aria-hidden="true"
|
|
90
95
|
tabIndex={-1}
|
|
96
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
91
97
|
className={
|
|
92
98
|
hidden
|
|
93
99
|
? `${styles.expandButton} ${styles.expandButtonHidden}`
|