@recursica/mui-adapter 0.27.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 +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/mui-adapter.cjs +55 -55
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +3502 -3443
- package/dist/mui-adapter.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Breadcrumb/Breadcrumb.module.css +10 -5
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +0 -6
- package/src/components/Breadcrumb/Breadcrumb.tsx +1 -0
- 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 +90 -12
- package/src/components/Dropdown/USAGE.md +7 -0
- package/src/components/HoverCard/HoverCard.module.css +25 -3
- package/src/components/Menu/Menu.module.css +3 -0
- package/src/components/Menu/Menu.stories.tsx +128 -5
- package/src/components/Menu/Menu.tsx +1 -1
package/package.json
CHANGED
|
@@ -3,18 +3,21 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
.root {
|
|
6
|
-
/* HARDCODE: Basic structural flex layout for breadcrumbs */
|
|
7
|
-
display: flex;
|
|
8
|
-
align-items: center;
|
|
9
|
-
|
|
10
6
|
/* Base style resets */
|
|
11
7
|
background-color: transparent;
|
|
12
8
|
color: inherit;
|
|
13
9
|
|
|
14
10
|
/* Apply global breadcrumb properties */
|
|
15
11
|
padding: var(--recursica_ui-kit_components_breadcrumb_properties_padding);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* MUI renders items/separators inside the <ol>, not directly under <nav> (.root) — the
|
|
15
|
+
gap must live here to land uniformly between item/separator/item, matching Mantine. */
|
|
16
|
+
.ol {
|
|
17
|
+
/* HARDCODE: Basic structural flex layout for breadcrumbs */
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
16
20
|
|
|
17
|
-
/* Mantine's breadcrumbs uses gap on the root or pseudo classes, but standard gap is safest since flex is used */
|
|
18
21
|
gap: var(--recursica_ui-kit_components_breadcrumb_properties_item-gap);
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -23,4 +26,6 @@
|
|
|
23
26
|
display: flex;
|
|
24
27
|
align-items: center;
|
|
25
28
|
justify-content: center;
|
|
29
|
+
/* Neutralize MUI's built-in 8px marginLeft/marginRight so .ol's gap alone controls spacing */
|
|
30
|
+
margin: 0;
|
|
26
31
|
}
|
|
@@ -56,12 +56,6 @@ export const Default: Story = {
|
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
export const StaticExample: Story = {
|
|
60
|
-
args: {
|
|
61
|
-
items: ["Store", "Electronics", "Computers", "Laptops"],
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
59
|
export const CustomSeparator: Story = {
|
|
66
60
|
args: {
|
|
67
61
|
items: ["Root", "Branch", "Leaf"],
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chevron rendered in the right section alongside the clear button (item 3a). Only used for that
|
|
5
|
+
* combined case — MUI's own default `IconComponent` (`ArrowDropDown`) is left untouched everywhere
|
|
6
|
+
* else, so this is a hand-rolled copy of the exact same MUI glyph path, not a new icon design.
|
|
7
|
+
*/
|
|
8
|
+
export function ChevronIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
9
|
+
return (
|
|
10
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden {...props}>
|
|
11
|
+
<path d="M7 10l5 5 5-5z" />
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Clear ("x") icon for the clearable Dropdown's clear button (item 3a). */
|
|
17
|
+
export function ClearIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
18
|
+
return (
|
|
19
|
+
<svg
|
|
20
|
+
viewBox="0 0 24 24"
|
|
21
|
+
fill="none"
|
|
22
|
+
stroke="currentColor"
|
|
23
|
+
strokeWidth="2"
|
|
24
|
+
strokeLinecap="round"
|
|
25
|
+
strokeLinejoin="round"
|
|
26
|
+
aria-hidden
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
30
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -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,7 +1,8 @@
|
|
|
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";
|
|
@@ -12,10 +13,18 @@ import {
|
|
|
12
13
|
} from "../../utils/filterStylingProps";
|
|
13
14
|
import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
|
|
14
15
|
import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
|
|
16
|
+
import { ChevronIcon, ClearIcon } from "./Dropdown.icons";
|
|
15
17
|
import styles from "./Dropdown.module.css";
|
|
16
18
|
|
|
17
19
|
import { type RecursicaDropdownProps as BaseRecursicaDropdownProps } from "@recursica/adapter-common";
|
|
18
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
|
+
|
|
19
28
|
export interface RecursicaDropdownProps
|
|
20
29
|
extends Omit<
|
|
21
30
|
MuiSelectProps,
|
|
@@ -28,9 +37,13 @@ export interface RecursicaDropdownProps
|
|
|
28
37
|
| "ref"
|
|
29
38
|
| "error"
|
|
30
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">,
|
|
31
44
|
Omit<
|
|
32
45
|
RecursicaFormControlWrapperProps,
|
|
33
|
-
"controlMaxWidth" | "controlMinWidth"
|
|
46
|
+
"controlMaxWidth" | "controlMinWidth" | "onChange"
|
|
34
47
|
>,
|
|
35
48
|
ReadOnlyControlProps,
|
|
36
49
|
BaseRecursicaDropdownProps {}
|
|
@@ -67,11 +80,12 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
67
80
|
emptyValueComponent,
|
|
68
81
|
value,
|
|
69
82
|
defaultValue,
|
|
83
|
+
onChange,
|
|
70
84
|
data,
|
|
85
|
+
startAdornment,
|
|
71
86
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
72
87
|
searchable, // Not natively supported by basic MUI Select, stubbed
|
|
73
|
-
|
|
74
|
-
clearable, // Not natively supported by basic MUI Select, stubbed
|
|
88
|
+
clearable,
|
|
75
89
|
...rest
|
|
76
90
|
} = props;
|
|
77
91
|
// Props this component intentionally doesn't support — deleted at runtime so they can't leak
|
|
@@ -87,6 +101,47 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
87
101
|
);
|
|
88
102
|
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
89
103
|
|
|
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
|
+
};
|
|
144
|
+
|
|
90
145
|
const injectedStyles = {
|
|
91
146
|
...((style as React.CSSProperties) || {}),
|
|
92
147
|
width: containerWidth || "100%",
|
|
@@ -105,12 +160,11 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
105
160
|
key={`${item}-${index}`}
|
|
106
161
|
value={item}
|
|
107
162
|
className={styles.option}
|
|
163
|
+
disableRipple
|
|
108
164
|
// Dropdown.module.css's own selected-state tint (`.option[data-selected="true"]`)
|
|
109
165
|
// needs this explicitly — MUI's own `Mui-selected` class carries its default primary-
|
|
110
166
|
// color tint instead, which is what shows through without it.
|
|
111
|
-
data-selected={
|
|
112
|
-
item === (value ?? defaultValue) ? "true" : undefined
|
|
113
|
-
}
|
|
167
|
+
data-selected={item === internalValue ? "true" : undefined}
|
|
114
168
|
>
|
|
115
169
|
{item}
|
|
116
170
|
</MenuItem>
|
|
@@ -122,9 +176,8 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
122
176
|
value={item.value}
|
|
123
177
|
disabled={item.disabled}
|
|
124
178
|
className={styles.option}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
179
|
+
disableRipple
|
|
180
|
+
data-selected={item.value === internalValue ? "true" : undefined}
|
|
128
181
|
>
|
|
129
182
|
{item.label}
|
|
130
183
|
</MenuItem>
|
|
@@ -132,6 +185,26 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
132
185
|
});
|
|
133
186
|
};
|
|
134
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
|
+
|
|
135
208
|
return (
|
|
136
209
|
<WithReadOnlyWrapper
|
|
137
210
|
className={wrapperClass}
|
|
@@ -168,8 +241,8 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
168
241
|
ref={ref}
|
|
169
242
|
{...(sanitizedProps as unknown as MuiSelectProps)}
|
|
170
243
|
disabled={disabled}
|
|
171
|
-
value={
|
|
172
|
-
|
|
244
|
+
value={internalValue}
|
|
245
|
+
onChange={handleChange}
|
|
173
246
|
error={!!error}
|
|
174
247
|
required={required}
|
|
175
248
|
displayEmpty
|
|
@@ -178,6 +251,9 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
178
251
|
select: styles.input,
|
|
179
252
|
icon: styles.icon,
|
|
180
253
|
}}
|
|
254
|
+
IconComponent={showClear ? HiddenIcon : undefined}
|
|
255
|
+
startAdornment={wrappedStartAdornment}
|
|
256
|
+
endAdornment={wrappedEndAdornment}
|
|
181
257
|
MenuProps={{
|
|
182
258
|
classes: { paper: styles.dropdown },
|
|
183
259
|
}}
|
|
@@ -188,6 +264,8 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
188
264
|
// inputProps, which meant the error border never actually appeared on the Dropdown.)
|
|
189
265
|
data-disabled={disabled ? "true" : undefined}
|
|
190
266
|
data-error={error ? "true" : undefined}
|
|
267
|
+
data-with-left-section={startAdornment ? "true" : undefined}
|
|
268
|
+
data-with-right-section={showClear ? "true" : undefined}
|
|
191
269
|
inputProps={{
|
|
192
270
|
"data-disabled": disabled ? "true" : undefined,
|
|
193
271
|
"data-error": error ? "true" : undefined,
|
|
@@ -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`.
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|