@recursica/mui-adapter 0.32.1 → 0.34.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 +23 -0
- package/dist/index.d.ts +48 -7
- package/dist/mui-adapter.cjs +66 -66
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +6841 -6756
- package/dist/mui-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Autocomplete/AUTOCOMPLETE_IMPLEMENTATION_NOTES.md +17 -0
- package/src/components/Autocomplete/Autocomplete.module.css +121 -12
- package/src/components/Autocomplete/Autocomplete.stories.tsx +149 -0
- package/src/components/Autocomplete/Autocomplete.tsx +47 -2
- package/src/components/Autocomplete/USAGE.md +26 -0
- package/src/components/Dropdown/BareDropdown.tsx +34 -7
- package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +15 -0
- package/src/components/Dropdown/Dropdown.module.css +113 -4
- package/src/components/Dropdown/Dropdown.stories.tsx +149 -0
- package/src/components/Dropdown/Dropdown.tsx +37 -4
- package/src/components/Dropdown/USAGE.md +20 -0
- package/src/components/Menu/IMPLEMENTATION_NOTES.md +1 -0
- package/src/components/Menu/Menu.stories.tsx +37 -0
- package/src/components/Menu/Menu.tsx +25 -5
- package/src/components/Menu/USAGE.md +6 -0
- package/src/utils/renderRichOption.tsx +51 -0
|
@@ -366,12 +366,25 @@
|
|
|
366
366
|
color: var(
|
|
367
367
|
--recursica_ui-kit_components_dropdown_properties_colors_text-color
|
|
368
368
|
);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
369
|
+
/* Padding/inter-item gap reuse the menu-item component's tokens (same reasoning as the icon/
|
|
370
|
+
supporting-text tokens below) instead of an arbitrary scaled dropdown padding — matches the
|
|
371
|
+
mantine-adapter's equivalent Menu.module.css `.item` padding + gap exactly. `!important` on
|
|
372
|
+
padding: MUI's `MenuItem` root sets its own paddingTop/Bottom (MenuItem.js) plus paddingLeft/
|
|
373
|
+
Right 16px via its `gutters` variant, both at equal selector specificity to this rule, so
|
|
374
|
+
without it the winner depends on emotion's injection order rather than this stylesheet. */
|
|
375
|
+
padding: var(
|
|
376
|
+
--recursica_ui-kit_components_menu-item_properties_vertical-padding
|
|
372
377
|
)
|
|
373
|
-
var(--recursica_ui-
|
|
378
|
+
var(--recursica_ui-kit_components_menu-item_properties_horizontal-padding) !important;
|
|
379
|
+
margin-bottom: var(--recursica_ui-kit_components_menu_properties_item-gap);
|
|
374
380
|
cursor: pointer;
|
|
381
|
+
/* MUI's `MenuItem` also forces `white-space: nowrap` by default; override it so long
|
|
382
|
+
labels/supportingText wrap instead of overflowing. Same equal-specificity reasoning. */
|
|
383
|
+
white-space: normal !important;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.option:last-of-type {
|
|
387
|
+
margin-bottom: 0;
|
|
375
388
|
}
|
|
376
389
|
|
|
377
390
|
/* No per-option hovered token exists in the schema; use the generic overlay tint (same technique
|
|
@@ -402,3 +415,99 @@
|
|
|
402
415
|
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_text-color
|
|
403
416
|
) !important;
|
|
404
417
|
}
|
|
418
|
+
|
|
419
|
+
/* Rich option content (leadingIcon/supportingText — see MANTINE_ADAPTER_RICH_OPTION_DATA.md). No
|
|
420
|
+
dedicated dropdown-option icon or supporting-text token exists in the schema either; reuse the
|
|
421
|
+
menu-item component's tokens for the same reason the selected-state colors above do. Matches
|
|
422
|
+
the mantine-adapter's equivalent classes. */
|
|
423
|
+
.optionContent {
|
|
424
|
+
display: flex;
|
|
425
|
+
align-items: center;
|
|
426
|
+
gap: var(--recursica_ui-kit_components_menu-item_properties_icon-text-gap);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.optionIcon {
|
|
430
|
+
display: flex;
|
|
431
|
+
flex-shrink: 0;
|
|
432
|
+
width: var(
|
|
433
|
+
--recursica_ui-kit_components_menu-item_properties_icon-leading-size
|
|
434
|
+
);
|
|
435
|
+
height: var(
|
|
436
|
+
--recursica_ui-kit_components_menu-item_properties_icon-leading-size
|
|
437
|
+
);
|
|
438
|
+
color: var(
|
|
439
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_leading-icon-color
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.optionIcon :global(svg) {
|
|
444
|
+
width: 100%;
|
|
445
|
+
height: 100%;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.optionText {
|
|
449
|
+
display: flex;
|
|
450
|
+
flex-direction: column;
|
|
451
|
+
min-width: 0;
|
|
452
|
+
gap: var(--recursica_ui-kit_components_menu-item_properties_text-gap);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* Default: label/supportingText each truncate to a single line with an ellipsis. */
|
|
456
|
+
.optionText > * {
|
|
457
|
+
overflow: hidden;
|
|
458
|
+
text-overflow: ellipsis;
|
|
459
|
+
white-space: nowrap;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/* Applied alongside .optionText when `wrapItemText` is true — long values wrap onto additional
|
|
463
|
+
lines instead of overflowing the fixed-width dropdown. */
|
|
464
|
+
.optionTextWrap > * {
|
|
465
|
+
overflow: visible;
|
|
466
|
+
text-overflow: clip;
|
|
467
|
+
white-space: normal;
|
|
468
|
+
overflow-wrap: anywhere;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.optionSupportingText {
|
|
472
|
+
font-family: var(
|
|
473
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-family
|
|
474
|
+
);
|
|
475
|
+
font-size: var(
|
|
476
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-size
|
|
477
|
+
);
|
|
478
|
+
font-style: var(
|
|
479
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-style
|
|
480
|
+
);
|
|
481
|
+
font-weight: var(
|
|
482
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-weight
|
|
483
|
+
);
|
|
484
|
+
letter-spacing: var(
|
|
485
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_letter-spacing
|
|
486
|
+
);
|
|
487
|
+
line-height: var(
|
|
488
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_line-height
|
|
489
|
+
);
|
|
490
|
+
text-decoration: var(
|
|
491
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_text-decoration
|
|
492
|
+
);
|
|
493
|
+
text-transform: var(
|
|
494
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_text-transform
|
|
495
|
+
);
|
|
496
|
+
color: var(
|
|
497
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_supporting-text-color
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.option[data-selected="true"] .optionIcon,
|
|
502
|
+
.option[data-combobox-selected="true"] .optionIcon {
|
|
503
|
+
color: var(
|
|
504
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_leading-icon-color
|
|
505
|
+
) !important;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.option[data-selected="true"] .optionSupportingText,
|
|
509
|
+
.option[data-combobox-selected="true"] .optionSupportingText {
|
|
510
|
+
color: var(
|
|
511
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_supporting-text-color
|
|
512
|
+
) !important;
|
|
513
|
+
}
|
|
@@ -2,6 +2,8 @@ import React from "react";
|
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { Dropdown } from "./Dropdown";
|
|
4
4
|
import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
|
|
5
|
+
import { renderRichOptionContent } from "../../utils/renderRichOption";
|
|
6
|
+
import styles from "./Dropdown.module.css";
|
|
5
7
|
|
|
6
8
|
type DropdownStoryProps = React.ComponentProps<typeof Dropdown>;
|
|
7
9
|
|
|
@@ -42,6 +44,11 @@ const meta: Meta<DropdownStoryProps> = {
|
|
|
42
44
|
clearable: {
|
|
43
45
|
control: "boolean",
|
|
44
46
|
},
|
|
47
|
+
wrapItemText: {
|
|
48
|
+
control: "boolean",
|
|
49
|
+
description:
|
|
50
|
+
"Wraps option label/supportingText onto additional lines instead of truncating with an ellipsis.",
|
|
51
|
+
},
|
|
45
52
|
containerWidth: {
|
|
46
53
|
table: { disable: true },
|
|
47
54
|
},
|
|
@@ -86,6 +93,148 @@ export const WithLeadingIcon: Story = {
|
|
|
86
93
|
},
|
|
87
94
|
};
|
|
88
95
|
|
|
96
|
+
const UserIcon = (
|
|
97
|
+
<svg
|
|
98
|
+
width="16"
|
|
99
|
+
height="16"
|
|
100
|
+
viewBox="0 0 24 24"
|
|
101
|
+
fill="none"
|
|
102
|
+
stroke="currentColor"
|
|
103
|
+
strokeWidth="2"
|
|
104
|
+
strokeLinecap="round"
|
|
105
|
+
strokeLinejoin="round"
|
|
106
|
+
>
|
|
107
|
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
|
108
|
+
<circle cx="12" cy="7" r="4"></circle>
|
|
109
|
+
</svg>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
export const WithRichOptions: Story = {
|
|
113
|
+
args: {
|
|
114
|
+
label: "Assignee",
|
|
115
|
+
placeholder: "Pick a team member",
|
|
116
|
+
assistiveText:
|
|
117
|
+
"Each option can show a leading icon and supporting text — see MANTINE_ADAPTER_RICH_OPTION_DATA.md.",
|
|
118
|
+
data: [
|
|
119
|
+
{
|
|
120
|
+
value: "jdoe",
|
|
121
|
+
label: "Jane Doe",
|
|
122
|
+
leadingIcon: UserIcon,
|
|
123
|
+
supportingText: "jane.doe@example.com",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
value: "asmith",
|
|
127
|
+
label: "Alex Smith",
|
|
128
|
+
leadingIcon: UserIcon,
|
|
129
|
+
supportingText: "alex.smith@example.com",
|
|
130
|
+
},
|
|
131
|
+
{ value: "unassigned", label: "Unassigned" },
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const WithRichOptionsWrapped: Story = {
|
|
137
|
+
args: {
|
|
138
|
+
label: "Assignee",
|
|
139
|
+
placeholder: "Pick a team member",
|
|
140
|
+
wrapItemText: true,
|
|
141
|
+
data: [
|
|
142
|
+
{
|
|
143
|
+
value: "jdoe",
|
|
144
|
+
label: "Jane Doe, Senior Staff Engineer, Platform Infrastructure",
|
|
145
|
+
leadingIcon: UserIcon,
|
|
146
|
+
supportingText:
|
|
147
|
+
"jane.doe@example.com — Platform Infrastructure team, on-call rotation lead",
|
|
148
|
+
},
|
|
149
|
+
{ value: "unassigned", label: "Unassigned" },
|
|
150
|
+
],
|
|
151
|
+
assistiveText:
|
|
152
|
+
"wrapItemText=true — long label/supportingText wrap instead of truncating.",
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const optionRowPreviewClassNames = {
|
|
157
|
+
optionContent: styles.optionContent,
|
|
158
|
+
optionIcon: styles.optionIcon,
|
|
159
|
+
optionText: styles.optionText,
|
|
160
|
+
optionTextWrap: styles.optionTextWrap,
|
|
161
|
+
optionSupportingText: styles.optionSupportingText,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const OPTION_ROW_PREVIEW_ITEMS = [
|
|
165
|
+
{
|
|
166
|
+
value: "icon-and-supporting",
|
|
167
|
+
label: "Jane Doe",
|
|
168
|
+
leadingIcon: UserIcon,
|
|
169
|
+
supportingText: "jane.doe@example.com",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
value: "no-icon",
|
|
173
|
+
label: "Alex Smith",
|
|
174
|
+
supportingText:
|
|
175
|
+
"No leadingIcon — label/supportingText shift left, no reserved icon space",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
value: "no-supporting-text",
|
|
179
|
+
label: "Taylor Rivera",
|
|
180
|
+
leadingIcon: UserIcon,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
value: "plain",
|
|
184
|
+
label: "Plain option — no leadingIcon, no supportingText",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
value: "long-text",
|
|
188
|
+
label:
|
|
189
|
+
"A very long option label that, with wrapItemText, wraps onto a second line instead of overflowing the fixed-width dropdown — otherwise it truncates with an ellipsis",
|
|
190
|
+
leadingIcon: UserIcon,
|
|
191
|
+
supportingText:
|
|
192
|
+
"A similarly long supporting text string, to confirm the same wrap-or-truncate behavior applies to it too",
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
// Renders the option row content directly — outside the MUI Menu portal — inside a container
|
|
197
|
+
// sized to Dropdown's own max-width token. Spacing between rows, icon/supportingText presence-
|
|
198
|
+
// or-absence alignment, and long-text wrapping/truncation are all much easier to inspect this way
|
|
199
|
+
// than by opening the real (portal-rendered) MUI Select menu. See
|
|
200
|
+
// MANTINE_ADAPTER_RICH_OPTION_DATA.md.
|
|
201
|
+
const renderOptionRowPreview = (wrapItemText: boolean) => (
|
|
202
|
+
<div
|
|
203
|
+
className={styles.dropdown}
|
|
204
|
+
style={{
|
|
205
|
+
width:
|
|
206
|
+
"var(--recursica_ui-kit_components_dropdown_variants_layouts_stacked_properties_max-width)",
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
{OPTION_ROW_PREVIEW_ITEMS.map((item) => (
|
|
210
|
+
<div key={item.value} className={styles.option}>
|
|
211
|
+
{renderRichOptionContent(
|
|
212
|
+
item,
|
|
213
|
+
optionRowPreviewClassNames,
|
|
214
|
+
wrapItemText,
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
))}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Default: `wrapItemText` is false — label/supportingText truncate to a single line with an
|
|
222
|
+
// ellipsis instead of wrapping.
|
|
223
|
+
export const RichOptionRowPreview: Story = {
|
|
224
|
+
parameters: {
|
|
225
|
+
controls: { disable: true },
|
|
226
|
+
},
|
|
227
|
+
render: () => renderOptionRowPreview(false),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// `wrapItemText: true` — label/supportingText wrap onto additional lines instead of truncating.
|
|
231
|
+
export const RichOptionRowPreviewWrapped: Story = {
|
|
232
|
+
parameters: {
|
|
233
|
+
controls: { disable: true },
|
|
234
|
+
},
|
|
235
|
+
render: () => renderOptionRowPreview(true),
|
|
236
|
+
};
|
|
237
|
+
|
|
89
238
|
export const StaticError: Story = {
|
|
90
239
|
args: {
|
|
91
240
|
error: "You must choose a valid destination.",
|
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
SelectChangeEvent,
|
|
6
6
|
MenuItem,
|
|
7
7
|
} from "@mui/material";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type ReadOnlyControlProps,
|
|
10
|
+
normalizeComboboxData,
|
|
11
|
+
} from "@recursica/adapter-common";
|
|
9
12
|
import {
|
|
10
13
|
filterStylingProps,
|
|
11
14
|
omitUnsupportedProps,
|
|
@@ -13,6 +16,7 @@ import {
|
|
|
13
16
|
} from "../../utils/filterStylingProps";
|
|
14
17
|
import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
|
|
15
18
|
import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
|
|
19
|
+
import { renderRichOptionContent } from "../../utils/renderRichOption";
|
|
16
20
|
import { ChevronIcon, ClearIcon } from "./Dropdown.icons";
|
|
17
21
|
import styles from "./Dropdown.module.css";
|
|
18
22
|
|
|
@@ -86,6 +90,7 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
86
90
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
87
91
|
searchable, // Not natively supported by basic MUI Select, stubbed
|
|
88
92
|
clearable,
|
|
93
|
+
wrapItemText = false,
|
|
89
94
|
...rest
|
|
90
95
|
} = props;
|
|
91
96
|
// Props this component intentionally doesn't support — deleted at runtime so they can't leak
|
|
@@ -151,9 +156,22 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
151
156
|
? `${styles.layoutOverride} ${className}`
|
|
152
157
|
: styles.layoutOverride;
|
|
153
158
|
|
|
159
|
+
const optionClassNames = {
|
|
160
|
+
optionContent: styles.optionContent,
|
|
161
|
+
optionIcon: styles.optionIcon,
|
|
162
|
+
optionText: styles.optionText,
|
|
163
|
+
optionTextWrap: styles.optionTextWrap,
|
|
164
|
+
optionSupportingText: styles.optionSupportingText,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// See AutoComplete.tsx's identical use of `normalizeComboboxData` (adapter-common) — items
|
|
168
|
+
// always have a real `label` after this, so downstream code doesn't need its own `?? value`
|
|
169
|
+
// fallback at every read.
|
|
170
|
+
const normalizedData = normalizeComboboxData(data);
|
|
171
|
+
|
|
154
172
|
const renderOptions = () => {
|
|
155
|
-
if (!
|
|
156
|
-
return
|
|
173
|
+
if (!normalizedData) return null;
|
|
174
|
+
return normalizedData.map((item, index) => {
|
|
157
175
|
if (typeof item === "string") {
|
|
158
176
|
return (
|
|
159
177
|
<MenuItem
|
|
@@ -179,12 +197,26 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
179
197
|
disableRipple
|
|
180
198
|
data-selected={item.value === internalValue ? "true" : undefined}
|
|
181
199
|
>
|
|
182
|
-
{item
|
|
200
|
+
{renderRichOptionContent(item, optionClassNames, wrapItemText)}
|
|
183
201
|
</MenuItem>
|
|
184
202
|
);
|
|
185
203
|
});
|
|
186
204
|
};
|
|
187
205
|
|
|
206
|
+
// MUI's closed-field display otherwise mirrors the selected `MenuItem`'s children directly —
|
|
207
|
+
// now that those can be a rich icon+label+supportingText row, `renderValue` keeps the closed
|
|
208
|
+
// field showing just the plain label, matching the field's own single-line text control.
|
|
209
|
+
const renderValue = (selected: unknown) => {
|
|
210
|
+
if (selected === "" || selected === undefined || selected === null) {
|
|
211
|
+
return "";
|
|
212
|
+
}
|
|
213
|
+
const match = normalizedData?.find((item) =>
|
|
214
|
+
typeof item === "string" ? item === selected : item.value === selected,
|
|
215
|
+
);
|
|
216
|
+
if (!match) return "";
|
|
217
|
+
return typeof match === "string" ? match : match.label;
|
|
218
|
+
};
|
|
219
|
+
|
|
188
220
|
const wrappedStartAdornment = startAdornment ? (
|
|
189
221
|
<span className={styles.section} data-position="left">
|
|
190
222
|
{startAdornment}
|
|
@@ -246,6 +278,7 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
246
278
|
error={!!error}
|
|
247
279
|
required={required}
|
|
248
280
|
displayEmpty
|
|
281
|
+
renderValue={renderValue}
|
|
249
282
|
className={styles.root}
|
|
250
283
|
classes={{
|
|
251
284
|
select: styles.input,
|
|
@@ -46,3 +46,23 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
|
|
|
46
46
|
|
|
47
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
48
|
- `onChange` follows MUI's native `Select` signature: `(event: SelectChangeEvent, child: ReactNode) => void`.
|
|
49
|
+
- `data` items can carry a `leadingIcon` and `supportingText`, rendered inside each option row
|
|
50
|
+
(the closed field always shows the plain `label`, matching the mantine-adapter):
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<Dropdown
|
|
54
|
+
label="Assignee"
|
|
55
|
+
data={[
|
|
56
|
+
{
|
|
57
|
+
value: "jdoe",
|
|
58
|
+
label: "Jane Doe",
|
|
59
|
+
leadingIcon: <UserIcon />,
|
|
60
|
+
supportingText: "jane.doe@example.com",
|
|
61
|
+
},
|
|
62
|
+
{ value: "asmith", label: "Alex Smith" },
|
|
63
|
+
]}
|
|
64
|
+
/>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
By default `label`/`supportingText` truncate to a single line with an ellipsis. Set
|
|
68
|
+
`wrapItemText` to wrap them onto additional lines instead: `<Dropdown data={data} wrapItemText />`.
|
|
@@ -2,3 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
- **Compositional API Dropped:** Mantine uses `<Menu.Target>`, `<Menu.Dropdown>`, `<Menu.Item>`, etc., and manages state natively via React context within `<Menu>`. MUI's API is fully monolithic.
|
|
4
4
|
- **Monolithic API Adopted:** Following architectural review, we have abandoned the fabricated context wrappers for `mui-adapter`. We now natively export `Menu`, `MenuItem`, and `MenuDivider` wrapping their `@mui/material` counterparts. Developers are expected to manage `anchorEl` state themselves, just like native MUI. Storybook tests have been updated to simulate this open state so visual regressions still cover the dropdown menu visually.
|
|
5
|
+
- **`maxHeight` Override:** `<Menu maxHeight={...}>` overrides the token-driven dropdown max-height with an explicit pixel (or other CSS length) value — the one deliberate exception to "no inline design tokens in TSX" (see `COMPONENT_DEV_GUIDE.md`). Implemented by merging `maxHeight` into `slotProps.paper.style`, alongside any caller-supplied `slotProps`, rather than a CSS module change; the CSS module's token-driven `max-height` stays untouched when the prop isn't passed.
|
|
@@ -300,6 +300,43 @@ export const WithSubmenus: Story = {
|
|
|
300
300
|
},
|
|
301
301
|
};
|
|
302
302
|
|
|
303
|
+
export const WithMaxHeight: Story = {
|
|
304
|
+
render: (args) => (
|
|
305
|
+
<InteractiveMenu {...args}>
|
|
306
|
+
<MenuItem>
|
|
307
|
+
<SettingsIcon style={{ marginRight: 8 }} /> Settings
|
|
308
|
+
</MenuItem>
|
|
309
|
+
<MenuItem>
|
|
310
|
+
<MessageIcon style={{ marginRight: 8 }} /> Messages
|
|
311
|
+
</MenuItem>
|
|
312
|
+
<MenuItem>
|
|
313
|
+
<ImageIcon style={{ marginRight: 8 }} /> Gallery
|
|
314
|
+
</MenuItem>
|
|
315
|
+
<MenuItem>
|
|
316
|
+
<SearchIcon style={{ marginRight: 8 }} /> Search
|
|
317
|
+
</MenuItem>
|
|
318
|
+
<MenuItem>
|
|
319
|
+
<ArrowsIcon style={{ marginRight: 8 }} /> Transfer my data
|
|
320
|
+
</MenuItem>
|
|
321
|
+
<MenuItem>
|
|
322
|
+
<TrashIcon style={{ marginRight: 8 }} /> Delete my account
|
|
323
|
+
</MenuItem>
|
|
324
|
+
</InteractiveMenu>
|
|
325
|
+
),
|
|
326
|
+
args: {
|
|
327
|
+
opened: true,
|
|
328
|
+
maxHeight: 160,
|
|
329
|
+
},
|
|
330
|
+
parameters: {
|
|
331
|
+
docs: {
|
|
332
|
+
description: {
|
|
333
|
+
story:
|
|
334
|
+
"`maxHeight` overrides the token-driven dropdown max-height with an explicit pixel value, scrolling the item list once it's exceeded.",
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
303
340
|
// mui-adapter's Menu has no native hover-trigger support (unlike Mantine's `trigger` prop),
|
|
304
341
|
// so this story implements open-on-hover itself: hovering the target opens the menu, and a
|
|
305
342
|
// short close delay (mirroring Mantine's `closeDelay`) keeps it open while the pointer moves
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
1
|
+
import { forwardRef, type CSSProperties } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Menu as MuiMenu,
|
|
4
4
|
type MenuProps as MuiMenuProps,
|
|
@@ -19,27 +19,47 @@ import { type RecursicaMenuProps } from "@recursica/adapter-common";
|
|
|
19
19
|
export type MenuProps = RecursicaOverStyled<MuiMenuProps & RecursicaMenuProps>;
|
|
20
20
|
|
|
21
21
|
export const Menu = forwardRef<HTMLDivElement, MenuProps>(function Menu(
|
|
22
|
-
{ overStyled = false, className, ...rest },
|
|
22
|
+
{ overStyled = false, className, maxHeight, ...rest },
|
|
23
23
|
ref,
|
|
24
24
|
) {
|
|
25
25
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
26
|
+
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
26
27
|
|
|
27
28
|
const mergedClassNames = mergeClassNames(
|
|
28
29
|
{
|
|
29
30
|
paper: styles.dropdown,
|
|
30
31
|
list: styles.dropdown,
|
|
31
32
|
},
|
|
32
|
-
|
|
33
|
-
| Partial<Record<string, string>>
|
|
34
|
-
| undefined,
|
|
33
|
+
restRecord.classes as Partial<Record<string, string>> | undefined,
|
|
35
34
|
);
|
|
36
35
|
|
|
36
|
+
// `maxHeight` is a caller-supplied override of the token-driven dropdown max-height, applied
|
|
37
|
+
// to the Paper slot's inline style — an explicit per-instance escape hatch, not a design token.
|
|
38
|
+
const callerSlotProps = restRecord.slotProps as
|
|
39
|
+
| { paper?: Record<string, unknown> }
|
|
40
|
+
| undefined;
|
|
41
|
+
const mergedSlotProps = maxHeight
|
|
42
|
+
? {
|
|
43
|
+
...callerSlotProps,
|
|
44
|
+
paper: {
|
|
45
|
+
...callerSlotProps?.paper,
|
|
46
|
+
style: {
|
|
47
|
+
...(callerSlotProps?.paper?.style as CSSProperties | undefined),
|
|
48
|
+
maxHeight,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
: callerSlotProps;
|
|
53
|
+
|
|
37
54
|
return (
|
|
38
55
|
<MuiMenu
|
|
39
56
|
ref={ref}
|
|
40
57
|
{...(sanitizedProps as MuiMenuProps)}
|
|
41
58
|
className={className}
|
|
42
59
|
classes={mergedClassNames}
|
|
60
|
+
{...(mergedSlotProps
|
|
61
|
+
? { slotProps: mergedSlotProps as MuiMenuProps["slotProps"] }
|
|
62
|
+
: {})}
|
|
43
63
|
/>
|
|
44
64
|
);
|
|
45
65
|
});
|
|
@@ -45,3 +45,9 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
|
|
|
45
45
|
> - **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.
|
|
46
46
|
> - **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.
|
|
47
47
|
> - **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.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 4. Notes
|
|
52
|
+
|
|
53
|
+
- `maxHeight` on `<Menu>` overrides the dropdown's token-driven max-height with an explicit pixel (or other CSS length) value, e.g. `<Menu maxHeight={320}>`. It's a per-instance escape hatch, not a design token — leave it unset to use the token default.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type RecursicaComboboxItemWithLabel } from "@recursica/adapter-common";
|
|
3
|
+
|
|
4
|
+
export interface RichOptionClassNames {
|
|
5
|
+
optionContent: string;
|
|
6
|
+
optionIcon: string;
|
|
7
|
+
optionText: string;
|
|
8
|
+
/** Combined with `optionText` when `wrapItemText` is true — see `renderRichOptionContent` below. */
|
|
9
|
+
optionTextWrap: string;
|
|
10
|
+
optionSupportingText: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `MenuItem`/option children for Dropdown/Autocomplete that renders `leadingIcon`/`supportingText`
|
|
15
|
+
* (see MANTINE_ADAPTER_RICH_OPTION_DATA.md) inside the row. Falls back to plain `label` when
|
|
16
|
+
* neither is present, so items without the new fields render exactly as before.
|
|
17
|
+
*
|
|
18
|
+
* `wrapItemText` (default `false`) controls whether `label`/`supportingText` wrap onto additional
|
|
19
|
+
* lines or truncate to a single line with an ellipsis.
|
|
20
|
+
*/
|
|
21
|
+
export function renderRichOptionContent(
|
|
22
|
+
item: Pick<
|
|
23
|
+
RecursicaComboboxItemWithLabel,
|
|
24
|
+
"label" | "leadingIcon" | "supportingText"
|
|
25
|
+
>,
|
|
26
|
+
classNames: RichOptionClassNames,
|
|
27
|
+
wrapItemText = false,
|
|
28
|
+
): React.ReactNode {
|
|
29
|
+
const { label, leadingIcon, supportingText } = item;
|
|
30
|
+
if (!leadingIcon && !supportingText) {
|
|
31
|
+
return label;
|
|
32
|
+
}
|
|
33
|
+
const optionTextClassName = wrapItemText
|
|
34
|
+
? `${classNames.optionText} ${classNames.optionTextWrap}`
|
|
35
|
+
: classNames.optionText;
|
|
36
|
+
return (
|
|
37
|
+
<span className={classNames.optionContent}>
|
|
38
|
+
{leadingIcon && (
|
|
39
|
+
<span className={classNames.optionIcon}>{leadingIcon}</span>
|
|
40
|
+
)}
|
|
41
|
+
<span className={optionTextClassName}>
|
|
42
|
+
<span>{label}</span>
|
|
43
|
+
{supportingText && (
|
|
44
|
+
<span className={classNames.optionSupportingText}>
|
|
45
|
+
{supportingText}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</span>
|
|
49
|
+
</span>
|
|
50
|
+
);
|
|
51
|
+
}
|