@recursica/mui-adapter 0.26.0 → 0.28.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 +17 -0
- package/dist/index.d.ts +83 -1
- package/dist/mui-adapter.cjs +73 -73
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +9400 -9255
- package/dist/mui-adapter.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Autocomplete/Autocomplete.tsx +46 -34
- package/src/components/Avatar/Avatar.tsx +11 -21
- package/src/components/Badge/Badge.tsx +9 -13
- package/src/components/Breadcrumb/Breadcrumb.module.css +10 -5
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +0 -6
- package/src/components/Breadcrumb/Breadcrumb.tsx +13 -18
- package/src/components/Button/Button.tsx +17 -11
- package/src/components/Card/Card.tsx +9 -19
- package/src/components/Checkbox/Checkbox.tsx +22 -26
- package/src/components/Checkbox/CheckboxGroup.tsx +3 -3
- package/src/components/Chip/Chip.tsx +12 -19
- package/src/components/Dropdown/BareDropdown.tsx +14 -8
- package/src/components/Dropdown/Dropdown.icons.tsx +33 -0
- package/src/components/Dropdown/Dropdown.module.css +117 -15
- package/src/components/Dropdown/Dropdown.stories.tsx +21 -0
- package/src/components/Dropdown/Dropdown.tsx +102 -17
- package/src/components/Dropdown/USAGE.md +7 -0
- package/src/components/FileInput/FileInput.tsx +1 -1
- package/src/components/FileUpload/FileUpload.tsx +1 -1
- package/src/components/FormControlLayout/FormControlLayout.tsx +1 -1
- package/src/components/FormControlWrapper/FormControlWrapper.tsx +1 -1
- package/src/components/HoverCard/HoverCard.module.css +25 -3
- package/src/components/HoverCard/HoverCard.tsx +13 -21
- package/src/components/Menu/Menu.module.css +3 -0
- package/src/components/Menu/Menu.stories.tsx +128 -5
- package/src/components/Menu/Menu.tsx +15 -7
- package/src/components/Modal/Modal.tsx +12 -6
- package/src/components/NumberInput/NumberInput.tsx +30 -15
- package/src/components/Pagination/Pagination.tsx +5 -15
- package/src/components/Panel/Panel.tsx +17 -25
- package/src/components/Popover/Popover.tsx +10 -18
- package/src/components/Radio/Radio.tsx +21 -26
- package/src/components/Radio/RadioGroup.tsx +3 -4
- package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +1 -1
- package/src/components/SegmentedControl/SegmentedControl.tsx +20 -25
- package/src/components/Slider/Slider.tsx +40 -43
- package/src/components/Stepper/Stepper.tsx +34 -14
- package/src/components/Switch/Switch.tsx +33 -49
- package/src/components/Switch/SwitchGroup.tsx +3 -3
- package/src/components/Tabs/Tabs.tsx +12 -4
- package/src/components/TextArea/TextArea.tsx +30 -7
- package/src/components/TextField/TextField.tsx +30 -26
- package/src/components/Timeline/Timeline.tsx +9 -1
- package/src/components/Timeline/TimelineItem.tsx +2 -2
- package/src/components/Toast/Toast.tsx +14 -23
- package/src/components/Tooltip/Tooltip.tsx +14 -27
- package/src/components/TransferList/TransferList.tsx +1 -1
- package/src/utils/filterStylingProps.ts +4 -0
|
@@ -59,20 +59,28 @@
|
|
|
59
59
|
box-sizing: border-box;
|
|
60
60
|
margin: 0;
|
|
61
61
|
|
|
62
|
-
/* Box Geometry
|
|
63
|
-
|
|
62
|
+
/* Box Geometry
|
|
63
|
+
`!important` throughout: MUI's own `.MuiSelect-select`/`.MuiOutlinedInput-input` styled rules
|
|
64
|
+
(e.g. `minHeight: 1.4375em`, and outlined-variant default padding) are equal-specificity
|
|
65
|
+
single-class selectors injected by emotion at runtime, after this CSS module's own stylesheet
|
|
66
|
+
— on that tie they win by source order, which is what silently shrank the control below the
|
|
67
|
+
min-height token (measured 42.8px vs Mantine's 48px) even though the token was already wired
|
|
68
|
+
here. Same root cause as the notchedOutline double-border fix below. */
|
|
69
|
+
min-height: var(
|
|
70
|
+
--recursica_ui-kit_components_dropdown_properties_min-height
|
|
71
|
+
) !important;
|
|
64
72
|
padding-top: var(
|
|
65
73
|
--recursica_ui-kit_components_dropdown_properties_vertical-padding
|
|
66
|
-
);
|
|
74
|
+
) !important;
|
|
67
75
|
padding-bottom: var(
|
|
68
76
|
--recursica_ui-kit_components_dropdown_properties_vertical-padding
|
|
69
|
-
);
|
|
77
|
+
) !important;
|
|
70
78
|
padding-left: var(
|
|
71
79
|
--recursica_ui-kit_components_dropdown_properties_horizontal-padding
|
|
72
|
-
);
|
|
80
|
+
) !important;
|
|
73
81
|
padding-right: var(
|
|
74
82
|
--recursica_ui-kit_components_dropdown_properties_horizontal-padding
|
|
75
|
-
);
|
|
83
|
+
) !important;
|
|
76
84
|
border-radius: var(
|
|
77
85
|
--recursica_ui-kit_components_dropdown_properties_border-radius
|
|
78
86
|
);
|
|
@@ -125,23 +133,47 @@
|
|
|
125
133
|
color: inherit;
|
|
126
134
|
}
|
|
127
135
|
|
|
136
|
+
/* MUI's outlined variant renders its own `<fieldset class="MuiOutlinedInput-notchedOutline">` as a
|
|
137
|
+
separate absolutely-positioned sibling of `.input` — independent of the border we already draw
|
|
138
|
+
on `.input` above. Left alone, it shows its own default/focused/error border (theme grays/blue/
|
|
139
|
+
red) UNDERNEATH ours, which is what produced the double border (bug: static-error story showing
|
|
140
|
+
a leftover dark border under the red one) and MUI's own blue focus ring leaking in alongside the
|
|
141
|
+
Recursica focus box-shadow. We own the border entirely via `.input`, so this native outline is
|
|
142
|
+
never needed — always neutralize it. */
|
|
143
|
+
.root :global(.MuiOutlinedInput-notchedOutline) {
|
|
144
|
+
border: none !important;
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
/* Dynamic Padding Overrides */
|
|
129
148
|
.root[data-with-left-section] .input {
|
|
130
|
-
padding-left: var(--input-left-section-size);
|
|
149
|
+
padding-left: var(--input-left-section-size) !important;
|
|
131
150
|
}
|
|
132
151
|
|
|
133
152
|
.root[data-with-right-section] .input {
|
|
134
|
-
padding-right: var(--input-right-section-size);
|
|
153
|
+
padding-right: var(--input-right-section-size) !important;
|
|
135
154
|
}
|
|
136
155
|
|
|
137
|
-
/* Flexible End-Caps (Icons, actions)
|
|
156
|
+
/* Flexible End-Caps (Icons, actions)
|
|
157
|
+
Absolutely positioned within `.root` (already `position: relative`), NOT flex siblings of
|
|
158
|
+
`.input` in the row. MUI passes `startAdornment`/`endAdornment` as ordinary flex children next
|
|
159
|
+
to the select div, so as siblings they'd add their own width on top of `.input`'s already-100%-
|
|
160
|
+
width box instead of sharing it — the reserved padding above only carves out space *inside*
|
|
161
|
+
`.input`'s own bordered box, so a sibling rendered outside that box overflows past the visible
|
|
162
|
+
border (bug: leading icon / the clear+chevron pair sat outside the control's right edge).
|
|
163
|
+
Absolute positioning is Mantine's own technique for the equivalent `Input.Section` slot, which
|
|
164
|
+
is why the mantine-adapter never had this bug. */
|
|
138
165
|
.section {
|
|
166
|
+
position: absolute;
|
|
167
|
+
top: 0;
|
|
168
|
+
bottom: 0;
|
|
139
169
|
display: flex;
|
|
140
170
|
align-items: center;
|
|
141
|
-
|
|
171
|
+
gap: var(--recursica_ui-kit_components_dropdown_properties_icon-text-gap);
|
|
172
|
+
pointer-events: none;
|
|
142
173
|
}
|
|
143
174
|
|
|
144
175
|
.section[data-position="left"] {
|
|
176
|
+
left: 0;
|
|
145
177
|
justify-content: flex-start;
|
|
146
178
|
padding-left: var(
|
|
147
179
|
--recursica_ui-kit_components_dropdown_properties_horizontal-padding
|
|
@@ -149,10 +181,14 @@
|
|
|
149
181
|
}
|
|
150
182
|
|
|
151
183
|
.section[data-position="right"] {
|
|
184
|
+
right: 0;
|
|
152
185
|
justify-content: flex-end;
|
|
153
186
|
padding-right: var(
|
|
154
187
|
--recursica_ui-kit_components_dropdown_properties_horizontal-padding
|
|
155
188
|
);
|
|
189
|
+
/* Unlike the left section (decorative icon only), this one can hold the interactive clear
|
|
190
|
+
button, so it needs to opt back into pointer events itself. */
|
|
191
|
+
pointer-events: auto;
|
|
156
192
|
}
|
|
157
193
|
|
|
158
194
|
.section :global(svg) {
|
|
@@ -169,6 +205,42 @@
|
|
|
169
205
|
);
|
|
170
206
|
}
|
|
171
207
|
|
|
208
|
+
/* Clear ("x") button rendered inside the right section when `clearable` and a value is present.
|
|
209
|
+
Sized/colored via the same icon tokens as the rest of the right section (rather than any
|
|
210
|
+
default browser/library button chrome), same treatment as the mantine-adapter's equivalent
|
|
211
|
+
fix to its native clear button (see DROPDOWN_IMPLEMENTATION_NOTES.md). */
|
|
212
|
+
.clearButton {
|
|
213
|
+
display: inline-flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
justify-content: center;
|
|
216
|
+
flex-shrink: 0;
|
|
217
|
+
width: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
|
|
218
|
+
height: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
|
|
219
|
+
padding: 0;
|
|
220
|
+
border: none;
|
|
221
|
+
border-radius: 50%;
|
|
222
|
+
background: transparent;
|
|
223
|
+
color: var(
|
|
224
|
+
--recursica_ui-kit_components_dropdown_properties_colors_trailing-icon
|
|
225
|
+
);
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
outline: none;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.clearButton svg {
|
|
231
|
+
width: 100%;
|
|
232
|
+
height: 100%;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.clearButton:focus-visible {
|
|
236
|
+
box-shadow:
|
|
237
|
+
0 0 0 var(--recursica_brand_states_focus_border-size)
|
|
238
|
+
var(--recursica_brand_states_focus_color),
|
|
239
|
+
0 0 var(--recursica_brand_states_focus_blur)
|
|
240
|
+
var(--recursica_brand_states_focus_margin)
|
|
241
|
+
var(--recursica_brand_states_focus_color);
|
|
242
|
+
}
|
|
243
|
+
|
|
172
244
|
/* -------------------------------------
|
|
173
245
|
STATE CASCADE ARCHITECTURE
|
|
174
246
|
-------------------------------------- */
|
|
@@ -256,6 +328,21 @@
|
|
|
256
328
|
overflow: hidden;
|
|
257
329
|
}
|
|
258
330
|
|
|
331
|
+
/* The visible top/bottom gap inside the open menu, before/after the option list, is MUI's own
|
|
332
|
+
`.MuiList-padding` default (8px) on the inner `<ul>` — not driven by any Recursica token. Wire
|
|
333
|
+
it to the same dropdown vertical-padding token the closed control itself uses, matching the
|
|
334
|
+
mantine-adapter fix (its `.dropdown` had the same gap, driven by Mantine's own untokenized
|
|
335
|
+
4px `--combobox-padding` default). `!important` needed to beat MUI's own emotion-generated
|
|
336
|
+
`.MuiList-padding` rule at equal selector specificity. */
|
|
337
|
+
.dropdown :global(.MuiMenu-list) {
|
|
338
|
+
padding-top: var(
|
|
339
|
+
--recursica_ui-kit_components_dropdown_properties_vertical-padding
|
|
340
|
+
) !important;
|
|
341
|
+
padding-bottom: var(
|
|
342
|
+
--recursica_ui-kit_components_dropdown_properties_vertical-padding
|
|
343
|
+
) !important;
|
|
344
|
+
}
|
|
345
|
+
|
|
259
346
|
.option {
|
|
260
347
|
font-family: var(
|
|
261
348
|
--recursica_ui-kit_components_dropdown_properties_text_font-family
|
|
@@ -277,10 +364,11 @@
|
|
|
277
364
|
cursor: pointer;
|
|
278
365
|
}
|
|
279
366
|
|
|
280
|
-
/* No per-option
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
367
|
+
/* No per-option hovered token exists in the schema; use the generic overlay tint (same technique
|
|
368
|
+
as the Mantine reference implementation) to highlight a hovered-but-not-selected option.
|
|
369
|
+
`!important` is required here: MUI's own `.Mui-selected` (and `.Mui-selected:hover`) classes
|
|
370
|
+
carry their own default primary-blue tint at equal selector specificity, and without it that
|
|
371
|
+
default tint wins over this token-driven one. */
|
|
284
372
|
.option[data-hovered="true"],
|
|
285
373
|
.option:hover {
|
|
286
374
|
background-color: color-mix(
|
|
@@ -288,5 +376,19 @@
|
|
|
288
376
|
var(--recursica_brand_states_hover_color)
|
|
289
377
|
calc(var(--recursica_brand_states_hover_opacity) * 100%),
|
|
290
378
|
transparent
|
|
291
|
-
);
|
|
379
|
+
) !important;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* No dedicated dropdown-option "selected" token exists in the schema either, but the menu-item
|
|
383
|
+
component's selected-state colors are the closest real token family for the same concept (a
|
|
384
|
+
selected row in a list) — reused here rather than the neutral hover tint above, which is what
|
|
385
|
+
previously made the selected option read as grey instead of Recursica's brand color. */
|
|
386
|
+
.option[data-selected="true"],
|
|
387
|
+
.option[data-combobox-selected="true"] {
|
|
388
|
+
background-color: var(
|
|
389
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_background-color
|
|
390
|
+
) !important;
|
|
391
|
+
color: var(
|
|
392
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_text-color
|
|
393
|
+
) !important;
|
|
292
394
|
}
|
|
@@ -65,6 +65,27 @@ export const SearchableClearable: Story = {
|
|
|
65
65
|
},
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
export const WithLeadingIcon: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
label: "Destination",
|
|
71
|
+
startAdornment: (
|
|
72
|
+
<svg
|
|
73
|
+
width="24"
|
|
74
|
+
height="24"
|
|
75
|
+
viewBox="0 0 24 24"
|
|
76
|
+
fill="none"
|
|
77
|
+
stroke="currentColor"
|
|
78
|
+
strokeWidth="2"
|
|
79
|
+
strokeLinecap="round"
|
|
80
|
+
strokeLinejoin="round"
|
|
81
|
+
>
|
|
82
|
+
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
|
|
83
|
+
<circle cx="12" cy="10" r="3"></circle>
|
|
84
|
+
</svg>
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
68
89
|
export const StaticError: Story = {
|
|
69
90
|
args: {
|
|
70
91
|
error: "You must choose a valid destination.",
|
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
import React, { forwardRef, ReactNode } from "react";
|
|
1
|
+
import React, { forwardRef, ReactNode, useEffect, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Select as MuiSelect,
|
|
4
4
|
SelectProps as MuiSelectProps,
|
|
5
|
+
SelectChangeEvent,
|
|
5
6
|
MenuItem,
|
|
6
7
|
} from "@mui/material";
|
|
7
8
|
import { type ReadOnlyControlProps } from "@recursica/adapter-common";
|
|
8
9
|
import {
|
|
9
10
|
filterStylingProps,
|
|
11
|
+
omitUnsupportedProps,
|
|
10
12
|
type RecursicaOverStyled,
|
|
11
13
|
} from "../../utils/filterStylingProps";
|
|
12
14
|
import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
|
|
13
15
|
import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
|
|
16
|
+
import { ChevronIcon, ClearIcon } from "./Dropdown.icons";
|
|
14
17
|
import styles from "./Dropdown.module.css";
|
|
15
18
|
|
|
16
19
|
import { type RecursicaDropdownProps as BaseRecursicaDropdownProps } from "@recursica/adapter-common";
|
|
17
20
|
|
|
21
|
+
// Swapped in for MUI's default `IconComponent` only when we're rendering our own combined
|
|
22
|
+
// clear+chevron `endAdornment` (see `showClear` below) — a stable reference so it isn't recreated
|
|
23
|
+
// every render.
|
|
24
|
+
function HiddenIcon() {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
export interface RecursicaDropdownProps
|
|
19
29
|
extends Omit<
|
|
20
30
|
MuiSelectProps,
|
|
@@ -27,9 +37,13 @@ export interface RecursicaDropdownProps
|
|
|
27
37
|
| "ref"
|
|
28
38
|
| "error"
|
|
29
39
|
>,
|
|
40
|
+
// `onChange` is swept up by the `keyof React.HTMLAttributes<HTMLDivElement>` omission above
|
|
41
|
+
// (it's a standard DOM attribute name too) even though MUI's own `Select#onChange` has a
|
|
42
|
+
// completely different, non-DOM signature — add it back explicitly with MUI's real signature.
|
|
43
|
+
Pick<MuiSelectProps, "onChange">,
|
|
30
44
|
Omit<
|
|
31
45
|
RecursicaFormControlWrapperProps,
|
|
32
|
-
"controlMaxWidth" | "controlMinWidth"
|
|
46
|
+
"controlMaxWidth" | "controlMinWidth" | "onChange"
|
|
33
47
|
>,
|
|
34
48
|
ReadOnlyControlProps,
|
|
35
49
|
BaseRecursicaDropdownProps {}
|
|
@@ -66,19 +80,67 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
66
80
|
emptyValueComponent,
|
|
67
81
|
value,
|
|
68
82
|
defaultValue,
|
|
83
|
+
onChange,
|
|
69
84
|
data,
|
|
85
|
+
startAdornment,
|
|
70
86
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
71
87
|
searchable, // Not natively supported by basic MUI Select, stubbed
|
|
72
|
-
|
|
73
|
-
clearable, // Not natively supported by basic MUI Select, stubbed
|
|
88
|
+
clearable,
|
|
74
89
|
...rest
|
|
75
90
|
} = props;
|
|
76
|
-
|
|
91
|
+
// Props this component intentionally doesn't support — deleted at runtime so they can't leak
|
|
92
|
+
// through even if a caller forces them via plain JavaScript, bypassing the Omit<> above.
|
|
93
|
+
const UNSUPPORTED_PROPS = [
|
|
94
|
+
"size", // Recursica controls sizing via design tokens, not MUI's native small/medium size
|
|
95
|
+
"variant", // Recursica styles the naked select directly; MUI's standard/filled/outlined unused
|
|
96
|
+
] as const satisfies readonly (keyof MuiSelectProps)[];
|
|
97
|
+
|
|
98
|
+
const sanitizedProps = omitUnsupportedProps(
|
|
99
|
+
filterStylingProps(rest, overStyled),
|
|
100
|
+
UNSUPPORTED_PROPS,
|
|
101
|
+
);
|
|
77
102
|
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
78
103
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
// MUI's basic `Select` has no built-in "clearable" concept (unlike Mantine's `Select`, which
|
|
105
|
+
// manages its own internal value so it can reset itself to `null` on demand) — the clear
|
|
106
|
+
// button needs somewhere to write an empty value to, so the value is lifted here rather than
|
|
107
|
+
// left for MUI's own internal uncontrolled state.
|
|
108
|
+
const [internalValue, setInternalValue] = useState<unknown>(
|
|
109
|
+
() => value ?? defaultValue ?? "",
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (value !== undefined) setInternalValue(value);
|
|
114
|
+
}, [value]);
|
|
115
|
+
|
|
116
|
+
const handleChange = (
|
|
117
|
+
event: SelectChangeEvent<unknown>,
|
|
118
|
+
child: ReactNode,
|
|
119
|
+
) => {
|
|
120
|
+
setInternalValue(event.target.value);
|
|
121
|
+
(
|
|
122
|
+
onChange as
|
|
123
|
+
| ((event: SelectChangeEvent<unknown>, child: ReactNode) => void)
|
|
124
|
+
| undefined
|
|
125
|
+
)?.(event, child);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const hasValue =
|
|
129
|
+
internalValue !== "" &&
|
|
130
|
+
internalValue !== null &&
|
|
131
|
+
internalValue !== undefined;
|
|
132
|
+
const showClear = !!clearable && hasValue && !disabled && !readOnly;
|
|
133
|
+
|
|
134
|
+
const handleClear = (event: React.MouseEvent) => {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
event.stopPropagation();
|
|
137
|
+
setInternalValue("");
|
|
138
|
+
(
|
|
139
|
+
onChange as
|
|
140
|
+
| ((event: SelectChangeEvent<unknown>, child: ReactNode) => void)
|
|
141
|
+
| undefined
|
|
142
|
+
)?.({ target: { value: "" } } as SelectChangeEvent<unknown>, null);
|
|
143
|
+
};
|
|
82
144
|
|
|
83
145
|
const injectedStyles = {
|
|
84
146
|
...((style as React.CSSProperties) || {}),
|
|
@@ -98,12 +160,11 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
98
160
|
key={`${item}-${index}`}
|
|
99
161
|
value={item}
|
|
100
162
|
className={styles.option}
|
|
163
|
+
disableRipple
|
|
101
164
|
// Dropdown.module.css's own selected-state tint (`.option[data-selected="true"]`)
|
|
102
165
|
// needs this explicitly — MUI's own `Mui-selected` class carries its default primary-
|
|
103
166
|
// color tint instead, which is what shows through without it.
|
|
104
|
-
data-selected={
|
|
105
|
-
item === (value ?? defaultValue) ? "true" : undefined
|
|
106
|
-
}
|
|
167
|
+
data-selected={item === internalValue ? "true" : undefined}
|
|
107
168
|
>
|
|
108
169
|
{item}
|
|
109
170
|
</MenuItem>
|
|
@@ -115,9 +176,8 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
115
176
|
value={item.value}
|
|
116
177
|
disabled={item.disabled}
|
|
117
178
|
className={styles.option}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
179
|
+
disableRipple
|
|
180
|
+
data-selected={item.value === internalValue ? "true" : undefined}
|
|
121
181
|
>
|
|
122
182
|
{item.label}
|
|
123
183
|
</MenuItem>
|
|
@@ -125,6 +185,26 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
125
185
|
});
|
|
126
186
|
};
|
|
127
187
|
|
|
188
|
+
const wrappedStartAdornment = startAdornment ? (
|
|
189
|
+
<span className={styles.section} data-position="left">
|
|
190
|
+
{startAdornment}
|
|
191
|
+
</span>
|
|
192
|
+
) : undefined;
|
|
193
|
+
|
|
194
|
+
const wrappedEndAdornment = showClear ? (
|
|
195
|
+
<span className={styles.section} data-position="right">
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
className={styles.clearButton}
|
|
199
|
+
aria-label="Clear"
|
|
200
|
+
onClick={handleClear}
|
|
201
|
+
>
|
|
202
|
+
<ClearIcon />
|
|
203
|
+
</button>
|
|
204
|
+
<ChevronIcon />
|
|
205
|
+
</span>
|
|
206
|
+
) : undefined;
|
|
207
|
+
|
|
128
208
|
return (
|
|
129
209
|
<WithReadOnlyWrapper
|
|
130
210
|
className={wrapperClass}
|
|
@@ -159,9 +239,10 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
159
239
|
activeComponent={
|
|
160
240
|
<MuiSelect
|
|
161
241
|
ref={ref}
|
|
242
|
+
{...(sanitizedProps as unknown as MuiSelectProps)}
|
|
162
243
|
disabled={disabled}
|
|
163
|
-
value={
|
|
164
|
-
|
|
244
|
+
value={internalValue}
|
|
245
|
+
onChange={handleChange}
|
|
165
246
|
error={!!error}
|
|
166
247
|
required={required}
|
|
167
248
|
displayEmpty
|
|
@@ -170,6 +251,9 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
170
251
|
select: styles.input,
|
|
171
252
|
icon: styles.icon,
|
|
172
253
|
}}
|
|
254
|
+
IconComponent={showClear ? HiddenIcon : undefined}
|
|
255
|
+
startAdornment={wrappedStartAdornment}
|
|
256
|
+
endAdornment={wrappedEndAdornment}
|
|
173
257
|
MenuProps={{
|
|
174
258
|
classes: { paper: styles.dropdown },
|
|
175
259
|
}}
|
|
@@ -180,12 +264,13 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
180
264
|
// inputProps, which meant the error border never actually appeared on the Dropdown.)
|
|
181
265
|
data-disabled={disabled ? "true" : undefined}
|
|
182
266
|
data-error={error ? "true" : undefined}
|
|
267
|
+
data-with-left-section={startAdornment ? "true" : undefined}
|
|
268
|
+
data-with-right-section={showClear ? "true" : undefined}
|
|
183
269
|
inputProps={{
|
|
184
270
|
"data-disabled": disabled ? "true" : undefined,
|
|
185
271
|
"data-error": error ? "true" : undefined,
|
|
186
272
|
...(restRecord.inputProps as Record<string, unknown>),
|
|
187
273
|
}}
|
|
188
|
-
{...(sanitizedProps as unknown as MuiSelectProps)}
|
|
189
274
|
>
|
|
190
275
|
{renderOptions()}
|
|
191
276
|
</MuiSelect>
|
|
@@ -39,3 +39,10 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
|
|
|
39
39
|
> - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
|
|
40
40
|
> - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
|
|
41
41
|
> - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 4. Notes
|
|
46
|
+
|
|
47
|
+
- Pass `startAdornment` for a leading icon, and `clearable` (with a value present) to show a clear button — both render using the dropdown's own icon-color tokens, matching the mantine-adapter's `leftSection`/`clearable` behavior.
|
|
48
|
+
- `onChange` follows MUI's native `Select` signature: `(event: SelectChangeEvent, child: ReactNode) => void`.
|
|
@@ -308,6 +308,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
|
|
|
308
308
|
>
|
|
309
309
|
<div
|
|
310
310
|
ref={ref}
|
|
311
|
+
{...restRecord}
|
|
311
312
|
className={styles.root}
|
|
312
313
|
role="button"
|
|
313
314
|
aria-label={browseLabel}
|
|
@@ -323,7 +324,6 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
|
|
|
323
324
|
onDragLeave={interactive ? handleDragLeave : undefined}
|
|
324
325
|
onDragOver={interactive ? handleDragOver : undefined}
|
|
325
326
|
onDrop={interactive ? handleDrop : undefined}
|
|
326
|
-
{...restRecord}
|
|
327
327
|
>
|
|
328
328
|
<span className={styles.leadingIcon} aria-hidden>
|
|
329
329
|
{icon ?? <UploadIcon />}
|
|
@@ -267,10 +267,10 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
267
267
|
>
|
|
268
268
|
<div
|
|
269
269
|
ref={ref}
|
|
270
|
+
{...restRecord}
|
|
270
271
|
className={styles.root}
|
|
271
272
|
data-disabled={disabled ? "true" : undefined}
|
|
272
273
|
data-error={effectiveError ? "true" : undefined}
|
|
273
|
-
{...restRecord}
|
|
274
274
|
>
|
|
275
275
|
{!readOnly && (
|
|
276
276
|
<div
|
|
@@ -32,6 +32,7 @@ export const FormControlLayout = React.forwardRef<
|
|
|
32
32
|
return (
|
|
33
33
|
<div
|
|
34
34
|
ref={ref}
|
|
35
|
+
{...sanitizedProps}
|
|
35
36
|
className={className ? `${styles.root} ${className}` : styles.root}
|
|
36
37
|
data-form-layout={formLayout}
|
|
37
38
|
style={
|
|
@@ -45,7 +46,6 @@ export const FormControlLayout = React.forwardRef<
|
|
|
45
46
|
: {}),
|
|
46
47
|
} as React.CSSProperties
|
|
47
48
|
}
|
|
48
|
-
{...sanitizedProps}
|
|
49
49
|
>
|
|
50
50
|
{/*
|
|
51
51
|
The left section strictly enforces the Label boundary sizes
|
|
@@ -94,6 +94,7 @@ export const FormControlWrapper = React.forwardRef<
|
|
|
94
94
|
return (
|
|
95
95
|
<FormControl
|
|
96
96
|
ref={ref}
|
|
97
|
+
{...(sanitizedProps as FormControlProps)}
|
|
97
98
|
error={!!error}
|
|
98
99
|
required={required}
|
|
99
100
|
disabled={disabled}
|
|
@@ -103,7 +104,6 @@ export const FormControlWrapper = React.forwardRef<
|
|
|
103
104
|
data-error={error ? "true" : undefined}
|
|
104
105
|
data-disabled={disabled ? "true" : undefined}
|
|
105
106
|
data-focused={focused ? "true" : undefined}
|
|
106
|
-
{...(sanitizedProps as FormControlProps)}
|
|
107
107
|
>
|
|
108
108
|
<FormControlLayout
|
|
109
109
|
formLayout={formLayout}
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
and positioning (-arrowSize/2) calculations that cannot be CSS-driven. The default matches
|
|
6
6
|
the Recursica beak-size token (16px). See COMPONENT_ISSUES.md for details.
|
|
7
7
|
- All structural layout (display, position, overflow) is deferred to Mantine's native
|
|
8
|
-
behavior. We only override visual design tokens (colors, typography, spacing, borders).
|
|
8
|
+
behavior. We only override visual design tokens (colors, typography, spacing, borders).
|
|
9
|
+
- margin: 0 !important on .dropdown. Mui's Tooltip hardcodes a per-placement margin
|
|
10
|
+
(14px, reset to 0 only when arrow is enabled) on top of whatever offset the popper
|
|
11
|
+
"offset" modifier applies, so with withBeak=false the trigger-to-dropdown gap was
|
|
12
|
+
offset + 14px instead of just offset. There is no Recursica token for this spacing —
|
|
13
|
+
the gap is driven purely by the `offset` prop (a plain number, same as Mantine's
|
|
14
|
+
HoverCard), so we zero out Mui's extra margin to make offset the only source of gap. */
|
|
9
15
|
|
|
10
16
|
/* ======================================
|
|
11
17
|
DROPDOWN CONTAINER
|
|
@@ -72,14 +78,30 @@
|
|
|
72
78
|
color: var(
|
|
73
79
|
--recursica_ui-kit_components_hover-card-popover_properties_colors_content
|
|
74
80
|
);
|
|
81
|
+
|
|
82
|
+
margin: 0 !important; /* HARDCODE: cancel Mui's built-in per-placement margin, see note above */
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
/* ======================================
|
|
78
86
|
ARROW / BEAK
|
|
79
87
|
====================================== */
|
|
80
88
|
|
|
89
|
+
/* Mui's arrow is a rotated square (via ::before, background-color: currentColor)
|
|
90
|
+
sized in em units off font-size, not a bordered CSS triangle. `color` (not
|
|
91
|
+
border-color) is what actually fills it, which is why setting border-color here
|
|
92
|
+
previously had no visible effect and left it defaulting to the inherited text
|
|
93
|
+
color (solid black). Fill it with the same background-color token the card body
|
|
94
|
+
uses so it blends into the card exactly like Mantine's beak, and size it off the
|
|
95
|
+
beak-size token instead of Mui's default 1em/0.71em (font-size-relative) box. */
|
|
81
96
|
.arrow {
|
|
82
|
-
|
|
83
|
-
--recursica_ui-kit_components_hover-card-
|
|
97
|
+
color: var(
|
|
98
|
+
--recursica_ui-kit_components_hover-card-popover_properties_colors_background-color
|
|
99
|
+
);
|
|
100
|
+
width: var(
|
|
101
|
+
--recursica_ui-kit_components_hover-card-popover_properties_beak-size
|
|
84
102
|
);
|
|
103
|
+
height: calc(
|
|
104
|
+
var(--recursica_ui-kit_components_hover-card-popover_properties_beak-size) *
|
|
105
|
+
0.7071
|
|
106
|
+
); /* = width / sqrt(2), same ratio Mui's default arrow uses */
|
|
85
107
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@mui/material";
|
|
6
6
|
import {
|
|
7
7
|
filterStylingProps,
|
|
8
|
+
mergeClassNames,
|
|
8
9
|
type RecursicaOverStyled,
|
|
9
10
|
} from "../../utils/filterStylingProps";
|
|
10
11
|
import styles from "./HoverCard.module.css";
|
|
@@ -35,27 +36,18 @@ const HoverCardBase = function HoverCard({
|
|
|
35
36
|
overStyled,
|
|
36
37
|
);
|
|
37
38
|
|
|
38
|
-
// Bind CSS module classes to Mui's internal classNames API
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
const o = classNamesProp as Record<string, string>;
|
|
51
|
-
Object.keys(o).forEach((key) => {
|
|
52
|
-
if (mergedClassNames[key]) {
|
|
53
|
-
mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
|
|
54
|
-
} else {
|
|
55
|
-
mergedClassNames[key] = o[key];
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
}
|
|
39
|
+
// Bind CSS module classes to Mui's internal classNames API. Note MUI's actual prop is
|
|
40
|
+
// "classes", not "classNames" (that's Mantine's naming) — this used to read the wrong key,
|
|
41
|
+
// silently no-op-ing any caller-supplied classes.
|
|
42
|
+
const mergedClassNames = mergeClassNames(
|
|
43
|
+
{
|
|
44
|
+
tooltip: styles.dropdown,
|
|
45
|
+
arrow: styles.arrow,
|
|
46
|
+
},
|
|
47
|
+
(sanitizedProps as Record<string, unknown>).classes as
|
|
48
|
+
| Partial<Record<string, string>>
|
|
49
|
+
| undefined,
|
|
50
|
+
);
|
|
59
51
|
|
|
60
52
|
// Find Target and Dropdown children
|
|
61
53
|
let targetNode: React.ReactNode = null;
|
|
@@ -271,4 +271,7 @@
|
|
|
271
271
|
position: relative;
|
|
272
272
|
z-index: 1;
|
|
273
273
|
color: inherit;
|
|
274
|
+
/* HARDCODE: prevent the flex row from shrinking the chevron below its intrinsic 14px size
|
|
275
|
+
when the row is tight, which otherwise reads as the submenu indicator being clipped. */
|
|
276
|
+
flex-shrink: 0;
|
|
274
277
|
}
|