@recursica/mantine-adapter 0.38.1 → 0.40.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/ARCHITECTURE.md +3 -0
- package/CHANGELOG.md +51 -0
- package/dist/index.d.ts +186 -20
- 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 +2724 -2491
- package/dist/mantine-adapter.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/ACCORDION_IMPLEMENTATION_NOTES.md +16 -2
- package/src/components/Accordion/Accordion.module.css +10 -1
- package/src/components/Accordion/Accordion.stories.tsx +36 -0
- package/src/components/Accordion/Accordion.tsx +40 -7
- package/src/components/AssistiveElement/ASSISTIVEELEMENT_IMPLEMENTATION_NOTES.md +45 -0
- package/src/components/AssistiveElement/AssistiveElement.tsx +8 -1
- package/src/components/AutoComplete/AutoComplete.tsx +1 -4
- package/src/components/Avatar/AVATAR_IMPLEMENTATION_NOTES.md +10 -0
- package/src/components/Button/Button.module.css +13 -0
- package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +59 -0
- package/src/components/Chip/Chip.module.css +36 -4
- package/src/components/Chip/Chip.tsx +14 -5
- package/src/components/Chip/USAGE.md +7 -0
- package/src/components/FileUpload/FILEUPLOAD_IMPLEMENTATION_NOTES.md +244 -0
- package/src/components/FileUpload/FileUpload.module.css +204 -42
- package/src/components/FileUpload/FileUpload.stories.tsx +348 -4
- package/src/components/FileUpload/FileUpload.tsx +352 -5
- package/src/components/FileUpload/USAGE.md +163 -5
- package/src/components/Switch/SWITCH_IMPLEMENTATION_NOTES.md +36 -0
- package/src/components/Switch/Switch.module.css +19 -2
- package/src/components/Switch/SwitchGroup.tsx +6 -3
- package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +10 -0
- package/src/components/TimePicker/TimePicker.tsx +1 -1
package/package.json
CHANGED
|
@@ -16,8 +16,8 @@ Decisions and design tweaks strictly tailored for the UI Kit's Accordion wrapped
|
|
|
16
16
|
|
|
17
17
|
## 2. Default Configuration Reset (`unstyled`)
|
|
18
18
|
|
|
19
|
-
**Decision:** We strip Mantine's inner styles away completely from Accordion mappings by leveraging
|
|
20
|
-
**Implementation:**
|
|
19
|
+
**Decision:** We strip Mantine's inner styles away completely from Accordion mappings by leveraging Mantine's own `variant="unstyled"`. That Mantine sentinel is an internal implementation detail, not something a Recursica consumer should ever set directly — `RecursicaAccordionProps.variant` is `"default" | (string & {})`: `"default"` is the only value with dedicated Recursica styling, mapped internally via `mapVariant` in `Accordion.tsx`; any other string is a caller-supplied custom variant and passes straight through to Mantine unmapped.
|
|
20
|
+
**Implementation:** `AccordionBase` defaults `variant` to Recursica's `"default"`, looks it up in `mapVariant` (`{ default: "unstyled" }`), and passes the resolved value (or the original string, if it wasn't a recognized key) to `<MantineAccordion variant={...}>`. The `"default"` → `"unstyled"` mapping effectively deletes Mantine's precomputed padding, borders, and shadow mappings allowing our targeted `classNames` inside `Accordion.module.css` to become the exact source of foundational truth without "fighting" `!important` tags or unpredictable flex-layouts inherited globally.
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -46,3 +46,17 @@ Decisions and design tweaks strictly tailored for the UI Kit's Accordion wrapped
|
|
|
46
46
|
|
|
47
47
|
**Decision:** We do not bind isolated `open={true}` state properties natively on individual `<AccordionItem>` configurations.
|
|
48
48
|
**Implementation:** Recursica natively dictates an item-level `open` tracking mapping. However, internally mapping boolean flags structurally across specific tree nodes heavily corrupts Mantine's DOM layout algorithms mapping parent-driven transition listeners. Mantine forces all expanded-height logic to run symmetrically off the `<Accordion value="...">` string matching array to accurately bind ARIA transitions. We explicitly ignore isolated item `<AccordionItem open={...}>` booleans to shield the rendering sequence cleanly.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 7. Native `icon` slot is omitted on `AccordionControl`
|
|
53
|
+
|
|
54
|
+
**Decision:** Mantine's `AccordionControl` has its own native `icon` prop — a distinct,
|
|
55
|
+
documented slot ("icon displayed next to the label") — which the adapter reuses internally
|
|
56
|
+
to render Recursica's `leftIcon`. Left unguarded, a caller could pass native `icon` directly
|
|
57
|
+
and silently override the `leftIcon`-derived element (spread order would let it win).
|
|
58
|
+
**Implementation:** `AccordionControlWrapperProps` omits `icon` from Mantine's native
|
|
59
|
+
`AccordionControlProps` before merging in `RecursicaAccordionControlProps`, so the only way
|
|
60
|
+
to set that slot is through `leftIcon`. Mantine's separate, per-control `chevron` override
|
|
61
|
+
is _not_ omitted — nothing here computes it, so it still passes through safely if a caller
|
|
62
|
+
wants to override the cascaded container-level chevron for a single item.
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
constant background (Mantine's native :hover CSS would otherwise show through) and use an
|
|
160
160
|
overlay ::after structure with the generic hover tokens for the actual hover tint; no per-item
|
|
161
161
|
hover-color/hover-opacity tokens exist anymore. */
|
|
162
|
-
.control:hover {
|
|
162
|
+
.control:hover:not(:disabled) {
|
|
163
163
|
background-color: var(
|
|
164
164
|
--recursica_ui-kit_components_accordion-header_variants_appearance_closed_properties_colors_background-color
|
|
165
165
|
);
|
|
@@ -179,6 +179,15 @@
|
|
|
179
179
|
opacity: var(--recursica_brand_states_hover_opacity);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/* Dims the whole item — control and any currently-visible panel content alike — using the
|
|
183
|
+
generic disabled opacity token, same convention as every other disabled component in the
|
|
184
|
+
design system (no accordion-item-specific disabled token exists). The Control's own native
|
|
185
|
+
`disabled` attribute already blocks click/keyboard interaction on its own; this only
|
|
186
|
+
handles the visual dim. */
|
|
187
|
+
.item[data-disabled] {
|
|
188
|
+
opacity: var(--recursica_brand_states_disabled);
|
|
189
|
+
}
|
|
190
|
+
|
|
182
191
|
/* Recursica focus ring instead of Mantine's default `.mantine-focus-auto:focus-visible` outline
|
|
183
192
|
(same class+pseudo-class specificity, so !important guarantees ours wins regardless of CSS
|
|
184
193
|
import order). */
|
|
@@ -153,6 +153,42 @@ export const LongTitleTruncation: StoryObj<typeof Accordion> = {
|
|
|
153
153
|
},
|
|
154
154
|
};
|
|
155
155
|
|
|
156
|
+
export const Disabled: StoryObj<typeof Accordion> = {
|
|
157
|
+
render: () => {
|
|
158
|
+
return (
|
|
159
|
+
<Accordion defaultValue="expanded-disabled" chevron={<ChevronIcon />}>
|
|
160
|
+
<Accordion.Item value="expanded-disabled" disabled>
|
|
161
|
+
<Accordion.Control leftIcon={<SVGIcon />}>
|
|
162
|
+
Expanded and Disabled
|
|
163
|
+
</Accordion.Control>
|
|
164
|
+
<Accordion.Panel>
|
|
165
|
+
This item starts expanded so the panel content's dimming can be
|
|
166
|
+
verified alongside the control's, not just the collapsed header.
|
|
167
|
+
</Accordion.Panel>
|
|
168
|
+
</Accordion.Item>
|
|
169
|
+
|
|
170
|
+
<Accordion.Item value="collapsed-disabled" disabled>
|
|
171
|
+
<Accordion.Control leftIcon={<SVGIcon />}>
|
|
172
|
+
Collapsed and Disabled
|
|
173
|
+
</Accordion.Control>
|
|
174
|
+
<Accordion.Panel>
|
|
175
|
+
Clicking or tabbing to this control should have no effect.
|
|
176
|
+
</Accordion.Panel>
|
|
177
|
+
</Accordion.Item>
|
|
178
|
+
|
|
179
|
+
<Accordion.Item value="enabled">
|
|
180
|
+
<Accordion.Control leftIcon={<SVGIcon />}>
|
|
181
|
+
Enabled, for Comparison
|
|
182
|
+
</Accordion.Control>
|
|
183
|
+
<Accordion.Panel>
|
|
184
|
+
A normal, interactive item alongside the disabled ones above.
|
|
185
|
+
</Accordion.Panel>
|
|
186
|
+
</Accordion.Item>
|
|
187
|
+
</Accordion>
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
156
192
|
/** Demonstrates the component nested inside a non-default layer — the one case where an
|
|
157
193
|
* explicit `<Layer>` wrap belongs in a story (see COMPONENT_STORYBOOK_GUIDE.md §9). */
|
|
158
194
|
export const LayerOne: StoryObj<typeof Accordion> = {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type RecursicaAccordionProps,
|
|
17
17
|
type RecursicaAccordionItemProps,
|
|
18
18
|
type RecursicaAccordionControlProps,
|
|
19
|
+
type RecursicaAccordionPanelProps,
|
|
19
20
|
} from "@recursica/adapter-common";
|
|
20
21
|
|
|
21
22
|
// ==== ACCORDION CONTAINER ====
|
|
@@ -27,11 +28,24 @@ export type AccordionProps = RecursicaOverStyled<
|
|
|
27
28
|
RecursicaAccordionProps
|
|
28
29
|
>;
|
|
29
30
|
|
|
31
|
+
// Maps Recursica's public variant vocabulary to Mantine's own. `unstyled` is a Mantine
|
|
32
|
+
// sentinel with no meaning to Recursica consumers — it exists only to suppress Mantine's
|
|
33
|
+
// built-in variant CSS so our module CSS is the sole source of styling. Anything outside
|
|
34
|
+
// this map (a caller-supplied custom variant string) passes straight through to Mantine.
|
|
35
|
+
const mapVariant: Record<string, string> = {
|
|
36
|
+
default: "unstyled",
|
|
37
|
+
};
|
|
38
|
+
|
|
30
39
|
const AccordionBase = function Accordion({
|
|
31
|
-
variant = "
|
|
40
|
+
variant = "default",
|
|
32
41
|
overStyled = false,
|
|
33
42
|
...rest
|
|
34
43
|
}: AccordionProps) {
|
|
44
|
+
const resolvedVariant =
|
|
45
|
+
typeof variant === "string" && mapVariant[variant]
|
|
46
|
+
? mapVariant[variant]
|
|
47
|
+
: variant;
|
|
48
|
+
|
|
35
49
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
36
50
|
|
|
37
51
|
// Bind all deep CSS module references natively into the global class mapping schema
|
|
@@ -67,7 +81,7 @@ const AccordionBase = function Accordion({
|
|
|
67
81
|
|
|
68
82
|
return (
|
|
69
83
|
<MantineAccordion
|
|
70
|
-
variant={variant}
|
|
84
|
+
variant={resolvedVariant as MantineAccordionProps["variant"]}
|
|
71
85
|
className={classNameProp}
|
|
72
86
|
classNames={mergedClassNames}
|
|
73
87
|
{...(sanitizedProps as unknown as MantineAccordionProps)}
|
|
@@ -85,7 +99,15 @@ export const AccordionItem = forwardRef<
|
|
|
85
99
|
HTMLDivElement,
|
|
86
100
|
AccordionItemWrapperProps
|
|
87
101
|
>(function AccordionItem(
|
|
88
|
-
{
|
|
102
|
+
{
|
|
103
|
+
title,
|
|
104
|
+
leftIcon,
|
|
105
|
+
divider = true,
|
|
106
|
+
children,
|
|
107
|
+
disabled = false,
|
|
108
|
+
overStyled = false,
|
|
109
|
+
...rest
|
|
110
|
+
},
|
|
89
111
|
ref,
|
|
90
112
|
) {
|
|
91
113
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
@@ -99,15 +121,22 @@ export const AccordionItem = forwardRef<
|
|
|
99
121
|
|
|
100
122
|
// If the user utilizes the explicit 'title' prop from Recursica, we securely auto-construct the Mantine sub-hierarchy natively!
|
|
101
123
|
// If not, we defer to raw composable children (meaning the integrator maps `<Accordion.Control>` manually).
|
|
124
|
+
// `disabled` has no native concept at the Item level — only Control has a real `disabled`
|
|
125
|
+
// prop (it renders a `<button>`, so the native HTML `disabled` attribute alone blocks click,
|
|
126
|
+
// focus, and keyboard activation with no extra guards needed). We forward it there in the
|
|
127
|
+
// auto-composed path; a manually-composed `<Accordion.Control>` needs it passed explicitly.
|
|
102
128
|
return (
|
|
103
129
|
<MantineAccordion.Item
|
|
104
130
|
ref={ref}
|
|
105
131
|
className={finalClass}
|
|
132
|
+
data-disabled={disabled || undefined}
|
|
106
133
|
{...(sanitizedProps as unknown as AccordionItemProps)}
|
|
107
134
|
>
|
|
108
135
|
{title ? (
|
|
109
136
|
<>
|
|
110
|
-
<AccordionControl leftIcon={leftIcon}
|
|
137
|
+
<AccordionControl leftIcon={leftIcon} disabled={disabled}>
|
|
138
|
+
{title}
|
|
139
|
+
</AccordionControl>
|
|
111
140
|
<AccordionPanel>{children}</AccordionPanel>
|
|
112
141
|
</>
|
|
113
142
|
) : (
|
|
@@ -120,7 +149,10 @@ AccordionItem.displayName = "AccordionItem";
|
|
|
120
149
|
|
|
121
150
|
// ==== ACCORDION CONTROL ====
|
|
122
151
|
export type AccordionControlWrapperProps = RecursicaOverStyled<
|
|
123
|
-
|
|
152
|
+
// Mantine's native `icon` slot is resolved internally from `leftIcon` — omitted so a
|
|
153
|
+
// caller can't silently override it by passing `icon` directly. Mantine's per-control
|
|
154
|
+
// `chevron` override is left as-is; nothing here computes it, so it passes through safely.
|
|
155
|
+
Omit<AccordionControlProps, "icon"> & RecursicaAccordionControlProps
|
|
124
156
|
>;
|
|
125
157
|
|
|
126
158
|
export const AccordionControl = forwardRef<
|
|
@@ -154,8 +186,9 @@ export const AccordionControl = forwardRef<
|
|
|
154
186
|
AccordionControl.displayName = "AccordionControl";
|
|
155
187
|
|
|
156
188
|
// ==== ACCORDION PANEL ====
|
|
157
|
-
export type AccordionPanelWrapperProps =
|
|
158
|
-
|
|
189
|
+
export type AccordionPanelWrapperProps = RecursicaOverStyled<
|
|
190
|
+
AccordionPanelProps & RecursicaAccordionPanelProps
|
|
191
|
+
>;
|
|
159
192
|
|
|
160
193
|
export const AccordionPanel = forwardRef<
|
|
161
194
|
HTMLDivElement,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# AssistiveElement – implementation notes
|
|
2
|
+
|
|
3
|
+
Decisions and design tweaks specific to the UI Kit's AssistiveElement wrapped against
|
|
4
|
+
`@mantine/core`.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Plain `<div>` — no underlying component wrapped
|
|
9
|
+
|
|
10
|
+
**Decision:** Unlike most components in this adapter, `AssistiveElement` doesn't wrap any real
|
|
11
|
+
`@mantine/core` component at all — it's a bare `<div>` with two `<span>` wrappers (icon, text).
|
|
12
|
+
Mantine has no dedicated helper/error-text component to wrap in the first place (it composes
|
|
13
|
+
that presentation ad hoc per-field via `TextInput`'s own `error`/`description` props instead of
|
|
14
|
+
a standalone component), so there's nothing here to inherit layout or ARIA semantics from.
|
|
15
|
+
**Implementation:** A `<div>` is a reasonable generic container, but it carries no semantic
|
|
16
|
+
meaning of its own. `role="alert"` is now defaulted automatically for the `"error"` variant
|
|
17
|
+
(see §3) — the one piece of ARIA behavior that genuinely belongs at this component's own level.
|
|
18
|
+
`aria-describedby`/`aria-errormessage` association with a field is a different story: this
|
|
19
|
+
component has no reference to any sibling field, so it can't wire that itself. It's already
|
|
20
|
+
handled one layer up, in `FormControlWrapper` — which generates the ids, passes them down via
|
|
21
|
+
this component's own `id` prop, and clones `aria-describedby`/`aria-errormessage` onto the
|
|
22
|
+
field. Only a fully standalone `<AssistiveElement>` used outside `FormControlWrapper` still
|
|
23
|
+
needs that wiring done by hand, via the standard HTML attributes that pass through `{...rest}`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. Fixed icon per variant, no custom icon slot
|
|
28
|
+
|
|
29
|
+
**Decision:** `assistiveWithIcon` only toggles visibility — there's no prop to swap in a custom
|
|
30
|
+
icon. The icon is always the one belonging to the current `assistiveVariant` (`InfoIcon` for
|
|
31
|
+
`"help"`, `AlertIcon` for `"error"`).
|
|
32
|
+
**Implementation:** `IconComponent = assistiveVariant === "error" ? AlertIcon : InfoIcon` is
|
|
33
|
+
resolved directly in the render body; there's no icon prop in `RecursicaAssistiveElementProps`
|
|
34
|
+
to intercept.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 3. `role="alert"` defaults on for the error variant
|
|
39
|
+
|
|
40
|
+
**Decision:** Error text needs to be announced by assistive tech as it appears or changes;
|
|
41
|
+
static help text doesn't. Rather than requiring every integrator to remember `role="alert"`,
|
|
42
|
+
`assistiveVariant="error"` defaults it automatically.
|
|
43
|
+
**Implementation:** `role` is destructured out and resolved as `role ?? (assistiveVariant ===
|
|
44
|
+
"error" ? "alert" : undefined)` before the spread — an explicit caller-supplied `role` always
|
|
45
|
+
wins over the default, and `"help"` gets no default at all.
|
|
@@ -52,6 +52,7 @@ export const AssistiveElement = forwardRef<
|
|
|
52
52
|
assistiveWithIcon = true,
|
|
53
53
|
children,
|
|
54
54
|
className,
|
|
55
|
+
role,
|
|
55
56
|
overStyled = false,
|
|
56
57
|
...rest
|
|
57
58
|
},
|
|
@@ -65,13 +66,19 @@ export const AssistiveElement = forwardRef<
|
|
|
65
66
|
|
|
66
67
|
const IconComponent = assistiveVariant === "error" ? AlertIcon : InfoIcon;
|
|
67
68
|
|
|
69
|
+
// Announce error text as it appears/changes; a caller-supplied `role` always wins. No
|
|
70
|
+
// default for `help` — static descriptive text doesn't need a live region.
|
|
71
|
+
const resolvedRole =
|
|
72
|
+
role ?? (assistiveVariant === "error" ? "alert" : undefined);
|
|
73
|
+
|
|
68
74
|
return (
|
|
69
75
|
<div
|
|
70
76
|
ref={ref}
|
|
71
77
|
className={finalClass}
|
|
72
78
|
data-variant={assistiveVariant}
|
|
79
|
+
role={resolvedRole}
|
|
73
80
|
style={restRecord.style as React.CSSProperties}
|
|
74
|
-
{...restRecord} // Spread standard HTML attributes (like id, aria
|
|
81
|
+
{...restRecord} // Spread standard HTML attributes (like id, aria-*) natively.
|
|
75
82
|
>
|
|
76
83
|
{assistiveWithIcon && (
|
|
77
84
|
<span className={styles.iconWrapper}>
|
|
@@ -151,10 +151,7 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
|
|
|
151
151
|
disabled={disabled}
|
|
152
152
|
value={value as string | undefined}
|
|
153
153
|
defaultValue={defaultValue as string | undefined}
|
|
154
|
-
|
|
155
|
-
"data-disabled": disabled ? "true" : undefined,
|
|
156
|
-
"data-error": error ? "true" : undefined,
|
|
157
|
-
}}
|
|
154
|
+
error={!!error}
|
|
158
155
|
{...(sanitizedProps as unknown as MantineAutocompleteProps)}
|
|
159
156
|
/>
|
|
160
157
|
}
|
|
@@ -22,3 +22,13 @@ Since Avatar children (icons or initials) require robust centering that might di
|
|
|
22
22
|
### CSS Reset Hacks
|
|
23
23
|
|
|
24
24
|
Noticeable `/* HARDCODE: ... */` hacks are deployed within `.root` to completely zero-out Mantine's `--avatar-bg` and internal variables statically since Recursica handles background-colors inherently via the CSS variants cascade.
|
|
25
|
+
|
|
26
|
+
### `variant` mapping is a genuine match here (unlike MUI's)
|
|
27
|
+
|
|
28
|
+
Mantine's native `Avatar.variant` (`'filled'|'light'|'gradient'|'outline'|'transparent'|...`)
|
|
29
|
+
really is the same color-treatment concept Recursica's own `variant` is, so `mapVariant`
|
|
30
|
+
(`solid→filled, outline→outline, ghost→transparent`) is a correct, meaningful mapping — even
|
|
31
|
+
though the CSS reset hack above means it's visually inert either way. Worth noting only because
|
|
32
|
+
the MUI adapter's `Avatar.variant` means something completely different (shape) and had a real
|
|
33
|
+
bug from assuming the same word meant the same thing there; see mui-adapter's own
|
|
34
|
+
`AVATAR_IMPLEMENTATION_NOTES.md`.
|
|
@@ -401,3 +401,16 @@
|
|
|
401
401
|
--recursica_ui-kit_components_button_variants_styles_text_variants_states_disabled_properties_elevation
|
|
402
402
|
);
|
|
403
403
|
}
|
|
404
|
+
|
|
405
|
+
/* Focus State Mapping (native browser focus outline replaced with the recursica focus ring).
|
|
406
|
+
Placed after every variant's own box-shadow (elevation) rule so it wins the cascade at equal
|
|
407
|
+
attribute-selector specificity regardless of variant. */
|
|
408
|
+
.root:focus-visible {
|
|
409
|
+
outline: none;
|
|
410
|
+
box-shadow:
|
|
411
|
+
0 0 0 var(--recursica_brand_states_focus_border-size)
|
|
412
|
+
var(--recursica_brand_states_focus_color),
|
|
413
|
+
0 0 var(--recursica_brand_states_focus_blur)
|
|
414
|
+
var(--recursica_brand_states_focus_margin)
|
|
415
|
+
var(--recursica_brand_states_focus_color);
|
|
416
|
+
}
|
|
@@ -36,3 +36,62 @@ To accommodate this, the visual "close" icon uses a `<span>` element configured
|
|
|
36
36
|
### Removing Sizing Properties
|
|
37
37
|
|
|
38
38
|
During implementation, the parsed Figma design tokens natively exported specific height/padding vectors dynamically (e.g., `--recursica_ui-kit_components_chip_properties_icon-size`) rather than explicit string variants (`sm`, `md`, `lg`). Therefore, we omitted `size` conceptually from the `RecursicaChipProps` wrapper to lock down size evaluation natively against the active layer variables.
|
|
39
|
+
|
|
40
|
+
### Long-label truncation was hiding the remove icon (Matt Massey, 2026-08-17)
|
|
41
|
+
|
|
42
|
+
`.children` already had `text-overflow: ellipsis; overflow: hidden; white-space: nowrap;`, but
|
|
43
|
+
neither it nor its parent `.innerWrapper` had `min-width: 0` — a flex child without that refuses
|
|
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 `.removeIcon` had no
|
|
46
|
+
`flex-shrink: 0`, it got squeezed out of the clipped (`max-width`-bounded) chip entirely. Fixed by
|
|
47
|
+
adding `min-width: 0` to `.innerWrapper`/`.children` and `flex-shrink: 0` to `.leadingIcon`/
|
|
48
|
+
`.removeIcon`. Reproduces easily with `FileUpload`'s file list, since its chip `max-width`
|
|
49
|
+
(`--recursica_ui-kit_components_chip_properties_max-width`, 200px) is small enough that most real
|
|
50
|
+
filenames overflow it.
|
|
51
|
+
|
|
52
|
+
### Descenders were being clipped on `.children` and `.root` (Matt Massey, 2026-08-18)
|
|
53
|
+
|
|
54
|
+
A label with a descender (e.g. the "g" in "image.png") had its bottom clipped off. Both `.children`
|
|
55
|
+
and `.root.root` set plain `overflow: hidden` — needed on the x-axis so `.children`'s
|
|
56
|
+
`text-overflow: ellipsis` (and `.root`'s `max-width`) actually clip long labels, but clipping the
|
|
57
|
+
y-axis too cuts off any glyph ink that extends past the line box, which happens whenever the
|
|
58
|
+
`text_line-height` token is tighter than the font's natural ascent+descent. Fixed by splitting both
|
|
59
|
+
into `overflow-x: hidden; overflow-y: visible;` — ellipsis/max-width truncation is unaffected (still
|
|
60
|
+
x-axis only), but descenders can now paint outside a too-tight line box instead of being clipped.
|
|
61
|
+
`.root` has no background/border-radius of its own to protect (that's `.label`), so opening its
|
|
62
|
+
y-axis has no visual side effect.
|
|
63
|
+
|
|
64
|
+
### Roving tabindex support for chip groups (Matt Massey, 2026-08-17)
|
|
65
|
+
|
|
66
|
+
Added two optional pass-through props — `removeTabIndex` and `removeIconRef` — purely so a parent
|
|
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 `removeTabIndex={-1}` on every
|
|
69
|
+
chip but the currently-active one, and use `removeIconRef` to move real DOM focus there
|
|
70
|
+
imperatively on arrow-key press. Both are no-ops for a standalone `Chip` (defaults: `tabIndex={0}`,
|
|
71
|
+
no ref) — this doesn't change any existing single-chip behavior.
|
|
72
|
+
|
|
73
|
+
### `isInteractive` was measuring the wrong signal, and leaked a pointer cursor + phantom Tab stop (Matt Massey, 2026-08-18)
|
|
74
|
+
|
|
75
|
+
A `Chip` rendered with `checked` but no real handler (e.g. `FileUpload`'s `readOnly` file list —
|
|
76
|
+
`<Chip checked={false} tabIndex={-1}>`, no `onRemove`) still looked and behaved clickable, from two
|
|
77
|
+
separate bugs:
|
|
78
|
+
|
|
79
|
+
- `isInteractive` treated `checked !== undefined` (even `false`) as proof of interactivity. That's
|
|
80
|
+
the wrong signal — a `checked`-controlled chip with no `onChange` can't actually be toggled by a
|
|
81
|
+
click (Mantine's `useUncontrolled` discards the click when `value` is externally controlled), so
|
|
82
|
+
clicking it does nothing observable regardless. `isInteractive` now only looks at whether
|
|
83
|
+
something actually responds: `onRemove`, `onClick`, or `onChange`.
|
|
84
|
+
- `.label.label` never set its own `cursor`, so Mantine's base style (`cursor: pointer`, hardcoded
|
|
85
|
+
on the underlying `mantine-Chip-label` class) always leaked through, independent of whether the
|
|
86
|
+
chip was actually interactive. Reset it to `cursor: default` and added a `data-interactive`
|
|
87
|
+
attribute (driven by the fixed `isInteractive` above, set via `wrapperProps` on `MantineChip` so
|
|
88
|
+
it lands on `.root`) that re-enables `pointer` via `.root[data-interactive] .label.label` only
|
|
89
|
+
when there's a real handler.
|
|
90
|
+
|
|
91
|
+
Separately, `.children`'s `overflow-x: hidden` (added for ellipsis truncation) made it a scroll
|
|
92
|
+
container, and Chromium auto-adds scroll containers with actually-overflowing content to the Tab
|
|
93
|
+
order — with no `tabindex` attribute at all — so a solo Tab press could land on a chip's plain
|
|
94
|
+
filename text before ever reaching a real control. Switched to `overflow-x: clip`, which doesn't
|
|
95
|
+
establish a scrollport (same visual clipping, still x-axis only per the descender fix above), so
|
|
96
|
+
it's no longer a focus candidate. This affected every chip with long enough content, not just
|
|
97
|
+
read-only ones — see `FileUpload`'s own `IMPLEMENTATION_NOTES.md` for how it surfaced there.
|
|
@@ -53,9 +53,11 @@
|
|
|
53
53
|
--recursica_ui-kit_components_chip_properties_border-radius
|
|
54
54
|
);
|
|
55
55
|
|
|
56
|
-
cursor: pointer;
|
|
57
56
|
user-select: none;
|
|
58
|
-
overflow: hidden;
|
|
57
|
+
overflow-x: hidden; /* HARDCODE: horizontal-only, same descender reasoning as .children below —
|
|
58
|
+
.root has no visible background of its own (that's .label), so there's nothing here that
|
|
59
|
+
needs vertical clipping */
|
|
60
|
+
overflow-y: visible;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/* We target the mantine label directly because that is the visible container in Mantine's Chip */
|
|
@@ -67,6 +69,10 @@
|
|
|
67
69
|
|
|
68
70
|
/* Reset mantine styles */
|
|
69
71
|
background: transparent; /* HARDCODE: Mantine forces background styling on label */
|
|
72
|
+
cursor: default; /* HARDCODE: Mantine's own base styles hardcode cursor: pointer on this class —
|
|
73
|
+
override it to plain default, then only re-enable it below for a chip with a real handler
|
|
74
|
+
(onRemove/onClick/onChange — see Chip.tsx's `isInteractive`). A display-only chip (e.g. a
|
|
75
|
+
read-only FileUpload file list) has nothing for a click to do, so it shouldn't look clickable. */
|
|
70
76
|
border-style: solid;
|
|
71
77
|
border-width: var(--recursica_ui-kit_components_chip_properties_border-size);
|
|
72
78
|
padding: var(--recursica_ui-kit_components_chip_properties_vertical-padding)
|
|
@@ -110,12 +116,22 @@
|
|
|
110
116
|
.label.label > span:not(.mantineIconWrapper) {
|
|
111
117
|
display: inline-flex;
|
|
112
118
|
align-items: center;
|
|
119
|
+
flex: 1 1 auto; /* HARDCODE: let this anonymous wrapper actually fill/shrink to .label's real
|
|
120
|
+
width instead of sizing to its content — otherwise a long .children label overflows this
|
|
121
|
+
span well past .label's 200px max-width, and .children's own text-overflow: ellipsis never
|
|
122
|
+
engages because ITS box is still as wide as the unwrapped text (see CHIP_IMPLEMENTATION_NOTES.md) */
|
|
123
|
+
min-width: 0; /* HARDCODE: flex items default to min-width: auto (their content's min-content
|
|
124
|
+
size) — without this override, flex-shrink is a no-op and the span never shrinks below that */
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
.label.label:hover {
|
|
116
128
|
background-color: var(--chip-bg);
|
|
117
129
|
}
|
|
118
130
|
|
|
131
|
+
.root[data-interactive] .label.label {
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
/* Default (Unselected) hover/active interactions (Mantine overrides) */
|
|
120
136
|
.label.label[data-checked] {
|
|
121
137
|
--chip-bg: var(
|
|
@@ -191,12 +207,21 @@
|
|
|
191
207
|
align-items: center;
|
|
192
208
|
gap: var(--recursica_ui-kit_components_chip_properties_icon-text-gap);
|
|
193
209
|
width: 100%;
|
|
210
|
+
min-width: 0; /* HARDCODE: lets .children actually shrink and ellipsize instead of overflowing */
|
|
194
211
|
}
|
|
195
212
|
|
|
196
213
|
.children {
|
|
197
214
|
flex-grow: 1;
|
|
215
|
+
min-width: 0; /* HARDCODE: a flex child needs this for text-overflow: ellipsis to engage at all */
|
|
198
216
|
text-overflow: ellipsis;
|
|
199
|
-
overflow:
|
|
217
|
+
overflow-x: clip; /* HARDCODE: text-overflow: ellipsis only needs the x-axis clipped — clipping
|
|
218
|
+
y too (plain `overflow: hidden`) cut off descenders (e.g. the "g" in "image.png") whenever the
|
|
219
|
+
line-height token is tighter than the font's natural glyph extent (Matt Massey, 2026-08-18).
|
|
220
|
+
`clip` rather than `hidden` because `hidden` makes this a scroll container, and Chromium
|
|
221
|
+
auto-adds scroll containers with overflowing content to the Tab order (so a browser user can
|
|
222
|
+
arrow-key-scroll them) — `clip` doesn't create a scrollport, so long/truncated chip labels
|
|
223
|
+
(e.g. a read-only FileUpload file list) don't pick up a phantom, un-styled tab stop. */
|
|
224
|
+
overflow-y: visible;
|
|
200
225
|
white-space: nowrap;
|
|
201
226
|
}
|
|
202
227
|
|
|
@@ -204,6 +229,7 @@
|
|
|
204
229
|
display: inline-flex;
|
|
205
230
|
align-items: center;
|
|
206
231
|
justify-content: center;
|
|
232
|
+
flex-shrink: 0; /* HARDCODE: keep the icon from being squeezed by a long, truncating label */
|
|
207
233
|
color: var(--chip-icon);
|
|
208
234
|
width: var(--recursica_ui-kit_components_chip_properties_icon-size);
|
|
209
235
|
height: var(--recursica_ui-kit_components_chip_properties_icon-size);
|
|
@@ -218,6 +244,7 @@
|
|
|
218
244
|
display: inline-flex;
|
|
219
245
|
align-items: center;
|
|
220
246
|
justify-content: center;
|
|
247
|
+
flex-shrink: 0; /* HARDCODE: keep the remove icon visible when the label truncates instead */
|
|
221
248
|
color: var(--chip-close);
|
|
222
249
|
width: var(--recursica_ui-kit_components_chip_properties_close-icon-size);
|
|
223
250
|
height: var(--recursica_ui-kit_components_chip_properties_close-icon-size);
|
|
@@ -227,7 +254,12 @@
|
|
|
227
254
|
}
|
|
228
255
|
|
|
229
256
|
.removeIcon:focus-visible {
|
|
230
|
-
box-shadow:
|
|
257
|
+
box-shadow:
|
|
258
|
+
0 0 0 var(--recursica_brand_states_focus_border-size)
|
|
259
|
+
var(--recursica_brand_states_focus_color),
|
|
260
|
+
0 0 var(--recursica_brand_states_focus_blur)
|
|
261
|
+
var(--recursica_brand_states_focus_margin)
|
|
262
|
+
var(--recursica_brand_states_focus_color);
|
|
231
263
|
}
|
|
232
264
|
|
|
233
265
|
.removeIcon svg {
|
|
@@ -41,6 +41,8 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
|
|
|
41
41
|
icon,
|
|
42
42
|
onRemove,
|
|
43
43
|
removeLabel = "Remove",
|
|
44
|
+
removeTabIndex,
|
|
45
|
+
removeIconRef,
|
|
44
46
|
children,
|
|
45
47
|
overStyled = false,
|
|
46
48
|
...rest
|
|
@@ -79,18 +81,24 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
|
|
|
79
81
|
// Determine state
|
|
80
82
|
const dataError = error ? "" : undefined;
|
|
81
83
|
const isIconOnly = !children && (!!icon || !!onRemove);
|
|
84
|
+
// A chip only counts as interactive when something actually responds to it — merely passing a
|
|
85
|
+
// `checked` value (e.g. to pin a display-only chip to a fixed visual state, as FileUpload's
|
|
86
|
+
// read-only file list does) isn't itself an interaction, since clicking it with no onChange/
|
|
87
|
+
// onClick wired does nothing observable.
|
|
82
88
|
const isInteractive =
|
|
83
|
-
restRecord.checked !== undefined ||
|
|
84
|
-
restRecord.defaultChecked !== undefined ||
|
|
85
89
|
onRemove !== undefined ||
|
|
86
|
-
restRecord.onClick !== undefined
|
|
90
|
+
restRecord.onClick !== undefined ||
|
|
91
|
+
restRecord.onChange !== undefined;
|
|
87
92
|
|
|
88
93
|
return (
|
|
89
94
|
<MantineChip
|
|
90
95
|
ref={ref}
|
|
91
96
|
className={finalClass}
|
|
92
97
|
classNames={mergedClassNames}
|
|
93
|
-
wrapperProps={
|
|
98
|
+
wrapperProps={{
|
|
99
|
+
...(dataError !== undefined ? { "data-error": "" } : {}),
|
|
100
|
+
...(isInteractive ? { "data-interactive": "" } : {}),
|
|
101
|
+
}}
|
|
94
102
|
{...(isIconOnly ? { "data-icon-only": "" } : {})}
|
|
95
103
|
{...(!isInteractive ? { tabIndex: -1, "aria-hidden": true } : {})}
|
|
96
104
|
{...sanitizedProps}
|
|
@@ -106,6 +114,7 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
|
|
|
106
114
|
|
|
107
115
|
{onRemove && (
|
|
108
116
|
<span
|
|
117
|
+
ref={removeIconRef}
|
|
109
118
|
role="button"
|
|
110
119
|
className={styles.removeIcon}
|
|
111
120
|
onClick={(e) => {
|
|
@@ -123,7 +132,7 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
|
|
|
123
132
|
);
|
|
124
133
|
}
|
|
125
134
|
}}
|
|
126
|
-
tabIndex={0}
|
|
135
|
+
tabIndex={removeTabIndex ?? 0}
|
|
127
136
|
>
|
|
128
137
|
<CloseIcon />
|
|
129
138
|
</span>
|
|
@@ -46,3 +46,10 @@ The remove/close action is keyboard accessible — it can be activated via keybo
|
|
|
46
46
|
### Sizing
|
|
47
47
|
|
|
48
48
|
Chip does not support a `size` prop; chips render at a fixed size.
|
|
49
|
+
|
|
50
|
+
### Building a keyboard-navigable chip group
|
|
51
|
+
|
|
52
|
+
`removeTabIndex` and `removeIconRef` are optional escape hatches for composing a _group_ of chips
|
|
53
|
+
with roving-tabindex keyboard navigation (Tab reaches one chip at a time, arrow keys move between
|
|
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}` remove icon with no ref.
|