@elabs-ai/components-ui 4.1.0 → 4.2.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/dist/{chunk-JKPPTL63.js → chunk-FCH4ZN6G.js} +12 -3
- package/dist/chunk-FCH4ZN6G.js.map +1 -0
- package/dist/form.js +3 -3
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +126 -35
- package/dist/index.js +3346 -2079
- package/dist/index.js.map +1 -1
- package/dist/lib/cn.js +10 -1
- package/dist/lib/cn.js.map +1 -1
- package/package.json +3 -3
- package/src/__contract__/attribution-panel.contract.test.tsx +49 -0
- package/src/__contract__/badge.contract.test.tsx +49 -0
- package/src/__contract__/bounded-number.contract.test.tsx +49 -0
- package/src/__contract__/button.contract.test.tsx +49 -0
- package/src/__contract__/color-picker.contract.test.tsx +49 -0
- package/src/__contract__/combobox.contract.test.tsx +49 -0
- package/src/__contract__/command-trigger.contract.test.tsx +49 -0
- package/src/__contract__/copyable-value.contract.test.tsx +49 -0
- package/src/__contract__/date-picker.contract.test.tsx +49 -0
- package/src/__contract__/date-range-picker.contract.test.tsx +49 -0
- package/src/__contract__/empty-state.contract.test.tsx +49 -0
- package/src/__contract__/error-state.contract.test.tsx +49 -0
- package/src/__contract__/icon-button.contract.test.tsx +49 -0
- package/src/__contract__/input.contract.test.tsx +49 -0
- package/src/__contract__/key-value-editor.contract.test.tsx +49 -0
- package/src/__contract__/list-editor.contract.test.tsx +49 -0
- package/src/__contract__/loading-state.contract.test.tsx +49 -0
- package/src/__contract__/metric-card.contract.test.tsx +49 -0
- package/src/__contract__/model-picker.contract.test.tsx +49 -0
- package/src/__contract__/nav-notifications.contract.test.tsx +49 -0
- package/src/__contract__/number-input.contract.test.tsx +49 -0
- package/src/__contract__/rating.contract.test.tsx +49 -0
- package/src/__contract__/schema-form.contract.test.tsx +49 -0
- package/src/__contract__/section-header.contract.test.tsx +49 -0
- package/src/__contract__/segmented-field.contract.test.tsx +49 -0
- package/src/__contract__/slider-number.contract.test.tsx +49 -0
- package/src/__contract__/status-badge.contract.test.tsx +49 -0
- package/src/__contract__/tag-input.contract.test.tsx +49 -0
- package/src/__contract__/text.contract.test.tsx +49 -0
- package/src/__contract__/textarea.contract.test.tsx +49 -0
- package/src/__contract__/timeline.contract.test.tsx +49 -0
- package/src/__contract__/toggle.contract.test.tsx +49 -0
- package/src/__contract__/transfer.contract.test.tsx +49 -0
- package/src/__contract__/tree-select.contract.test.tsx +49 -0
- package/src/__contract__/tree.contract.test.tsx +49 -0
- package/src/__contract__/virtual-select.contract.test.tsx +49 -0
- package/src/__contract__/workspace-picker.contract.test.tsx +49 -0
- package/src/components/accordion/accordion.tsx +13 -4
- package/src/components/alert/alert.tsx +11 -3
- package/src/components/alert-dialog/alert-dialog.test.tsx +32 -0
- package/src/components/alert-dialog/alert-dialog.tsx +28 -6
- package/src/components/app-shell/app-shell.test.tsx +99 -0
- package/src/components/app-shell/app-shell.tsx +29 -10
- package/src/components/attribution-panel/attribution-panel.tsx +2 -0
- package/src/components/attribution-panel/attributions.generated.ts +2 -2
- package/src/components/avatar/avatar.tsx +4 -1
- package/src/components/badge/badge.test.tsx +8 -0
- package/src/components/badge/badge.tsx +14 -4
- package/src/components/bento-grid/bento-grid.test.tsx +23 -2
- package/src/components/bento-grid/bento-grid.tsx +10 -0
- package/src/components/bounded-number/bounded-number.test.tsx +1 -1
- package/src/components/bounded-number/bounded-number.tsx +78 -75
- package/src/components/breadcrumb/breadcrumb.tsx +3 -2
- package/src/components/button/button.tsx +18 -12
- package/src/components/button-group/button-group.tsx +1 -1
- package/src/components/calendar/calendar.tsx +3 -2
- package/src/components/card/card.test.tsx +24 -2
- package/src/components/card/card.tsx +47 -6
- package/src/components/carousel/carousel.test.tsx +169 -0
- package/src/components/carousel/carousel.tsx +93 -32
- package/src/components/change-review/change-review.tsx +38 -29
- package/src/components/checkbox/checkbox.tsx +1 -0
- package/src/components/collapsible-panel/use-collapsible-panel.ts +0 -3
- package/src/components/color-picker/color-picker.tsx +17 -11
- package/src/components/combobox/combobox.stories.tsx +10 -0
- package/src/components/combobox/combobox.test.tsx +54 -0
- package/src/components/combobox/combobox.tsx +25 -11
- package/src/components/command/command.test.tsx +15 -0
- package/src/components/command/command.tsx +8 -6
- package/src/components/context-menu/context-menu.tsx +16 -7
- package/src/components/context-rail/context-rail.tsx +4 -4
- package/src/components/copyable-value/copyable-value.stories.tsx +10 -0
- package/src/components/copyable-value/copyable-value.test.tsx +28 -1
- package/src/components/copyable-value/copyable-value.tsx +12 -1
- package/src/components/date-picker/date-picker.stories.tsx +9 -0
- package/src/components/date-picker/date-picker.test.tsx +52 -0
- package/src/components/date-picker/date-picker.tsx +13 -15
- package/src/components/date-range-picker/date-range-picker.stories.tsx +9 -0
- package/src/components/date-range-picker/date-range-picker.test.tsx +76 -1
- package/src/components/date-range-picker/date-range-picker.tsx +39 -28
- package/src/components/descriptions/descriptions.tsx +1 -1
- package/src/components/dialog/dialog.test.tsx +8 -3
- package/src/components/dialog/dialog.tsx +12 -4
- package/src/components/drawer/drawer.tsx +23 -6
- package/src/components/dropdown-menu/dropdown-menu.tsx +4 -4
- package/src/components/field/field.tsx +2 -0
- package/src/components/field-row/field-row.tsx +2 -0
- package/src/components/file-upload/file-upload.test.tsx +83 -10
- package/src/components/file-upload/file-upload.tsx +150 -28
- package/src/components/form/form.tsx +4 -2
- package/src/components/hover-card/hover-card.tsx +1 -0
- package/src/components/icon-button/icon-button.tsx +1 -1
- package/src/components/input/input.tsx +7 -3
- package/src/components/input-group/input-group.tsx +6 -6
- package/src/components/input-otp/input-otp.tsx +3 -1
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +2 -2
- package/src/components/label/label.tsx +1 -1
- package/src/components/list-editor/list-editor.test.tsx +24 -0
- package/src/components/list-editor/list-editor.tsx +126 -52
- package/src/components/locale-provider/locale-keys.test.ts +134 -0
- package/src/components/locale-provider/messages.ts +321 -0
- package/src/components/mention-input/mention-input.stories.tsx +3 -3
- package/src/components/mention-input/mention-input.test.tsx +51 -1
- package/src/components/mention-input/mention-input.tsx +58 -8
- package/src/components/menubar/menubar.tsx +16 -6
- package/src/components/nav-main/nav-main.tsx +7 -3
- package/src/components/nav-notifications/nav-notifications.test.tsx +11 -0
- package/src/components/nav-notifications/nav-notifications.tsx +11 -5
- package/src/components/nav-user/nav-user.tsx +5 -5
- package/src/components/navigation-menu/navigation-menu.tsx +16 -4
- package/src/components/number-input/number-input.stories.tsx +7 -3
- package/src/components/number-input/number-input.test.tsx +47 -0
- package/src/components/number-input/number-input.tsx +65 -24
- package/src/components/pagination/pagination.test.tsx +20 -1
- package/src/components/pagination/pagination.tsx +18 -14
- package/src/components/popover/popover.tsx +1 -0
- package/src/components/progress/progress.tsx +2 -0
- package/src/components/rating/rating.tsx +2 -0
- package/src/components/resizable/resizable.test.tsx +76 -0
- package/src/components/resizable/resizable.tsx +31 -5
- package/src/components/revision-timeline/revision-timeline.tsx +7 -2
- package/src/components/schema-form/schema-form-store.ts +506 -0
- package/src/components/schema-form/schema-form.test.tsx +122 -1
- package/src/components/schema-form/schema-form.tsx +163 -327
- package/src/components/section-header/section-header.tsx +2 -2
- package/src/components/select/select.tsx +7 -5
- package/src/components/separator/separator.tsx +1 -0
- package/src/components/sheet/sheet.test.tsx +39 -0
- package/src/components/sheet/sheet.tsx +40 -9
- package/src/components/side-dock/side-dock.test.tsx +26 -0
- package/src/components/side-dock/side-dock.tsx +15 -2
- package/src/components/sidebar/sidebar.test.tsx +35 -1
- package/src/components/sidebar/sidebar.tsx +20 -6
- package/src/components/skeleton/skeleton.tsx +1 -0
- package/src/components/slider-number/slider-number.stories.tsx +4 -1
- package/src/components/slider-number/slider-number.test.tsx +1 -1
- package/src/components/sonner/sonner.tsx +2 -0
- package/src/components/spinner/spinner.test.tsx +18 -0
- package/src/components/spinner/spinner.tsx +14 -4
- package/src/components/state-panel/state-panel.test.tsx +3 -3
- package/src/components/state-panel/state-panel.tsx +15 -8
- package/src/components/switch/switch.tsx +2 -0
- package/src/components/table/table.stories.tsx +3 -1
- package/src/components/table/table.test.tsx +9 -2
- package/src/components/table/table.tsx +18 -8
- package/src/components/tabs/index.ts +10 -1
- package/src/components/tabs/tabs.stories.tsx +48 -0
- package/src/components/tabs/tabs.test.tsx +67 -0
- package/src/components/tabs/tabs.tsx +175 -77
- package/src/components/tag-input/tag-input.tsx +4 -2
- package/src/components/team-switcher/team-switcher.tsx +3 -3
- package/src/components/textarea/textarea.tsx +1 -1
- package/src/components/theme-switcher/theme-switcher.stories.tsx +76 -0
- package/src/components/theme-switcher/theme-switcher.test.tsx +182 -0
- package/src/components/theme-switcher/theme-switcher.tsx +134 -10
- package/src/components/timeline/timeline.tsx +0 -3
- package/src/components/toggle/toggle.tsx +7 -2
- package/src/components/toggle-group/toggle-group.tsx +2 -0
- package/src/components/tooltip/tooltip.tsx +2 -1
- package/src/components/top-nav/top-nav.tsx +1 -1
- package/src/components/transfer/transfer.tsx +35 -30
- package/src/components/tree/tree.test.tsx +41 -0
- package/src/components/tree/tree.tsx +77 -22
- package/src/components/tree/use-tree-keyboard.ts +37 -14
- package/src/components/tree-select/tree-select.stories.tsx +9 -0
- package/src/components/tree-select/tree-select.test.tsx +5 -0
- package/src/components/tree-select/tree-select.tsx +11 -14
- package/src/components/typography/prose.test.tsx +3 -2
- package/src/components/typography/prose.tsx +3 -3
- package/src/components/virtual-select/virtual-select.stories.tsx +9 -0
- package/src/components/virtual-select/virtual-select.test.tsx +28 -2
- package/src/components/virtual-select/virtual-select.tsx +56 -18
- package/src/components/wizard/wizard.test.tsx +9 -0
- package/src/components/wizard/wizard.tsx +12 -4
- package/src/index.ts +5 -0
- package/src/lib/cn.ts +9 -0
- package/src/lib/csv.ts +48 -0
- package/src/lib/diff-rows.ts +2 -0
- package/src/lib/merge-refs.ts +2 -0
- package/src/lib/use-controllable-state.ts +47 -0
- package/src/lib/use-mobile.test.tsx +47 -0
- package/src/lib/use-mobile.ts +26 -10
- package/dist/chunk-JKPPTL63.js.map +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
createContext,
|
|
3
5
|
forwardRef,
|
|
@@ -6,13 +8,29 @@ import {
|
|
|
6
8
|
useEffect,
|
|
7
9
|
useState,
|
|
8
10
|
type ComponentProps,
|
|
11
|
+
type ElementRef,
|
|
9
12
|
type HTMLAttributes,
|
|
10
13
|
type KeyboardEvent,
|
|
11
14
|
} from "react";
|
|
15
|
+
import { useDirection } from "@radix-ui/react-direction";
|
|
12
16
|
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
|
|
13
17
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
|
14
18
|
import { cn } from "../../lib/cn";
|
|
15
19
|
import { Button } from "../button";
|
|
20
|
+
import { useLocale } from "../locale-provider";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* True when the keyboard event's target already owns arrow-key semantics of
|
|
24
|
+
* its own (a text field's caret, a contenteditable's selection) — the
|
|
25
|
+
* carousel must not steal ArrowLeft/ArrowRight from a form control that
|
|
26
|
+
* happens to live inside the active slide.
|
|
27
|
+
*/
|
|
28
|
+
function isEditableTarget(target: EventTarget | null): boolean {
|
|
29
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
30
|
+
if (target.isContentEditable) return true;
|
|
31
|
+
const tag = target.tagName;
|
|
32
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
33
|
+
}
|
|
16
34
|
|
|
17
35
|
type CarouselApi = UseEmblaCarouselType[1];
|
|
18
36
|
type CarouselOptions = NonNullable<Parameters<typeof useEmblaCarousel>[0]>;
|
|
@@ -62,17 +80,20 @@ export const Carousel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement
|
|
|
62
80
|
// aria-roledescription changes the announcement, it does not name the
|
|
63
81
|
// landmark. Default is overridable; pass a specific label ("Product
|
|
64
82
|
// photos") when the carousel's purpose isn't generic. Issue #279.
|
|
65
|
-
"aria-label": ariaLabel
|
|
83
|
+
"aria-label": ariaLabel,
|
|
66
84
|
...props
|
|
67
85
|
},
|
|
68
86
|
ref,
|
|
69
87
|
) {
|
|
88
|
+
const { t } = useLocale();
|
|
89
|
+
const resolvedAriaLabel = ariaLabel ?? t("ui.carousel.label");
|
|
70
90
|
const [carouselRef, api] = useEmblaCarousel(
|
|
71
91
|
{ ...opts, axis: orientation === "horizontal" ? "x" : "y" },
|
|
72
92
|
plugins,
|
|
73
93
|
);
|
|
74
94
|
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
|
75
95
|
const [canScrollNext, setCanScrollNext] = useState(false);
|
|
96
|
+
const dir = useDirection();
|
|
76
97
|
|
|
77
98
|
const onSelect = useCallback((a: CarouselApi) => {
|
|
78
99
|
if (!a) return;
|
|
@@ -85,15 +106,42 @@ export const Carousel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement
|
|
|
85
106
|
|
|
86
107
|
const onKeyDown = useCallback(
|
|
87
108
|
(e: KeyboardEvent<HTMLDivElement>) => {
|
|
109
|
+
// Don't steal ArrowLeft/ArrowRight from a text field's caret or a
|
|
110
|
+
// contenteditable's selection inside the active slide.
|
|
111
|
+
if (isEditableTarget(e.target)) return;
|
|
112
|
+
|
|
113
|
+
if (orientation === "vertical") {
|
|
114
|
+
if (e.key === "ArrowUp") {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
scrollPrev();
|
|
117
|
+
} else if (e.key === "ArrowDown") {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
scrollNext();
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Horizontal: the PHYSICAL key maps to the visually-adjacent slide,
|
|
125
|
+
// which flips with writing direction — in RTL, ArrowLeft moves toward
|
|
126
|
+
// the next slide (visually to the left of the current one), not prev.
|
|
127
|
+
const isRtl = dir === "rtl";
|
|
88
128
|
if (e.key === "ArrowLeft") {
|
|
89
129
|
e.preventDefault();
|
|
90
|
-
|
|
130
|
+
if (isRtl) {
|
|
131
|
+
scrollNext();
|
|
132
|
+
} else {
|
|
133
|
+
scrollPrev();
|
|
134
|
+
}
|
|
91
135
|
} else if (e.key === "ArrowRight") {
|
|
92
136
|
e.preventDefault();
|
|
93
|
-
|
|
137
|
+
if (isRtl) {
|
|
138
|
+
scrollPrev();
|
|
139
|
+
} else {
|
|
140
|
+
scrollNext();
|
|
141
|
+
}
|
|
94
142
|
}
|
|
95
143
|
},
|
|
96
|
-
[scrollPrev, scrollNext],
|
|
144
|
+
[orientation, dir, scrollPrev, scrollNext],
|
|
97
145
|
);
|
|
98
146
|
|
|
99
147
|
useEffect(() => {
|
|
@@ -105,6 +153,7 @@ export const Carousel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement
|
|
|
105
153
|
api.on("reInit", onSelect);
|
|
106
154
|
api.on("select", onSelect);
|
|
107
155
|
return () => {
|
|
156
|
+
api.off("reInit", onSelect);
|
|
108
157
|
api.off("select", onSelect);
|
|
109
158
|
};
|
|
110
159
|
}, [api, onSelect]);
|
|
@@ -128,7 +177,7 @@ export const Carousel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement
|
|
|
128
177
|
className={cn("relative", className)}
|
|
129
178
|
role="region"
|
|
130
179
|
aria-roledescription="carousel"
|
|
131
|
-
aria-label={
|
|
180
|
+
aria-label={resolvedAriaLabel}
|
|
132
181
|
{...props}
|
|
133
182
|
>
|
|
134
183
|
{children}
|
|
@@ -176,20 +225,28 @@ export const CarouselItem = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEle
|
|
|
176
225
|
},
|
|
177
226
|
);
|
|
178
227
|
|
|
179
|
-
export
|
|
228
|
+
export const CarouselPrevious = forwardRef<
|
|
229
|
+
ElementRef<typeof Button>,
|
|
230
|
+
ComponentProps<typeof Button>
|
|
231
|
+
>(function CarouselPrevious({ className, ...props }, ref) {
|
|
180
232
|
const { scrollPrev, canScrollPrev, orientation } = useCarousel();
|
|
233
|
+
const { t } = useLocale();
|
|
181
234
|
return (
|
|
182
235
|
<Button
|
|
236
|
+
ref={ref}
|
|
183
237
|
variant="outline"
|
|
184
238
|
size="icon"
|
|
185
239
|
disabled={!canScrollPrev}
|
|
186
240
|
onClick={scrollPrev}
|
|
187
|
-
aria-label="
|
|
241
|
+
aria-label={t("previousSlide")}
|
|
188
242
|
className={cn(
|
|
189
243
|
"absolute size-8 rounded-full",
|
|
244
|
+
// Inset on narrow screens (no room for the button to sit outside
|
|
245
|
+
// the carousel box without overflowing horizontally); pushed
|
|
246
|
+
// outside the box only from `sm` up, where there's margin for it.
|
|
190
247
|
orientation === "horizontal"
|
|
191
|
-
? "
|
|
192
|
-
: "
|
|
248
|
+
? "start-2 top-1/2 -translate-y-1/2 sm:-start-12"
|
|
249
|
+
: "top-2 left-1/2 -translate-x-1/2 rotate-90 sm:-top-12",
|
|
193
250
|
className,
|
|
194
251
|
)}
|
|
195
252
|
{...props}
|
|
@@ -197,29 +254,33 @@ export function CarouselPrevious({ className, ...props }: ComponentProps<typeof
|
|
|
197
254
|
<ArrowLeft className="size-4" data-rtl-flip />
|
|
198
255
|
</Button>
|
|
199
256
|
);
|
|
200
|
-
}
|
|
257
|
+
});
|
|
201
258
|
|
|
202
|
-
export
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
259
|
+
export const CarouselNext = forwardRef<ElementRef<typeof Button>, ComponentProps<typeof Button>>(
|
|
260
|
+
function CarouselNext({ className, ...props }, ref) {
|
|
261
|
+
const { scrollNext, canScrollNext, orientation } = useCarousel();
|
|
262
|
+
const { t } = useLocale();
|
|
263
|
+
return (
|
|
264
|
+
<Button
|
|
265
|
+
ref={ref}
|
|
266
|
+
variant="outline"
|
|
267
|
+
size="icon"
|
|
268
|
+
disabled={!canScrollNext}
|
|
269
|
+
onClick={scrollNext}
|
|
270
|
+
aria-label={t("nextSlide")}
|
|
271
|
+
className={cn(
|
|
272
|
+
"absolute size-8 rounded-full",
|
|
273
|
+
orientation === "horizontal"
|
|
274
|
+
? "end-2 top-1/2 -translate-y-1/2 sm:-end-12"
|
|
275
|
+
: "bottom-2 left-1/2 -translate-x-1/2 rotate-90 sm:-bottom-12",
|
|
276
|
+
className,
|
|
277
|
+
)}
|
|
278
|
+
{...props}
|
|
279
|
+
>
|
|
280
|
+
<ArrowRight className="size-4" data-rtl-flip />
|
|
281
|
+
</Button>
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
);
|
|
224
285
|
|
|
225
286
|
export type { CarouselApi };
|
|
@@ -67,6 +67,7 @@ import { Badge } from "../badge/badge";
|
|
|
67
67
|
import { StatePanel } from "../state-panel/state-panel";
|
|
68
68
|
import { StatusIcon } from "../status-badge/status-badge";
|
|
69
69
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../collapsible/collapsible";
|
|
70
|
+
import { useLocale } from "../locale-provider";
|
|
70
71
|
|
|
71
72
|
// ─── Public types ─────────────────────────────────────────────────────────────
|
|
72
73
|
|
|
@@ -283,6 +284,7 @@ export type ChangeReviewHeaderProps = HTMLAttributes<HTMLDivElement>;
|
|
|
283
284
|
*/
|
|
284
285
|
export const ChangeReviewHeader = forwardRef<HTMLDivElement, ChangeReviewHeaderProps>(
|
|
285
286
|
function ChangeReviewHeader({ className, ...props }, ref) {
|
|
287
|
+
const { t } = useLocale();
|
|
286
288
|
const { state, actions, headingId } = useChangeReview();
|
|
287
289
|
const { approvedCount, totalCount, allApproved, noneApproved } = state;
|
|
288
290
|
|
|
@@ -294,10 +296,10 @@ export const ChangeReviewHeader = forwardRef<HTMLDivElement, ChangeReviewHeaderP
|
|
|
294
296
|
>
|
|
295
297
|
<div className="flex items-center gap-2 min-w-0">
|
|
296
298
|
<span id={headingId} className="text-subtitle font-semibold text-foreground truncate">
|
|
297
|
-
|
|
299
|
+
{t("ui.changeReview.heading")}
|
|
298
300
|
</span>
|
|
299
301
|
<Badge variant={allApproved ? "success" : approvedCount > 0 ? "info" : "secondary"}>
|
|
300
|
-
{approvedCount
|
|
302
|
+
{t("ui.changeReview.approvedCount", { approved: approvedCount, total: totalCount })}
|
|
301
303
|
</Badge>
|
|
302
304
|
</div>
|
|
303
305
|
|
|
@@ -308,20 +310,20 @@ export const ChangeReviewHeader = forwardRef<HTMLDivElement, ChangeReviewHeaderP
|
|
|
308
310
|
variant="outline-subtle"
|
|
309
311
|
disabled={noneApproved}
|
|
310
312
|
onClick={actions.rejectAll}
|
|
311
|
-
aria-label="
|
|
313
|
+
aria-label={t("ui.changeReview.rejectAllLabel")}
|
|
312
314
|
>
|
|
313
315
|
<X aria-hidden="true" />
|
|
314
|
-
|
|
316
|
+
{t("ui.changeReview.rejectAll")}
|
|
315
317
|
</Button>
|
|
316
318
|
<Button
|
|
317
319
|
size="sm"
|
|
318
320
|
variant={allApproved ? "secondary" : "default"}
|
|
319
321
|
disabled={allApproved}
|
|
320
322
|
onClick={actions.approveAll}
|
|
321
|
-
aria-label="
|
|
323
|
+
aria-label={t("ui.changeReview.approveAllLabel")}
|
|
322
324
|
>
|
|
323
325
|
<CheckCheck aria-hidden="true" />
|
|
324
|
-
|
|
326
|
+
{t("ui.changeReview.approveAll")}
|
|
325
327
|
</Button>
|
|
326
328
|
</div>
|
|
327
329
|
)}
|
|
@@ -357,6 +359,7 @@ function isChangeProvenance(v: unknown): v is ChangeProvenance {
|
|
|
357
359
|
*/
|
|
358
360
|
export const ChangeReviewProvenance = forwardRef<HTMLDivElement, ChangeReviewProvenanceProps>(
|
|
359
361
|
function ChangeReviewProvenance({ provenance, className, ...props }, ref) {
|
|
362
|
+
const { t } = useLocale();
|
|
360
363
|
if (isChangeProvenance(provenance)) {
|
|
361
364
|
const { author, model, timestamp, note } = provenance;
|
|
362
365
|
return (
|
|
@@ -367,7 +370,7 @@ export const ChangeReviewProvenance = forwardRef<HTMLDivElement, ChangeReviewPro
|
|
|
367
370
|
className,
|
|
368
371
|
)}
|
|
369
372
|
role="group"
|
|
370
|
-
aria-label="
|
|
373
|
+
aria-label={t("ui.changeReview.provenanceLabel")}
|
|
371
374
|
{...props}
|
|
372
375
|
>
|
|
373
376
|
{author && (
|
|
@@ -399,7 +402,7 @@ export const ChangeReviewProvenance = forwardRef<HTMLDivElement, ChangeReviewPro
|
|
|
399
402
|
ref={ref}
|
|
400
403
|
className={cn("pb-3 text-caption text-muted-foreground", className)}
|
|
401
404
|
role="group"
|
|
402
|
-
aria-label="
|
|
405
|
+
aria-label={t("ui.changeReview.provenanceLabel")}
|
|
403
406
|
{...props}
|
|
404
407
|
>
|
|
405
408
|
{provenance as ReactNode}
|
|
@@ -418,14 +421,15 @@ export type ChangeReviewListProps = HTMLAttributes<HTMLOListElement>;
|
|
|
418
421
|
*/
|
|
419
422
|
export const ChangeReviewList = forwardRef<HTMLOListElement, ChangeReviewListProps>(
|
|
420
423
|
function ChangeReviewList({ className, children, ...props }, ref) {
|
|
424
|
+
const { t } = useLocale();
|
|
421
425
|
const { hunks, headingId } = useChangeReview();
|
|
422
426
|
|
|
423
427
|
if (hunks.length === 0) {
|
|
424
428
|
return (
|
|
425
429
|
<StatePanel
|
|
426
430
|
kind="empty"
|
|
427
|
-
title="
|
|
428
|
-
description="
|
|
431
|
+
title={t("ui.changeReview.emptyTitle")}
|
|
432
|
+
description={t("ui.changeReview.emptyDescription")}
|
|
429
433
|
className="my-2"
|
|
430
434
|
/>
|
|
431
435
|
);
|
|
@@ -468,10 +472,10 @@ const HUNK_STATUS_ICONS: Record<ChangeHunkStatus, ReactNode> = {
|
|
|
468
472
|
),
|
|
469
473
|
};
|
|
470
474
|
|
|
471
|
-
const
|
|
472
|
-
added: "
|
|
473
|
-
removed: "
|
|
474
|
-
modified: "
|
|
475
|
+
const HUNK_STATUS_LABEL_KEYS: Record<ChangeHunkStatus, string> = {
|
|
476
|
+
added: "ui.changeReview.statusAdded",
|
|
477
|
+
removed: "ui.changeReview.statusRemoved",
|
|
478
|
+
modified: "ui.changeReview.statusModified",
|
|
475
479
|
};
|
|
476
480
|
|
|
477
481
|
// ─── Checks (verification evidence) ────────────────────────────────────────────
|
|
@@ -487,9 +491,9 @@ function isLongCheckDetail(detail: string): boolean {
|
|
|
487
491
|
return detail.length > CHECK_DETAIL_COLLAPSE_THRESHOLD || detail.includes("\n");
|
|
488
492
|
}
|
|
489
493
|
|
|
490
|
-
const
|
|
491
|
-
before: "
|
|
492
|
-
after: "
|
|
494
|
+
const CHECK_PHASE_LABEL_KEYS: Record<"before" | "after", string> = {
|
|
495
|
+
before: "ui.changeReview.phaseBefore",
|
|
496
|
+
after: "ui.changeReview.phaseAfter",
|
|
493
497
|
};
|
|
494
498
|
|
|
495
499
|
/**
|
|
@@ -526,9 +530,10 @@ function groupChecksByPhase(
|
|
|
526
530
|
* decorative (`aria-hidden`); the status word is what reaches assistive tech.
|
|
527
531
|
*/
|
|
528
532
|
function ChangeCheckRow({ check }: { check: CheckResult }) {
|
|
533
|
+
const { t } = useLocale();
|
|
529
534
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
530
535
|
const { label, ok, detail, durationMs } = check;
|
|
531
|
-
const statusWord = ok ? "
|
|
536
|
+
const statusWord = ok ? t("ui.changeReview.checkPassed") : t("ui.changeReview.checkFailed");
|
|
532
537
|
const hasDetail = detail !== undefined && detail !== "";
|
|
533
538
|
const longDetail = hasDetail && isLongCheckDetail(detail);
|
|
534
539
|
|
|
@@ -552,7 +557,7 @@ function ChangeCheckRow({ check }: { check: CheckResult }) {
|
|
|
552
557
|
<Collapsible open={detailOpen} onOpenChange={setDetailOpen}>
|
|
553
558
|
<CollapsibleTrigger
|
|
554
559
|
className={cn(
|
|
555
|
-
"flex items-center gap-1 self-start rounded-sm
|
|
560
|
+
"flex items-center gap-1 self-start rounded-sm ps-5 text-caption text-muted-foreground",
|
|
556
561
|
"hover:text-foreground focus-ring",
|
|
557
562
|
)}
|
|
558
563
|
>
|
|
@@ -563,16 +568,16 @@ function ChangeCheckRow({ check }: { check: CheckResult }) {
|
|
|
563
568
|
detailOpen ? "rotate-180" : "rotate-0",
|
|
564
569
|
)}
|
|
565
570
|
/>
|
|
566
|
-
{detailOpen ? "
|
|
571
|
+
{detailOpen ? t("ui.changeReview.hideDetail") : t("ui.changeReview.showDetail")}
|
|
567
572
|
</CollapsibleTrigger>
|
|
568
573
|
<CollapsibleContent data-slot="change-review-hunk-check-detail">
|
|
569
|
-
<p className="
|
|
574
|
+
<p className="ps-5 pt-1 text-caption text-muted-foreground break-words">{detail}</p>
|
|
570
575
|
</CollapsibleContent>
|
|
571
576
|
</Collapsible>
|
|
572
577
|
) : (
|
|
573
578
|
<p
|
|
574
579
|
data-slot="change-review-hunk-check-detail"
|
|
575
|
-
className="
|
|
580
|
+
className="ps-5 text-caption text-muted-foreground break-words"
|
|
576
581
|
>
|
|
577
582
|
{detail}
|
|
578
583
|
</p>
|
|
@@ -583,6 +588,7 @@ function ChangeCheckRow({ check }: { check: CheckResult }) {
|
|
|
583
588
|
|
|
584
589
|
/** The full checks section for one hunk — grouped by phase when applicable. */
|
|
585
590
|
function ChangeReviewChecks({ checks }: { checks: CheckResult[] }) {
|
|
591
|
+
const { t } = useLocale();
|
|
586
592
|
const groups = groupChecksByPhase(checks);
|
|
587
593
|
const showPhaseLabels = groups.some((g) => g.phase !== undefined);
|
|
588
594
|
|
|
@@ -599,7 +605,7 @@ function ChangeReviewChecks({ checks }: { checks: CheckResult[] }) {
|
|
|
599
605
|
>
|
|
600
606
|
{showPhaseLabels && group.phase && (
|
|
601
607
|
<span className="text-meta font-medium text-muted-foreground">
|
|
602
|
-
{
|
|
608
|
+
{t(CHECK_PHASE_LABEL_KEYS[group.phase])}
|
|
603
609
|
</span>
|
|
604
610
|
)}
|
|
605
611
|
<div className="flex flex-col gap-1.5">
|
|
@@ -636,12 +642,15 @@ export interface ChangeReviewHunkProps
|
|
|
636
642
|
*/
|
|
637
643
|
export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>(
|
|
638
644
|
function ChangeReviewHunk({ hunk, renderHunk, className, ...props }, ref) {
|
|
645
|
+
const { t } = useLocale();
|
|
639
646
|
const { state, actions } = useChangeReview();
|
|
640
647
|
const { id, title, before, after, status = "modified", provenance, checks } = hunk;
|
|
641
648
|
const approved = state.approved.has(id);
|
|
642
649
|
|
|
643
|
-
const statusLabel =
|
|
644
|
-
const toggleLabel = approved ?
|
|
650
|
+
const statusLabel = t(HUNK_STATUS_LABEL_KEYS[status]);
|
|
651
|
+
const toggleLabel = t(approved ? "ui.changeReview.rejectHunk" : "ui.changeReview.approveHunk", {
|
|
652
|
+
title: title ?? id,
|
|
653
|
+
});
|
|
645
654
|
|
|
646
655
|
// Quiet, no-fill diff: before = struck muted text with a "−" marker, after =
|
|
647
656
|
// foreground text with a "+" marker — separated by space, NOT colored
|
|
@@ -659,7 +668,7 @@ export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>
|
|
|
659
668
|
>
|
|
660
669
|
−
|
|
661
670
|
</span>
|
|
662
|
-
<span className="sr-only">
|
|
671
|
+
<span className="sr-only">{t("ui.changeReview.beforePrefix")}</span>
|
|
663
672
|
<div className="min-w-0 break-words text-muted-foreground line-through decoration-muted-foreground/30">
|
|
664
673
|
{before}
|
|
665
674
|
</div>
|
|
@@ -670,7 +679,7 @@ export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>
|
|
|
670
679
|
<span aria-hidden="true" className="shrink-0 select-none font-mono text-foreground">
|
|
671
680
|
+
|
|
672
681
|
</span>
|
|
673
|
-
<span className="sr-only">
|
|
682
|
+
<span className="sr-only">{t("ui.changeReview.afterPrefix")}</span>
|
|
674
683
|
<div className="min-w-0 break-words text-foreground">{after}</div>
|
|
675
684
|
</div>
|
|
676
685
|
)}
|
|
@@ -691,7 +700,7 @@ export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>
|
|
|
691
700
|
{/* Status indicator: NON-color cue via symbol + color together */}
|
|
692
701
|
<span
|
|
693
702
|
role="img"
|
|
694
|
-
aria-label={
|
|
703
|
+
aria-label={t("ui.changeReview.changeType", { status: statusLabel })}
|
|
695
704
|
className="shrink-0 inline-flex items-center justify-center size-5"
|
|
696
705
|
>
|
|
697
706
|
{HUNK_STATUS_ICONS[status]}
|
|
@@ -732,7 +741,7 @@ export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>
|
|
|
732
741
|
<div className="flex items-center gap-1.5">
|
|
733
742
|
<Badge variant="success" className="animate-in fade-in-0 duration-fast ease-entrance">
|
|
734
743
|
<Check aria-hidden="true" className="size-3" />
|
|
735
|
-
|
|
744
|
+
{t("ui.changeReview.approvedBadge")}
|
|
736
745
|
</Badge>
|
|
737
746
|
</div>
|
|
738
747
|
)}
|
|
@@ -10,6 +10,7 @@ export const Checkbox = forwardRef<
|
|
|
10
10
|
return (
|
|
11
11
|
<CheckboxPrimitive.Root
|
|
12
12
|
ref={ref}
|
|
13
|
+
data-slot="checkbox"
|
|
13
14
|
className={cn(
|
|
14
15
|
"peer size-4 shrink-0 rounded-sm border border-input shadow-sm transition-colors duration-fast ease-standard",
|
|
15
16
|
"focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
@@ -25,9 +25,6 @@
|
|
|
25
25
|
* state-keyed defaults for a generic panel. Sidebar passes its
|
|
26
26
|
* `--sidebar-width` + `group-data-[collapsible=offcanvas]` literals and gets
|
|
27
27
|
* back its exact original fragments (the byte-identical re-point).
|
|
28
|
-
*
|
|
29
|
-
* Fork-prevention: a second gap-spacer + fixed-slide width tween outside this
|
|
30
|
-
* folder fails `pnpm collapse-fork:check` (scripts/check-collapse-fork.mjs).
|
|
31
28
|
*/
|
|
32
29
|
import { useCallback, useMemo, useState } from "react";
|
|
33
30
|
import { cn } from "../../lib/cn";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
// a2ui.exposed: yes
|
|
2
4
|
import {
|
|
3
5
|
forwardRef,
|
|
@@ -10,6 +12,7 @@ import {
|
|
|
10
12
|
} from "react";
|
|
11
13
|
import { cn } from "../../lib/cn";
|
|
12
14
|
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
|
15
|
+
import { useLocale } from "../locale-provider";
|
|
13
16
|
|
|
14
17
|
// ---------------------------------------------------------------------------
|
|
15
18
|
// Token vocabulary
|
|
@@ -194,12 +197,14 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
194
197
|
onValueChange,
|
|
195
198
|
swatches = COLOR_TOKENS,
|
|
196
199
|
allowCustom = false,
|
|
197
|
-
"aria-label": ariaLabel
|
|
200
|
+
"aria-label": ariaLabel,
|
|
198
201
|
className,
|
|
199
202
|
...props
|
|
200
203
|
},
|
|
201
204
|
ref,
|
|
202
205
|
) {
|
|
206
|
+
const { t } = useLocale();
|
|
207
|
+
const resolvedAriaLabel = ariaLabel ?? t("ui.colorPicker.pickColor");
|
|
203
208
|
const isControlled = valueProp !== undefined;
|
|
204
209
|
const [internalValue, setInternalValue] = useState<ColorToken | string>(
|
|
205
210
|
defaultValue ?? "chart-1",
|
|
@@ -259,11 +264,11 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
259
264
|
<PopoverTrigger asChild>
|
|
260
265
|
<button
|
|
261
266
|
type="button"
|
|
262
|
-
aria-label={
|
|
267
|
+
aria-label={resolvedAriaLabel}
|
|
263
268
|
aria-haspopup="dialog"
|
|
264
269
|
aria-expanded={open}
|
|
265
270
|
className={cn(
|
|
266
|
-
"inline-flex items-center gap-2 rounded-md border border-input bg-background px-3 py-1.5 text-
|
|
271
|
+
"inline-flex items-center gap-2 rounded-md border border-input bg-background px-3 py-1.5 text-body shadow-sm",
|
|
267
272
|
"transition-[color,box-shadow] duration-fast",
|
|
268
273
|
"hover:bg-accent hover:text-accent-foreground",
|
|
269
274
|
"focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
@@ -271,7 +276,7 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
271
276
|
)}
|
|
272
277
|
>
|
|
273
278
|
<TriggerSwatch value={current} />
|
|
274
|
-
<span className="text-muted-foreground">{current || "
|
|
279
|
+
<span className="text-muted-foreground">{current || t("ui.colorPicker.none")}</span>
|
|
275
280
|
</button>
|
|
276
281
|
</PopoverTrigger>
|
|
277
282
|
|
|
@@ -279,7 +284,7 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
279
284
|
<div
|
|
280
285
|
ref={groupRef}
|
|
281
286
|
role="radiogroup"
|
|
282
|
-
aria-label="
|
|
287
|
+
aria-label={t("ui.colorPicker.swatches")}
|
|
283
288
|
className="flex flex-wrap gap-1.5"
|
|
284
289
|
onKeyDown={handleGroupKeyDown}
|
|
285
290
|
>
|
|
@@ -296,18 +301,19 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
296
301
|
{allowCustom && (
|
|
297
302
|
<div className="mt-3 border-t border-border pt-3">
|
|
298
303
|
{/* Custom hex is NOT theme-aware — it will not adapt to theme changes. */}
|
|
299
|
-
<p className="mb-1.5 text-
|
|
304
|
+
<p className="mb-1.5 text-meta text-muted-foreground">
|
|
300
305
|
{/* #124: inline running text — ink rung, not the 3:1 mark rung. */}
|
|
301
|
-
|
|
306
|
+
{t("ui.colorPicker.customHexLabel")}{" "}
|
|
307
|
+
<span className="text-warning-text">{t("ui.colorPicker.notThemeAware")}</span>
|
|
302
308
|
</p>
|
|
303
309
|
<div className="flex gap-2">
|
|
304
310
|
<input
|
|
305
311
|
type="text"
|
|
306
312
|
value={customHex}
|
|
307
|
-
placeholder="#3b82f6"
|
|
313
|
+
placeholder="#3b82f6" // i18n-exempt: example hex value, not prose
|
|
308
314
|
maxLength={7}
|
|
309
315
|
spellCheck={false}
|
|
310
|
-
aria-label="
|
|
316
|
+
aria-label={t("ui.colorPicker.customHex")}
|
|
311
317
|
onChange={(e) => setCustomHex(e.target.value)}
|
|
312
318
|
onKeyDown={(e) => {
|
|
313
319
|
if (e.key === "Enter") {
|
|
@@ -316,7 +322,7 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
316
322
|
}
|
|
317
323
|
}}
|
|
318
324
|
className={cn(
|
|
319
|
-
"h-8 w-28 rounded-md border border-input bg-background px-2 text-
|
|
325
|
+
"h-8 w-28 rounded-md border border-input bg-background px-2 text-body",
|
|
320
326
|
"focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
321
327
|
"placeholder:text-muted-foreground",
|
|
322
328
|
)}
|
|
@@ -325,7 +331,7 @@ export const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(function
|
|
|
325
331
|
type="button"
|
|
326
332
|
onClick={handleCustomSubmit}
|
|
327
333
|
className={cn(
|
|
328
|
-
"h-8 rounded-md border border-input bg-background px-2 text-
|
|
334
|
+
"h-8 rounded-md border border-input bg-background px-2 text-body",
|
|
329
335
|
"hover:bg-accent hover:text-accent-foreground",
|
|
330
336
|
"focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
331
337
|
)}
|
|
@@ -5,6 +5,16 @@ const meta = {
|
|
|
5
5
|
title: "Forms/Combobox",
|
|
6
6
|
component: Combobox,
|
|
7
7
|
tags: ["autodocs"],
|
|
8
|
+
// The trigger fills its container by default (no hard-fixed width) — cap
|
|
9
|
+
// the docs canvas to a realistic form-field width instead of letting it
|
|
10
|
+
// stretch the full autodocs page.
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<div className="w-full max-w-md">
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
],
|
|
8
18
|
argTypes: {
|
|
9
19
|
options: {
|
|
10
20
|
description: "Array of `{ label, value }` options.",
|
|
@@ -66,6 +66,60 @@ describe("Combobox", () => {
|
|
|
66
66
|
);
|
|
67
67
|
expect(screen.getByRole("combobox")).toHaveTextContent("Custom Value");
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it("does not hard-fix the trigger width — fills its container by default", () => {
|
|
71
|
+
render(<Combobox options={[{ label: "A", value: "a" }]} />);
|
|
72
|
+
expect(screen.getByRole("combobox").className).not.toMatch(/\bw-56\b/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// jsdom doesn't implement Element.scrollIntoView, which cmdk calls internally
|
|
77
|
+
// to keep the auto-highlighted item visible — see the comment on the
|
|
78
|
+
// `allowCustomValue` describe below for the full explanation.
|
|
79
|
+
describe("Combobox (options interaction)", () => {
|
|
80
|
+
let originalScrollIntoView: typeof Element.prototype.scrollIntoView;
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
originalScrollIntoView = Element.prototype.scrollIntoView;
|
|
84
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
Element.prototype.scrollIntoView = originalScrollIntoView;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// #reviewed 1.7 — a hand-rolled `onValueChange ? onValueChange(v) : setInternal(v)`
|
|
92
|
+
// never updates internal state once a listener is attached, so an
|
|
93
|
+
// uncontrolled Combobox with an onValueChange handler never shows its own
|
|
94
|
+
// selection on the trigger.
|
|
95
|
+
it("updates the trigger's own displayed value when uncontrolled even with an onValueChange listener", async () => {
|
|
96
|
+
const onValueChange = vi.fn();
|
|
97
|
+
render(<Combobox options={[{ label: "Alpha", value: "a" }]} onValueChange={onValueChange} />);
|
|
98
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
99
|
+
await userEvent.click(await screen.findByRole("option", { name: "Alpha" }));
|
|
100
|
+
expect(onValueChange).toHaveBeenCalledWith("a");
|
|
101
|
+
expect(screen.getByRole("combobox")).toHaveTextContent("Alpha");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Duplicate labels used to collide because CommandItem's `value` (cmdk's
|
|
105
|
+
// identity/scoring key) was the LABEL, not the unique option value.
|
|
106
|
+
it("distinguishes options that share the same label", async () => {
|
|
107
|
+
const onValueChange = vi.fn();
|
|
108
|
+
render(
|
|
109
|
+
<Combobox
|
|
110
|
+
options={[
|
|
111
|
+
{ label: "Production", value: "prod-us" },
|
|
112
|
+
{ label: "Production", value: "prod-eu" },
|
|
113
|
+
]}
|
|
114
|
+
onValueChange={onValueChange}
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
118
|
+
const options = await screen.findAllByRole("option", { name: "Production" });
|
|
119
|
+
expect(options).toHaveLength(2);
|
|
120
|
+
await userEvent.click(options[1]!);
|
|
121
|
+
expect(onValueChange).toHaveBeenCalledWith("prod-eu");
|
|
122
|
+
});
|
|
69
123
|
});
|
|
70
124
|
|
|
71
125
|
// #359 — allowCustomValue. A nested describe because these tests TYPE into the
|