@recursica/mantine-adapter 0.45.0 → 0.47.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 +24 -0
- package/dist/index.d.ts +72 -52
- package/dist/mantine-adapter.cjs +2 -2
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +1945 -1934
- package/dist/mantine-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.tsx +1 -1
- package/src/components/Checkbox/CheckboxGroup.tsx +1 -1
- package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +8 -8
- package/src/components/Chip/Chip.module.css +5 -5
- package/src/components/Chip/Chip.stories.tsx +2 -2
- package/src/components/Chip/Chip.tsx +13 -13
- package/src/components/Chip/USAGE.md +2 -2
- package/src/components/Dropdown/Dropdown.module.css +1 -1
- package/src/components/FileInput/FILEINPUT_IMPLEMENTATION_NOTES.md +6 -4
- package/src/components/FileInput/FileInput.stories.tsx +3 -0
- package/src/components/FileInput/FileInput.tsx +11 -10
- package/src/components/FileInput/USAGE.md +1 -0
- package/src/components/FileUpload/FILEUPLOAD_IMPLEMENTATION_NOTES.md +12 -12
- package/src/components/FileUpload/FileUpload.tsx +9 -9
- package/src/components/Radio/RadioGroup.tsx +1 -1
- package/src/components/SegmentedControl/IMPLEMENTATION_NOTES.md +8 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +17 -28
- package/src/components/SegmentedControl/SegmentedControl.tsx +29 -17
- package/src/components/SegmentedControl/USAGE.md +13 -0
- package/src/components/Slider/IMPLEMENTATION_NOTES.md +13 -0
- package/src/components/Slider/Slider.stories.tsx +44 -0
- package/src/components/Slider/Slider.tsx +23 -4
- package/src/components/Slider/USAGE.md +1 -1
- package/src/components/Switch/SwitchGroup.tsx +1 -1
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "git+https://github.com/borderux/recursica.git",
|
|
14
14
|
"directory": "packages/mantine-adapter"
|
|
15
15
|
},
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.47.0",
|
|
17
17
|
"type": "module",
|
|
18
18
|
"main": "./dist/mantine-adapter.cjs",
|
|
19
19
|
"module": "./dist/mantine-adapter.js",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"vitest": "^3.2.4"
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
|
-
"@recursica/adapter-common": "^0.
|
|
97
|
+
"@recursica/adapter-common": "^0.22.0",
|
|
98
98
|
"@recursica/official-release": "^2.8.0"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
@@ -19,7 +19,7 @@ import { type RecursicaCheckboxGroupProps as BaseRecursicaCheckboxGroupProps } f
|
|
|
19
19
|
export interface RecursicaCheckboxGroupProps
|
|
20
20
|
extends Omit<
|
|
21
21
|
MantineCheckboxGroupProps,
|
|
22
|
-
"size" | "labelProps" | "defaultValue" | "value"
|
|
22
|
+
"size" | "labelProps" | "defaultValue" | "value"
|
|
23
23
|
>,
|
|
24
24
|
Omit<
|
|
25
25
|
RecursicaFormControlWrapperProps,
|
|
@@ -20,7 +20,7 @@ To achieve this without breaking Mantine's `Chip` input architecture, we wrapped
|
|
|
20
20
|
<span className={styles.innerWrapper}>
|
|
21
21
|
{icon}
|
|
22
22
|
<span className={styles.children}>{children}</span>
|
|
23
|
-
{
|
|
23
|
+
{onDelete}
|
|
24
24
|
</span>
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -42,10 +42,10 @@ During implementation, the parsed Figma design tokens natively exported specific
|
|
|
42
42
|
`.children` already had `text-overflow: ellipsis; overflow: hidden; white-space: nowrap;`, but
|
|
43
43
|
neither it nor its parent `.innerWrapper` had `min-width: 0` — a flex child without that refuses
|
|
44
44
|
to shrink below its intrinsic (full, unwrapped) content width, so `text-overflow: ellipsis` never
|
|
45
|
-
actually engaged. A long label overflowed the flex row instead, and since `.
|
|
45
|
+
actually engaged. A long label overflowed the flex row instead, and since `.deleteIcon` had no
|
|
46
46
|
`flex-shrink: 0`, it got squeezed out of the clipped (`max-width`-bounded) chip entirely. Fixed by
|
|
47
47
|
adding `min-width: 0` to `.innerWrapper`/`.children` and `flex-shrink: 0` to `.leadingIcon`/
|
|
48
|
-
`.
|
|
48
|
+
`.deleteIcon`. Reproduces easily with `FileUpload`'s file list, since its chip `max-width`
|
|
49
49
|
(`--recursica_ui-kit_components_chip_properties_max-width`, 200px) is small enough that most real
|
|
50
50
|
filenames overflow it.
|
|
51
51
|
|
|
@@ -63,24 +63,24 @@ y-axis has no visual side effect.
|
|
|
63
63
|
|
|
64
64
|
### Roving tabindex support for chip groups (Matt Massey, 2026-08-17)
|
|
65
65
|
|
|
66
|
-
Added two optional pass-through props — `
|
|
66
|
+
Added two optional pass-through props — `deleteTabIndex` and `deleteIconRef` — purely so a parent
|
|
67
67
|
managing a _group_ of chips (e.g. `FileUpload`'s file list, see its own `IMPLEMENTATION_NOTES.md`)
|
|
68
|
-
can implement roving-tabindex/arrow-key navigation across them: set `
|
|
69
|
-
chip but the currently-active one, and use `
|
|
68
|
+
can implement roving-tabindex/arrow-key navigation across them: set `deleteTabIndex={-1}` on every
|
|
69
|
+
chip but the currently-active one, and use `deleteIconRef` to move real DOM focus there
|
|
70
70
|
imperatively on arrow-key press. Both are no-ops for a standalone `Chip` (defaults: `tabIndex={0}`,
|
|
71
71
|
no ref) — this doesn't change any existing single-chip behavior.
|
|
72
72
|
|
|
73
73
|
### `isInteractive` was measuring the wrong signal, and leaked a pointer cursor + phantom Tab stop (Matt Massey, 2026-08-18)
|
|
74
74
|
|
|
75
75
|
A `Chip` rendered with `checked` but no real handler (e.g. `FileUpload`'s `readOnly` file list —
|
|
76
|
-
`<Chip checked={false} tabIndex={-1}>`, no `
|
|
76
|
+
`<Chip checked={false} tabIndex={-1}>`, no `onDelete`) still looked and behaved clickable, from two
|
|
77
77
|
separate bugs:
|
|
78
78
|
|
|
79
79
|
- `isInteractive` treated `checked !== undefined` (even `false`) as proof of interactivity. That's
|
|
80
80
|
the wrong signal — a `checked`-controlled chip with no `onChange` can't actually be toggled by a
|
|
81
81
|
click (Mantine's `useUncontrolled` discards the click when `value` is externally controlled), so
|
|
82
82
|
clicking it does nothing observable regardless. `isInteractive` now only looks at whether
|
|
83
|
-
something actually responds: `
|
|
83
|
+
something actually responds: `onDelete`, `onClick`, or `onChange`.
|
|
84
84
|
- `.label.label` never set its own `cursor`, so Mantine's base style (`cursor: pointer`, hardcoded
|
|
85
85
|
on the underlying `mantine-Chip-label` class) always leaked through, independent of whether the
|
|
86
86
|
chip was actually interactive. Reset it to `cursor: default` and added a `data-interactive`
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
background: transparent; /* HARDCODE: Mantine forces background styling on label */
|
|
56
56
|
cursor: default; /* HARDCODE: Mantine's own base styles hardcode cursor: pointer on this class —
|
|
57
57
|
override it to plain default, then only re-enable it below for a chip with a real handler
|
|
58
|
-
(
|
|
58
|
+
(onDelete/onClick/onChange — see Chip.tsx's `isInteractive`). A display-only chip (e.g. a
|
|
59
59
|
read-only FileUpload file list) has nothing for a click to do, so it shouldn't look clickable. */
|
|
60
60
|
border-style: solid;
|
|
61
61
|
border-width: var(--recursica_ui-kit_components_chip_properties_border-size);
|
|
@@ -224,11 +224,11 @@
|
|
|
224
224
|
height: 100%;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
.
|
|
227
|
+
.deleteIcon {
|
|
228
228
|
display: inline-flex;
|
|
229
229
|
align-items: center;
|
|
230
230
|
justify-content: center;
|
|
231
|
-
flex-shrink: 0; /* HARDCODE: keep the
|
|
231
|
+
flex-shrink: 0; /* HARDCODE: keep the delete icon visible when the label truncates instead */
|
|
232
232
|
color: var(--chip-close);
|
|
233
233
|
width: var(--recursica_ui-kit_components_chip_properties_close-icon-size);
|
|
234
234
|
height: var(--recursica_ui-kit_components_chip_properties_close-icon-size);
|
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
outline: none;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
.
|
|
240
|
+
.deleteIcon:focus-visible {
|
|
241
241
|
box-shadow:
|
|
242
242
|
0 0 0 var(--recursica_brand_states_focus_border-size)
|
|
243
243
|
var(--recursica_brand_states_focus_color),
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
var(--recursica_brand_states_focus_color);
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
.
|
|
249
|
+
.deleteIcon svg {
|
|
250
250
|
width: 100%;
|
|
251
251
|
height: 100%;
|
|
252
252
|
}
|
|
@@ -12,7 +12,7 @@ const meta: Meta<ChipStoryProps> = {
|
|
|
12
12
|
docs: {
|
|
13
13
|
description: {
|
|
14
14
|
component:
|
|
15
|
-
"The Chip component is used to represent interactive selections, descriptive tags, or dynamic filters natively bounded to Recursica variables. It can be used as a toggleable input, can render a custom leading `icon`, and handles close constraints automatically via the `
|
|
15
|
+
"The Chip component is used to represent interactive selections, descriptive tags, or dynamic filters natively bounded to Recursica variables. It can be used as a toggleable input, can render a custom leading `icon`, and handles close constraints automatically via the `onDelete` property.",
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
18
|
},
|
|
@@ -86,7 +86,7 @@ export const Removable: Story = {
|
|
|
86
86
|
args: {
|
|
87
87
|
children: "Dismissible",
|
|
88
88
|
checked: false,
|
|
89
|
-
|
|
89
|
+
onDelete: () => console.log("Removal Action Triggered"),
|
|
90
90
|
},
|
|
91
91
|
render: (args: ChipStoryProps) => <Chip {...args} />,
|
|
92
92
|
};
|
|
@@ -41,10 +41,10 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
|
|
|
41
41
|
{
|
|
42
42
|
error = false,
|
|
43
43
|
icon,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
onDelete,
|
|
45
|
+
deleteLabel = "Delete",
|
|
46
|
+
deleteTabIndex,
|
|
47
|
+
deleteIconRef,
|
|
48
48
|
children,
|
|
49
49
|
overStyled = false,
|
|
50
50
|
wrapperProps,
|
|
@@ -75,13 +75,13 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
|
|
|
75
75
|
|
|
76
76
|
// Determine state
|
|
77
77
|
const dataError = error ? "" : undefined;
|
|
78
|
-
const isIconOnly = !children && (!!icon || !!
|
|
78
|
+
const isIconOnly = !children && (!!icon || !!onDelete);
|
|
79
79
|
// A chip only counts as interactive when something actually responds to it — merely passing a
|
|
80
80
|
// `checked` value (e.g. to pin a display-only chip to a fixed visual state, as FileUpload's
|
|
81
81
|
// read-only file list does) isn't itself an interaction, since clicking it with no onChange/
|
|
82
82
|
// onClick wired does nothing observable.
|
|
83
83
|
const isInteractive =
|
|
84
|
-
|
|
84
|
+
onDelete !== undefined ||
|
|
85
85
|
restRecord.onClick !== undefined ||
|
|
86
86
|
restRecord.onChange !== undefined;
|
|
87
87
|
|
|
@@ -119,27 +119,27 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
|
|
|
119
119
|
|
|
120
120
|
<span className={styles.children}>{children}</span>
|
|
121
121
|
|
|
122
|
-
{
|
|
122
|
+
{onDelete && (
|
|
123
123
|
<span
|
|
124
|
-
ref={
|
|
124
|
+
ref={deleteIconRef}
|
|
125
125
|
role="button"
|
|
126
|
-
className={styles.
|
|
126
|
+
className={styles.deleteIcon}
|
|
127
127
|
onClick={(e) => {
|
|
128
128
|
e.preventDefault();
|
|
129
129
|
e.stopPropagation();
|
|
130
|
-
|
|
130
|
+
onDelete(e);
|
|
131
131
|
}}
|
|
132
|
-
aria-label={
|
|
132
|
+
aria-label={deleteLabel}
|
|
133
133
|
onKeyDown={(e) => {
|
|
134
134
|
if (e.key === "Enter" || e.key === " ") {
|
|
135
135
|
e.preventDefault();
|
|
136
136
|
e.stopPropagation();
|
|
137
|
-
|
|
137
|
+
onDelete(
|
|
138
138
|
e as unknown as React.MouseEvent<HTMLSpanElement, MouseEvent>,
|
|
139
139
|
);
|
|
140
140
|
}
|
|
141
141
|
}}
|
|
142
|
-
tabIndex={
|
|
142
|
+
tabIndex={deleteTabIndex ?? 0}
|
|
143
143
|
>
|
|
144
144
|
<CloseIcon />
|
|
145
145
|
</span>
|
|
@@ -49,7 +49,7 @@ Chip does not support a `size` prop; chips render at a fixed size.
|
|
|
49
49
|
|
|
50
50
|
### Building a keyboard-navigable chip group
|
|
51
51
|
|
|
52
|
-
`
|
|
52
|
+
`deleteTabIndex` and `deleteIconRef` are optional escape hatches for composing a _group_ of chips
|
|
53
53
|
with roving-tabindex keyboard navigation (Tab reaches one chip at a time, arrow keys move between
|
|
54
54
|
them) — see `FileUpload`'s file list for a working example. Ignore both for a standalone chip; they
|
|
55
|
-
default to a normal `tabIndex={0}`
|
|
55
|
+
default to a normal `tabIndex={0}` delete icon with no ref.
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
hover background, neither of which is Recursica-token-driven. The double class selector (same
|
|
175
175
|
specificity-boosting trick as Chip.module.css's `.root.root`) beats that default without
|
|
176
176
|
needing `!important`. Same icon-size/trailing-icon tokens as the rest of this section, same
|
|
177
|
-
focus-visible ring technique as Chip's own `.
|
|
177
|
+
focus-visible ring technique as Chip's own `.deleteIcon` — the established inline icon-button
|
|
178
178
|
pattern elsewhere in this adapter. */
|
|
179
179
|
.clearButton.clearButton {
|
|
180
180
|
width: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
|
|
@@ -20,7 +20,7 @@ The control is one clickable, focusable `<div role="button">` containing:
|
|
|
20
20
|
single- and multiple-file mode render identically here; only the selection/replace behavior
|
|
21
21
|
differs (see "`multiple` defaults to `false`" below).
|
|
22
22
|
3. **A trailing "clear" `Button`** — shown whenever a file is selected; clears the entire current
|
|
23
|
-
selection (see "Trailing clear button" below).
|
|
23
|
+
selection, icon overridable via `clearIcon` (see "Trailing clear button" below).
|
|
24
24
|
4. **A hidden `<input type="file">`**, triggered programmatically — same approach as
|
|
25
25
|
`FileUpload`'s dropzone, not the "invisible overlay input" pattern some styled file inputs use
|
|
26
26
|
(that pattern would need every interactive child — chip remove icons, the clear icon — to sit
|
|
@@ -107,7 +107,9 @@ color/disabled states, so `file-input`'s own `properties_colors_trailing-icon` t
|
|
|
107
107
|
entirely. Clicking/activating it calls `onFileRemove` once per currently-selected file (reusing
|
|
108
108
|
the same callback `FileUpload`'s individual chip removal uses, rather than introducing a separate
|
|
109
109
|
`onClear` prop) — in single-file mode that's one call; in multiple-file mode it clears everything
|
|
110
|
-
at once, distinct from a chip's own individual remove icon.
|
|
110
|
+
at once, distinct from a chip's own individual remove icon. The icon itself defaults to the
|
|
111
|
+
built-in X glyph and is overridable via `clearIcon` — the same pattern as the leading `icon`,
|
|
112
|
+
giving Forge's `leading-icon`/`trailing-icon` token pair a matching pair of override props.
|
|
111
113
|
|
|
112
114
|
## Why each chip is wrapped with its own `stopPropagation`
|
|
113
115
|
|
|
@@ -121,7 +123,7 @@ it needed no changes for this.
|
|
|
121
123
|
|
|
122
124
|
## Keyboard navigation for the chip row
|
|
123
125
|
|
|
124
|
-
Same roving-tabindex model as `FileUpload`'s file list (`activeChipIndex`, `
|
|
126
|
+
Same roving-tabindex model as `FileUpload`'s file list (`activeChipIndex`, `deleteIconRefs`,
|
|
125
127
|
Left/Right/Up/Down roving, focus-survives-removal `useEffect`) — reused rather than reinvented,
|
|
126
128
|
and applies in single-file mode too (a one-chip roving group is a no-op but needs no special
|
|
127
129
|
casing). The only addition specific to `FileInput` is that `Tab` reaches the root control itself
|
|
@@ -140,7 +142,7 @@ already made for `.value`'s ellipsis truncation.
|
|
|
140
142
|
|
|
141
143
|
`readOnly` renders the same content (placeholder/filename/chip row) but the root loses its
|
|
142
144
|
`role="button"` interactivity (no `tabIndex`, no click/keyboard/drag handlers), chips render with
|
|
143
|
-
no `
|
|
145
|
+
no `deleteLabel`/`onDelete` (so `Chip` itself renders no delete icon, same as `FileUpload`'s
|
|
144
146
|
read-only chips), and the trailing clear icon is omitted entirely. `disabled` keeps the control
|
|
145
147
|
structurally the same but inert (`tabIndex={-1}`, `aria-disabled`, no handlers, native `<input>`
|
|
146
148
|
disabled) — both are computed together as a single `interactive` flag used throughout, but remain
|
|
@@ -125,6 +125,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
|
|
|
125
125
|
? `Maximum of ${maxFiles} files allowed`
|
|
126
126
|
: "Only one file is allowed",
|
|
127
127
|
icon,
|
|
128
|
+
clearIcon,
|
|
128
129
|
placeholder = "Select a file...",
|
|
129
130
|
browseLabel = "Choose file",
|
|
130
131
|
removeFileLabel = "Remove",
|
|
@@ -235,10 +236,10 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
|
|
|
235
236
|
};
|
|
236
237
|
|
|
237
238
|
// Roving tabindex across the file chip list (single- or multiple-file mode): only the
|
|
238
|
-
// "active" chip's
|
|
239
|
+
// "active" chip's delete icon is a tab stop, and Left/Right/Up/Down move it — same pattern
|
|
239
240
|
// as FileUpload, see FILEINPUT_IMPLEMENTATION_NOTES.md.
|
|
240
241
|
const [activeChipIndex, setActiveChipIndex] = useState(0);
|
|
241
|
-
const
|
|
242
|
+
const deleteIconRefs = useRef<Array<HTMLSpanElement | null>>([]);
|
|
242
243
|
const prevFileCountRef = useRef(files?.length ?? 0);
|
|
243
244
|
|
|
244
245
|
useEffect(() => {
|
|
@@ -246,7 +247,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
|
|
|
246
247
|
if (count > 0 && count < prevFileCountRef.current) {
|
|
247
248
|
const nextIndex = Math.min(activeChipIndex, count - 1);
|
|
248
249
|
setActiveChipIndex(nextIndex);
|
|
249
|
-
|
|
250
|
+
deleteIconRefs.current[nextIndex]?.focus();
|
|
250
251
|
}
|
|
251
252
|
prevFileCountRef.current = count;
|
|
252
253
|
// Only react to the file list itself shrinking/growing, not to activeChipIndex changes.
|
|
@@ -268,7 +269,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
|
|
|
268
269
|
event.preventDefault();
|
|
269
270
|
event.stopPropagation();
|
|
270
271
|
setActiveChipIndex(nextIndex);
|
|
271
|
-
|
|
272
|
+
deleteIconRefs.current[nextIndex]?.focus();
|
|
272
273
|
};
|
|
273
274
|
|
|
274
275
|
// The built-in `accept`/cap-mismatch message is only shown when the integrator hasn't
|
|
@@ -353,14 +354,14 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
|
|
|
353
354
|
<Chip
|
|
354
355
|
checked={false}
|
|
355
356
|
tabIndex={-1}
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
deleteLabel={readOnly ? undefined : removeFileLabel}
|
|
358
|
+
deleteTabIndex={
|
|
358
359
|
!readOnly && index === activeChipIndex ? 0 : -1
|
|
359
360
|
}
|
|
360
|
-
|
|
361
|
-
|
|
361
|
+
deleteIconRef={(el) => {
|
|
362
|
+
deleteIconRefs.current[index] = el;
|
|
362
363
|
}}
|
|
363
|
-
|
|
364
|
+
onDelete={
|
|
364
365
|
readOnly || disabled
|
|
365
366
|
? undefined
|
|
366
367
|
: () => onFileRemove?.(itemId)
|
|
@@ -380,7 +381,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
|
|
|
380
381
|
overStyled
|
|
381
382
|
variant="text"
|
|
382
383
|
size="small"
|
|
383
|
-
icon={<ClearIcon />}
|
|
384
|
+
icon={clearIcon ?? <ClearIcon />}
|
|
384
385
|
aria-label={clearLabel}
|
|
385
386
|
className={styles.trailingIcon}
|
|
386
387
|
disabled={disabled}
|
|
@@ -57,6 +57,7 @@ rather than appending — picking a new file in single-file mode always replaces
|
|
|
57
57
|
| `invalidFileTypeMessage` | `React.ReactNode` | Error message shown when a file is rejected for not matching `accept`. Defaults to `"File type not accepted"`. An explicit `error` prop always takes priority over this. |
|
|
58
58
|
| `maxFilesMessage` | `React.ReactNode` | Error message shown when a file is rejected for exceeding the cap. Defaults to `"Maximum of {maxFiles} files allowed"` when `multiple`, or `"Only one file is allowed"` otherwise. An explicit `error` prop always wins. |
|
|
59
59
|
| `icon` | `React.ReactNode` | Leading icon shown inside the control. Defaults to the built-in upload icon. |
|
|
60
|
+
| `clearIcon` | `React.ReactNode` | Trailing clear-all icon shown once a file is selected. Defaults to the built-in X icon. |
|
|
60
61
|
| `placeholder` | `React.ReactNode` | Text shown when no file is selected. Defaults to `"Select a file..."`. |
|
|
61
62
|
| `browseLabel` | `string` | Screen-reader label for the control itself (it's the sole interactive/focusable surface, there being no separate "Browse" button). Defaults to `"Choose file"`. |
|
|
62
63
|
| `removeFileLabel` | `string` | Screen-reader label for a file chip's remove button. Defaults to `"Remove"`. |
|
|
@@ -11,7 +11,7 @@ deliberately did not add — see "Why not `@mantine/dropzone`" below). The compo
|
|
|
11
11
|
(`onDragOver`/`onDrop`), containing an upload icon, instructional text, and a hidden
|
|
12
12
|
`<input type="file">` triggered by...
|
|
13
13
|
2. **The browse button** — this adapter's own `<Button variant="outline" size="small">`.
|
|
14
|
-
3. **The file list** — this adapter's own `<Chip>` component (with `
|
|
14
|
+
3. **The file list** — this adapter's own `<Chip>` component (with `onDelete`), one per entry in
|
|
15
15
|
the controlled `files` prop.
|
|
16
16
|
|
|
17
17
|
This shape is **not configurable** — there is no prop to render a bare native file input without
|
|
@@ -34,7 +34,7 @@ Massey, 2026-08-11.)
|
|
|
34
34
|
(spacing between/around file entries) but **no color/border tokens of its own for the file
|
|
35
35
|
entries themselves** — the visual design intentionally delegates that to the existing `Chip`
|
|
36
36
|
component's own token namespace (`--recursica_ui-kit_components_chip_...`). Reusing `<Chip
|
|
37
|
-
|
|
37
|
+
onDelete={...}>` directly (rather than reimplementing a similar-looking element) keeps that
|
|
38
38
|
separation intact per the canonical guide's "Component Specificity" rule — `FileUpload.module.css`
|
|
39
39
|
never reaches into `chip`'s namespace, and `Chip.module.css` never reaches into `file-upload`'s.
|
|
40
40
|
|
|
@@ -95,9 +95,9 @@ list's read-only form is the same chip list, just without the ability to remove
|
|
|
95
95
|
`readOnly` is handled directly in `FileUpload.tsx` rather than reusing `WithReadOnlyWrapper`:
|
|
96
96
|
|
|
97
97
|
- The dropzone (icon, instructional text, Browse button, hidden `<input>`) is omitted entirely.
|
|
98
|
-
- Each `Chip` is rendered with no `
|
|
98
|
+
- Each `Chip` is rendered with no `onDelete` (and no `deleteTabIndex`/`deleteIconRef`/roving
|
|
99
99
|
keyboard handlers, which only exist to manage the remove icon) — `Chip` itself already renders no
|
|
100
|
-
remove icon at all when `
|
|
100
|
+
remove icon at all when `onDelete` is `undefined`, so this falls out for free rather than needing
|
|
101
101
|
a separate `readOnly` prop on `Chip`.
|
|
102
102
|
- `disabled` is independent of `readOnly` and has no effect when `readOnly` is set (there's no
|
|
103
103
|
dropzone/remove icon left for it to disable).
|
|
@@ -171,12 +171,12 @@ adapters).
|
|
|
171
171
|
The file list previously had no group-level keyboard model at all — each chip's remove icon was
|
|
172
172
|
simply the next `tabIndex={0}` element in natural DOM order (and, in the Mantine adapter, so was
|
|
173
173
|
each chip's own hidden checkbox `<input>`, since `Chip`'s `isInteractive` check treats any chip
|
|
174
|
-
with `
|
|
174
|
+
with `onDelete` as tabbable-by-default — a second, redundant tab stop specific to this adapter).
|
|
175
175
|
Implemented a standard roving-tabindex pattern instead, matching `Tree`'s existing keyboard model
|
|
176
176
|
(see `../Tree/IMPLEMENTATION_NOTES.md`) rather than inventing a new one:
|
|
177
177
|
|
|
178
178
|
- **Tab reaches exactly one stop per chip list, landing on the first chip.** `FileUpload` tracks
|
|
179
|
-
`activeChipIndex` (initially `0`) and passes `
|
|
179
|
+
`activeChipIndex` (initially `0`) and passes `deleteTabIndex={index === activeChipIndex ? 0 : -1}`
|
|
180
180
|
to each `Chip` — a new pass-through prop added to `RecursicaChipProps`/both adapters' `Chip.tsx`
|
|
181
181
|
(see `../Chip/CHIP_IMPLEMENTATION_NOTES.md`) that overrides the remove icon's own tabIndex. Every
|
|
182
182
|
chip in the Mantine adapter is also given a plain `tabIndex={-1}` directly (flows through to
|
|
@@ -184,13 +184,13 @@ Implemented a standard roving-tabindex pattern instead, matching `Tree`'s existi
|
|
|
184
184
|
kill that second, unwanted tab stop — the chip's label/checkbox was never meant to be
|
|
185
185
|
interactive here (`checked={false}`, no `onChange`), only its remove icon is.
|
|
186
186
|
- **Enter removes the focused chip, with focus already on its remove icon.** No new code needed —
|
|
187
|
-
`Chip`'s remove icon already calls `
|
|
187
|
+
`Chip`'s remove icon already calls `onDelete` on `Enter`/`Space`, and it's already the focused
|
|
188
188
|
element by construction (see above), so "focus ring on the remove icon" falls out for free from
|
|
189
|
-
the existing `.
|
|
189
|
+
the existing `.deleteIcon:focus-visible` style.
|
|
190
190
|
- **Left/Right or Up/Down move focus between chips.** A `onKeyDown` handler on the file list `<div>`
|
|
191
191
|
(event delegation — it fires for keydowns on any focused chip inside it) computes the next index
|
|
192
|
-
(wrapping at both ends) and moves real DOM focus there via `
|
|
193
|
-
populated through the new `
|
|
192
|
+
(wrapping at both ends) and moves real DOM focus there via `deleteIconRefs`, an array of refs
|
|
193
|
+
populated through the new `deleteIconRef` prop on `Chip` (same PR as `deleteTabIndex`).
|
|
194
194
|
- **Focus survives removal.** Since `files` is a controlled prop `FileUpload` doesn't mutate
|
|
195
195
|
itself, removing a chip doesn't shrink `files` until the consumer's own state update flows back
|
|
196
196
|
down as a new prop — a `useEffect` keyed on `files` detects the length decreasing, clamps
|
|
@@ -211,7 +211,7 @@ triggers both.
|
|
|
211
211
|
## Read-only chips were still interactive (Matt Massey, 2026-08-18)
|
|
212
212
|
|
|
213
213
|
The `readOnly` file list (added 2026-08-18, see USAGE.md §7) rendered each filename as a
|
|
214
|
-
`<Chip checked={false} tabIndex={-1}>` with no `
|
|
214
|
+
`<Chip checked={false} tabIndex={-1}>` with no `onDelete`, expecting that to be fully inert. Two
|
|
215
215
|
separate, previously-latent bugs made it look and behave otherwise — both fixed in
|
|
216
216
|
`Chip`/`Chip.module.css`, not `FileUpload` itself, so every consumer of a non-interactive `Chip`
|
|
217
217
|
benefits, not just this one:
|
|
@@ -221,7 +221,7 @@ benefits, not just this one:
|
|
|
221
221
|
the tabbable/exposed branch — but a `checked`-controlled chip with no `onChange` can't actually be
|
|
222
222
|
toggled by a click (Mantine's `useUncontrolled` just ignores the click when `value` is externally
|
|
223
223
|
controlled), so that heuristic was measuring the wrong thing. `isInteractive` now only looks at
|
|
224
|
-
whether something actually responds to interaction: `
|
|
224
|
+
whether something actually responds to interaction: `onDelete`, `onClick`, or `onChange`.
|
|
225
225
|
- **A read-only chip's cursor still showed `pointer` on hover.** Mantine's own base styles hardcode
|
|
226
226
|
`cursor: pointer` on the chip label class; our CSS never overrode it, so it always leaked through
|
|
227
227
|
regardless of the chip's actual behavior. `.label.label` now resets it to `cursor: default` and a
|
|
@@ -194,11 +194,11 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
194
194
|
event.target.value = "";
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
-
// Roving tabindex across the file chip list: only the "active" chip's
|
|
197
|
+
// Roving tabindex across the file chip list: only the "active" chip's delete icon is a tab
|
|
198
198
|
// stop (Tab lands on the first chip), and Left/Right/Up/Down move it — see
|
|
199
199
|
// FILEUPLOAD_IMPLEMENTATION_NOTES.md.
|
|
200
200
|
const [activeChipIndex, setActiveChipIndex] = useState(0);
|
|
201
|
-
const
|
|
201
|
+
const deleteIconRefs = useRef<Array<HTMLSpanElement | null>>([]);
|
|
202
202
|
const prevFileCountRef = useRef(files?.length ?? 0);
|
|
203
203
|
|
|
204
204
|
useEffect(() => {
|
|
@@ -208,7 +208,7 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
208
208
|
if (count > 0 && count < prevFileCountRef.current) {
|
|
209
209
|
const nextIndex = Math.min(activeChipIndex, count - 1);
|
|
210
210
|
setActiveChipIndex(nextIndex);
|
|
211
|
-
|
|
211
|
+
deleteIconRefs.current[nextIndex]?.focus();
|
|
212
212
|
}
|
|
213
213
|
prevFileCountRef.current = count;
|
|
214
214
|
// Only react to the file list itself shrinking/growing, not to activeChipIndex changes.
|
|
@@ -229,7 +229,7 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
229
229
|
if (nextIndex === undefined) return;
|
|
230
230
|
event.preventDefault();
|
|
231
231
|
setActiveChipIndex(nextIndex);
|
|
232
|
-
|
|
232
|
+
deleteIconRefs.current[nextIndex]?.focus();
|
|
233
233
|
};
|
|
234
234
|
|
|
235
235
|
// The built-in `accept`-mismatch message is only shown when the integrator hasn't supplied
|
|
@@ -330,12 +330,12 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
330
330
|
key={itemId}
|
|
331
331
|
checked={false}
|
|
332
332
|
tabIndex={-1}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
333
|
+
deleteLabel={removeFileLabel}
|
|
334
|
+
deleteTabIndex={index === activeChipIndex ? 0 : -1}
|
|
335
|
+
deleteIconRef={(el) => {
|
|
336
|
+
deleteIconRefs.current[index] = el;
|
|
337
337
|
}}
|
|
338
|
-
|
|
338
|
+
onDelete={
|
|
339
339
|
disabled ? undefined : () => onFileRemove?.(itemId)
|
|
340
340
|
}
|
|
341
341
|
>
|
|
@@ -19,7 +19,7 @@ import { type RecursicaRadioGroupProps as BaseRecursicaRadioGroupProps } from "@
|
|
|
19
19
|
export interface RecursicaRadioGroupProps
|
|
20
20
|
extends Omit<
|
|
21
21
|
MantineRadioGroupProps,
|
|
22
|
-
"size" | "labelProps" | "defaultValue" | "value"
|
|
22
|
+
"size" | "labelProps" | "defaultValue" | "value"
|
|
23
23
|
>,
|
|
24
24
|
Omit<
|
|
25
25
|
RecursicaFormControlWrapperProps,
|
|
@@ -15,3 +15,11 @@ Mantine uses an `::before` pseudo-element on the `.control` block to draw standa
|
|
|
15
15
|
## 4. Indicator Mapping
|
|
16
16
|
|
|
17
17
|
The moving active background element (`.indicator`) is decoupled from the actual text label. It is styled natively with its own background color, border size, and elevation shadow variables to match the exact visual parity of a "floating active chip" as defined in the Recursica properties map.
|
|
18
|
+
|
|
19
|
+
## 5. Per-item `icon`
|
|
20
|
+
|
|
21
|
+
`RecursicaSegmentedControlProps.data` objects accept an optional `icon`. Mantine's own `data` item has no such slot, so this component `Omit`s Mantine's native `data` type and composes `icon`+`label` into a single `label` ReactNode before handing it to Mantine — Mantine's own `innerLabel` wrapper then lays it out using the icon-size/gap tokens already present in `SegmentedControl.module.css`.
|
|
22
|
+
|
|
23
|
+
## 6. Whole-control `disabled`
|
|
24
|
+
|
|
25
|
+
`RecursicaSegmentedControlProps.disabled` was previously typed `never` and stripped at runtime. It's now a real `boolean?`, passed straight through to Mantine's own native `disabled` — Mantine's `SegmentedControl` already disables every item when set. Per-item disabling via `data[].disabled` continues to work independently of it.
|
|
@@ -21,7 +21,9 @@ const meta: Meta<typeof SegmentedControl> = {
|
|
|
21
21
|
fullWidth: {
|
|
22
22
|
control: "boolean",
|
|
23
23
|
},
|
|
24
|
-
disabled: {
|
|
24
|
+
disabled: {
|
|
25
|
+
control: "boolean",
|
|
26
|
+
},
|
|
25
27
|
data: { table: { disable: true } },
|
|
26
28
|
defaultChecked: { table: { disable: true } },
|
|
27
29
|
},
|
|
@@ -64,6 +66,17 @@ export const Vertical: Story = {
|
|
|
64
66
|
},
|
|
65
67
|
};
|
|
66
68
|
|
|
69
|
+
export const Disabled: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
data: ["Preview", "Code", "Edit"],
|
|
72
|
+
disabled: true,
|
|
73
|
+
},
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
|
|
75
|
+
render: ({ withLayer, layer, ...args }: any) => {
|
|
76
|
+
return <SegmentedControl {...args} />;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
67
80
|
const CheckIcon = () => (
|
|
68
81
|
<svg
|
|
69
82
|
viewBox="0 0 24 24"
|
|
@@ -81,33 +94,9 @@ const CheckIcon = () => (
|
|
|
81
94
|
export const WithIcons: Story = {
|
|
82
95
|
args: {
|
|
83
96
|
data: [
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<>
|
|
88
|
-
<CheckIcon />
|
|
89
|
-
<span>Daily</span>
|
|
90
|
-
</>
|
|
91
|
-
),
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
value: "weekly",
|
|
95
|
-
label: (
|
|
96
|
-
<>
|
|
97
|
-
<CheckIcon />
|
|
98
|
-
<span>Weekly</span>
|
|
99
|
-
</>
|
|
100
|
-
),
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
value: "monthly",
|
|
104
|
-
label: (
|
|
105
|
-
<>
|
|
106
|
-
<CheckIcon />
|
|
107
|
-
<span>Monthly</span>
|
|
108
|
-
</>
|
|
109
|
-
),
|
|
110
|
-
},
|
|
97
|
+
{ value: "daily", label: "Daily", icon: <CheckIcon /> },
|
|
98
|
+
{ value: "weekly", label: "Weekly", icon: <CheckIcon /> },
|
|
99
|
+
{ value: "monthly", label: "Monthly", icon: <CheckIcon /> },
|
|
111
100
|
],
|
|
112
101
|
},
|
|
113
102
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
|