@ponti-studios/ui 1.2.0 → 1.3.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/package.json +1 -1
- package/src/components/data-display/accordion.tsx +10 -1
- package/src/components/feedback/empty-state.tsx +1 -1
- package/src/components/feedback/file-upload-status.tsx +14 -6
- package/src/components/feedback/progress-bar.tsx +3 -2
- package/src/components/feedback/spinner.tsx +3 -4
- package/src/components/feedback/update-guard.stories.tsx +1 -1
- package/src/components/feedback/update-guard.tsx +2 -2
- package/src/components/filters/active-filters-bar.stories.tsx +2 -0
- package/src/components/filters/active-filters-bar.tsx +21 -23
- package/src/components/filters/filter-select.tsx +4 -2
- package/src/components/filters/index.ts +0 -4
- package/src/components/forms/date-month-select.tsx +2 -2
- package/src/components/forms/date-picker.tsx +2 -2
- package/src/components/forms/drop-zone.stories.tsx +112 -28
- package/src/components/forms/drop-zone.tsx +265 -112
- package/src/components/forms/input.tsx +5 -1
- package/src/components/forms/radio-group.tsx +3 -2
- package/src/components/forms/select.tsx +14 -4
- package/src/components/forms/slider.stories.tsx +2 -2
- package/src/components/forms/slider.tsx +12 -9
- package/src/components/forms/switch.tsx +5 -2
- package/src/components/forms/textarea.tsx +5 -1
- package/src/components/layout/index.ts +0 -2
- package/src/components/layout/section-intro.tsx +1 -1
- package/src/components/layout/surface-panel.tsx +10 -2
- package/src/components/navigation/tabs.tsx +4 -1
- package/src/components/overlays/command.tsx +39 -14
- package/src/components/overlays/popover.stories.tsx +16 -4
- package/src/components/primitives/badge.tsx +1 -1
- package/src/components/primitives/button.tsx +6 -1
- package/src/components/filters/filters.stories.tsx +0 -72
- package/src/components/filters/filters.tsx +0 -72
- package/src/components/filters/search-filter-bar.stories.tsx +0 -39
- package/src/components/filters/search-filter-bar.tsx +0 -46
package/package.json
CHANGED
|
@@ -6,8 +6,9 @@ import { cn } from "../../lib/utils";
|
|
|
6
6
|
|
|
7
7
|
function Accordion({
|
|
8
8
|
type,
|
|
9
|
-
collapsible
|
|
9
|
+
collapsible = true,
|
|
10
10
|
className,
|
|
11
|
+
onValueChange,
|
|
11
12
|
...props
|
|
12
13
|
}: React.ComponentProps<typeof AccordionPrimitive.Root> & {
|
|
13
14
|
type?: "single" | "multiple";
|
|
@@ -17,6 +18,14 @@ function Accordion({
|
|
|
17
18
|
<AccordionPrimitive.Root
|
|
18
19
|
data-slot="accordion"
|
|
19
20
|
multiple={type === "multiple"}
|
|
21
|
+
onValueChange={(value, details) => {
|
|
22
|
+
if (type !== "multiple" && !collapsible && value.length === 0) {
|
|
23
|
+
details.cancel();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onValueChange?.(value, details);
|
|
28
|
+
}}
|
|
20
29
|
{...props}
|
|
21
30
|
className={cn("w-full", className)}
|
|
22
31
|
/>
|
|
@@ -40,7 +40,7 @@ export function EmptyState({
|
|
|
40
40
|
variant === "default" && "bg-card border",
|
|
41
41
|
variant === "dashed" && "bg-card border border-dashed",
|
|
42
42
|
variant === "search" && "bg-muted border border-dashed",
|
|
43
|
-
variant === "quiet" && "bg-
|
|
43
|
+
variant === "quiet" && "bg-card",
|
|
44
44
|
centered
|
|
45
45
|
? cn(
|
|
46
46
|
"flex flex-col items-center justify-center text-center",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { XIcon } from "lucide-react";
|
|
2
2
|
import { memo } from "react";
|
|
3
3
|
|
|
4
|
-
import { cn } from "../../lib/utils";
|
|
5
4
|
import { ProgressBar } from "./progress-bar";
|
|
6
5
|
|
|
7
6
|
export interface FileUploadStats {
|
|
@@ -47,11 +46,12 @@ export const FileUploadStatus = memo(function FileUploadStatus({
|
|
|
47
46
|
{typeof progress === "number" ? (
|
|
48
47
|
<ProgressBar
|
|
49
48
|
progress={progress}
|
|
50
|
-
className=
|
|
49
|
+
className="bg-muted h-2"
|
|
50
|
+
indicatorClassName="border-primary"
|
|
51
51
|
/>
|
|
52
52
|
) : (
|
|
53
53
|
<div className="bg-muted h-2 overflow-hidden">
|
|
54
|
-
<div className="bg-
|
|
54
|
+
<div className="bg-primary void-anim-breezy-progress h-full" />
|
|
55
55
|
</div>
|
|
56
56
|
)}
|
|
57
57
|
</div>
|
|
@@ -66,7 +66,11 @@ export const FileUploadStatus = memo(function FileUploadStatus({
|
|
|
66
66
|
<span className="text-muted-foreground text-sm font-medium">Queue position</span>
|
|
67
67
|
</div>
|
|
68
68
|
<div className="w-full">
|
|
69
|
-
<ProgressBar
|
|
69
|
+
<ProgressBar
|
|
70
|
+
progress={0}
|
|
71
|
+
className="bg-warning/10 h-2"
|
|
72
|
+
indicatorClassName="border-warning"
|
|
73
|
+
/>
|
|
70
74
|
</div>
|
|
71
75
|
</output>
|
|
72
76
|
);
|
|
@@ -76,7 +80,11 @@ export const FileUploadStatus = memo(function FileUploadStatus({
|
|
|
76
80
|
return (
|
|
77
81
|
<output className="space-y-3">
|
|
78
82
|
<div className="w-full">
|
|
79
|
-
<ProgressBar
|
|
83
|
+
<ProgressBar
|
|
84
|
+
progress={100}
|
|
85
|
+
className="bg-muted h-2"
|
|
86
|
+
indicatorClassName="border-primary"
|
|
87
|
+
/>
|
|
80
88
|
</div>
|
|
81
89
|
{stats && <ProcessingStats stats={stats} />}
|
|
82
90
|
</output>
|
|
@@ -87,7 +95,7 @@ export const FileUploadStatus = memo(function FileUploadStatus({
|
|
|
87
95
|
return (
|
|
88
96
|
<output className="mt-2" role="alert">
|
|
89
97
|
<div className="bg-destructive/10 text-destructive-text flex items-start gap-2 p-3">
|
|
90
|
-
<XIcon className="mt-0.5 h-5 w-5
|
|
98
|
+
<XIcon className="mt-0.5 h-5 w-5 shrink-0" aria-hidden="true" />
|
|
91
99
|
<span className="text-sm">{error}</span>
|
|
92
100
|
</div>
|
|
93
101
|
</output>
|
|
@@ -2,15 +2,16 @@ import { cn } from "../../lib/utils";
|
|
|
2
2
|
|
|
3
3
|
export interface ProgressBarProps {
|
|
4
4
|
className?: string;
|
|
5
|
+
indicatorClassName?: string;
|
|
5
6
|
progress?: number;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
/** Thin linear progress indicator, distinct from the Radix-backed `Progress`. */
|
|
9
|
-
export function ProgressBar({ className, progress = 0 }: ProgressBarProps) {
|
|
10
|
+
export function ProgressBar({ className, indicatorClassName, progress = 0 }: ProgressBarProps) {
|
|
10
11
|
return (
|
|
11
12
|
<div className={cn("border-border h-[2px] w-full overflow-hidden border-t", className)}>
|
|
12
13
|
<div
|
|
13
|
-
className={cn("border-warning h-full border-t")}
|
|
14
|
+
className={cn("border-warning h-full border-t", indicatorClassName)}
|
|
14
15
|
style={{
|
|
15
16
|
width: `${Math.min(100, Math.max(0, progress))}%`,
|
|
16
17
|
opacity: progress === 100 ? 0 : 0.8,
|
|
@@ -9,7 +9,7 @@ const sizeClasses = {
|
|
|
9
9
|
xl: "size-12",
|
|
10
10
|
} as const;
|
|
11
11
|
|
|
12
|
-
export interface SpinnerProps extends React.ComponentProps<"
|
|
12
|
+
export interface SpinnerProps extends React.ComponentProps<"output"> {
|
|
13
13
|
label?: string | false;
|
|
14
14
|
presentation?: "inline" | "centered";
|
|
15
15
|
size?: keyof typeof sizeClasses;
|
|
@@ -25,13 +25,12 @@ export function Spinner({
|
|
|
25
25
|
const accessibleLabel = typeof label === "string" ? label : "Loading";
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
|
-
<
|
|
28
|
+
<output
|
|
29
29
|
className={cn(
|
|
30
30
|
"inline-flex items-center justify-center gap-2",
|
|
31
31
|
presentation === "centered" && "flex w-full py-12",
|
|
32
32
|
className,
|
|
33
33
|
)}
|
|
34
|
-
role="status"
|
|
35
34
|
aria-label={accessibleLabel}
|
|
36
35
|
{...props}
|
|
37
36
|
>
|
|
@@ -43,6 +42,6 @@ export function Spinner({
|
|
|
43
42
|
aria-hidden="true"
|
|
44
43
|
/>
|
|
45
44
|
{label && <span className="text-muted-foreground text-xs">{label}</span>}
|
|
46
|
-
</
|
|
45
|
+
</output>
|
|
47
46
|
);
|
|
48
47
|
}
|
|
@@ -23,7 +23,7 @@ export const Default: Story = {
|
|
|
23
23
|
args: {
|
|
24
24
|
hideInDev: false,
|
|
25
25
|
children: (
|
|
26
|
-
<div className="border-border bg-
|
|
26
|
+
<div className="border-border bg-card text-muted-foreground rounded-md border p-6 text-sm">
|
|
27
27
|
App content renders here. Offline/update prompts appear fixed to the bottom of the viewport
|
|
28
28
|
when the service worker reports a change.
|
|
29
29
|
</div>
|
|
@@ -225,7 +225,7 @@ function UpdateGuardClient({
|
|
|
225
225
|
<>
|
|
226
226
|
{offlineMessage ? (
|
|
227
227
|
<div className="fixed inset-x-0 bottom-16 z-50 flex justify-center px-4">
|
|
228
|
-
<div className="border-border bg-
|
|
228
|
+
<div className="border-border bg-card flex items-center gap-3 rounded-md border px-4 py-2">
|
|
229
229
|
<span className="text-foreground text-sm">{offlineMessage}</span>
|
|
230
230
|
</div>
|
|
231
231
|
</div>
|
|
@@ -233,7 +233,7 @@ function UpdateGuardClient({
|
|
|
233
233
|
|
|
234
234
|
{offlineReady || needRefresh ? (
|
|
235
235
|
<div className="fixed inset-x-0 bottom-4 z-50 flex justify-center px-4">
|
|
236
|
-
<div className="border-border bg-
|
|
236
|
+
<div className="border-border bg-card flex items-center gap-3 rounded-md border px-4 py-2">
|
|
237
237
|
<span className="text-foreground text-sm">
|
|
238
238
|
{offlineReady
|
|
239
239
|
? (copy?.offlineReady ?? "App ready to work offline")
|
|
@@ -26,6 +26,7 @@ export const Default: Story = {
|
|
|
26
26
|
{ id: "1", label: "Status: Active", onRemove: fn() },
|
|
27
27
|
{ id: "2", label: "Created after Jan 1", onRemove: fn() },
|
|
28
28
|
],
|
|
29
|
+
onClearAll: fn(),
|
|
29
30
|
},
|
|
30
31
|
play: async ({ canvasElement }) => {
|
|
31
32
|
const canvas = within(canvasElement);
|
|
@@ -62,6 +63,7 @@ export const WithClearAll: Story = {
|
|
|
62
63
|
export const WithSortOnly: Story = {
|
|
63
64
|
args: {
|
|
64
65
|
filters: [{ id: "1", label: "Date", onRemove: fn(), variant: "sort", direction: "desc" }],
|
|
66
|
+
onClearAll: fn(),
|
|
65
67
|
},
|
|
66
68
|
play: async ({ canvasElement }) => {
|
|
67
69
|
const canvas = within(canvasElement);
|
|
@@ -33,31 +33,29 @@ export function ActiveFiltersBar({ filters, className, onClearAll }: ActiveFilte
|
|
|
33
33
|
<span className="text-muted-foreground shrink-0 self-stretch flex items-center text-sm border-r px-4">
|
|
34
34
|
Filters
|
|
35
35
|
</span>
|
|
36
|
-
<
|
|
37
|
-
role="list"
|
|
38
|
-
className="flex flex-1 min-w-0 items-center gap-2 overflow-x-auto scroll-fade-x pl-4 py-1.5"
|
|
39
|
-
>
|
|
36
|
+
<ul className="flex flex-1 min-w-0 items-center gap-2 overflow-x-auto scroll-fade-x pl-4 py-1.5">
|
|
40
37
|
{filters.map((filter) => (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/>
|
|
38
|
+
<li key={filter.id} className="shrink-0">
|
|
39
|
+
<FilterChip
|
|
40
|
+
label={filter.label}
|
|
41
|
+
onRemove={filter.onRemove}
|
|
42
|
+
onClick={filter.onClick}
|
|
43
|
+
variant={filter.variant === "sort" ? "sort" : "default"}
|
|
44
|
+
direction={filter.direction}
|
|
45
|
+
/>
|
|
46
|
+
</li>
|
|
51
47
|
))}
|
|
52
|
-
</
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
</ul>
|
|
49
|
+
{onClearAll ? (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={onClearAll}
|
|
53
|
+
className="hover:bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex shrink-0 items-center justify-center self-stretch aspect-square rounded-r-lg border-l border-border transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
54
|
+
aria-label="Clear all"
|
|
55
|
+
>
|
|
56
|
+
<X className="size-4" aria-hidden />
|
|
57
|
+
</button>
|
|
58
|
+
) : null}
|
|
61
59
|
</div>
|
|
62
60
|
);
|
|
63
61
|
}
|
|
@@ -14,6 +14,7 @@ export interface FilterSelectProps {
|
|
|
14
14
|
onChange: (value: string) => void;
|
|
15
15
|
options: FilterSelectOption[];
|
|
16
16
|
label: string;
|
|
17
|
+
loadingText?: string;
|
|
17
18
|
placeholder?: string;
|
|
18
19
|
disabled?: boolean;
|
|
19
20
|
isLoading?: boolean;
|
|
@@ -29,6 +30,7 @@ export function FilterSelect({
|
|
|
29
30
|
value,
|
|
30
31
|
options,
|
|
31
32
|
onChange,
|
|
33
|
+
loadingText = "Loading...",
|
|
32
34
|
placeholder = "All",
|
|
33
35
|
disabled = false,
|
|
34
36
|
isLoading = false,
|
|
@@ -44,7 +46,7 @@ export function FilterSelect({
|
|
|
44
46
|
);
|
|
45
47
|
|
|
46
48
|
return (
|
|
47
|
-
<div className={cn("space-y-2 min-w-36
|
|
49
|
+
<div className={cn("space-y-2 min-w-36", className)}>
|
|
48
50
|
<Label htmlFor={selectId}>{label}</Label>
|
|
49
51
|
<Select value={value} onValueChange={onChange} items={items}>
|
|
50
52
|
<SelectTrigger id={selectId} disabled={disabled} aria-label={label} className="w-full">
|
|
@@ -53,7 +55,7 @@ export function FilterSelect({
|
|
|
53
55
|
<SelectContent className="bg-background max-h-[250px] min-w-36 overflow-y-auto">
|
|
54
56
|
<SelectItem value={ALL_VALUE}>{placeholder}</SelectItem>
|
|
55
57
|
{isLoading ? (
|
|
56
|
-
<div className="text-muted-foreground px-2 py-1.5 text-sm">
|
|
58
|
+
<div className="text-muted-foreground px-2 py-1.5 text-sm">{loadingText}</div>
|
|
57
59
|
) : options.length === 0 ? (
|
|
58
60
|
<div className="text-muted-foreground px-2 py-1.5 text-sm">{emptyLabel}</div>
|
|
59
61
|
) : (
|
|
@@ -4,7 +4,3 @@ export { FilterChip } from "./filter-chip";
|
|
|
4
4
|
export type { FilterChipProps } from "./filter-chip";
|
|
5
5
|
export { FilterSelect } from "./filter-select";
|
|
6
6
|
export type { FilterSelectOption, FilterSelectProps } from "./filter-select";
|
|
7
|
-
export { Filters } from "./filters";
|
|
8
|
-
export type { FiltersProps } from "./filters";
|
|
9
|
-
export { SearchFilterBar } from "./search-filter-bar";
|
|
10
|
-
export type { SearchFilterBarProps } from "./search-filter-bar";
|
|
@@ -42,7 +42,7 @@ export function DateMonthSelect({
|
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
<div className="flex items-center">
|
|
45
|
-
<div className="flex items-center justify-center
|
|
45
|
+
<div className="border-border bg-background text-foreground flex size-10 items-center justify-center rounded-l-xl border">
|
|
46
46
|
<Calendar className="size-4" />
|
|
47
47
|
</div>
|
|
48
48
|
<Select
|
|
@@ -51,7 +51,7 @@ export function DateMonthSelect({
|
|
|
51
51
|
>
|
|
52
52
|
<SelectTrigger
|
|
53
53
|
className={cn(
|
|
54
|
-
"
|
|
54
|
+
"border-border bg-background text-foreground min-h-10 max-h-10 rounded-l-none rounded-r-xl border-l-0",
|
|
55
55
|
className,
|
|
56
56
|
)}
|
|
57
57
|
aria-label={placeholder}
|
|
@@ -41,7 +41,7 @@ export function DatePicker({
|
|
|
41
41
|
) : null}
|
|
42
42
|
<Popover>
|
|
43
43
|
<div className="flex w-full items-center">
|
|
44
|
-
<div className="border-border bg-
|
|
44
|
+
<div className="border-border bg-background text-foreground flex size-10 shrink-0 items-center justify-center rounded-l-xl border">
|
|
45
45
|
<CalendarIcon className="size-4" aria-hidden />
|
|
46
46
|
</div>
|
|
47
47
|
<PopoverTrigger asChild>
|
|
@@ -49,7 +49,7 @@ export function DatePicker({
|
|
|
49
49
|
id={id}
|
|
50
50
|
variant={variant}
|
|
51
51
|
disabled={disabled}
|
|
52
|
-
className="border-border bg-
|
|
52
|
+
className="border-border bg-background text-foreground placeholder:text-muted-foreground min-h-10 max-h-10 w-full justify-start rounded-l-none rounded-r-xl border-l-0 text-left font-normal"
|
|
53
53
|
>
|
|
54
54
|
{value ? format(value, dateFormat) : <span>{placeholder}</span>}
|
|
55
55
|
</Button>
|
|
@@ -1,56 +1,140 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import { expect, fireEvent, userEvent, within } from "storybook/test";
|
|
3
4
|
|
|
4
|
-
import { DropZone } from "./drop-zone";
|
|
5
|
+
import { DropZone, type DropZoneFileInfo, type DropZoneStatus } from "./drop-zone";
|
|
5
6
|
|
|
6
7
|
const meta = {
|
|
7
8
|
title: "Forms/DropZone",
|
|
8
9
|
component: DropZone,
|
|
9
10
|
tags: ["autodocs"],
|
|
11
|
+
args: {
|
|
12
|
+
status: "empty",
|
|
13
|
+
onFiles: () => {},
|
|
14
|
+
},
|
|
10
15
|
} satisfies Meta<typeof DropZone>;
|
|
11
16
|
|
|
12
17
|
export default meta;
|
|
13
18
|
type Story = StoryObj<typeof meta>;
|
|
14
19
|
|
|
15
|
-
function DropZonePreview({
|
|
16
|
-
const [
|
|
17
|
-
const [
|
|
20
|
+
function DropZonePreview({ initialStatus = "empty" }: { initialStatus?: DropZoneStatus }) {
|
|
21
|
+
const [status, setStatus] = useState<DropZoneStatus>(initialStatus);
|
|
22
|
+
const [file, setFile] = useState<DropZoneFileInfo | null>(
|
|
23
|
+
initialStatus === "empty" ? null : { name: "applications.csv", size: 24_576 },
|
|
24
|
+
);
|
|
18
25
|
|
|
19
26
|
return (
|
|
20
|
-
<div className="w-96
|
|
27
|
+
<div className="w-96">
|
|
21
28
|
<DropZone
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
onDragOver={() => setDragActive(true)}
|
|
25
|
-
onDragLeave={() => setDragActive(false)}
|
|
26
|
-
onDrop={setFiles}
|
|
27
|
-
onChange={setFiles}
|
|
29
|
+
status={status}
|
|
30
|
+
file={file}
|
|
28
31
|
accept=".csv"
|
|
29
|
-
|
|
32
|
+
emptyHint="Supported formats: CSV"
|
|
33
|
+
error={status === "failed" ? "The file could not be processed." : undefined}
|
|
34
|
+
busyDescription="This usually takes a moment."
|
|
35
|
+
onFiles={(files) => {
|
|
36
|
+
const selectedFile = files[0];
|
|
37
|
+
if (!selectedFile) return;
|
|
38
|
+
setFile({ name: selectedFile.name, size: selectedFile.size });
|
|
39
|
+
setStatus("armed");
|
|
40
|
+
}}
|
|
41
|
+
onSubmit={() => setStatus("busy")}
|
|
42
|
+
onCancel={() => setStatus("armed")}
|
|
43
|
+
onRetry={() => setStatus("busy")}
|
|
44
|
+
onClear={() => {
|
|
45
|
+
setFile(null);
|
|
46
|
+
setStatus("empty");
|
|
47
|
+
}}
|
|
30
48
|
/>
|
|
31
|
-
{files.length > 0 ? (
|
|
32
|
-
<p className="text-muted-foreground text-sm">
|
|
33
|
-
{files.length} file{files.length > 1 ? "s" : ""} selected
|
|
34
|
-
</p>
|
|
35
|
-
) : null}
|
|
36
49
|
</div>
|
|
37
50
|
);
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
export const Default: Story = {
|
|
54
|
+
render: () => <DropZonePreview />,
|
|
55
|
+
play: async ({ canvasElement }) => {
|
|
56
|
+
const canvas = within(canvasElement);
|
|
57
|
+
const input = canvasElement.querySelector('input[type="file"]') as HTMLInputElement;
|
|
58
|
+
const file = new File(["name,email\nAda,ada@example.com"], "applications.csv", {
|
|
59
|
+
type: "text/csv",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await expect(canvas.getByText("Drop a file here, or")).toBeInTheDocument();
|
|
63
|
+
await fireEvent.change(input, { target: { files: [file] } });
|
|
64
|
+
await expect(canvas.getByText("applications.csv")).toBeInTheDocument();
|
|
65
|
+
await expect(canvas.getByRole("button", { name: /Upload/ })).toBeInTheDocument();
|
|
66
|
+
|
|
67
|
+
await userEvent.click(canvas.getByRole("button", { name: /Upload/ }));
|
|
68
|
+
await expect(canvas.getByRole("progressbar", { name: "Processing…" })).toBeInTheDocument();
|
|
69
|
+
await expect(canvas.getByRole("button", { name: /Cancel/ })).toBeInTheDocument();
|
|
70
|
+
},
|
|
46
71
|
};
|
|
47
72
|
|
|
48
|
-
export const
|
|
49
|
-
|
|
73
|
+
export const Armed: Story = {
|
|
74
|
+
render: () => <DropZonePreview initialStatus="armed" />,
|
|
75
|
+
play: async ({ canvasElement }) => {
|
|
76
|
+
const canvas = within(canvasElement);
|
|
77
|
+
|
|
78
|
+
await expect(canvas.getByText("applications.csv")).toBeInTheDocument();
|
|
79
|
+
await expect(canvas.getByText("24.0 KB")).toBeInTheDocument();
|
|
80
|
+
await userEvent.click(canvas.getByRole("button", { name: /Use a different file/ }));
|
|
81
|
+
await expect(canvas.getByText("Drop a file here, or")).toBeInTheDocument();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Busy: Story = {
|
|
86
|
+
render: () => <DropZonePreview initialStatus="busy" />,
|
|
87
|
+
play: async ({ canvasElement }) => {
|
|
88
|
+
const canvas = within(canvasElement);
|
|
89
|
+
|
|
90
|
+
await expect(canvas.getByRole("progressbar", { name: "Processing…" })).toBeInTheDocument();
|
|
91
|
+
await expect(canvas.getByRole("button", { name: /Cancel/ })).toBeInTheDocument();
|
|
92
|
+
await expect(canvas.queryByRole("button", { name: /Upload/ })).not.toBeInTheDocument();
|
|
93
|
+
await userEvent.click(canvas.getByRole("button", { name: /Cancel/ }));
|
|
94
|
+
await expect(canvas.getByRole("button", { name: /Upload/ })).toBeInTheDocument();
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const Dragging: Story = {
|
|
50
99
|
render: () => <DropZonePreview />,
|
|
100
|
+
play: async ({ canvasElement }) => {
|
|
101
|
+
const canvas = within(canvasElement);
|
|
102
|
+
const zone = canvas.getByRole("button", { name: "File upload drop zone" });
|
|
103
|
+
const file = new File(["data"], "applications.csv", { type: "text/csv" });
|
|
104
|
+
|
|
105
|
+
await fireEvent.dragEnter(zone);
|
|
106
|
+
await expect(canvas.getByText("Release to attach")).toBeInTheDocument();
|
|
107
|
+
await fireEvent.dragLeave(zone);
|
|
108
|
+
await expect(canvas.getByText("Drop a file here, or")).toBeInTheDocument();
|
|
109
|
+
const dataTransfer = new DataTransfer();
|
|
110
|
+
dataTransfer.items.add(file);
|
|
111
|
+
Object.defineProperty(dataTransfer, "files", { value: [file] });
|
|
112
|
+
await fireEvent.drop(zone, { dataTransfer });
|
|
113
|
+
await expect(canvas.getByText("applications.csv")).toBeInTheDocument();
|
|
114
|
+
},
|
|
51
115
|
};
|
|
52
116
|
|
|
53
|
-
export const
|
|
54
|
-
|
|
55
|
-
|
|
117
|
+
export const Failed: Story = {
|
|
118
|
+
render: () => <DropZonePreview initialStatus="failed" />,
|
|
119
|
+
play: async ({ canvasElement }) => {
|
|
120
|
+
const canvas = within(canvasElement);
|
|
121
|
+
|
|
122
|
+
await expect(canvas.getByText("The file could not be processed.")).toBeInTheDocument();
|
|
123
|
+
await expect(canvas.getByRole("button", { name: /Try again/ })).toBeInTheDocument();
|
|
124
|
+
await userEvent.click(canvas.getByRole("button", { name: /Try again/ }));
|
|
125
|
+
await expect(canvas.getByRole("progressbar", { name: "Processing…" })).toBeInTheDocument();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const Disabled: Story = {
|
|
130
|
+
args: { status: "empty", disabled: true },
|
|
131
|
+
play: async ({ canvasElement }) => {
|
|
132
|
+
const canvas = within(canvasElement);
|
|
133
|
+
const zone = canvas.getByRole("button", { name: "File upload drop zone" });
|
|
134
|
+
const input = canvasElement.querySelector('input[type="file"]') as HTMLInputElement;
|
|
135
|
+
|
|
136
|
+
await expect(zone).toHaveAttribute("aria-disabled", "true");
|
|
137
|
+
await expect(zone).toHaveAttribute("tabindex", "-1");
|
|
138
|
+
await expect(input).toBeDisabled();
|
|
139
|
+
},
|
|
56
140
|
};
|