@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,7 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import { forwardRef, useState, type ComponentPropsWithoutRef } from "react";
|
|
2
4
|
import { defaultFilter } from "cmdk";
|
|
3
5
|
import { Check, ChevronsUpDown, Plus } from "lucide-react";
|
|
4
6
|
import { cn } from "../../lib/cn";
|
|
7
|
+
import { useControllableState } from "../../lib/use-controllable-state";
|
|
5
8
|
import { Button } from "../button";
|
|
6
9
|
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
|
7
10
|
import { useLocale } from "../locale-provider";
|
|
@@ -20,6 +23,12 @@ export interface ComboboxOption {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export interface ComboboxProps {
|
|
26
|
+
/**
|
|
27
|
+
* The option list — every option is mounted in the DOM (cmdk has no built-in
|
|
28
|
+
* windowing), so this is fine for typical form/menu-sized lists but not for
|
|
29
|
+
* open-ended data. For thousands of options, use `VirtualSelect` instead,
|
|
30
|
+
* which windows rendering via `@tanstack/react-virtual`.
|
|
31
|
+
*/
|
|
23
32
|
options: ComboboxOption[];
|
|
24
33
|
value?: string;
|
|
25
34
|
onValueChange?: (value: string) => void;
|
|
@@ -97,8 +106,8 @@ export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(function Co
|
|
|
97
106
|
options,
|
|
98
107
|
value,
|
|
99
108
|
onValueChange,
|
|
100
|
-
placeholder
|
|
101
|
-
searchPlaceholder
|
|
109
|
+
placeholder,
|
|
110
|
+
searchPlaceholder,
|
|
102
111
|
emptyText,
|
|
103
112
|
className,
|
|
104
113
|
disabled = false,
|
|
@@ -111,11 +120,11 @@ export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(function Co
|
|
|
111
120
|
ref,
|
|
112
121
|
) {
|
|
113
122
|
const { t } = useLocale();
|
|
123
|
+
const resolvedPlaceholder = placeholder ?? t("ui.combobox.placeholder");
|
|
124
|
+
const resolvedSearchPlaceholder = searchPlaceholder ?? t("ui.combobox.searchPlaceholder");
|
|
114
125
|
const [open, setOpen] = useState(false);
|
|
115
|
-
const [
|
|
126
|
+
const [selected, setValue] = useControllableState(value, "", onValueChange);
|
|
116
127
|
const [search, setSearch] = useState("");
|
|
117
|
-
const selected = value ?? internal;
|
|
118
|
-
const setValue = (v: string) => (onValueChange ? onValueChange(v) : setInternal(v));
|
|
119
128
|
const current = options.find((o) => o.value === selected);
|
|
120
129
|
|
|
121
130
|
const trimmedSearch = search.trim();
|
|
@@ -150,30 +159,35 @@ export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(function Co
|
|
|
150
159
|
aria-expanded={open}
|
|
151
160
|
disabled={disabled}
|
|
152
161
|
data-slot="combobox"
|
|
153
|
-
className={cn("w-
|
|
162
|
+
className={cn("w-full justify-between font-normal", triggerProps?.className, className)}
|
|
154
163
|
>
|
|
155
|
-
{current ? current.label : selected ||
|
|
164
|
+
{current ? current.label : selected || resolvedPlaceholder}
|
|
156
165
|
<ChevronsUpDown className="ms-2 size-4 shrink-0 opacity-50" />
|
|
157
166
|
</Button>
|
|
158
167
|
</PopoverTrigger>
|
|
159
168
|
<PopoverContent
|
|
160
|
-
className="w-56 p-0"
|
|
169
|
+
className="w-[var(--radix-popover-trigger-width)] min-w-56 p-0"
|
|
161
170
|
data-slot="combobox-content"
|
|
162
171
|
// Radix Popover.Content renders role="dialog"; without a name axe's
|
|
163
172
|
// aria-dialog-name rule (rightly) flags it. Reuse the trigger's own
|
|
164
173
|
// purpose label when the consumer set one, else fall back to the
|
|
165
174
|
// (already-translatable) search placeholder — no new hardcoded copy.
|
|
166
|
-
aria-label={ariaLabel ??
|
|
175
|
+
aria-label={ariaLabel ?? resolvedSearchPlaceholder}
|
|
167
176
|
>
|
|
168
177
|
<Command filter={allowCustomValue ? customValueFilter : undefined}>
|
|
169
|
-
<CommandInput
|
|
178
|
+
<CommandInput
|
|
179
|
+
placeholder={resolvedSearchPlaceholder}
|
|
180
|
+
value={search}
|
|
181
|
+
onValueChange={setSearch}
|
|
182
|
+
/>
|
|
170
183
|
<CommandList>
|
|
171
184
|
<CommandEmpty>{resolvedEmptyText}</CommandEmpty>
|
|
172
185
|
<CommandGroup>
|
|
173
186
|
{options.map((o) => (
|
|
174
187
|
<CommandItem
|
|
175
188
|
key={o.value}
|
|
176
|
-
value={o.
|
|
189
|
+
value={o.value}
|
|
190
|
+
keywords={[o.label]}
|
|
177
191
|
onSelect={() => {
|
|
178
192
|
setValue(o.value === selected ? "" : o.value);
|
|
179
193
|
setOpen(false);
|
|
@@ -5,6 +5,7 @@ import userEvent from "@testing-library/user-event";
|
|
|
5
5
|
import {
|
|
6
6
|
Command,
|
|
7
7
|
CommandDialog,
|
|
8
|
+
CommandEmpty,
|
|
8
9
|
CommandGroup,
|
|
9
10
|
CommandInput,
|
|
10
11
|
CommandItem,
|
|
@@ -351,3 +352,17 @@ describe("Command — CommandSeparator a11y (#157)", () => {
|
|
|
351
352
|
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
352
353
|
});
|
|
353
354
|
});
|
|
355
|
+
|
|
356
|
+
describe("CommandEmpty — merges className instead of replacing it", () => {
|
|
357
|
+
it("keeps the base py-6/text-center classes alongside a caller className", () => {
|
|
358
|
+
render(
|
|
359
|
+
<Command>
|
|
360
|
+
<CommandList>
|
|
361
|
+
<CommandEmpty className="custom-empty">No results</CommandEmpty>
|
|
362
|
+
</CommandList>
|
|
363
|
+
</Command>,
|
|
364
|
+
);
|
|
365
|
+
const empty = screen.getByText("No results");
|
|
366
|
+
expect(empty).toHaveClass("py-6", "text-center", "custom-empty");
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
createContext,
|
|
3
5
|
forwardRef,
|
|
@@ -199,7 +201,7 @@ export const CommandInput = forwardRef<
|
|
|
199
201
|
ref={ref}
|
|
200
202
|
data-slot="command-input"
|
|
201
203
|
className={cn(
|
|
202
|
-
"flex h-10 w-full rounded-md bg-transparent py-3 text-
|
|
204
|
+
"flex h-10 w-full rounded-md bg-transparent py-3 text-body outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
203
205
|
className,
|
|
204
206
|
)}
|
|
205
207
|
{...props}
|
|
@@ -225,12 +227,12 @@ export const CommandList = forwardRef<
|
|
|
225
227
|
export const CommandEmpty = forwardRef<
|
|
226
228
|
ElementRef<typeof CommandPrimitive.Empty>,
|
|
227
229
|
ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
228
|
-
>(function CommandEmpty(props, ref) {
|
|
230
|
+
>(function CommandEmpty({ className, ...props }, ref) {
|
|
229
231
|
return (
|
|
230
232
|
<CommandPrimitive.Empty
|
|
231
233
|
ref={ref}
|
|
232
234
|
data-slot="command-empty"
|
|
233
|
-
className="py-6 text-center text-
|
|
235
|
+
className={cn("py-6 text-center text-body", className)}
|
|
234
236
|
{...props}
|
|
235
237
|
/>
|
|
236
238
|
);
|
|
@@ -245,7 +247,7 @@ export const CommandGroup = forwardRef<
|
|
|
245
247
|
ref={ref}
|
|
246
248
|
data-slot="command-group"
|
|
247
249
|
className={cn(
|
|
248
|
-
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-
|
|
250
|
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-meta [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
249
251
|
className,
|
|
250
252
|
)}
|
|
251
253
|
{...props}
|
|
@@ -305,7 +307,7 @@ export const CommandItem = forwardRef<
|
|
|
305
307
|
ref={ref}
|
|
306
308
|
data-slot="command-item"
|
|
307
309
|
className={cn(
|
|
308
|
-
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-
|
|
310
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4",
|
|
309
311
|
className,
|
|
310
312
|
)}
|
|
311
313
|
{...props}
|
|
@@ -317,7 +319,7 @@ export function CommandShortcut({ className, ...props }: HTMLAttributes<HTMLSpan
|
|
|
317
319
|
return (
|
|
318
320
|
<span
|
|
319
321
|
data-slot="command-shortcut"
|
|
320
|
-
className={cn("ms-auto text-
|
|
322
|
+
className={cn("ms-auto text-meta tracking-widest text-muted-foreground", className)}
|
|
321
323
|
{...props}
|
|
322
324
|
/>
|
|
323
325
|
);
|
|
@@ -21,8 +21,9 @@ export const ContextMenuSubTrigger = forwardRef<
|
|
|
21
21
|
return (
|
|
22
22
|
<ContextMenuPrimitive.SubTrigger
|
|
23
23
|
ref={ref}
|
|
24
|
+
data-slot="context-menu-sub-trigger"
|
|
24
25
|
className={cn(
|
|
25
|
-
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-
|
|
26
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast focus:bg-accent data-[state=open]:bg-accent",
|
|
26
27
|
inset && "ps-8",
|
|
27
28
|
className,
|
|
28
29
|
)}
|
|
@@ -41,6 +42,7 @@ export const ContextMenuSubContent = forwardRef<
|
|
|
41
42
|
return (
|
|
42
43
|
<ContextMenuPrimitive.SubContent
|
|
43
44
|
ref={ref}
|
|
45
|
+
data-slot="context-menu-sub-content"
|
|
44
46
|
className={cn(
|
|
45
47
|
"z-50 min-w-[8rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
46
48
|
className,
|
|
@@ -58,8 +60,9 @@ export const ContextMenuContent = forwardRef<
|
|
|
58
60
|
<ContextMenuPrimitive.Portal>
|
|
59
61
|
<ContextMenuPrimitive.Content
|
|
60
62
|
ref={ref}
|
|
63
|
+
data-slot="context-menu-content"
|
|
61
64
|
className={cn(
|
|
62
|
-
"z-50 min-w-[
|
|
65
|
+
"z-50 min-w-[12rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
63
66
|
className,
|
|
64
67
|
)}
|
|
65
68
|
{...props}
|
|
@@ -75,8 +78,9 @@ export const ContextMenuItem = forwardRef<
|
|
|
75
78
|
return (
|
|
76
79
|
<ContextMenuPrimitive.Item
|
|
77
80
|
ref={ref}
|
|
81
|
+
data-slot="context-menu-item"
|
|
78
82
|
className={cn(
|
|
79
|
-
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-
|
|
83
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4",
|
|
80
84
|
inset && "ps-8",
|
|
81
85
|
className,
|
|
82
86
|
)}
|
|
@@ -92,9 +96,10 @@ export const ContextMenuCheckboxItem = forwardRef<
|
|
|
92
96
|
return (
|
|
93
97
|
<ContextMenuPrimitive.CheckboxItem
|
|
94
98
|
ref={ref}
|
|
99
|
+
data-slot="context-menu-checkbox-item"
|
|
95
100
|
checked={checked}
|
|
96
101
|
className={cn(
|
|
97
|
-
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-
|
|
102
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-body outline-none transition-colors duration-fast focus:bg-accent",
|
|
98
103
|
className,
|
|
99
104
|
)}
|
|
100
105
|
{...props}
|
|
@@ -116,8 +121,9 @@ export const ContextMenuRadioItem = forwardRef<
|
|
|
116
121
|
return (
|
|
117
122
|
<ContextMenuPrimitive.RadioItem
|
|
118
123
|
ref={ref}
|
|
124
|
+
data-slot="context-menu-radio-item"
|
|
119
125
|
className={cn(
|
|
120
|
-
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-
|
|
126
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-body outline-none transition-colors duration-fast focus:bg-accent",
|
|
121
127
|
className,
|
|
122
128
|
)}
|
|
123
129
|
{...props}
|
|
@@ -139,8 +145,9 @@ export const ContextMenuLabel = forwardRef<
|
|
|
139
145
|
return (
|
|
140
146
|
<ContextMenuPrimitive.Label
|
|
141
147
|
ref={ref}
|
|
148
|
+
data-slot="context-menu-label"
|
|
142
149
|
className={cn(
|
|
143
|
-
"px-2 py-1.5 text-
|
|
150
|
+
"px-2 py-1.5 text-meta font-semibold text-muted-foreground",
|
|
144
151
|
inset && "ps-8",
|
|
145
152
|
className,
|
|
146
153
|
)}
|
|
@@ -156,6 +163,7 @@ export const ContextMenuSeparator = forwardRef<
|
|
|
156
163
|
return (
|
|
157
164
|
<ContextMenuPrimitive.Separator
|
|
158
165
|
ref={ref}
|
|
166
|
+
data-slot="context-menu-separator"
|
|
159
167
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
160
168
|
{...props}
|
|
161
169
|
/>
|
|
@@ -165,7 +173,8 @@ export const ContextMenuSeparator = forwardRef<
|
|
|
165
173
|
export function ContextMenuShortcut({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
|
166
174
|
return (
|
|
167
175
|
<span
|
|
168
|
-
|
|
176
|
+
data-slot="context-menu-shortcut"
|
|
177
|
+
className={cn("ms-auto text-meta tracking-widest text-muted-foreground", className)}
|
|
169
178
|
{...props}
|
|
170
179
|
/>
|
|
171
180
|
);
|
|
@@ -162,7 +162,7 @@ const COUNT_BADGE_POSITION = "end-0 -top-1 h-4 min-w-4 px-0.5";
|
|
|
162
162
|
// The rail's own header band. Two jobs, and both are geometry the rail cannot
|
|
163
163
|
// leave to its children:
|
|
164
164
|
//
|
|
165
|
-
// 1. EXPANDED it is `h-
|
|
165
|
+
// 1. EXPANDED it is `h-header` (56px by default) — the same height an app shell's top bar carries
|
|
166
166
|
// (`AppShell`, and both shipped shell blocks) — and it holds the section
|
|
167
167
|
// heading AND the switcher in ONE row. Before this, the switcher row (32px,
|
|
168
168
|
// flush at y=0) and a separately-bordered heading (53px) stacked to 85px, so
|
|
@@ -177,7 +177,7 @@ const COUNT_BADGE_POSITION = "end-0 -top-1 h-4 min-w-4 px-0.5";
|
|
|
177
177
|
// centres the FIRST icon in the same 56px band as the top bar's controls, so
|
|
178
178
|
// the two rails' first rows agree.
|
|
179
179
|
const CONTEXT_RAIL_HEADER =
|
|
180
|
-
"flex h-
|
|
180
|
+
"flex h-header shrink-0 items-center gap-2 border-b border-border-strong px-2 group-data-[collapsible=icon]:h-auto group-data-[collapsible=icon]:flex-col group-data-[collapsible=icon]:items-stretch group-data-[collapsible=icon]:border-b-0 group-data-[collapsible=icon]:px-2 group-data-[collapsible=icon]:pt-3 group-data-[collapsible=icon]:pb-2";
|
|
181
181
|
|
|
182
182
|
function ContextRailSwitcher({
|
|
183
183
|
sections,
|
|
@@ -491,10 +491,10 @@ const ContextRailNarrow = forwardRef<HTMLDivElement, ContextRailNarrowProps>(
|
|
|
491
491
|
above; this is the visible heading the body sits under. */}
|
|
492
492
|
<h2
|
|
493
493
|
data-slot="context-rail-heading"
|
|
494
|
-
// `h-
|
|
494
|
+
// `h-header`, matching the wide branch's header band and the app
|
|
495
495
|
// shell's top bar — the sheet is the same rail at a narrower
|
|
496
496
|
// width, so its header keeps the same height.
|
|
497
|
-
className="flex h-
|
|
497
|
+
className="flex h-header shrink-0 items-center truncate border-b border-border-strong px-4 text-title"
|
|
498
498
|
>
|
|
499
499
|
{activeSection.label}
|
|
500
500
|
</h2>
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
+
import { Toaster } from "../sonner";
|
|
3
4
|
import { CopyableValue } from "./copyable-value";
|
|
4
5
|
|
|
5
6
|
const meta = {
|
|
6
7
|
title: "Display/CopyableValue",
|
|
7
8
|
component: CopyableValue,
|
|
8
9
|
tags: ["autodocs"],
|
|
10
|
+
// The copy toast needs a host; apps mount one `<Toaster />` at their root.
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<>
|
|
14
|
+
<Story />
|
|
15
|
+
<Toaster />
|
|
16
|
+
</>
|
|
17
|
+
),
|
|
18
|
+
],
|
|
9
19
|
parameters: {
|
|
10
20
|
docs: {
|
|
11
21
|
description: {
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
import { describe, expect, it, vi } from "vitest";
|
|
12
12
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
13
13
|
import userEvent from "@testing-library/user-event";
|
|
14
|
-
import {
|
|
14
|
+
import { toast } from "../sonner";
|
|
15
|
+
import { CopyableValue, COPYABLE_VALUE_TOAST_MS } from "./copyable-value";
|
|
16
|
+
|
|
17
|
+
vi.mock("../sonner", () => ({ toast: vi.fn() }));
|
|
15
18
|
|
|
16
19
|
/** Install a clipboard stub for one test; returns the writeText spy. */
|
|
17
20
|
function stubClipboard(impl: () => Promise<void> = () => Promise.resolve()) {
|
|
@@ -38,6 +41,30 @@ describe("CopyableValue", () => {
|
|
|
38
41
|
});
|
|
39
42
|
});
|
|
40
43
|
|
|
44
|
+
it("shows a one-second toast once the copy succeeds", async () => {
|
|
45
|
+
vi.mocked(toast).mockClear();
|
|
46
|
+
stubClipboard();
|
|
47
|
+
render(<CopyableValue value="42">42</CopyableValue>);
|
|
48
|
+
|
|
49
|
+
fireEvent.click(screen.getByRole("button"));
|
|
50
|
+
await waitFor(() => {
|
|
51
|
+
expect(toast).toHaveBeenCalledWith("Copied", { duration: COPYABLE_VALUE_TOAST_MS });
|
|
52
|
+
});
|
|
53
|
+
expect(COPYABLE_VALUE_TOAST_MS).toBe(1000);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("shows no toast when the copy fails", async () => {
|
|
57
|
+
vi.mocked(toast).mockClear();
|
|
58
|
+
stubClipboard(() => Promise.reject(new Error("denied")));
|
|
59
|
+
render(<CopyableValue value="42">42</CopyableValue>);
|
|
60
|
+
|
|
61
|
+
fireEvent.click(screen.getByRole("button"));
|
|
62
|
+
await waitFor(() => {
|
|
63
|
+
expect(screen.getByRole("status")).toHaveTextContent("");
|
|
64
|
+
});
|
|
65
|
+
expect(toast).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
41
68
|
it("falls back to the exact value as its own display", () => {
|
|
42
69
|
render(<CopyableValue value="1234" />);
|
|
43
70
|
expect(screen.getByRole("button")).toHaveTextContent("1234");
|
|
@@ -19,6 +19,10 @@ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
|
19
19
|
import { cn } from "../../lib/cn";
|
|
20
20
|
import { useCopyToClipboard } from "../../lib/use-copy-to-clipboard";
|
|
21
21
|
import { useLocale } from "../locale-provider";
|
|
22
|
+
import { toast } from "../sonner";
|
|
23
|
+
|
|
24
|
+
/** ms the copy-confirmation toast stays on screen. */
|
|
25
|
+
export const COPYABLE_VALUE_TOAST_MS = 1000;
|
|
22
26
|
|
|
23
27
|
export interface CopyableValueProps extends Omit<
|
|
24
28
|
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
@@ -79,7 +83,14 @@ export const CopyableValue = forwardRef<HTMLButtonElement, CopyableValueProps>(
|
|
|
79
83
|
onClick={(event) => {
|
|
80
84
|
onClick?.(event);
|
|
81
85
|
if (!event.defaultPrevented) {
|
|
82
|
-
|
|
86
|
+
// The toast is the visible confirmation; it needs an app-mounted
|
|
87
|
+
// `<Toaster />` and is a silent no-op without one, which is why
|
|
88
|
+
// the live region below stays as the always-present fallback.
|
|
89
|
+
void copy(value).then((ok) => {
|
|
90
|
+
if (ok) {
|
|
91
|
+
toast(t("ui.copyableValue.copied"), { duration: COPYABLE_VALUE_TOAST_MS });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
83
94
|
}
|
|
84
95
|
}}
|
|
85
96
|
>
|
|
@@ -5,6 +5,15 @@ const meta = {
|
|
|
5
5
|
title: "Forms/DatePicker",
|
|
6
6
|
component: DatePicker,
|
|
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.
|
|
10
|
+
decorators: [
|
|
11
|
+
(Story) => (
|
|
12
|
+
<div className="w-full max-w-xs">
|
|
13
|
+
<Story />
|
|
14
|
+
</div>
|
|
15
|
+
),
|
|
16
|
+
],
|
|
8
17
|
argTypes: {
|
|
9
18
|
value: {
|
|
10
19
|
description: "Controlled selected date.",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { render, screen, within } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { DatePicker } from "./date-picker";
|
|
6
|
+
import { LocaleProvider } from "../locale-provider";
|
|
7
|
+
|
|
8
|
+
describe("DatePicker", () => {
|
|
9
|
+
it("forwards the ref to the trigger button", () => {
|
|
10
|
+
const ref = createRef<HTMLButtonElement>();
|
|
11
|
+
render(<DatePicker ref={ref} />);
|
|
12
|
+
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
|
13
|
+
expect(ref.current).toBe(screen.getByRole("button"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("does not hard-fix the trigger width — fills its container by default", () => {
|
|
17
|
+
render(<DatePicker />);
|
|
18
|
+
expect(screen.getByRole("button").className).not.toMatch(/\bw-56\b/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// #reviewed 1.7 — same `value ?? internal` / ternary-setter bug as Combobox:
|
|
22
|
+
// an uncontrolled DatePicker with an onValueChange listener never updated
|
|
23
|
+
// its own displayed date.
|
|
24
|
+
it("updates its own displayed date when uncontrolled even with an onValueChange listener", async () => {
|
|
25
|
+
const onValueChange = vi.fn();
|
|
26
|
+
const user = userEvent.setup();
|
|
27
|
+
render(<DatePicker onValueChange={onValueChange} placeholder="Pick a date" />);
|
|
28
|
+
const trigger = screen.getByRole("button", { name: /pick a date/i });
|
|
29
|
+
await user.click(trigger);
|
|
30
|
+
const grid = await screen.findByRole("grid");
|
|
31
|
+
const dayButtons = within(grid)
|
|
32
|
+
.getAllByRole("button")
|
|
33
|
+
.filter((b) => !/previous|next/i.test(b.getAttribute("aria-label") ?? ""));
|
|
34
|
+
const firstEnabled = dayButtons.find((b) => !(b as HTMLButtonElement).disabled)!;
|
|
35
|
+
await user.click(firstEnabled);
|
|
36
|
+
expect(onValueChange).toHaveBeenCalled();
|
|
37
|
+
expect(trigger).not.toHaveTextContent("Pick a date");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("formats the displayed date using the active locale", () => {
|
|
41
|
+
const date = new Date(2024, 0, 15); // Jan 15, 2024
|
|
42
|
+
render(
|
|
43
|
+
<LocaleProvider locale="de-DE">
|
|
44
|
+
<DatePicker value={date} />
|
|
45
|
+
</LocaleProvider>,
|
|
46
|
+
);
|
|
47
|
+
// de-DE "medium" dateStyle renders like "15. Jan. 2024" — distinct from
|
|
48
|
+
// the en-US default ("Jan 15, 2024").
|
|
49
|
+
expect(screen.getByRole("button")).toHaveTextContent("2024");
|
|
50
|
+
expect(screen.getByRole("button").textContent).not.toMatch(/Jan 15, 2024/);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
2
|
import { Calendar as CalendarIcon } from "lucide-react";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
|
+
import { useControllableState } from "../../lib/use-controllable-state";
|
|
4
5
|
import { Button } from "../button";
|
|
5
6
|
import { Calendar } from "../calendar";
|
|
7
|
+
import { useLocale } from "../locale-provider";
|
|
6
8
|
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
|
7
9
|
|
|
8
10
|
export interface DatePickerProps {
|
|
@@ -13,26 +15,22 @@ export interface DatePickerProps {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/** Single-date picker (Popover + Calendar). Controlled or uncontrolled. */
|
|
16
|
-
export function DatePicker(
|
|
17
|
-
value,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const date = value ?? internal;
|
|
24
|
-
const setDate = (d?: Date) => (onValueChange ? onValueChange(d) : setInternal(d));
|
|
25
|
-
const label = date
|
|
26
|
-
? new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(date)
|
|
27
|
-
: placeholder;
|
|
18
|
+
export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(function DatePicker(
|
|
19
|
+
{ value, onValueChange, placeholder = "Pick a date", className },
|
|
20
|
+
ref,
|
|
21
|
+
) {
|
|
22
|
+
const { formatDate } = useLocale();
|
|
23
|
+
const [date, setDate] = useControllableState<Date | undefined>(value, undefined, onValueChange);
|
|
24
|
+
const label = date ? formatDate(date, { dateStyle: "medium" }) : placeholder;
|
|
28
25
|
|
|
29
26
|
return (
|
|
30
27
|
<Popover>
|
|
31
28
|
<PopoverTrigger asChild>
|
|
32
29
|
<Button
|
|
30
|
+
ref={ref}
|
|
33
31
|
variant="outline"
|
|
34
32
|
className={cn(
|
|
35
|
-
"w-
|
|
33
|
+
"w-full justify-start gap-2 text-start font-normal",
|
|
36
34
|
!date && "text-muted-foreground",
|
|
37
35
|
className,
|
|
38
36
|
)}
|
|
@@ -46,4 +44,4 @@ export function DatePicker({
|
|
|
46
44
|
</PopoverContent>
|
|
47
45
|
</Popover>
|
|
48
46
|
);
|
|
49
|
-
}
|
|
47
|
+
});
|
|
@@ -12,6 +12,15 @@ const meta = {
|
|
|
12
12
|
parameters: {
|
|
13
13
|
layout: "centered",
|
|
14
14
|
},
|
|
15
|
+
// The trigger fills its container by default (no hard-fixed width) — cap
|
|
16
|
+
// the docs canvas to a realistic form-field width.
|
|
17
|
+
decorators: [
|
|
18
|
+
(Story) => (
|
|
19
|
+
<div className="w-80">
|
|
20
|
+
<Story />
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
],
|
|
15
24
|
argTypes: {
|
|
16
25
|
value: {
|
|
17
26
|
description: "Controlled date range `{ from, to }`.",
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* select interaction and accessibility are covered by the Storybook stories
|
|
7
7
|
* (run via `pnpm --filter @elabs-ai/components-docs test-storybook` or the MCP story tests).
|
|
8
8
|
*/
|
|
9
|
+
import { useState } from "react";
|
|
9
10
|
import { describe, expect, it } from "vitest";
|
|
10
11
|
import { render, screen } from "@testing-library/react";
|
|
11
|
-
import
|
|
12
|
+
import userEvent from "@testing-library/user-event";
|
|
13
|
+
import { DateRangePicker, type DateRange } from "./date-range-picker";
|
|
14
|
+
import { LocaleProvider } from "../locale-provider";
|
|
12
15
|
|
|
13
16
|
describe("DateRangePicker", () => {
|
|
14
17
|
it("renders the placeholder on the trigger when no range is set", () => {
|
|
@@ -59,4 +62,76 @@ describe("DateRangePicker", () => {
|
|
|
59
62
|
const button = screen.getByRole("button");
|
|
60
63
|
expect(button.className).toContain("my-custom-class");
|
|
61
64
|
});
|
|
65
|
+
|
|
66
|
+
it("does not hard-fix the trigger width — fills its container by default", () => {
|
|
67
|
+
render(<DateRangePicker />);
|
|
68
|
+
expect(screen.getByRole("button").className).not.toMatch(/\bw-72\b/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("spreads extra props (e.g. data-testid) onto the trigger button", () => {
|
|
72
|
+
render(<DateRangePicker data-testid="range-trigger" />);
|
|
73
|
+
expect(screen.getByTestId("range-trigger")).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("formats the range label using the active locale", () => {
|
|
77
|
+
const from = new Date(2024, 0, 5);
|
|
78
|
+
const to = new Date(2024, 0, 20);
|
|
79
|
+
render(
|
|
80
|
+
<LocaleProvider locale="de-DE">
|
|
81
|
+
<DateRangePicker value={{ from, to }} />
|
|
82
|
+
</LocaleProvider>,
|
|
83
|
+
);
|
|
84
|
+
// de-DE renders "5. Jan. 2024" style — distinct from the en-US default.
|
|
85
|
+
expect(screen.getByRole("button").textContent).not.toMatch(/Jan 5, 2024/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// #reviewed 1.7 — clearing a controlled range to `undefined` must not fall
|
|
89
|
+
// through to a stale uncontrolled `internal` value: once controlled, the
|
|
90
|
+
// component must stay controlled for its whole lifetime.
|
|
91
|
+
it("collapses to a single month below the sm breakpoint (phone-width popover)", async () => {
|
|
92
|
+
const originalWidth = window.innerWidth;
|
|
93
|
+
Object.defineProperty(window, "innerWidth", {
|
|
94
|
+
writable: true,
|
|
95
|
+
configurable: true,
|
|
96
|
+
value: 375,
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
render(<DateRangePicker />);
|
|
101
|
+
await user.click(screen.getByRole("button"));
|
|
102
|
+
const grids = await screen.findAllByRole("grid");
|
|
103
|
+
expect(grids).toHaveLength(1);
|
|
104
|
+
} finally {
|
|
105
|
+
Object.defineProperty(window, "innerWidth", {
|
|
106
|
+
writable: true,
|
|
107
|
+
configurable: true,
|
|
108
|
+
value: originalWidth,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("shows the placeholder (not a stale value) when a controlled range is cleared to undefined", async () => {
|
|
114
|
+
function Harness() {
|
|
115
|
+
const [range, setRange] = useState<DateRange | undefined>({
|
|
116
|
+
from: new Date(2024, 0, 5),
|
|
117
|
+
to: new Date(2024, 0, 20),
|
|
118
|
+
});
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
<DateRangePicker value={range} onValueChange={setRange} placeholder="Pick a range" />
|
|
122
|
+
<button type="button" onClick={() => setRange(undefined)}>
|
|
123
|
+
Clear
|
|
124
|
+
</button>
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const user = userEvent.setup();
|
|
129
|
+
render(<Harness />);
|
|
130
|
+
const trigger = screen.getByRole("button", { name: /–/ });
|
|
131
|
+
expect(trigger.textContent).not.toBe("Pick a range");
|
|
132
|
+
|
|
133
|
+
await user.click(screen.getByRole("button", { name: "Clear" }));
|
|
134
|
+
|
|
135
|
+
expect(trigger).toHaveTextContent("Pick a range");
|
|
136
|
+
});
|
|
62
137
|
});
|