@eifi1/ui-kit 0.6.1 → 0.7.1
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/README.md +69 -33
- package/dist/chart.d.ts +1 -0
- package/dist/components/account-settings.js +3 -2
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +5 -0
- package/dist/components/amount-input.js +6 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +33 -6
- package/dist/components/autocomplete.js +62 -16
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/calculator.d.ts +5 -0
- package/dist/components/calculator.js +2 -1
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.js +2 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chart.d.ts +33 -2
- package/dist/components/chart.js +20 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +34 -14
- package/dist/components/chip.js +63 -28
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +18 -1
- package/dist/components/choice-card.js +21 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +61 -2
- package/dist/components/combobox-core.js +68 -17
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +11 -7
- package/dist/components/combobox.js +51 -28
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +30 -6
- package/dist/components/danger-confirm.js +51 -32
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +6 -3
- package/dist/components/data-table-pagination.js +6 -5
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +332 -248
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +17 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +28 -3
- package/dist/components/dialog-frame.js +47 -11
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +22 -2
- package/dist/components/disclosure.js +41 -8
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +7 -3
- package/dist/components/dropdown.js +6 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +17 -8
- package/dist/components/entity-combobox.js +7 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +38 -5
- package/dist/components/file-button.js +23 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +11 -5
- package/dist/components/file-dropzone.js +14 -19
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/grouped-picker.js +2 -2
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +8 -3
- package/dist/components/hover-menu.js +88 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +5 -1
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/measured-grid.d.ts +165 -0
- package/dist/components/measured-grid.js +559 -0
- package/dist/components/measured-grid.js.map +1 -0
- package/dist/components/mini-calendar.js +7 -8
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/month-picker.js +13 -9
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +9 -1
- package/dist/components/multi-select.js +22 -8
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +5 -0
- package/dist/components/number-input.d.ts +5 -0
- package/dist/components/number-input.js +3 -3
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +5 -0
- package/dist/components/numpad-sheet.js +3 -3
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +11 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +13 -0
- package/dist/components/series-chart-ticks.js +29 -0
- package/dist/components/series-chart-ticks.js.map +1 -0
- package/dist/components/series-chart.js +33 -27
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +2 -0
- package/dist/components/swatch-picker.d.ts +10 -1
- package/dist/components/swatch-picker.js +2 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +11 -2
- package/dist/components/swipeable-row.js +5 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/tile-radio.js +5 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +2 -0
- package/dist/components/toggle-group.d.ts +13 -3
- package/dist/components/toggle-group.js +21 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +1 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +10 -4
- package/dist/components/tooltip.js +23 -6
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +6 -1
- package/dist/components/treemap.js +16 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +36 -8
- package/dist/components/ui.js +27 -19
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-anchor-dir.d.ts +15 -0
- package/dist/components/use-anchor-dir.js +14 -0
- package/dist/components/use-anchor-dir.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +8 -1
- package/dist/components/use-mobile-reveal.js +3 -2
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +4 -4
- package/dist/components/use-table-state.js +12 -5
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +22 -3
- package/dist/feedback/feedback-attachment.js +15 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +3 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-windowed-rows.d.ts +29 -0
- package/dist/hooks/use-windowed-rows.js +31 -0
- package/dist/hooks/use-windowed-rows.js.map +1 -0
- package/dist/i18n/defaults.d.ts +5 -0
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +7 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +45 -0
- package/dist/i18n/locales/de-CH.js +21 -0
- package/dist/i18n/locales/de-CH.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +45 -0
- package/dist/i18n/locales/de.js +344 -0
- package/dist/i18n/locales/de.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +45 -0
- package/dist/i18n/locales/es.js +339 -0
- package/dist/i18n/locales/es.js.map +1 -0
- package/dist/i18n/locales/fr.d.ts +45 -0
- package/dist/i18n/locales/fr.js +344 -0
- package/dist/i18n/locales/fr.js.map +1 -0
- package/dist/i18n/locales/hu.d.ts +45 -0
- package/dist/i18n/locales/hu.js +343 -0
- package/dist/i18n/locales/hu.js.map +1 -0
- package/dist/i18n/locales/it.d.ts +45 -0
- package/dist/i18n/locales/it.js +342 -0
- package/dist/i18n/locales/it.js.map +1 -0
- package/dist/i18n/locales/zh.d.ts +45 -0
- package/dist/i18n/locales/zh.js +338 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/direction.d.ts +24 -0
- package/dist/lib/direction.js +19 -0
- package/dist/lib/direction.js.map +1 -0
- package/dist/rhf/form.d.ts +2 -0
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf.d.ts +2 -0
- package/dist/search/command-palette.d.ts +5 -0
- package/dist/search/command-palette.js +80 -50
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/app-shell.js +134 -85
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +7 -3
- package/dist/shell/topbar-controls.js +38 -31
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +7 -1
- package/dist/theme/chart-palette.js +2 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/tour/tour.d.ts +11 -5
- package/dist/tour/tour.js +124 -58
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/use-wizard.js +30 -13
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +12 -2
- package/src/components/account-settings.tsx +6 -2
- package/src/components/amount-input.tsx +10 -8
- package/src/components/autocomplete.tsx +110 -24
- package/src/components/calculator.tsx +5 -1
- package/src/components/chart-zoom.tsx +8 -1
- package/src/components/chart.tsx +68 -7
- package/src/components/chip.tsx +131 -49
- package/src/components/choice-card.tsx +77 -36
- package/src/components/combobox-core.tsx +147 -20
- package/src/components/combobox.tsx +100 -44
- package/src/components/currency-select.tsx +6 -6
- package/src/components/danger-confirm.tsx +90 -36
- package/src/components/data-table-pagination.tsx +32 -19
- package/src/components/data-table.tsx +198 -36
- package/src/components/date-picker.tsx +24 -7
- package/src/components/dialog-frame.tsx +97 -22
- package/src/components/disclosure.tsx +75 -13
- package/src/components/dropdown.tsx +17 -5
- package/src/components/entity-combobox.tsx +26 -13
- package/src/components/file-button.tsx +74 -5
- package/src/components/file-dropzone.tsx +44 -35
- package/src/components/grouped-picker.tsx +2 -2
- package/src/components/hover-menu.tsx +139 -10
- package/src/components/icon-picker.tsx +14 -1
- package/src/components/measured-grid.tsx +852 -0
- package/src/components/mini-calendar.tsx +11 -8
- package/src/components/month-picker.tsx +18 -11
- package/src/components/multi-entity-combobox.tsx +2 -2
- package/src/components/multi-select.tsx +34 -7
- package/src/components/number-input.tsx +4 -4
- package/src/components/numpad-sheet.tsx +5 -3
- package/src/components/picker-sheet.tsx +1 -1
- package/src/components/popover.tsx +18 -3
- package/src/components/series-chart-ticks.ts +52 -0
- package/src/components/series-chart.tsx +39 -31
- package/src/components/swatch-picker.tsx +11 -0
- package/src/components/swipeable-row.tsx +21 -6
- package/src/components/tile-radio.tsx +5 -7
- package/src/components/toggle-group.tsx +39 -3
- package/src/components/toggle-legend.tsx +5 -2
- package/src/components/tooltip.tsx +40 -12
- package/src/components/treemap.tsx +28 -5
- package/src/components/ui.tsx +79 -33
- package/src/components/use-anchor-dir.ts +20 -0
- package/src/components/use-mobile-reveal.ts +10 -2
- package/src/components/use-table-state.ts +44 -7
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-attachment.tsx +36 -9
- package/src/feedback/feedback-dialog.tsx +3 -2
- package/src/hooks/use-windowed-rows.ts +69 -0
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/de-CH.ts +30 -0
- package/src/i18n/locales/de.ts +365 -0
- package/src/i18n/locales/es.ts +358 -0
- package/src/i18n/locales/fr.ts +371 -0
- package/src/i18n/locales/hu.ts +359 -0
- package/src/i18n/locales/it.ts +360 -0
- package/src/i18n/locales/zh.ts +348 -0
- package/src/index.ts +10 -1
- package/src/lib/direction.ts +37 -0
- package/src/rhf/form.tsx +21 -0
- package/src/search/command-palette.tsx +77 -19
- package/src/shell/app-shell.tsx +201 -130
- package/src/shell/topbar-action-menu.tsx +1 -1
- package/src/shell/topbar-controls.tsx +19 -8
- package/src/theme/chart-palette.ts +9 -2
- package/src/tour/tour.tsx +119 -18
- package/src/wizard/use-wizard.ts +55 -17
- package/src/wizard/wizard-summary.tsx +1 -1
|
@@ -13,13 +13,29 @@ import { cn } from "../lib/cn";
|
|
|
13
13
|
import { DropdownSearchHeader } from "./dropdown";
|
|
14
14
|
import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet";
|
|
15
15
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
16
|
-
import { PHONE_QUERY } from "./ui";
|
|
16
|
+
import { FLOATING_LABEL_STATIC, PHONE_QUERY, Spinner } from "./ui";
|
|
17
|
+
import { useAnchorDir } from "./use-anchor-dir";
|
|
17
18
|
import { type AnchorRect } from "../hooks/use-anchored-rect";
|
|
18
19
|
import { useAnchoredPanel, type AnchoredPanel } from "../hooks/use-anchored-panel";
|
|
19
20
|
import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss";
|
|
20
21
|
import { DEFAULT_COMBOBOX_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
21
22
|
import { hasMessage, mergeDescribedBy } from "./choice-parts";
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* What an entity picker hands `onChange` when it is cleared — `clearValue` on
|
|
26
|
+
* {@link EntityCombobox} and {@link InlineEntityCombobox}. `null` by default.
|
|
27
|
+
*
|
|
28
|
+
* A closed set rather than any value, on purpose: every member is something no
|
|
29
|
+
* option can be (an option's value is a `string | number`, and `""` is never a real
|
|
30
|
+
* id), so the picker can also READ it back — a field whose `value` is the clear
|
|
31
|
+
* value shows as empty, with no clear button, exactly as `null` does. An arbitrary
|
|
32
|
+
* sentinel could collide with an id and would make that read ambiguous.
|
|
33
|
+
*
|
|
34
|
+
* No `undefined`: a prop set to `undefined` is a prop not passed, so it would read as
|
|
35
|
+
* the default and emit `null` — the one outcome the caller had asked not to get.
|
|
36
|
+
*/
|
|
37
|
+
export type ComboClearValue = null | "";
|
|
38
|
+
|
|
23
39
|
export interface ComboOption<V extends string | number> {
|
|
24
40
|
value: V;
|
|
25
41
|
label: string;
|
|
@@ -33,6 +49,76 @@ export interface ComboOption<V extends string | number> {
|
|
|
33
49
|
* Searchable like `sublabel`.
|
|
34
50
|
*/
|
|
35
51
|
group?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Shown but not takeable: rendered dimmed with `aria-disabled`, passed over by the
|
|
54
|
+
* arrow keys, and a click or Enter on it does nothing. For a row whose existence is
|
|
55
|
+
* the information — a write-locked result, an account that is closed — where
|
|
56
|
+
* leaving it out would read as "there is no such thing". Say WHY in `sublabel`: a
|
|
57
|
+
* hover tooltip is the one explanation a phone cannot show.
|
|
58
|
+
*
|
|
59
|
+
* Honoured by {@link Autocomplete}, {@link InlineEntityCombobox} and the panel
|
|
60
|
+
* pickers ({@link EntityCombobox}, {@link MultiEntityCombobox}); on those an already
|
|
61
|
+
* chosen disabled option stays chosen — the row only refuses to be toggled.
|
|
62
|
+
*/
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A row the user may take — present and not {@link ComboOption.disabled}. */
|
|
67
|
+
export const isOptionEnabled = (o: { disabled?: boolean } | undefined): boolean =>
|
|
68
|
+
o !== undefined && !o.disabled;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The next takeable row from `from`, stepping `dir` — what the arrow keys use so a
|
|
72
|
+
* disabled row is passed over, as the APG allows. Stays on `from` at the end of the
|
|
73
|
+
* list (the family's arrows clamp rather than wrap) and answers `-1` when `from` is
|
|
74
|
+
* not a row and there is nothing to step to.
|
|
75
|
+
*/
|
|
76
|
+
export function stepEnabled(
|
|
77
|
+
rows: readonly { disabled?: boolean }[],
|
|
78
|
+
from: number,
|
|
79
|
+
dir: 1 | -1,
|
|
80
|
+
): number {
|
|
81
|
+
for (let i = from + dir; i >= 0 && i < rows.length; i += dir) {
|
|
82
|
+
if (!rows[i].disabled) return i;
|
|
83
|
+
}
|
|
84
|
+
return from >= 0 && from < rows.length ? from : -1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** `i` when that row is takeable, else the nearest takeable one after it, else
|
|
88
|
+
* before it, else `-1`. For a highlight that starts on a fixed index (the panel
|
|
89
|
+
* pickers open on row 0) and must never rest on a disabled row. */
|
|
90
|
+
export function settleEnabled(rows: readonly { disabled?: boolean }[], i: number): number {
|
|
91
|
+
if (i < 0 || i >= rows.length) return -1;
|
|
92
|
+
if (!rows[i].disabled) return i;
|
|
93
|
+
const after = stepEnabled(rows, i, 1);
|
|
94
|
+
if (after !== i) return after;
|
|
95
|
+
const before = stepEnabled(rows, i, -1);
|
|
96
|
+
return before !== i ? before : -1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Classes a disabled row adds over its normal ones — dimmed, no hover, the
|
|
100
|
+
* kit's soft-disabled look (`DangerConfirm`, `SignaturePad`). */
|
|
101
|
+
export const DISABLED_ROW_CLASS = "cursor-not-allowed opacity-50 hover:bg-transparent";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The family's floating label, as a real `<label for>` (lenkbank): the input is then
|
|
105
|
+
* named the way {@link Input}'s is, so `getByLabelText` and any helper that walks
|
|
106
|
+
* `<label htmlFor>` finds it. Same placement and type as {@link FieldLabel}, which
|
|
107
|
+
* stays a `<span>` for the TRIGGER pickers — a `<button>` is labelable, but those
|
|
108
|
+
* compose their own "label: value" name. `pointer-events-none` (from the class)
|
|
109
|
+
* keeps a press on it landing on the field under it, as before.
|
|
110
|
+
*/
|
|
111
|
+
export function ComboboxFieldLabel({
|
|
112
|
+
htmlFor,
|
|
113
|
+
className,
|
|
114
|
+
children,
|
|
115
|
+
...rest
|
|
116
|
+
}: ComponentPropsWithoutRef<"label"> & { htmlFor: string }) {
|
|
117
|
+
return (
|
|
118
|
+
<label {...rest} htmlFor={htmlFor} className={cn(FLOATING_LABEL_STATIC, "z-10", className)}>
|
|
119
|
+
{children}
|
|
120
|
+
</label>
|
|
121
|
+
);
|
|
36
122
|
}
|
|
37
123
|
|
|
38
124
|
/**
|
|
@@ -323,7 +409,7 @@ export const SUGGESTION_LIST_CLASS =
|
|
|
323
409
|
|
|
324
410
|
export const suggestionRowClass = (isActive: boolean) =>
|
|
325
411
|
cn(
|
|
326
|
-
"block w-full truncate px-3 py-1.5 text-
|
|
412
|
+
"block w-full truncate px-3 py-1.5 text-start text-sm text-[var(--text-primary)]",
|
|
327
413
|
isActive ? "bg-[var(--bg-active)]" : "hover:bg-[var(--bg-hover)]",
|
|
328
414
|
);
|
|
329
415
|
|
|
@@ -373,7 +459,7 @@ export interface ComboboxPanelProps<V extends string | number>
|
|
|
373
459
|
|
|
374
460
|
/**
|
|
375
461
|
* The portalled dropdown (search header + result rows + optional create row)
|
|
376
|
-
* shared by the single- and multi-value comboboxes.
|
|
462
|
+
* shared by the single- and multi-value comboboxes. Start-aligned to the trigger
|
|
377
463
|
* and sized to its width; `multi` swaps the trailing check for a leading
|
|
378
464
|
* checkbox. `onChoose` decides whether to close (single) or stay open (multi).
|
|
379
465
|
*
|
|
@@ -434,34 +520,47 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
434
520
|
// Same core, same results, same handlers — only the container differs, so the
|
|
435
521
|
// two presentations cannot drift in what they offer.
|
|
436
522
|
const isPhone = useMediaQuery(PHONE_QUERY, false);
|
|
437
|
-
|
|
523
|
+
// Every row the keyboard can land on, the create row included (never disabled).
|
|
524
|
+
const rows: readonly { disabled?: boolean }[] = showCreate ? [...results, {}] : results;
|
|
525
|
+
const rowCount = rows.length;
|
|
438
526
|
const optionId = (index: number) => `${listboxId}-option-${index}`;
|
|
439
|
-
|
|
527
|
+
// The core opens on row 0 and knows nothing of `disabled`; the highlight is settled
|
|
528
|
+
// HERE, derived, so it can never rest on a row that cannot be taken — whatever the
|
|
529
|
+
// async list turned out to hold.
|
|
530
|
+
const current = settleEnabled(rows, active);
|
|
531
|
+
const activeId = current >= 0 ? optionId(current) : undefined;
|
|
440
532
|
useActiveOptionScroll(activeId);
|
|
441
533
|
// The consumers resolve their own props against the provider already; this is
|
|
442
534
|
// for the loading row, which no consumer names, and for a caller that renders the
|
|
443
535
|
// panel directly and leaves the two optional strings out.
|
|
444
536
|
const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS);
|
|
537
|
+
// Portalled to <body>, the panel leaves the subtree its `dir` came from — so the
|
|
538
|
+
// direction is read off the trigger and put back on the panel below.
|
|
539
|
+
const dir = useAnchorDir(core.triggerRef, core.open);
|
|
445
540
|
|
|
446
541
|
// The WAI-ARIA APG's listbox keyboard, in full: Up/Down step, Home/End jump, Enter
|
|
447
542
|
// commits, Tab leaves. Escape is the one key handled elsewhere — `useComboboxCore`
|
|
448
543
|
// registers it on the document, so it answers wherever focus has ended up.
|
|
449
544
|
const onKeyDown = (e: ReactKeyboardEvent) => {
|
|
545
|
+
// Disabled rows are passed over (`stepEnabled`), and each step starts from the
|
|
546
|
+
// SETTLED highlight, not the raw index, so the first Down from a disabled row 0
|
|
547
|
+
// goes to the row after the one already shown as highlighted.
|
|
450
548
|
if (e.key === "ArrowDown") {
|
|
451
549
|
e.preventDefault();
|
|
452
|
-
setActive((
|
|
550
|
+
setActive(stepEnabled(rows, current, 1));
|
|
453
551
|
} else if (e.key === "ArrowUp") {
|
|
454
552
|
e.preventDefault();
|
|
455
|
-
setActive(
|
|
553
|
+
setActive(current < 0 ? -1 : stepEnabled(rows, current, -1));
|
|
456
554
|
} else if (e.key === "Home") {
|
|
457
555
|
e.preventDefault();
|
|
458
|
-
setActive(
|
|
556
|
+
setActive(stepEnabled(rows, -1, 1));
|
|
459
557
|
} else if (e.key === "End") {
|
|
460
558
|
e.preventDefault();
|
|
461
|
-
setActive(rowCount -
|
|
559
|
+
setActive(stepEnabled(rows, rowCount, -1));
|
|
462
560
|
} else if (e.key === "Enter") {
|
|
463
561
|
e.preventDefault();
|
|
464
|
-
if (
|
|
562
|
+
if (current < 0) return;
|
|
563
|
+
if (current < results.length) onChoose(results[current]);
|
|
465
564
|
else if (showCreate) onCreate();
|
|
466
565
|
} else if (e.key === "Tab") {
|
|
467
566
|
// The panel is PORTALLED to <body>, so the browser's own Tab would move from
|
|
@@ -490,7 +589,16 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
490
589
|
<span aria-hidden>…</span>
|
|
491
590
|
<span className="sr-only">{labels.loading}</span>
|
|
492
591
|
</>
|
|
493
|
-
) :
|
|
592
|
+
) : busy ? (
|
|
593
|
+
// Loading with the LAST query's rows still up. This was `null`, so a refetch —
|
|
594
|
+
// a new query, or a caller's `loading` while it reloads — looked exactly like a
|
|
595
|
+
// finished list: rows that were about to be replaced, with nothing saying so.
|
|
596
|
+
// A line of its own under the rows, in words, so it reads without the spinner.
|
|
597
|
+
<span className="flex items-center gap-2">
|
|
598
|
+
<Spinner label={null} className="size-3.5 border" />
|
|
599
|
+
{labels.loading}
|
|
600
|
+
</span>
|
|
601
|
+
) : results.length === 0 && !showCreate ? (
|
|
494
602
|
failed ? (
|
|
495
603
|
(loadErrorLabel ?? labels.loadError)
|
|
496
604
|
) : tooShort ? (
|
|
@@ -525,6 +633,9 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
525
633
|
id={listboxId}
|
|
526
634
|
role="listbox"
|
|
527
635
|
aria-multiselectable={multi}
|
|
636
|
+
// The rows are provisional while a lookup is in flight — whether or not the
|
|
637
|
+
// previous query's are still showing.
|
|
638
|
+
aria-busy={busy || undefined}
|
|
528
639
|
className={cn("min-h-0 flex-1 overflow-y-auto py-1", isPhone && "flex-none")}
|
|
529
640
|
>
|
|
530
641
|
{results.map((o, i) => {
|
|
@@ -556,19 +667,26 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
556
667
|
id={optionId(i)}
|
|
557
668
|
role="option"
|
|
558
669
|
aria-selected={selected}
|
|
670
|
+
aria-disabled={o.disabled || undefined}
|
|
559
671
|
tabIndex={-1}
|
|
560
672
|
onMouseDown={(e) => {
|
|
561
673
|
e.preventDefault();
|
|
562
|
-
onChoose(o);
|
|
674
|
+
if (!o.disabled) onChoose(o);
|
|
675
|
+
}}
|
|
676
|
+
onMouseEnter={() => {
|
|
677
|
+
if (!o.disabled) setActive(i);
|
|
563
678
|
}}
|
|
564
|
-
onMouseEnter={() => setActive(i)}
|
|
565
679
|
className={cn(
|
|
566
680
|
isPhone
|
|
567
681
|
? SHEET_ROW_CLASS
|
|
568
|
-
: "flex w-full items-center gap-2 px-3 py-1.5 text-
|
|
569
|
-
|
|
570
|
-
?
|
|
571
|
-
:
|
|
682
|
+
: "flex w-full items-center gap-2 px-3 py-1.5 text-start text-sm",
|
|
683
|
+
o.disabled
|
|
684
|
+
? DISABLED_ROW_CLASS
|
|
685
|
+
: i === current && !isPhone
|
|
686
|
+
? "bg-[var(--bg-active)]"
|
|
687
|
+
: !isPhone && "hover:bg-[var(--bg-hover)]",
|
|
688
|
+
// After SHEET_ROW_CLASS, so its hover is the one cancelled.
|
|
689
|
+
o.disabled && isPhone && DISABLED_ROW_CLASS,
|
|
572
690
|
)}
|
|
573
691
|
>
|
|
574
692
|
{multi && (
|
|
@@ -620,9 +738,9 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
620
738
|
className={cn(
|
|
621
739
|
isPhone
|
|
622
740
|
? SHEET_ROW_CLASS
|
|
623
|
-
: "flex w-full items-center gap-2 px-3 py-1.5 text-
|
|
741
|
+
: "flex w-full items-center gap-2 px-3 py-1.5 text-start text-sm",
|
|
624
742
|
"text-[var(--text-secondary)]",
|
|
625
|
-
|
|
743
|
+
current === results.length && !isPhone
|
|
626
744
|
? "bg-[var(--bg-active)]"
|
|
627
745
|
: !isPhone && "hover:bg-[var(--bg-hover)]",
|
|
628
746
|
)}
|
|
@@ -663,6 +781,14 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
663
781
|
}
|
|
664
782
|
|
|
665
783
|
if (!rect) return null;
|
|
784
|
+
// The panel hangs from the trigger's START edge: its left in a left-to-right form,
|
|
785
|
+
// its right in a right-to-left one (it may be wider than the trigger, `minWidth`).
|
|
786
|
+
// `clientWidth` rather than `innerWidth`, because a fixed box's `right` is measured
|
|
787
|
+
// from the edge of the viewport inside any scrollbar.
|
|
788
|
+
const inline =
|
|
789
|
+
dir === "rtl"
|
|
790
|
+
? { right: (document.documentElement.clientWidth || window.innerWidth) - rect.right }
|
|
791
|
+
: { left: rect.left };
|
|
666
792
|
|
|
667
793
|
return createPortal(
|
|
668
794
|
<div
|
|
@@ -672,6 +798,7 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
672
798
|
// `aria-describedby` has nothing to collide with.
|
|
673
799
|
{...rest}
|
|
674
800
|
ref={panelRef}
|
|
801
|
+
dir={dir}
|
|
675
802
|
onKeyDown={onKeyDown}
|
|
676
803
|
// `placement` keeps the panel inside the region actually on screen: on a phone
|
|
677
804
|
// the search box below pulls up the keyboard, and a panel pinned under a
|
|
@@ -680,7 +807,7 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
680
807
|
...style,
|
|
681
808
|
position: "fixed",
|
|
682
809
|
top: placement.top,
|
|
683
|
-
|
|
810
|
+
...inline,
|
|
684
811
|
width: rect.width,
|
|
685
812
|
minWidth: 220,
|
|
686
813
|
maxHeight: placement.maxHeight,
|
|
@@ -2,17 +2,23 @@ import { Fragment, useId, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactNode, RefObject } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { ChevronDown, X } from "lucide-react";
|
|
5
|
-
import {
|
|
5
|
+
import { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, PHONE_QUERY } from "./ui";
|
|
6
6
|
import { cn } from "../lib/cn";
|
|
7
7
|
import { useDropdown } from "./dropdown";
|
|
8
8
|
import { useAnchoredPanel } from "../hooks/use-anchored-panel";
|
|
9
|
+
import { useAnchorDir } from "./use-anchor-dir";
|
|
9
10
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
10
11
|
import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet";
|
|
11
12
|
import {
|
|
13
|
+
ComboboxFieldLabel,
|
|
14
|
+
DISABLED_ROW_CLASS,
|
|
15
|
+
isOptionEnabled,
|
|
16
|
+
stepEnabled,
|
|
12
17
|
SUGGESTION_LIST_CLASS,
|
|
13
18
|
suggestionRowClass,
|
|
14
19
|
useActiveOptionScroll,
|
|
15
20
|
useComboboxFieldError,
|
|
21
|
+
type ComboClearValue,
|
|
16
22
|
type ComboOption,
|
|
17
23
|
} from "./combobox-core";
|
|
18
24
|
import { DEFAULT_COMBOBOX_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
@@ -94,6 +100,7 @@ function SuggestionList({
|
|
|
94
100
|
children: ReactNode;
|
|
95
101
|
}) {
|
|
96
102
|
const { rect, top, maxHeight } = useAnchoredPanel(anchorRef, true, { preferredHeight: 256 });
|
|
103
|
+
const dir = useAnchorDir(anchorRef, true);
|
|
97
104
|
if (!rect) return null;
|
|
98
105
|
return createPortal(
|
|
99
106
|
<div
|
|
@@ -101,6 +108,9 @@ function SuggestionList({
|
|
|
101
108
|
// z-50, not the old z-30: the list is a child of <body> now, so it is
|
|
102
109
|
// competing with the app's own overlays rather than with its own siblings.
|
|
103
110
|
className="fixed z-50"
|
|
111
|
+
// Portalled out of the form's `dir`; the field's is put back. The list is
|
|
112
|
+
// exactly the field's width, so `left` places it in either direction.
|
|
113
|
+
dir={dir}
|
|
104
114
|
style={{ top, left: rect.left, width: rect.width }}
|
|
105
115
|
>
|
|
106
116
|
<ul id={id} role="listbox" className={LIST_CLASS} style={{ maxHeight }}>
|
|
@@ -361,22 +371,31 @@ export function Combobox({
|
|
|
361
371
|
// the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
362
372
|
// handlers carrying live #309 and dev#549 cannot be replaced from outside.
|
|
363
373
|
<div {...rest} ref={wrapperRef} className={cn("relative", className)}>
|
|
364
|
-
{
|
|
374
|
+
{/* A real <label for> since lenkbank's tests met a field `getByLabelText` could
|
|
375
|
+
not find: it used to be a <span>, with the text copied onto `aria-label`. The
|
|
376
|
+
name a reader hears is the same words either way. */}
|
|
377
|
+
{label !== undefined && (
|
|
378
|
+
<ComboboxFieldLabel htmlFor={fieldId} className={disabled ? "opacity-50" : undefined}>
|
|
379
|
+
{label}
|
|
380
|
+
</ComboboxFieldLabel>
|
|
381
|
+
)}
|
|
365
382
|
{/* The chevron centers against this inner wrapper, which hugs the input.
|
|
366
383
|
The outer div can be taller than the input (as a grid item it
|
|
367
384
|
stretches to the row height, e.g. next to the editor's category cell
|
|
368
385
|
with its split button), which used to drag a top-1/2 chevron down to
|
|
369
386
|
the input's bottom edge (feedback #248). */}
|
|
370
|
-
|
|
387
|
+
{/* Dimmed as a whole when disabled, the way EntityCombobox dims its trigger.
|
|
388
|
+
FIELD_BASE's grey alone left a disabled picker looking like a filled-in
|
|
389
|
+
one beside the pickers that do dim (the label is not the input's `peer`,
|
|
390
|
+
so it is dimmed by hand above). */}
|
|
391
|
+
<div ref={fieldRef} className={cn("relative", disabled && "cursor-not-allowed opacity-50")}>
|
|
371
392
|
<input
|
|
372
393
|
id={fieldId}
|
|
373
394
|
value={value}
|
|
374
395
|
placeholder={placeholder}
|
|
375
|
-
// The
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
// form at once).
|
|
379
|
-
aria-label={ariaLabel ?? label}
|
|
396
|
+
// The caller's name, over the <label for> above — which is what names the
|
|
397
|
+
// field otherwise (dev#477: a field with neither announces only its text).
|
|
398
|
+
aria-label={ariaLabel}
|
|
380
399
|
role="combobox"
|
|
381
400
|
aria-expanded={open}
|
|
382
401
|
// The list this field is the mouth of. Required by the role, and the half
|
|
@@ -462,7 +481,7 @@ export function Combobox({
|
|
|
462
481
|
className={cn(
|
|
463
482
|
FIELD_BASE,
|
|
464
483
|
label !== undefined && FIELD_FLOATING_PAD,
|
|
465
|
-
"
|
|
484
|
+
"pe-9",
|
|
466
485
|
field.isInvalid && FIELD_INVALID,
|
|
467
486
|
)}
|
|
468
487
|
/>
|
|
@@ -474,8 +493,9 @@ export function Combobox({
|
|
|
474
493
|
if (!disabled) setOpen((o) => !o);
|
|
475
494
|
}}
|
|
476
495
|
className={cn(
|
|
477
|
-
"absolute
|
|
478
|
-
|
|
496
|
+
"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]",
|
|
497
|
+
// The box above does the dimming; dimming here too would halve it again.
|
|
498
|
+
!disabled && "cursor-pointer",
|
|
479
499
|
)}
|
|
480
500
|
/>
|
|
481
501
|
</div>
|
|
@@ -605,7 +625,7 @@ export function Combobox({
|
|
|
605
625
|
onMouseEnter={() => setActive(i)}
|
|
606
626
|
className={cn(
|
|
607
627
|
rowClass(i === active),
|
|
608
|
-
group != null && "
|
|
628
|
+
group != null && "ps-6",
|
|
609
629
|
// A row with a badge is a flex row so the label truncates and the
|
|
610
630
|
// badge keeps its width; without one it stays the plain block the
|
|
611
631
|
// other lists render, so nothing shifts for callers that pass none.
|
|
@@ -627,16 +647,20 @@ export function Combobox({
|
|
|
627
647
|
|
|
628
648
|
/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}
|
|
629
649
|
* for why the div's spellings of them are omitted. */
|
|
630
|
-
export interface InlineEntityComboboxProps<V extends string | number>
|
|
650
|
+
export interface InlineEntityComboboxProps<V extends string | number, C extends ComboClearValue = null>
|
|
631
651
|
extends Omit<ComponentPropsWithoutRef<"div">, "id" | "onChange"> {
|
|
632
|
-
/** Selected option id
|
|
633
|
-
value: V | null;
|
|
652
|
+
/** Selected option id; `null` or `clearValue` when nothing is selected. */
|
|
653
|
+
value: V | C | null;
|
|
634
654
|
/** Required and unanswered — see {@link Combobox}'s `invalid`. */
|
|
635
655
|
invalid?: boolean;
|
|
636
656
|
/** What is wrong with the value — see {@link Combobox}'s `error`. */
|
|
637
657
|
error?: ReactNode;
|
|
638
|
-
/** A picked/typed option emits its id; emptying the field
|
|
639
|
-
|
|
658
|
+
/** A picked/typed option emits its id; emptying the field, or the "×", emits
|
|
659
|
+
* `clearValue`. */
|
|
660
|
+
onChange: (v: V | C) => void;
|
|
661
|
+
/** What a clear emits — `null` by default, `""` for a schema that wants an empty
|
|
662
|
+
* string. See {@link EntityCombobox}'s `clearValue`. */
|
|
663
|
+
clearValue?: C;
|
|
640
664
|
options: ComboOption<V>[];
|
|
641
665
|
label?: string;
|
|
642
666
|
id?: string;
|
|
@@ -686,9 +710,10 @@ export interface InlineEntityComboboxProps<V extends string | number>
|
|
|
686
710
|
* sheet that emptied it to show the full list would read as "the user cleared the
|
|
687
711
|
* field" the moment it closed.
|
|
688
712
|
*/
|
|
689
|
-
export function InlineEntityCombobox<V extends string | number>({
|
|
690
|
-
value,
|
|
713
|
+
export function InlineEntityCombobox<V extends string | number, C extends ComboClearValue = null>({
|
|
714
|
+
value: rawValue,
|
|
691
715
|
onChange,
|
|
716
|
+
clearValue = null as C,
|
|
692
717
|
options,
|
|
693
718
|
label,
|
|
694
719
|
id,
|
|
@@ -705,7 +730,10 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
705
730
|
error,
|
|
706
731
|
"aria-label": ariaLabel,
|
|
707
732
|
...rest
|
|
708
|
-
}: InlineEntityComboboxProps<V>) {
|
|
733
|
+
}: InlineEntityComboboxProps<V, C>) {
|
|
734
|
+
// The clear value reads as "nothing selected", whichever one the caller picked —
|
|
735
|
+
// so from here down `value` is the id or `null`, as it always was.
|
|
736
|
+
const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);
|
|
709
737
|
const field = useComboboxFieldError(error, invalid);
|
|
710
738
|
const generated = useId();
|
|
711
739
|
const fieldId = id ?? generated;
|
|
@@ -779,7 +807,8 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
779
807
|
// Pointer-device only: the phone's rows live in a {@link PickerSheet} whose own
|
|
780
808
|
// search box holds focus, so the field behind it must not claim to be pointing at
|
|
781
809
|
// one of them.
|
|
782
|
-
|
|
810
|
+
// A disabled row is never the keyboard's, even if the list changed under it.
|
|
811
|
+
const activeId = !isPhone && isOptionEnabled(matches[active]) ? optionId(active) : undefined;
|
|
783
812
|
useActiveOptionScroll(activeId);
|
|
784
813
|
|
|
785
814
|
const close = () => {
|
|
@@ -788,6 +817,8 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
788
817
|
setSheetQuery("");
|
|
789
818
|
};
|
|
790
819
|
const commit = (o: ComboOption<V>) => {
|
|
820
|
+
// A `disabled` option is listed, never taken — by any path.
|
|
821
|
+
if (o.disabled) return;
|
|
791
822
|
if (o.value !== value) onChange(o.value);
|
|
792
823
|
setText(null);
|
|
793
824
|
close();
|
|
@@ -806,9 +837,13 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
806
837
|
if (text !== null) {
|
|
807
838
|
const q = text.trim();
|
|
808
839
|
if (!q) {
|
|
809
|
-
if (value != null) onChange(
|
|
840
|
+
if (value != null) onChange(clearValue);
|
|
810
841
|
} else {
|
|
811
|
-
|
|
842
|
+
// A disabled option's label typed out in full is not a way round `disabled`:
|
|
843
|
+
// it is not a hit, so the text reverts like any other non-answer.
|
|
844
|
+
const hits = options.filter(
|
|
845
|
+
(o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase(),
|
|
846
|
+
);
|
|
812
847
|
const ids = new Set(hits.map((h) => h.value));
|
|
813
848
|
if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);
|
|
814
849
|
}
|
|
@@ -823,19 +858,27 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
823
858
|
// the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
824
859
|
// handlers carrying live #309 and dev#549 cannot be replaced from outside.
|
|
825
860
|
<div {...rest} ref={wrapperRef} className={cn("relative", className)}>
|
|
826
|
-
{label
|
|
861
|
+
{/* A real <label for> — see {@link Combobox}. */}
|
|
862
|
+
{label !== undefined && (
|
|
863
|
+
<ComboboxFieldLabel htmlFor={fieldId} className={disabled ? "opacity-50" : undefined}>
|
|
864
|
+
{label}
|
|
865
|
+
</ComboboxFieldLabel>
|
|
866
|
+
)}
|
|
827
867
|
{/* Inner wrapper for chevron centering — same reasoning as Combobox above.
|
|
828
868
|
It is also what the portalled list anchors to. */}
|
|
829
|
-
|
|
869
|
+
{/* Dimmed as a whole when disabled, the way EntityCombobox dims its trigger.
|
|
870
|
+
FIELD_BASE's grey alone left a disabled picker looking like a filled-in
|
|
871
|
+
one beside the pickers that do dim (the label is not the input's `peer`,
|
|
872
|
+
so it is dimmed by hand above). */}
|
|
873
|
+
<div ref={fieldRef} className={cn("relative", disabled && "cursor-not-allowed opacity-50")}>
|
|
830
874
|
<input
|
|
831
875
|
id={fieldId}
|
|
832
876
|
value={shown}
|
|
833
877
|
placeholder={placeholder}
|
|
834
|
-
// The
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
aria-label={ariaLabel ?? (typeof label === "string" ? label : undefined)}
|
|
878
|
+
// The caller's name, over the <label for> above. Just the label, never
|
|
879
|
+
// "label: value" the way a trigger button has to compose it: an input
|
|
880
|
+
// already exposes its value separately.
|
|
881
|
+
aria-label={ariaLabel}
|
|
839
882
|
role="combobox"
|
|
840
883
|
aria-expanded={open}
|
|
841
884
|
// Required by the role, and the half that was missing: the field said it
|
|
@@ -877,15 +920,17 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
877
920
|
setActive(-1);
|
|
878
921
|
}}
|
|
879
922
|
onKeyDown={(e) => {
|
|
923
|
+
// Disabled rows are passed over; Up from "nothing highlighted" lands on
|
|
924
|
+
// the first takeable row, as it always landed on row 0.
|
|
880
925
|
if (e.key === "ArrowDown") {
|
|
881
926
|
e.preventDefault();
|
|
882
927
|
setOpen(true);
|
|
883
|
-
setActive((i) =>
|
|
928
|
+
setActive((i) => stepEnabled(matches, i, 1));
|
|
884
929
|
} else if (e.key === "ArrowUp") {
|
|
885
930
|
e.preventDefault();
|
|
886
|
-
setActive((i) =>
|
|
931
|
+
setActive((i) => (i < 0 ? stepEnabled(matches, -1, 1) : stepEnabled(matches, i, -1)));
|
|
887
932
|
} else if (e.key === "Enter") {
|
|
888
|
-
if (open && active
|
|
933
|
+
if (open && isOptionEnabled(matches[active])) {
|
|
889
934
|
e.preventDefault();
|
|
890
935
|
commit(matches[active]);
|
|
891
936
|
} else {
|
|
@@ -907,7 +952,7 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
907
952
|
className={cn(
|
|
908
953
|
FIELD_BASE,
|
|
909
954
|
label !== undefined && FIELD_FLOATING_PAD,
|
|
910
|
-
"
|
|
955
|
+
"pe-9",
|
|
911
956
|
field.isInvalid && FIELD_INVALID,
|
|
912
957
|
)}
|
|
913
958
|
/>
|
|
@@ -927,10 +972,10 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
927
972
|
onClick={() => {
|
|
928
973
|
setText(null);
|
|
929
974
|
close();
|
|
930
|
-
onChange(
|
|
975
|
+
onChange(clearValue);
|
|
931
976
|
}}
|
|
932
977
|
className={cn(
|
|
933
|
-
"absolute
|
|
978
|
+
"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)]",
|
|
934
979
|
"hover:text-[var(--text-secondary)]",
|
|
935
980
|
)}
|
|
936
981
|
>
|
|
@@ -947,8 +992,9 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
947
992
|
if (!disabled) setOpen((o) => !o);
|
|
948
993
|
}}
|
|
949
994
|
className={cn(
|
|
950
|
-
"absolute
|
|
951
|
-
|
|
995
|
+
"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]",
|
|
996
|
+
// The box above does the dimming; dimming here too would halve it again.
|
|
997
|
+
!disabled && "cursor-pointer",
|
|
952
998
|
)}
|
|
953
999
|
/>
|
|
954
1000
|
)}
|
|
@@ -989,8 +1035,13 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
989
1035
|
type="button"
|
|
990
1036
|
role="option"
|
|
991
1037
|
aria-selected={o.value === value}
|
|
1038
|
+
aria-disabled={o.disabled || undefined}
|
|
992
1039
|
onClick={() => commit(o)}
|
|
993
|
-
className={cn(
|
|
1040
|
+
className={cn(
|
|
1041
|
+
SHEET_ROW_CLASS,
|
|
1042
|
+
o.value === value && "font-medium",
|
|
1043
|
+
o.disabled && DISABLED_ROW_CLASS,
|
|
1044
|
+
)}
|
|
994
1045
|
>
|
|
995
1046
|
{o.label}
|
|
996
1047
|
</button>
|
|
@@ -1030,25 +1081,30 @@ export function InlineEntityCombobox<V extends string | number>({
|
|
|
1030
1081
|
// `aria-activedescendant`.
|
|
1031
1082
|
role="option"
|
|
1032
1083
|
aria-selected={o.value === value}
|
|
1084
|
+
aria-disabled={o.disabled || undefined}
|
|
1033
1085
|
tabIndex={-1}
|
|
1034
1086
|
onMouseDown={(e) => {
|
|
1035
1087
|
// mousedown (not click) so the input's blur can't close the
|
|
1036
|
-
// list before the selection registers.
|
|
1088
|
+
// list before the selection registers. Prevented on a disabled
|
|
1089
|
+
// row too, so pressing one leaves the field focused and open.
|
|
1037
1090
|
e.preventDefault();
|
|
1038
1091
|
commit(o);
|
|
1039
1092
|
}}
|
|
1040
|
-
onMouseEnter={() =>
|
|
1093
|
+
onMouseEnter={() => {
|
|
1094
|
+
if (!o.disabled) setActive(i);
|
|
1095
|
+
}}
|
|
1041
1096
|
className={cn(
|
|
1042
|
-
rowClass(i === active),
|
|
1097
|
+
rowClass(i === active && !o.disabled),
|
|
1098
|
+
o.disabled && DISABLED_ROW_CLASS,
|
|
1043
1099
|
// Indented under its heading, so the hierarchy is readable at a
|
|
1044
1100
|
// glance instead of inferred from grey trailing text.
|
|
1045
|
-
o.group && "
|
|
1101
|
+
o.group && "ps-6",
|
|
1046
1102
|
o.value === value && "font-medium",
|
|
1047
1103
|
)}
|
|
1048
1104
|
>
|
|
1049
1105
|
{o.label}
|
|
1050
1106
|
{o.sublabel && (
|
|
1051
|
-
<span className="
|
|
1107
|
+
<span className="ms-2 text-xs text-[var(--text-placeholder)]">
|
|
1052
1108
|
{o.sublabel}
|
|
1053
1109
|
</span>
|
|
1054
1110
|
)}
|
|
@@ -215,7 +215,7 @@ export function CurrencySelect({
|
|
|
215
215
|
aria-controls={listboxId}
|
|
216
216
|
aria-expanded={open}
|
|
217
217
|
aria-invalid={invalid || undefined}
|
|
218
|
-
className={cn(FIELD_TRIGGER, "
|
|
218
|
+
className={cn(FIELD_TRIGGER, "pe-9", label !== undefined && FIELD_FLOATING_PAD, invalid && FIELD_INVALID)}
|
|
219
219
|
>
|
|
220
220
|
{/* The trigger stays COMPACT — flag + code only — so it never clips in a
|
|
221
221
|
narrow field; the full names live in the (wider) popup (feedback
|
|
@@ -235,10 +235,10 @@ export function CurrencySelect({
|
|
|
235
235
|
{open && (
|
|
236
236
|
<DropdownPanel
|
|
237
237
|
// At least as wide as the trigger, but grows to fit the full currency
|
|
238
|
-
// names and caps so it never runs off-screen.
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
className="
|
|
238
|
+
// names and caps so it never runs off-screen. END-aligned so it grows
|
|
239
|
+
// toward the start instead of pushing the page width when there's content
|
|
240
|
+
// after the field (feedback #308).
|
|
241
|
+
className="end-0 min-w-full w-max max-w-[min(20rem,calc(100vw-2rem))]"
|
|
242
242
|
empty={filtered.length === 0}
|
|
243
243
|
listProps={{ id: listboxId }}
|
|
244
244
|
header={
|
|
@@ -261,7 +261,7 @@ export function CurrencySelect({
|
|
|
261
261
|
setOpen(false);
|
|
262
262
|
}}
|
|
263
263
|
className={cn(
|
|
264
|
-
"flex w-full items-center justify-between gap-2 px-3 py-2 text-sm text-
|
|
264
|
+
"flex w-full items-center justify-between gap-2 px-3 py-2 text-sm text-start hover:bg-[var(--bg-hover)]",
|
|
265
265
|
active && "bg-[var(--bg-active)]",
|
|
266
266
|
)}
|
|
267
267
|
>
|