@recursica/mantine-adapter 0.48.1 → 0.49.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/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 +1662 -1613
- 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/utils/renderRichOption.tsx +65 -0
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "git+https://github.com/borderux/recursica.git",
|
|
14
14
|
"directory": "packages/mantine-adapter"
|
|
15
15
|
},
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.49.0",
|
|
17
17
|
"type": "module",
|
|
18
18
|
"main": "./dist/mantine-adapter.cjs",
|
|
19
19
|
"module": "./dist/mantine-adapter.js",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"vitest": "^3.2.4"
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
|
-
"@recursica/adapter-common": "^0.
|
|
97
|
+
"@recursica/adapter-common": "^0.24.0",
|
|
98
98
|
"@recursica/official-release": "^2.8.0"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
@@ -16,6 +16,22 @@ The Mantine `<Autocomplete>` dropdown menu and options are styled strictly using
|
|
|
16
16
|
|
|
17
17
|
Focus, errors, and disabled visual states are enforced explicitly via the outer `<FormControlWrapper>` boundary emitting context down structurally (`[data-error]`, `[data-disabled]`) and evaluated efficiently against scoped nested selectors natively inside `AutoComplete.module.css`.
|
|
18
18
|
|
|
19
|
-
## Missing
|
|
19
|
+
## Missing Selected Option Highlight
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
`.option:hover`/`.option[data-hovered="true"]` work — hover has a real background tint. There's no
|
|
22
|
+
equivalent for "this option matches the current value" though, and it isn't fixable in CSS alone:
|
|
23
|
+
Mantine's `Autocomplete` never passes a `value` prop into its internal `OptionsDropdown` (only
|
|
24
|
+
`search`, for filtering — see `node_modules/@mantine/core/esm/components/Autocomplete/Autocomplete.mjs`),
|
|
25
|
+
so `OptionsDropdown`'s `checked` (and the `data-combobox-active` attribute it drives — the same one
|
|
26
|
+
`Dropdown`'s equivalent highlight below keys off) never gets set on any option, no matter what's
|
|
27
|
+
typed into the field. See `Dropdown.module.css`'s `.option[data-combobox-active="true"]` rule for
|
|
28
|
+
what a real fix would key off, if this ever gets solved upstream or by reimplementing option
|
|
29
|
+
rendering with our own value comparison.
|
|
30
|
+
|
|
31
|
+
## Rich Option Content (`leadingIcon`/`supportingText`)
|
|
32
|
+
|
|
33
|
+
`data` items accept optional `leadingIcon`/`supportingText` fields, via the shared `RecursicaComboboxItem` type in `@recursica/adapter-common` (see `MANTINE_ADAPTER_RICH_OPTION_DATA.md` at the repo root) — the same type `Dropdown` and both `mui-adapter` components use. `AutoComplete.tsx` installs a default `renderOption` (`../../utils/renderRichOption.tsx`, shared with `Dropdown`) that reads these off Mantine's parsed `option`. `label` on the shared type is optional — Mantine's `getParsedComboboxData` only preserves an item's extra fields when it already has both `value` and `label`; an item with `value` only is rebuilt into a bare `{value, label: value, disabled}` object first, dropping `leadingIcon`/`supportingText`. `AutoComplete.tsx` runs `data` through adapter-common's `normalizeComboboxData` (backfills `label` from `value`) before handing it to Mantine, so the rich fields always survive regardless of whether the caller set `label` (`Dropdown.tsx` does the same now that its `label` is optional too). New CSS classes (`.optionContent`/`.optionIcon`/`.optionText`/`.optionSupportingText`) reuse the menu-item component's icon/supporting-text tokens, matching Dropdown's equivalent addition — no dedicated autocomplete-option tokens exist for either. The icon is a conditional child (only rendered when `leadingIcon` is set, not a hidden reserved slot), so label/supportingText shift left when there's no icon; `.optionContent`'s `align-items: center` keeps the label vertically centered when there's no `supportingText`.
|
|
34
|
+
|
|
35
|
+
## `wrapItemText`
|
|
36
|
+
|
|
37
|
+
`label`/`supportingText` default to single-line truncation with an ellipsis (`.optionText > *` — `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`). Passing `wrapItemText` adds `.optionTextWrap` alongside `.optionText`, which re-enables wrapping (`white-space: normal; overflow-wrap: anywhere`) for both children — same later-cascade-wins mechanism (`.optionTextWrap > *` declared after `.optionText > *`, equal specificity) as the rest of this file's overrides. `renderRichOption`/`renderRichOptionContent` (shared util, both adapters) take `wrapItemText` as a third parameter and combine the two class names when it's true.
|
|
@@ -212,26 +212,31 @@
|
|
|
212
212
|
color: var(
|
|
213
213
|
--recursica_ui-kit_components_autocomplete_properties_colors_text-color
|
|
214
214
|
);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
215
|
+
/* Padding/inter-item gap reuse the menu-item component's tokens (same reasoning as the icon/
|
|
216
|
+
supporting-text tokens below) instead of an arbitrary scaled autocomplete padding — matches
|
|
217
|
+
Menu.module.css's `.item` padding + `margin-bottom` gap exactly. */
|
|
218
|
+
padding: var(
|
|
219
|
+
--recursica_ui-kit_components_menu-item_properties_vertical-padding
|
|
220
220
|
)
|
|
221
|
-
var(
|
|
222
|
-
|
|
223
|
-
);
|
|
221
|
+
var(--recursica_ui-kit_components_menu-item_properties_horizontal-padding);
|
|
222
|
+
margin-bottom: var(--recursica_ui-kit_components_menu_properties_item-gap);
|
|
224
223
|
cursor: pointer;
|
|
225
224
|
}
|
|
226
225
|
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
.option:last-of-type {
|
|
227
|
+
margin-bottom: 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* No per-option hovered token exists in the schema; use the generic overlay tint (same technique
|
|
231
|
+
as Table/Dropdown) to highlight a hovered-but-not-selected option. Mantine's own
|
|
232
|
+
`data-combobox-selected` here is its transient keyboard-navigation highlight (imperatively
|
|
233
|
+
set/cleared via arrow keys — see use-combobox.mjs's `selectOption`/`clearSelectedItem` — not
|
|
234
|
+
the persistently-chosen value), so it belongs in this hover-equivalent bucket, not the real
|
|
235
|
+
selected-value rule below. */
|
|
229
236
|
.option[data-selected="true"],
|
|
230
237
|
.option[data-selected],
|
|
231
238
|
.option[data-combobox-selected="true"],
|
|
232
239
|
.option[data-combobox-selected],
|
|
233
|
-
.option[data-combobox-active="true"],
|
|
234
|
-
.option[data-combobox-active],
|
|
235
240
|
.option[data-hovered="true"],
|
|
236
241
|
.option[data-hovered],
|
|
237
242
|
.option:hover {
|
|
@@ -243,6 +248,106 @@
|
|
|
243
248
|
);
|
|
244
249
|
}
|
|
245
250
|
|
|
251
|
+
/* No dedicated autocomplete-option "selected" token exists in the schema either, but the
|
|
252
|
+
menu-item component's selected-state colors are the closest real token family for the same
|
|
253
|
+
concept (a selected row in a list) — same reuse Dropdown.module.css makes. Despite the name,
|
|
254
|
+
Mantine's `data-combobox-active` is the attribute it sets when an option's value matches the
|
|
255
|
+
current field value (see OptionsDropdown.mjs: `active: checked`) — the actual "this is the
|
|
256
|
+
selected item" flag, distinct from the transient `data-combobox-selected` keyboard-highlight
|
|
257
|
+
grouped with hover above. Keying this rule off `data-combobox-selected` instead (as it
|
|
258
|
+
previously did) is why the selected option only showed a highlight while arrow-key navigating,
|
|
259
|
+
and lost it as soon as the dropdown was reopened normally. */
|
|
260
|
+
.option[data-combobox-active="true"],
|
|
261
|
+
.option[data-combobox-active] {
|
|
262
|
+
background-color: var(
|
|
263
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_background-color
|
|
264
|
+
);
|
|
265
|
+
color: var(
|
|
266
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_text-color
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* Rich option content (leadingIcon/supportingText — see MANTINE_ADAPTER_RICH_OPTION_DATA.md). No
|
|
271
|
+
dedicated autocomplete-option icon or supporting-text token exists in the schema either; reuse
|
|
272
|
+
the menu-item component's tokens, same reasoning as Dropdown.module.css. */
|
|
273
|
+
.optionContent {
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
gap: var(--recursica_ui-kit_components_menu-item_properties_icon-text-gap);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.optionIcon {
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-shrink: 0;
|
|
282
|
+
width: var(
|
|
283
|
+
--recursica_ui-kit_components_menu-item_properties_icon-leading-size
|
|
284
|
+
);
|
|
285
|
+
height: var(
|
|
286
|
+
--recursica_ui-kit_components_menu-item_properties_icon-leading-size
|
|
287
|
+
);
|
|
288
|
+
color: var(
|
|
289
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_leading-icon-color
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.optionIcon :global(svg) {
|
|
294
|
+
width: 100%;
|
|
295
|
+
height: 100%;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.optionText {
|
|
299
|
+
display: flex;
|
|
300
|
+
flex-direction: column;
|
|
301
|
+
min-width: 0;
|
|
302
|
+
gap: var(--recursica_ui-kit_components_menu-item_properties_text-gap);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Default: label/supportingText each truncate to a single line with an ellipsis. */
|
|
306
|
+
.optionText > * {
|
|
307
|
+
overflow: hidden;
|
|
308
|
+
text-overflow: ellipsis;
|
|
309
|
+
white-space: nowrap;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* Applied alongside .optionText when `wrapItemText` is true — long values wrap onto additional
|
|
313
|
+
lines instead of overflowing the fixed-width dropdown. */
|
|
314
|
+
.optionTextWrap > * {
|
|
315
|
+
overflow: visible;
|
|
316
|
+
text-overflow: clip;
|
|
317
|
+
white-space: normal;
|
|
318
|
+
overflow-wrap: anywhere;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.optionSupportingText {
|
|
322
|
+
font-family: var(
|
|
323
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-family
|
|
324
|
+
);
|
|
325
|
+
font-size: var(
|
|
326
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-size
|
|
327
|
+
);
|
|
328
|
+
font-style: var(
|
|
329
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-style
|
|
330
|
+
);
|
|
331
|
+
font-weight: var(
|
|
332
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-weight
|
|
333
|
+
);
|
|
334
|
+
letter-spacing: var(
|
|
335
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_letter-spacing
|
|
336
|
+
);
|
|
337
|
+
line-height: var(
|
|
338
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_line-height
|
|
339
|
+
);
|
|
340
|
+
text-decoration: var(
|
|
341
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_text-decoration
|
|
342
|
+
);
|
|
343
|
+
text-transform: var(
|
|
344
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_text-transform
|
|
345
|
+
);
|
|
346
|
+
color: var(
|
|
347
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_supporting-text-color
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
246
351
|
/* -------------------------------------
|
|
247
352
|
STATE CASCADE ARCHITECTURE
|
|
248
353
|
-------------------------------------- */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { AutoComplete } from "./AutoComplete";
|
|
3
3
|
import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
|
|
4
|
+
import { renderRichOption } from "../../utils/renderRichOption";
|
|
5
|
+
import styles from "./AutoComplete.module.css";
|
|
4
6
|
|
|
5
7
|
const meta: Meta<typeof AutoComplete> = {
|
|
6
8
|
title: "UI-Kit/AutoComplete",
|
|
@@ -55,6 +57,11 @@ Always structure horizontal architectures via the generic \`formLayout\` paramet
|
|
|
55
57
|
description:
|
|
56
58
|
"Toggles structural read-only data presentation explicitly blocking standard component bindings.",
|
|
57
59
|
},
|
|
60
|
+
wrapItemText: {
|
|
61
|
+
control: "boolean",
|
|
62
|
+
description:
|
|
63
|
+
"Wraps option label/supportingText onto additional lines instead of truncating with an ellipsis.",
|
|
64
|
+
},
|
|
58
65
|
},
|
|
59
66
|
};
|
|
60
67
|
|
|
@@ -144,6 +151,148 @@ export const WithTrailingIcon: Story = {
|
|
|
144
151
|
},
|
|
145
152
|
};
|
|
146
153
|
|
|
154
|
+
const UserIcon = (
|
|
155
|
+
<svg
|
|
156
|
+
width="16"
|
|
157
|
+
height="16"
|
|
158
|
+
viewBox="0 0 24 24"
|
|
159
|
+
fill="none"
|
|
160
|
+
stroke="currentColor"
|
|
161
|
+
strokeWidth="2"
|
|
162
|
+
strokeLinecap="round"
|
|
163
|
+
strokeLinejoin="round"
|
|
164
|
+
>
|
|
165
|
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
|
166
|
+
<circle cx="12" cy="7" r="4"></circle>
|
|
167
|
+
</svg>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
export const WithRichOptions: Story = {
|
|
171
|
+
args: {
|
|
172
|
+
label: "Assignee",
|
|
173
|
+
placeholder: "Search team members...",
|
|
174
|
+
data: [
|
|
175
|
+
{
|
|
176
|
+
value: "jdoe",
|
|
177
|
+
label: "Jane Doe",
|
|
178
|
+
leadingIcon: UserIcon,
|
|
179
|
+
supportingText: "jane.doe@example.com",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
value: "asmith",
|
|
183
|
+
label: "Alex Smith",
|
|
184
|
+
leadingIcon: UserIcon,
|
|
185
|
+
supportingText: "alex.smith@example.com",
|
|
186
|
+
},
|
|
187
|
+
{ value: "unassigned", label: "Unassigned" },
|
|
188
|
+
],
|
|
189
|
+
assistiveText:
|
|
190
|
+
"Each option can show a leading icon and supporting text — see MANTINE_ADAPTER_RICH_OPTION_DATA.md.",
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const WithRichOptionsWrapped: Story = {
|
|
195
|
+
args: {
|
|
196
|
+
label: "Assignee",
|
|
197
|
+
placeholder: "Search team members...",
|
|
198
|
+
wrapItemText: true,
|
|
199
|
+
data: [
|
|
200
|
+
{
|
|
201
|
+
value: "jdoe",
|
|
202
|
+
label: "Jane Doe, Senior Staff Engineer, Platform Infrastructure",
|
|
203
|
+
leadingIcon: UserIcon,
|
|
204
|
+
supportingText:
|
|
205
|
+
"jane.doe@example.com — Platform Infrastructure team, on-call rotation lead",
|
|
206
|
+
},
|
|
207
|
+
{ value: "unassigned", label: "Unassigned" },
|
|
208
|
+
],
|
|
209
|
+
assistiveText:
|
|
210
|
+
"wrapItemText=true — long label/supportingText wrap instead of truncating.",
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const optionRowPreviewClassNames = {
|
|
215
|
+
optionContent: styles.optionContent,
|
|
216
|
+
optionIcon: styles.optionIcon,
|
|
217
|
+
optionText: styles.optionText,
|
|
218
|
+
optionTextWrap: styles.optionTextWrap,
|
|
219
|
+
optionSupportingText: styles.optionSupportingText,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const OPTION_ROW_PREVIEW_ITEMS = [
|
|
223
|
+
{
|
|
224
|
+
value: "icon-and-supporting",
|
|
225
|
+
label: "Jane Doe",
|
|
226
|
+
leadingIcon: UserIcon,
|
|
227
|
+
supportingText: "jane.doe@example.com",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
value: "no-icon",
|
|
231
|
+
label: "Alex Smith",
|
|
232
|
+
supportingText:
|
|
233
|
+
"No leadingIcon — label/supportingText shift left, no reserved icon space",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
value: "no-supporting-text",
|
|
237
|
+
label: "Taylor Rivera",
|
|
238
|
+
leadingIcon: UserIcon,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
value: "plain",
|
|
242
|
+
label: "Plain option — no leadingIcon, no supportingText",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
value: "long-text",
|
|
246
|
+
label:
|
|
247
|
+
"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",
|
|
248
|
+
leadingIcon: UserIcon,
|
|
249
|
+
supportingText:
|
|
250
|
+
"A similarly long supporting text string, to confirm the same wrap-or-truncate behavior applies to it too",
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
// Renders the option row content directly — outside the floating/portal menu — inside a
|
|
255
|
+
// container sized to AutoComplete's own max-width token. Spacing between rows, icon/
|
|
256
|
+
// supportingText presence-or-absence alignment, and long-text wrapping/truncation are all much
|
|
257
|
+
// easier to inspect this way than by opening the real (portal-rendered) combobox dropdown. See
|
|
258
|
+
// MANTINE_ADAPTER_RICH_OPTION_DATA.md.
|
|
259
|
+
const renderOptionRowPreview = (wrapItemText: boolean) => (
|
|
260
|
+
<div
|
|
261
|
+
className={styles.dropdown}
|
|
262
|
+
style={{
|
|
263
|
+
width:
|
|
264
|
+
"var(--recursica_ui-kit_components_autocomplete_variants_layouts_stacked_properties_max-width)",
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
{OPTION_ROW_PREVIEW_ITEMS.map((item) => (
|
|
268
|
+
<div key={item.value} className={styles.option}>
|
|
269
|
+
{renderRichOption(
|
|
270
|
+
{ option: item },
|
|
271
|
+
optionRowPreviewClassNames,
|
|
272
|
+
wrapItemText,
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
))}
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Default: `wrapItemText` is false — label/supportingText truncate to a single line with an
|
|
280
|
+
// ellipsis instead of wrapping.
|
|
281
|
+
export const RichOptionRowPreview: Story = {
|
|
282
|
+
parameters: {
|
|
283
|
+
controls: { disable: true },
|
|
284
|
+
},
|
|
285
|
+
render: () => renderOptionRowPreview(false),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// `wrapItemText: true` — label/supportingText wrap onto additional lines instead of truncating.
|
|
289
|
+
export const RichOptionRowPreviewWrapped: Story = {
|
|
290
|
+
parameters: {
|
|
291
|
+
controls: { disable: true },
|
|
292
|
+
},
|
|
293
|
+
render: () => renderOptionRowPreview(true),
|
|
294
|
+
};
|
|
295
|
+
|
|
147
296
|
export const Disabled: Story = {
|
|
148
297
|
args: {
|
|
149
298
|
label: "Disabled Deployment Node",
|
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
type AutocompleteProps as MantineAutocompleteProps,
|
|
5
5
|
type InputWrapperProps,
|
|
6
6
|
} from "@mantine/core";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type ReadOnlyControlProps,
|
|
9
|
+
normalizeComboboxData,
|
|
10
|
+
} from "@recursica/adapter-common";
|
|
8
11
|
import {
|
|
9
12
|
filterStylingProps,
|
|
10
13
|
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 { renderRichOption } from "../../utils/renderRichOption";
|
|
16
20
|
import styles from "./AutoComplete.module.css";
|
|
17
21
|
|
|
18
22
|
import { type RecursicaAutocompleteProps as BaseRecursicaAutocompleteProps } from "@recursica/adapter-common";
|
|
@@ -80,6 +84,9 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
|
|
|
80
84
|
emptyValueComponent,
|
|
81
85
|
value,
|
|
82
86
|
defaultValue,
|
|
87
|
+
data,
|
|
88
|
+
renderOption,
|
|
89
|
+
wrapItemText = false,
|
|
83
90
|
...rest
|
|
84
91
|
} = props;
|
|
85
92
|
|
|
@@ -89,6 +96,13 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
|
|
|
89
96
|
);
|
|
90
97
|
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
91
98
|
|
|
99
|
+
// Mantine's own data parser only preserves extra fields (`leadingIcon`/`supportingText`) when
|
|
100
|
+
// an item already has both `value` and `label` — an item with `value` only is rebuilt into a
|
|
101
|
+
// bare `{value, label: value, disabled}` object, silently dropping them (see
|
|
102
|
+
// get-parsed-combobox-data.mjs). `normalizeComboboxData` backfills `label` so the rich fields
|
|
103
|
+
// always survive regardless of whether the caller set it.
|
|
104
|
+
const normalizedData = normalizeComboboxData(data);
|
|
105
|
+
|
|
92
106
|
// Securely map core native blocks down ensuring nested CSS modules map precisely
|
|
93
107
|
const mergedClassNames = mergeClassNames(
|
|
94
108
|
{
|
|
@@ -101,6 +115,14 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
|
|
|
101
115
|
restRecord.classNames as Partial<Record<string, string>> | undefined,
|
|
102
116
|
);
|
|
103
117
|
|
|
118
|
+
const optionClassNames = {
|
|
119
|
+
optionContent: styles.optionContent,
|
|
120
|
+
optionIcon: styles.optionIcon,
|
|
121
|
+
optionText: styles.optionText,
|
|
122
|
+
optionTextWrap: styles.optionTextWrap,
|
|
123
|
+
optionSupportingText: styles.optionSupportingText,
|
|
124
|
+
};
|
|
125
|
+
|
|
104
126
|
const wrapperClass = className
|
|
105
127
|
? `${styles.layoutOverride} ${className}`
|
|
106
128
|
: styles.layoutOverride;
|
|
@@ -141,6 +163,12 @@ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
|
|
|
141
163
|
value={value as string | undefined}
|
|
142
164
|
defaultValue={defaultValue as string | undefined}
|
|
143
165
|
error={!!error}
|
|
166
|
+
data={normalizedData as unknown as MantineAutocompleteProps["data"]}
|
|
167
|
+
renderOption={
|
|
168
|
+
renderOption ??
|
|
169
|
+
((input) =>
|
|
170
|
+
renderRichOption(input, optionClassNames, wrapItemText))
|
|
171
|
+
}
|
|
144
172
|
/>
|
|
145
173
|
}
|
|
146
174
|
/>
|
|
@@ -45,6 +45,36 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
|
|
|
45
45
|
|
|
46
46
|
## 4. Key Integration Features & Constraints
|
|
47
47
|
|
|
48
|
-
###
|
|
48
|
+
### Rich option content: `leadingIcon` / `supportingText`
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
`data` items can carry an icon and a secondary line of text, rendered inside each option row:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<AutoComplete
|
|
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
|
+
- `label` is optional, same as always — items with `value` only still work, falling back to
|
|
68
|
+
`value` as the displayed/matched text.
|
|
69
|
+
- Pass your own `renderOption` to opt out of this default rendering entirely for a given
|
|
70
|
+
component instance.
|
|
71
|
+
- By default `label`/`supportingText` truncate to a single line with an ellipsis. Set
|
|
72
|
+
`wrapItemText` to wrap them onto additional lines instead:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<AutoComplete data={data} wrapItemText />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Known Limitation: Selected Option Highlight
|
|
79
|
+
|
|
80
|
+
Hover works (see `.option:hover`/`.option[data-hovered="true"]`), but there's no distinct "this option matches the current value" background — unlike `Dropdown`, which has one. This isn't a styling gap: Mantine's `Autocomplete` never passes a `value` prop into its internal `OptionsDropdown` (only `search`, for filtering — see `Autocomplete.mjs`), so `checked`/`data-combobox-active` (the attribute that drives `Dropdown`'s equivalent highlight) never gets set on any option, regardless of what's typed into the field. There's no other DOM signal to key a CSS rule off. Would need either an upstream Mantine change or reimplementing option rendering ourselves with a value comparison.
|
|
@@ -7,3 +7,6 @@ The `Dropdown` component is mapped explicitly to Mantine's `<Select>` following
|
|
|
7
7
|
3. **Dropdown Appendages:** To correctly map Mantine's detached Popover `.dropdown` and list `.option` items, we targeted focus and geometric bindings appending standard padding structures matched to the dropdown height overrides dynamically into our `Dropdown.module.css`.
|
|
8
8
|
4. **Popup Vertical Padding:** The gap above/below the option list inside `.dropdown` is explicitly set from the same `--recursica_..._dropdown_properties_vertical-padding` token the closed control uses — it was previously left to Mantine's own untokenized `--combobox-padding` (4px) default.
|
|
9
9
|
5. **Clear Button Styling:** Mantine's native clear button (`clearButtonProps`, shown when `clearable` + a value are both present) is a bare `CloseButton` with its own hardcoded gray icon/hover styling by default. `Dropdown.tsx` merges in a `.clearButton` class (Dropdown.module.css) that overrides it with the same icon-size/trailing-icon-color/focus-ring tokens as the rest of the right section, using the double-class-selector specificity trick (same as Chip.module.css's `.root.root`) to beat Mantine's own CSS module rule without `!important`.
|
|
10
|
+
6. **Rich Option Content (`leadingIcon`/`supportingText`):** `data` items now accept optional `leadingIcon`/`supportingText` fields, via the shared `RecursicaComboboxItem` type in `@recursica/adapter-common` (see `MANTINE_ADAPTER_RICH_OPTION_DATA.md` at the repo root) — the same type `AutoComplete` and both `mui-adapter` components use, so it's declared once and not redeclared per component/adapter. `label` on that shared type is optional, falling back to `value` (matches Mantine's own runtime default for both `Select` and `Autocomplete`). `Dropdown.tsx` installs a default `renderOption` (`../../utils/renderRichOption.tsx`, shared with `AutoComplete`) that reads `leadingIcon`/`supportingText` straight off Mantine's own parsed `option` — no separate lookup-by-value map is needed, since Mantine's `getParsedComboboxData` passes extra fields on an item through untouched whenever the item already has both `value` and `label`. Because `label` is now optional, `data` is first run through adapter-common's `normalizeComboboxData` (backfilling `label` from `value`) before being handed to Mantine, so items missing `label` don't have their `leadingIcon`/`supportingText` silently dropped by that same parser (see `AutoComplete.tsx`'s identical, longer-standing use of this — `Dropdown`'s `label` used to be required, which is why it didn't need this before). A caller-supplied `renderOption` always wins over the default. New CSS classes (`.optionContent`/`.optionIcon`/`.optionText`/`.optionSupportingText`) reuse the menu-item component's icon/supporting-text tokens — no dedicated dropdown-option tokens exist for either, same reasoning as the `.option[data-selected]` reuse above. The icon is only rendered when `leadingIcon` is present (a conditional child, not a hidden reserved slot), so label/supportingText shift left when there's no icon; `.optionContent`'s `align-items: center` keeps the label vertically centered when there's no `supportingText` to stack under it.
|
|
11
|
+
7. **`wrapItemText`:** `label`/`supportingText` default to single-line truncation with an ellipsis (`.optionText > *` — `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`). Passing `wrapItemText` adds `.optionTextWrap` alongside `.optionText`, re-enabling wrapping (`white-space: normal; overflow-wrap: anywhere`) for both children — later-cascade-wins, equal specificity, same mechanism as this file's other CSS overrides. `renderRichOption`/`renderRichOptionContent` take `wrapItemText` as a third parameter and combine the two class names when it's true.
|
|
12
|
+
8. **Selected Option Highlight (bug fix):** The `.option[data-selected="true"], .option[data-combobox-active="true"]` background/text-color rule (and the matching `.optionIcon`/`.optionSupportingText` color rules) used to key off `data-combobox-selected` instead of `data-combobox-active`. Despite the name, `data-combobox-active` is what Mantine sets when an option's value matches the current field value (`OptionsDropdown.mjs`: `active: checked`, consumed by `ComboboxOption.mjs`'s `mod` map); `data-combobox-selected` is an unrelated, transient keyboard-navigation highlight that Mantine sets/clears imperatively via `element.setAttribute` outside React (`use-combobox.mjs`'s `selectOption`/`clearSelectedItem`), not tied to the chosen value at all. The bug: the real selected option only showed the brand background while being arrow-key-navigated, and lost it entirely on a normal open/close (matches Playwright verification — `data-combobox-active="true"` is present and gets the token background after a plain click-select-reopen, with no keyboard involved).
|
|
@@ -323,14 +323,21 @@
|
|
|
323
323
|
color: var(
|
|
324
324
|
--recursica_ui-kit_components_dropdown_properties_colors_text-color
|
|
325
325
|
);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
/* Padding/inter-item gap reuse the menu-item component's tokens (same reasoning as the icon/
|
|
327
|
+
supporting-text tokens below) instead of an arbitrary scaled dropdown padding — matches
|
|
328
|
+
Menu.module.css's `.item` padding + `margin-bottom` gap exactly. */
|
|
329
|
+
padding: var(
|
|
330
|
+
--recursica_ui-kit_components_menu-item_properties_vertical-padding
|
|
329
331
|
)
|
|
330
|
-
var(--recursica_ui-
|
|
332
|
+
var(--recursica_ui-kit_components_menu-item_properties_horizontal-padding);
|
|
333
|
+
margin-bottom: var(--recursica_ui-kit_components_menu_properties_item-gap);
|
|
331
334
|
cursor: pointer;
|
|
332
335
|
}
|
|
333
336
|
|
|
337
|
+
.option:last-of-type {
|
|
338
|
+
margin-bottom: 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
334
341
|
/* No per-option hovered token exists in the schema; use the generic overlay tint (same technique
|
|
335
342
|
as Table's row hover) to highlight a hovered-but-not-selected option. */
|
|
336
343
|
.option[data-hovered="true"],
|
|
@@ -347,9 +354,15 @@
|
|
|
347
354
|
component's selected-state colors are the closest real token family for the same concept (a
|
|
348
355
|
selected row in a list) — reused here rather than the neutral hover tint above, which is what
|
|
349
356
|
previously made the selected option read as grey instead of Recursica's brand color. Matches
|
|
350
|
-
the mui-adapter fix for the same gap.
|
|
357
|
+
the mui-adapter fix for the same gap. Despite the name, Mantine's `data-combobox-active` — not
|
|
358
|
+
`data-combobox-selected` — is the attribute it sets when an option's value matches the current
|
|
359
|
+
field value (see OptionsDropdown.mjs: `active: checked`); `data-combobox-selected` is an
|
|
360
|
+
unrelated transient keyboard-navigation highlight (imperatively set/cleared via arrow keys —
|
|
361
|
+
use-combobox.mjs's `selectOption`/`clearSelectedItem`). Keying this off `data-combobox-selected`
|
|
362
|
+
meant the real selected option only showed the brand color while arrow-key navigating, and lost
|
|
363
|
+
it as soon as the dropdown was reopened normally. */
|
|
351
364
|
.option[data-selected="true"],
|
|
352
|
-
.option[data-combobox-
|
|
365
|
+
.option[data-combobox-active="true"] {
|
|
353
366
|
background-color: var(
|
|
354
367
|
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_background-color
|
|
355
368
|
);
|
|
@@ -357,3 +370,98 @@
|
|
|
357
370
|
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_text-color
|
|
358
371
|
);
|
|
359
372
|
}
|
|
373
|
+
|
|
374
|
+
/* Rich option content (leadingIcon/supportingText — see MANTINE_ADAPTER_RICH_OPTION_DATA.md). No
|
|
375
|
+
dedicated dropdown-option icon or supporting-text token exists in the schema either; reuse the
|
|
376
|
+
menu-item component's tokens for the same reason the selected-state colors above do. */
|
|
377
|
+
.optionContent {
|
|
378
|
+
display: flex;
|
|
379
|
+
align-items: center;
|
|
380
|
+
gap: var(--recursica_ui-kit_components_menu-item_properties_icon-text-gap);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.optionIcon {
|
|
384
|
+
display: flex;
|
|
385
|
+
flex-shrink: 0;
|
|
386
|
+
width: var(
|
|
387
|
+
--recursica_ui-kit_components_menu-item_properties_icon-leading-size
|
|
388
|
+
);
|
|
389
|
+
height: var(
|
|
390
|
+
--recursica_ui-kit_components_menu-item_properties_icon-leading-size
|
|
391
|
+
);
|
|
392
|
+
color: var(
|
|
393
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_leading-icon-color
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.optionIcon :global(svg) {
|
|
398
|
+
width: 100%;
|
|
399
|
+
height: 100%;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.optionText {
|
|
403
|
+
display: flex;
|
|
404
|
+
flex-direction: column;
|
|
405
|
+
min-width: 0;
|
|
406
|
+
gap: var(--recursica_ui-kit_components_menu-item_properties_text-gap);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* Default: label/supportingText each truncate to a single line with an ellipsis. */
|
|
410
|
+
.optionText > * {
|
|
411
|
+
overflow: hidden;
|
|
412
|
+
text-overflow: ellipsis;
|
|
413
|
+
white-space: nowrap;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* Applied alongside .optionText when `wrapItemText` is true — long values wrap onto additional
|
|
417
|
+
lines instead of overflowing the fixed-width dropdown. */
|
|
418
|
+
.optionTextWrap > * {
|
|
419
|
+
overflow: visible;
|
|
420
|
+
text-overflow: clip;
|
|
421
|
+
white-space: normal;
|
|
422
|
+
overflow-wrap: anywhere;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.optionSupportingText {
|
|
426
|
+
font-family: var(
|
|
427
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-family
|
|
428
|
+
);
|
|
429
|
+
font-size: var(
|
|
430
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-size
|
|
431
|
+
);
|
|
432
|
+
font-style: var(
|
|
433
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-style
|
|
434
|
+
);
|
|
435
|
+
font-weight: var(
|
|
436
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_font-weight
|
|
437
|
+
);
|
|
438
|
+
letter-spacing: var(
|
|
439
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_letter-spacing
|
|
440
|
+
);
|
|
441
|
+
line-height: var(
|
|
442
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_line-height
|
|
443
|
+
);
|
|
444
|
+
text-decoration: var(
|
|
445
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_text-decoration
|
|
446
|
+
);
|
|
447
|
+
text-transform: var(
|
|
448
|
+
--recursica_ui-kit_components_menu-item_properties_supporting-text_text-transform
|
|
449
|
+
);
|
|
450
|
+
color: var(
|
|
451
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_supporting-text-color
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.option[data-selected="true"] .optionIcon,
|
|
456
|
+
.option[data-combobox-active="true"] .optionIcon {
|
|
457
|
+
color: var(
|
|
458
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_leading-icon-color
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.option[data-selected="true"] .optionSupportingText,
|
|
463
|
+
.option[data-combobox-active="true"] .optionSupportingText {
|
|
464
|
+
color: var(
|
|
465
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_supporting-text-color
|
|
466
|
+
);
|
|
467
|
+
}
|