@recursica/mantine-adapter 0.48.1 → 0.50.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/mantine-adapter.cjs +2 -2
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +1673 -1616
- package/dist/mantine-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/components/AutoComplete/AUTOCOMPLETE_IMPLEMENTATION_NOTES.md +18 -2
- package/src/components/AutoComplete/AutoComplete.module.css +117 -12
- package/src/components/AutoComplete/AutoComplete.stories.tsx +149 -0
- package/src/components/AutoComplete/AutoComplete.tsx +29 -1
- package/src/components/AutoComplete/USAGE.md +32 -2
- package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +3 -0
- package/src/components/Dropdown/Dropdown.module.css +114 -6
- package/src/components/Dropdown/Dropdown.stories.tsx +149 -0
- package/src/components/Dropdown/Dropdown.tsx +30 -2
- package/src/components/Dropdown/USAGE.md +21 -0
- package/src/components/Menu/MENU_IMPLEMENTATION_NOTES.md +10 -0
- package/src/components/Menu/Menu.stories.tsx +42 -0
- package/src/components/Menu/Menu.tsx +24 -2
- package/src/components/Menu/USAGE.md +1 -0
- package/src/utils/renderRichOption.tsx +65 -0
|
@@ -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 { renderRichOption } 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 floating/portal menu — inside a
|
|
197
|
+
// container sized to Dropdown's own max-width token. Spacing between rows, icon/supportingText
|
|
198
|
+
// presence-or-absence alignment, and long-text wrapping/truncation are all much easier to inspect
|
|
199
|
+
// this way than by opening the real (portal-rendered) combobox dropdown. 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
|
+
{renderRichOption(
|
|
212
|
+
{ option: 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.",
|
|
@@ -3,7 +3,10 @@ import {
|
|
|
3
3
|
Select as MantineSelect,
|
|
4
4
|
type SelectProps as MantineSelectProps,
|
|
5
5
|
} from "@mantine/core";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type ReadOnlyControlProps,
|
|
8
|
+
normalizeComboboxData,
|
|
9
|
+
} from "@recursica/adapter-common";
|
|
7
10
|
import {
|
|
8
11
|
filterStylingProps,
|
|
9
12
|
omitUnsupportedProps,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
} from "../../utils/filterStylingProps";
|
|
13
16
|
import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
|
|
14
17
|
import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
|
|
18
|
+
import { renderRichOption } from "../../utils/renderRichOption";
|
|
15
19
|
import styles from "./Dropdown.module.css";
|
|
16
20
|
|
|
17
21
|
import { type RecursicaDropdownProps as BaseRecursicaDropdownProps } from "@recursica/adapter-common";
|
|
@@ -69,6 +73,8 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
69
73
|
defaultValue,
|
|
70
74
|
data,
|
|
71
75
|
clearButtonProps,
|
|
76
|
+
renderOption,
|
|
77
|
+
wrapItemText = false,
|
|
72
78
|
...rest
|
|
73
79
|
} = props;
|
|
74
80
|
const sanitizedProps = omitUnsupportedProps(
|
|
@@ -77,6 +83,13 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
77
83
|
);
|
|
78
84
|
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
79
85
|
|
|
86
|
+
// Mantine's own data parser only preserves extra fields (`leadingIcon`/`supportingText`) when
|
|
87
|
+
// an item already has both `value` and `label` — an item with `value` only is rebuilt into a
|
|
88
|
+
// bare `{value, label: value, disabled}` object, silently dropping them (see
|
|
89
|
+
// get-parsed-combobox-data.mjs, and AutoComplete.tsx's identical use of this). Matters here
|
|
90
|
+
// too now that `label` is optional (shared `RecursicaComboboxItem` — see adapter-common).
|
|
91
|
+
const normalizedData = normalizeComboboxData(data);
|
|
92
|
+
|
|
80
93
|
// Mantine's own clear button (rendered when `clearable` + a value are both present) otherwise
|
|
81
94
|
// renders unstyled — bare `CloseButton` defaults, no Recursica icon-button treatment. Merge in
|
|
82
95
|
// our own class (see `.clearButton` in Dropdown.module.css) alongside anything the caller
|
|
@@ -104,6 +117,14 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
104
117
|
restRecord.classNames as Partial<Record<string, string>> | undefined,
|
|
105
118
|
);
|
|
106
119
|
|
|
120
|
+
const optionClassNames = {
|
|
121
|
+
optionContent: styles.optionContent,
|
|
122
|
+
optionIcon: styles.optionIcon,
|
|
123
|
+
optionText: styles.optionText,
|
|
124
|
+
optionTextWrap: styles.optionTextWrap,
|
|
125
|
+
optionSupportingText: styles.optionSupportingText,
|
|
126
|
+
};
|
|
127
|
+
|
|
107
128
|
const injectedStyles = {
|
|
108
129
|
...((style as React.CSSProperties) || {}),
|
|
109
130
|
width: containerWidth || "100%",
|
|
@@ -154,7 +175,14 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
154
175
|
disabled={disabled}
|
|
155
176
|
value={value}
|
|
156
177
|
defaultValue={defaultValue}
|
|
157
|
-
data={
|
|
178
|
+
data={
|
|
179
|
+
(normalizedData as unknown as MantineSelectProps["data"]) || []
|
|
180
|
+
}
|
|
181
|
+
renderOption={
|
|
182
|
+
renderOption ??
|
|
183
|
+
((input) =>
|
|
184
|
+
renderRichOption(input, optionClassNames, wrapItemText))
|
|
185
|
+
}
|
|
158
186
|
clearButtonProps={mergedClearButtonProps}
|
|
159
187
|
label={undefined}
|
|
160
188
|
description={undefined}
|
|
@@ -46,3 +46,24 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
|
|
|
46
46
|
|
|
47
47
|
- `Dropdown` is styled independently from `TextField`; even though they look similar, they are themed using separate design tokens.
|
|
48
48
|
- Pass `leftSection` 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.
|
|
49
|
+
- `data` items can carry a `leadingIcon` and `supportingText`, rendered inside each option row:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<Dropdown
|
|
53
|
+
label="Assignee"
|
|
54
|
+
data={[
|
|
55
|
+
{
|
|
56
|
+
value: "jdoe",
|
|
57
|
+
label: "Jane Doe",
|
|
58
|
+
leadingIcon: <UserIcon />,
|
|
59
|
+
supportingText: "jane.doe@example.com",
|
|
60
|
+
},
|
|
61
|
+
{ value: "asmith", label: "Alex Smith" },
|
|
62
|
+
]}
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Pass your own `renderOption` to opt out of this default rendering for a given instance.
|
|
67
|
+
|
|
68
|
+
By default `label`/`supportingText` truncate to a single line with an ellipsis. Set
|
|
69
|
+
`wrapItemText` to wrap them onto additional lines instead: `<Dropdown data={data} wrapItemText />`.
|
|
@@ -76,3 +76,13 @@ Decisions and design tweaks strictly tailored for the UI Kit's Menu wrapped agai
|
|
|
76
76
|
- `pointer-events` on disabled items (Mantine handles disabled natively)
|
|
77
77
|
|
|
78
78
|
**Rationale:** Early iterations included aggressive structural resets (like `overflow: hidden`, `box-sizing: border-box`, `margin: 0`) cargo-culted from the Dropdown component. These caused sub-menus to render clipped inside the parent dropdown with scrollbars. The lesson: default to Mantine's native behavior and only override what the Recursica token system explicitly needs to control.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 9. `maxHeight` Override
|
|
83
|
+
|
|
84
|
+
**Decision:** `<Menu maxHeight={...}>` lets a caller override the token-driven `.dropdown` max-height with an explicit pixel (or other CSS length) value.
|
|
85
|
+
|
|
86
|
+
**Implementation:** This is the one deliberate exception to "no inline design tokens in TSX" (see `COMPONENT_DEV_GUIDE.md`) — it's applied via Mantine's per-part `styles` API (`styles={{ dropdown: { maxHeight } }}`), the same mechanism already used for `classNames`, merged with any caller-supplied `styles`. It only takes effect when `maxHeight` is actually passed; otherwise the CSS module's token-driven `max-height` is untouched.
|
|
87
|
+
|
|
88
|
+
**Caveat:** Because sub-menu dropdowns inherit the root Menu's `classNames`/`styles` mapping (see §6), setting `maxHeight` on the root also caps `Menu.Sub.Dropdown`'s height, not just the top-level dropdown.
|
|
@@ -161,6 +161,11 @@ const meta: Meta = {
|
|
|
161
161
|
description:
|
|
162
162
|
"Controlled open state. Leave undefined for uncontrolled behavior.",
|
|
163
163
|
},
|
|
164
|
+
maxHeight: {
|
|
165
|
+
control: "number",
|
|
166
|
+
description:
|
|
167
|
+
"Overrides the token-driven dropdown max-height with an explicit pixel value.",
|
|
168
|
+
},
|
|
164
169
|
},
|
|
165
170
|
parameters: {
|
|
166
171
|
docs: {
|
|
@@ -303,6 +308,43 @@ export const WithSubmenus: Story = {
|
|
|
303
308
|
},
|
|
304
309
|
};
|
|
305
310
|
|
|
311
|
+
export const WithMaxHeight: Story = {
|
|
312
|
+
args: {
|
|
313
|
+
position: "bottom-start",
|
|
314
|
+
maxHeight: 160,
|
|
315
|
+
// Rendered open by default so this story is diffable against the MUI adapter
|
|
316
|
+
// without an interaction step.
|
|
317
|
+
opened: true,
|
|
318
|
+
},
|
|
319
|
+
parameters: {
|
|
320
|
+
docs: {
|
|
321
|
+
description: {
|
|
322
|
+
story:
|
|
323
|
+
"`maxHeight` overrides the token-driven dropdown max-height with an explicit pixel value, scrolling the item list once it's exceeded.",
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
328
|
+
render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
|
|
329
|
+
return (
|
|
330
|
+
<Menu {...args}>
|
|
331
|
+
<Menu.Target>
|
|
332
|
+
<Button variant="solid">Menu with maxHeight</Button>
|
|
333
|
+
</Menu.Target>
|
|
334
|
+
|
|
335
|
+
<Menu.Dropdown>
|
|
336
|
+
<Menu.Item leftSection={<SettingsIcon />}>Settings</Menu.Item>
|
|
337
|
+
<Menu.Item leftSection={<MessageIcon />}>Messages</Menu.Item>
|
|
338
|
+
<Menu.Item leftSection={<ImageIcon />}>Gallery</Menu.Item>
|
|
339
|
+
<Menu.Item leftSection={<SearchIcon />}>Search</Menu.Item>
|
|
340
|
+
<Menu.Item leftSection={<ArrowsIcon />}>Transfer my data</Menu.Item>
|
|
341
|
+
<Menu.Item leftSection={<TrashIcon />}>Delete my account</Menu.Item>
|
|
342
|
+
</Menu.Dropdown>
|
|
343
|
+
</Menu>
|
|
344
|
+
);
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
306
348
|
export const HoverTrigger: Story = {
|
|
307
349
|
args: {
|
|
308
350
|
trigger: "click-hover",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
1
|
+
import { forwardRef, type CSSProperties } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Menu as MantineMenu,
|
|
4
4
|
type MenuProps as MantineMenuProps,
|
|
@@ -49,6 +49,7 @@ export type MenuProps = RecursicaOverStyled<
|
|
|
49
49
|
const MenuBase = function Menu({ overStyled = false, ...rest }: MenuProps) {
|
|
50
50
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
51
51
|
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
52
|
+
const { maxHeight, ...menuRestRecord } = restRecord;
|
|
52
53
|
|
|
53
54
|
// Bind CSS module classes to Mantine's internal classNames API
|
|
54
55
|
const mergedClassNames = mergeClassNames(
|
|
@@ -64,10 +65,31 @@ const MenuBase = function Menu({ overStyled = false, ...rest }: MenuProps) {
|
|
|
64
65
|
restRecord.classNames as Partial<Record<string, string>> | undefined,
|
|
65
66
|
);
|
|
66
67
|
|
|
68
|
+
// `maxHeight` is a caller-supplied override of the token-driven dropdown max-height, applied
|
|
69
|
+
// via Mantine's per-part `styles` API (same mechanism as `classNames` above) rather than a CSS
|
|
70
|
+
// module change, since it's an explicit per-instance escape hatch, not a design token. The CSS
|
|
71
|
+
// module deliberately leaves `overflow` unset (see MENU_IMPLEMENTATION_NOTES.md §8) so it
|
|
72
|
+
// doesn't clip sub-menus at the token default height; once a caller opts into a fixed
|
|
73
|
+
// `maxHeight`, though, items need to scroll instead of spilling out past the boundary.
|
|
74
|
+
const callerStyles = menuRestRecord.styles as
|
|
75
|
+
| Partial<Record<string, CSSProperties>>
|
|
76
|
+
| undefined;
|
|
77
|
+
const mergedStyles = maxHeight
|
|
78
|
+
? {
|
|
79
|
+
...callerStyles,
|
|
80
|
+
dropdown: {
|
|
81
|
+
...callerStyles?.dropdown,
|
|
82
|
+
maxHeight: maxHeight as CSSProperties["maxHeight"],
|
|
83
|
+
overflowY: "auto" as CSSProperties["overflowY"],
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
: callerStyles;
|
|
87
|
+
|
|
67
88
|
return (
|
|
68
89
|
<MantineMenu
|
|
69
|
-
{...(
|
|
90
|
+
{...(menuRestRecord as unknown as MantineMenuProps)}
|
|
70
91
|
classNames={mergedClassNames}
|
|
92
|
+
{...(mergedStyles ? { styles: mergedStyles } : {})}
|
|
71
93
|
/>
|
|
72
94
|
);
|
|
73
95
|
};
|
|
@@ -52,3 +52,4 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
|
|
|
52
52
|
|
|
53
53
|
- The full Mantine composition API is supported, including sub-menus: `Menu`, `Menu.Target`, `Menu.Dropdown`, `Menu.Item`, `Menu.Divider`, `Menu.Label`, `Menu.Sub`, `Menu.Sub.Target`, `Menu.Sub.Item`, and `Menu.Sub.Dropdown`.
|
|
54
54
|
- The `color` prop on `Menu.Item` (and `Menu.Sub.Item`) — used by Mantine for semantics like a "danger" item — is ignored unless `overStyled={true}` is set.
|
|
55
|
+
- `maxHeight` on the root `<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,65 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
type ComboboxStringItem,
|
|
4
|
+
type ComboboxLikeRenderOptionInput,
|
|
5
|
+
} from "@mantine/core";
|
|
6
|
+
|
|
7
|
+
// `ComboboxStringItem`/`ComboboxItem` only type `value`/`label`/`disabled` —
|
|
8
|
+
// `leadingIcon`/`supportingText` are extra fields Mantine's own `getParsedComboboxData` passes
|
|
9
|
+
// through untouched whenever an item already has both `value` and `label` (see
|
|
10
|
+
// get-parsed-combobox-data.mjs), so they reach `option` here at runtime even though the upstream
|
|
11
|
+
// type doesn't know about them. Generic over both Autocomplete's `ComboboxStringItem` (optional
|
|
12
|
+
// `label`) and Dropdown's `ComboboxItem` (required `label`) — the same renderer serves both.
|
|
13
|
+
type RichComboboxItem = ComboboxStringItem & {
|
|
14
|
+
label?: string;
|
|
15
|
+
leadingIcon?: React.ReactNode;
|
|
16
|
+
supportingText?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface RichOptionClassNames {
|
|
20
|
+
optionContent: string;
|
|
21
|
+
optionIcon: string;
|
|
22
|
+
optionText: string;
|
|
23
|
+
/** Combined with `optionText` when `wrapItemText` is true — see `renderRichOption` below. */
|
|
24
|
+
optionTextWrap: string;
|
|
25
|
+
optionSupportingText: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `renderOption` for Autocomplete/Dropdown that renders an item's `leadingIcon`/`supportingText`
|
|
30
|
+
* (see MANTINE_ADAPTER_RICH_OPTION_DATA.md) inside the option row. Falls back to plain
|
|
31
|
+
* `option.label` — the same rendering Mantine uses when no `renderOption` is supplied at all — so
|
|
32
|
+
* items without the new fields are unaffected.
|
|
33
|
+
*
|
|
34
|
+
* `wrapItemText` (default `false`) controls whether `label`/`supportingText` wrap onto additional
|
|
35
|
+
* lines or truncate to a single line with an ellipsis.
|
|
36
|
+
*/
|
|
37
|
+
export function renderRichOption<T extends ComboboxStringItem>(
|
|
38
|
+
{ option }: ComboboxLikeRenderOptionInput<T>,
|
|
39
|
+
classNames: RichOptionClassNames,
|
|
40
|
+
wrapItemText = false,
|
|
41
|
+
): React.ReactNode {
|
|
42
|
+
const { label, leadingIcon, supportingText } = option as RichComboboxItem;
|
|
43
|
+
const displayLabel = label ?? option.value;
|
|
44
|
+
if (!leadingIcon && !supportingText) {
|
|
45
|
+
return displayLabel;
|
|
46
|
+
}
|
|
47
|
+
const optionTextClassName = wrapItemText
|
|
48
|
+
? `${classNames.optionText} ${classNames.optionTextWrap}`
|
|
49
|
+
: classNames.optionText;
|
|
50
|
+
return (
|
|
51
|
+
<span className={classNames.optionContent}>
|
|
52
|
+
{leadingIcon && (
|
|
53
|
+
<span className={classNames.optionIcon}>{leadingIcon}</span>
|
|
54
|
+
)}
|
|
55
|
+
<span className={optionTextClassName}>
|
|
56
|
+
<span>{displayLabel}</span>
|
|
57
|
+
{supportingText && (
|
|
58
|
+
<span className={classNames.optionSupportingText}>
|
|
59
|
+
{supportingText}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
</span>
|
|
63
|
+
</span>
|
|
64
|
+
);
|
|
65
|
+
}
|