@recursica/mui-adapter 0.32.1 → 0.33.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 +12 -0
- package/dist/index.d.ts +42 -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 +6825 -6751
- 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/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 />`.
|
|
@@ -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
|
+
}
|