@ponti-studios/ui 2.0.1 → 3.0.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/README.md +36 -3
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +3 -0
- package/dist/components/data-display/accordion.d.ts +10 -0
- package/dist/components/data-display/accordion.js +53 -0
- package/{src/components/data-display/index.ts → dist/components/data-display/index.d.ts} +1 -10
- package/dist/components/data-display/index.js +8 -0
- package/dist/components/data-display/metric-card.d.ts +9 -0
- package/dist/components/data-display/metric-card.js +25 -0
- package/dist/components/data-display/sort-controls.d.ts +13 -0
- package/dist/components/data-display/sort-controls.js +57 -0
- package/dist/components/data-display/sort-row.d.ts +10 -0
- package/dist/components/data-display/sort-row.js +52 -0
- package/dist/components/data-display/table.d.ts +10 -0
- package/dist/components/data-display/table.js +57 -0
- package/dist/components/feedback/alert.d.ts +9 -0
- package/dist/components/feedback/alert.js +38 -0
- package/dist/components/feedback/empty-state.d.ts +18 -0
- package/dist/components/feedback/empty-state.js +36 -0
- package/{src/components/feedback/index.ts → dist/components/feedback/index.d.ts} +2 -0
- package/dist/components/feedback/index.js +7 -0
- package/dist/components/feedback/progress.d.ts +6 -0
- package/dist/components/feedback/progress.js +25 -0
- package/dist/components/feedback/skeleton.d.ts +2 -0
- package/dist/components/feedback/skeleton.js +11 -0
- package/dist/components/feedback/spinner.d.ts +14 -0
- package/dist/components/feedback/spinner.js +27 -0
- package/dist/components/feedback/update-guard.d.ts +13 -0
- package/dist/components/feedback/update-guard.js +38 -0
- package/dist/components/filters/active-filters-bar.d.ts +14 -0
- package/dist/components/filters/active-filters-bar.js +44 -0
- package/dist/components/filters/filter-chip.d.ts +11 -0
- package/dist/components/filters/filter-chip.js +33 -0
- package/dist/components/filters/filter-select.d.ts +17 -0
- package/dist/components/filters/filter-select.js +46 -0
- package/dist/components/filters/index.js +4 -0
- package/dist/components/forms/checkbox.d.ts +4 -0
- package/dist/components/forms/checkbox.js +24 -0
- package/dist/components/forms/date-month-select.d.ts +11 -0
- package/dist/components/forms/date-month-select.js +47 -0
- package/dist/components/forms/date-picker.d.ts +60 -0
- package/dist/components/forms/date-picker.js +193 -0
- package/dist/components/forms/date-utils.d.ts +14 -0
- package/dist/components/forms/date-utils.js +18 -0
- package/dist/components/forms/drop-zone.d.ts +42 -0
- package/dist/components/forms/drop-zone.js +184 -0
- package/{src/components/forms/index.ts → dist/components/forms/index.d.ts} +3 -9
- package/dist/components/forms/index.js +17 -0
- package/dist/components/forms/input.d.ts +3 -0
- package/dist/components/forms/input.js +15 -0
- package/dist/components/forms/label.d.ts +3 -0
- package/dist/components/forms/label.js +14 -0
- package/dist/components/forms/passkey-management.d.ts +14 -0
- package/dist/components/forms/passkey-management.js +132 -0
- package/dist/components/forms/radio-group.d.ts +6 -0
- package/dist/components/forms/radio-group.js +28 -0
- package/dist/components/forms/select.d.ts +18 -0
- package/dist/components/forms/select.js +103 -0
- package/dist/components/forms/slider.d.ts +9 -0
- package/dist/components/forms/slider.js +25 -0
- package/dist/components/forms/stepper.d.ts +15 -0
- package/dist/components/forms/stepper.js +41 -0
- package/dist/components/forms/switch.d.ts +6 -0
- package/dist/components/forms/switch.js +21 -0
- package/dist/components/forms/text-field.d.ts +15 -0
- package/dist/components/forms/text-field.js +46 -0
- package/dist/components/forms/text-field.types.d.ts +8 -0
- package/dist/components/forms/textarea.d.ts +3 -0
- package/dist/components/forms/textarea.js +14 -0
- package/dist/components/layout/index.js +5 -0
- package/dist/components/layout/particle-background.d.ts +19 -0
- package/dist/components/layout/particle-background.js +171 -0
- package/dist/components/layout/scroll-area.d.ts +9 -0
- package/dist/components/layout/scroll-area.js +35 -0
- package/dist/components/layout/section-intro.d.ts +12 -0
- package/dist/components/layout/section-intro.js +28 -0
- package/dist/components/layout/surface-panel.d.ts +7 -0
- package/dist/components/layout/surface-panel.js +11 -0
- package/dist/components/navigation/app-navigation.d.ts +67 -0
- package/dist/components/navigation/app-navigation.js +106 -0
- package/{src/components/navigation/index.ts → dist/components/navigation/index.d.ts} +1 -7
- package/dist/components/navigation/index.js +4 -0
- package/dist/components/navigation/pagination-controls.d.ts +7 -0
- package/dist/components/navigation/pagination-controls.js +50 -0
- package/dist/components/navigation/tabs.d.ts +7 -0
- package/dist/components/navigation/tabs.js +35 -0
- package/dist/components/overlays/bottom-sheet.d.ts +20 -0
- package/dist/components/overlays/bottom-sheet.js +101 -0
- package/dist/components/overlays/command.d.ts +29 -0
- package/dist/components/overlays/command.js +123 -0
- package/dist/components/overlays/dialog.d.ts +30 -0
- package/dist/components/overlays/dialog.js +105 -0
- package/dist/components/overlays/dropdown-menu.d.ts +29 -0
- package/dist/components/overlays/dropdown-menu.js +128 -0
- package/dist/components/overlays/index.d.ts +7 -0
- package/dist/components/overlays/index.js +8 -0
- package/dist/components/overlays/popover.d.ts +14 -0
- package/dist/components/overlays/popover.js +56 -0
- package/dist/components/overlays/sheet.d.ts +27 -0
- package/dist/components/overlays/sheet.js +81 -0
- package/dist/components/overlays/tooltip.d.ts +10 -0
- package/dist/components/overlays/tooltip.js +23 -0
- package/dist/components/primitives/avatar.d.ts +14 -0
- package/dist/components/primitives/avatar.js +72 -0
- package/dist/components/primitives/badge.d.ts +7 -0
- package/dist/components/primitives/badge.js +27 -0
- package/dist/components/primitives/button.d.ts +13 -0
- package/dist/components/primitives/button.js +113 -0
- package/{src/components/primitives/button.types.ts → dist/components/primitives/button.types.d.ts} +4 -6
- package/dist/components/primitives/card.d.ts +9 -0
- package/dist/components/primitives/card.js +56 -0
- package/{src/components/primitives/index.ts → dist/components/primitives/index.d.ts} +1 -9
- package/dist/components/primitives/index.js +7 -0
- package/dist/components/primitives/segmented-control.d.ts +20 -0
- package/dist/components/primitives/segmented-control.js +27 -0
- package/dist/components/primitives/separator.d.ts +4 -0
- package/dist/components/primitives/separator.js +16 -0
- package/dist/constants/chart-colors.d.ts +33 -0
- package/dist/constants/chart-colors.js +31 -0
- package/dist/hooks/index.js +9 -0
- package/{src/hooks/sort.types.ts → dist/hooks/sort.types.d.ts} +2 -4
- package/dist/hooks/use-api-client.d.ts +30 -0
- package/dist/hooks/use-api-client.js +106 -0
- package/dist/hooks/use-debounced-value.d.ts +1 -0
- package/dist/hooks/use-debounced-value.js +12 -0
- package/dist/hooks/use-derived-filter-state.d.ts +20 -0
- package/dist/hooks/use-derived-filter-state.js +39 -0
- package/dist/hooks/use-filter-state.d.ts +12 -0
- package/dist/hooks/use-filter-state.js +37 -0
- package/dist/hooks/use-media-query.d.ts +6 -0
- package/dist/hooks/use-media-query.js +27 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +14 -0
- package/dist/hooks/use-sort.d.ts +11 -0
- package/dist/hooks/use-sort.js +14 -0
- package/{src/index.ts → dist/index.d.ts} +4 -32
- package/dist/index.js +15 -0
- package/dist/integrations/service-worker.d.ts +2 -0
- package/dist/integrations/service-worker.js +2 -0
- package/dist/lib/auth.d.ts +2 -0
- package/dist/lib/auth.js +7 -0
- package/dist/lib/clipboard.d.ts +1 -0
- package/dist/lib/clipboard.js +10 -0
- package/dist/lib/index.js +4 -0
- package/{src/lib/slot.tsx → dist/lib/slot.d.ts} +2 -15
- package/dist/lib/slot.js +14 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +8 -0
- package/dist/native/components.d.ts +41 -0
- package/dist/native/components.js +135 -0
- package/dist/native/index.js +3 -0
- package/dist/native/theme.d.ts +19 -0
- package/dist/native/theme.js +58 -0
- package/dist/styles/tokens/css-vars.d.ts +7 -0
- package/dist/styles/tokens/css-vars.js +5 -0
- package/dist/styles/tokens/generated/dark.colors.generated.d.ts +40 -0
- package/dist/styles/tokens/generated/dark.colors.generated.js +39 -0
- package/dist/styles/tokens/generated/foundations.generated.d.ts +323 -0
- package/dist/styles/tokens/generated/foundations.generated.js +453 -0
- package/dist/styles/tokens/generated/light.colors.generated.d.ts +40 -0
- package/dist/styles/tokens/generated/light.colors.generated.js +39 -0
- package/dist/styles/tokens/index.d.ts +317 -0
- package/dist/styles/tokens/index.js +14 -0
- package/dist/styles.css +1 -1
- package/dist/translations/en.d.ts +34 -0
- package/dist/translations/index.d.ts +39 -0
- package/dist/translations/ui-provider.d.ts +9 -0
- package/dist/translations/ui-provider.js +16 -0
- package/dist/types/upload.d.ts +36 -0
- package/package.json +50 -33
- package/src/components/data-display/accordion.stories.tsx +0 -57
- package/src/components/data-display/accordion.tsx +0 -86
- package/src/components/data-display/metric-card.stories.tsx +0 -48
- package/src/components/data-display/metric-card.tsx +0 -20
- package/src/components/data-display/sort-controls.stories.tsx +0 -45
- package/src/components/data-display/sort-controls.tsx +0 -96
- package/src/components/data-display/sort-row.tsx +0 -88
- package/src/components/data-display/table.stories.tsx +0 -75
- package/src/components/data-display/table.tsx +0 -80
- package/src/components/feedback/alert.stories.tsx +0 -51
- package/src/components/feedback/alert.tsx +0 -64
- package/src/components/feedback/empty-state.tsx +0 -76
- package/src/components/feedback/progress.stories.tsx +0 -54
- package/src/components/feedback/progress.tsx +0 -41
- package/src/components/feedback/skeleton.stories.tsx +0 -36
- package/src/components/feedback/skeleton.tsx +0 -7
- package/src/components/feedback/spinner.stories.tsx +0 -52
- package/src/components/feedback/spinner.tsx +0 -47
- package/src/components/feedback/update-guard.stories.tsx +0 -28
- package/src/components/feedback/update-guard.tsx +0 -57
- package/src/components/filters/active-filters-bar.stories.tsx +0 -114
- package/src/components/filters/active-filters-bar.tsx +0 -61
- package/src/components/filters/filter-chip.tsx +0 -57
- package/src/components/filters/filter-select.stories.tsx +0 -71
- package/src/components/filters/filter-select.tsx +0 -68
- package/src/components/forms/checkbox.stories.tsx +0 -35
- package/src/components/forms/checkbox.tsx +0 -38
- package/src/components/forms/date-month-select.stories.tsx +0 -26
- package/src/components/forms/date-month-select.tsx +0 -79
- package/src/components/forms/date-picker.stories.tsx +0 -188
- package/src/components/forms/date-picker.tsx +0 -303
- package/src/components/forms/date-utils.ts +0 -37
- package/src/components/forms/drop-zone.stories.tsx +0 -140
- package/src/components/forms/drop-zone.tsx +0 -298
- package/src/components/forms/input.stories.tsx +0 -47
- package/src/components/forms/input.tsx +0 -24
- package/src/components/forms/label.stories.tsx +0 -16
- package/src/components/forms/label.tsx +0 -25
- package/src/components/forms/passkey-management.stories.tsx +0 -85
- package/src/components/forms/passkey-management.tsx +0 -168
- package/src/components/forms/radio-group.stories.tsx +0 -46
- package/src/components/forms/radio-group.tsx +0 -44
- package/src/components/forms/select.stories.tsx +0 -207
- package/src/components/forms/select.tsx +0 -182
- package/src/components/forms/slider.stories.tsx +0 -70
- package/src/components/forms/slider.tsx +0 -50
- package/src/components/forms/stepper.stories.tsx +0 -111
- package/src/components/forms/stepper.tsx +0 -66
- package/src/components/forms/switch.stories.tsx +0 -144
- package/src/components/forms/switch.tsx +0 -39
- package/src/components/forms/text-field.stories.tsx +0 -87
- package/src/components/forms/text-field.tsx +0 -75
- package/src/components/forms/text-field.types.ts +0 -9
- package/src/components/forms/textarea.stories.tsx +0 -150
- package/src/components/forms/textarea.tsx +0 -22
- package/src/components/layout/particle-background.stories.tsx +0 -38
- package/src/components/layout/particle-background.tsx +0 -378
- package/src/components/layout/scroll-area.stories.tsx +0 -91
- package/src/components/layout/scroll-area.tsx +0 -79
- package/src/components/layout/section-intro.stories.tsx +0 -29
- package/src/components/layout/section-intro.tsx +0 -31
- package/src/components/layout/surface-panel.stories.tsx +0 -32
- package/src/components/layout/surface-panel.tsx +0 -23
- package/src/components/navigation/app-navigation.stories.tsx +0 -217
- package/src/components/navigation/app-navigation.tsx +0 -234
- package/src/components/navigation/pagination-controls.stories.tsx +0 -41
- package/src/components/navigation/pagination-controls.tsx +0 -46
- package/src/components/navigation/tabs.stories.tsx +0 -48
- package/src/components/navigation/tabs.tsx +0 -52
- package/src/components/overlays/bottom-sheet.stories.tsx +0 -52
- package/src/components/overlays/bottom-sheet.tsx +0 -179
- package/src/components/overlays/command.stories.tsx +0 -65
- package/src/components/overlays/command.tsx +0 -227
- package/src/components/overlays/dialog.stories.tsx +0 -148
- package/src/components/overlays/dialog.tsx +0 -203
- package/src/components/overlays/dropdown-menu.stories.tsx +0 -74
- package/src/components/overlays/dropdown-menu.tsx +0 -236
- package/src/components/overlays/index.ts +0 -56
- package/src/components/overlays/popover.stories.tsx +0 -51
- package/src/components/overlays/popover.tsx +0 -78
- package/src/components/overlays/sheet.stories.tsx +0 -68
- package/src/components/overlays/sheet.tsx +0 -155
- package/src/components/overlays/tooltip.stories.tsx +0 -29
- package/src/components/overlays/tooltip.tsx +0 -41
- package/src/components/primitives/avatar-placeholder.svg +0 -13
- package/src/components/primitives/avatar.stories.tsx +0 -72
- package/src/components/primitives/avatar.tsx +0 -136
- package/src/components/primitives/badge.stories.tsx +0 -43
- package/src/components/primitives/badge.tsx +0 -43
- package/src/components/primitives/button.stories.tsx +0 -113
- package/src/components/primitives/button.tsx +0 -131
- package/src/components/primitives/card.stories.tsx +0 -59
- package/src/components/primitives/card.tsx +0 -66
- package/src/components/primitives/segmented-control.stories.tsx +0 -54
- package/src/components/primitives/segmented-control.tsx +0 -73
- package/src/components/primitives/separator.stories.tsx +0 -34
- package/src/components/primitives/separator.tsx +0 -27
- package/src/constants/chart-colors.ts +0 -39
- package/src/env.d.ts +0 -9
- package/src/hooks/use-api-client.ts +0 -122
- package/src/hooks/use-debounced-value.ts +0 -12
- package/src/hooks/use-derived-filter-state.ts +0 -70
- package/src/hooks/use-filter-state.ts +0 -66
- package/src/hooks/use-media-query.ts +0 -69
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/hooks/use-sort.ts +0 -20
- package/src/lib/auth.ts +0 -5
- package/src/lib/clipboard.ts +0 -8
- package/src/lib/utils.ts +0 -6
- package/src/native/components.tsx +0 -198
- package/src/native/theme.tsx +0 -69
- package/src/storybook/controls.ts +0 -117
- package/src/storybook/options.ts +0 -52
- package/src/styles/build.ts +0 -1
- package/src/styles/index.css +0 -423
- package/src/styles/nokia.css +0 -36
- package/src/styles/package.css +0 -3
- package/src/styles/tokens/chart-colors.stories.tsx +0 -189
- package/src/styles/tokens/color-systems.stories.tsx +0 -190
- package/src/styles/tokens/css-vars.ts +0 -15
- package/src/styles/tokens/generated/dark-class.css +0 -39
- package/src/styles/tokens/generated/dark.colors.generated.ts +0 -39
- package/src/styles/tokens/generated/dark.css +0 -39
- package/src/styles/tokens/generated/foundations.css +0 -149
- package/src/styles/tokens/generated/foundations.generated.ts +0 -454
- package/src/styles/tokens/generated/light.colors.generated.ts +0 -39
- package/src/styles/tokens/generated/light.css +0 -39
- package/src/styles/tokens/index.ts +0 -70
- package/src/styles/tokens/layout.stories.tsx +0 -50
- package/src/styles/tokens/scripts/check-foundations.mjs +0 -126
- package/src/styles/tokens/scripts/check-source.mjs +0 -53
- package/src/styles/tokens/source/dark.tokens.json +0 -138
- package/src/styles/tokens/source/foundations.tokens.json +0 -344
- package/src/styles/tokens/source/light.tokens.json +0 -138
- package/src/styles/tokens/style-dictionary.dark-class.json +0 -16
- package/src/styles/tokens/style-dictionary.dark-storybook.json +0 -16
- package/src/styles/tokens/style-dictionary.dark.json +0 -21
- package/src/styles/tokens/style-dictionary.foundations.json +0 -21
- package/src/styles/tokens/style-dictionary.light-storybook.json +0 -16
- package/src/styles/tokens/style-dictionary.light.json +0 -21
- package/src/styles/tokens/typography.stories.tsx +0 -157
- package/src/translations/en.ts +0 -34
- package/src/translations/index.ts +0 -32
- package/src/types/upload.ts +0 -42
- /package/{src/components/filters/index.ts → dist/components/filters/index.d.ts} +0 -0
- /package/{src/components/layout/index.ts → dist/components/layout/index.d.ts} +0 -0
- /package/{src/hooks/index.ts → dist/hooks/index.d.ts} +0 -0
- /package/{src/lib/index.ts → dist/lib/index.d.ts} +0 -0
- /package/{src/native/index.ts → dist/native/index.d.ts} +0 -0
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { expect, userEvent } from "storybook/test";
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
|
|
5
|
-
import { DatePicker } from "./date-picker";
|
|
6
|
-
|
|
7
|
-
const meta = {
|
|
8
|
-
title: "Forms/DatePicker",
|
|
9
|
-
component: DatePicker,
|
|
10
|
-
tags: ["autodocs"],
|
|
11
|
-
} satisfies Meta<typeof DatePicker>;
|
|
12
|
-
|
|
13
|
-
export default meta;
|
|
14
|
-
type Story = StoryObj<typeof meta>;
|
|
15
|
-
|
|
16
|
-
function dateValue(canvasElement: HTMLElement, name: string): string {
|
|
17
|
-
const input = canvasElement.querySelector<HTMLInputElement>(`input[name="${name}"]`);
|
|
18
|
-
return input?.value ?? "";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// The native date input owns the picker UI, so stories assert on the input's
|
|
22
|
-
// "YYYY-MM-DD" value — the same string a form would submit.
|
|
23
|
-
export const Default: Story = {
|
|
24
|
-
args: {
|
|
25
|
-
name: "startDate",
|
|
26
|
-
defaultValue: "2020-01-15",
|
|
27
|
-
label: "Start date",
|
|
28
|
-
},
|
|
29
|
-
play: async ({ canvasElement }) => {
|
|
30
|
-
const input = canvasElement.querySelector<HTMLInputElement>('input[name="startDate"]');
|
|
31
|
-
await expect(input?.value).toBe("2020-01-15");
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const WithDateObjectDefault: Story = {
|
|
36
|
-
args: {
|
|
37
|
-
name: "endDate",
|
|
38
|
-
defaultValue: new Date(2020, 0, 15),
|
|
39
|
-
label: "End date",
|
|
40
|
-
},
|
|
41
|
-
play: async ({ canvasElement }) => {
|
|
42
|
-
const input = canvasElement.querySelector<HTMLInputElement>('input[name="endDate"]');
|
|
43
|
-
await expect(input?.value).toBe("2020-01-15");
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const Controlled: Story = {
|
|
48
|
-
render: () => {
|
|
49
|
-
const [value, setValue] = useState<string | null>("2026-08-20");
|
|
50
|
-
return (
|
|
51
|
-
<div className="flex flex-col gap-2">
|
|
52
|
-
<DatePicker
|
|
53
|
-
mode="single"
|
|
54
|
-
label="Due date"
|
|
55
|
-
value={value}
|
|
56
|
-
onValueChange={setValue}
|
|
57
|
-
helpText="Controlled — pick a new date"
|
|
58
|
-
/>
|
|
59
|
-
<p className="text-sm">value: {value ?? "(none)"}</p>
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
},
|
|
63
|
-
play: async ({ canvasElement }) => {
|
|
64
|
-
await expect(canvasElement.textContent).toContain("2026-08-20");
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const Disabled: Story = {
|
|
69
|
-
args: {
|
|
70
|
-
name: "startDate",
|
|
71
|
-
defaultValue: "2020-01-15",
|
|
72
|
-
label: "Locked date",
|
|
73
|
-
disabled: true,
|
|
74
|
-
},
|
|
75
|
-
play: async ({ canvasElement }) => {
|
|
76
|
-
const input = canvasElement.querySelector<HTMLInputElement>('input[name="startDate"]');
|
|
77
|
-
await expect(input?.disabled).toBe(true);
|
|
78
|
-
await expect(input?.value).toBe("2020-01-15");
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
export const WithHelpText: Story = {
|
|
83
|
-
args: {
|
|
84
|
-
name: "startDate",
|
|
85
|
-
label: "Start date",
|
|
86
|
-
helpText: "Format is YYYY-MM-DD.",
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export const WithError: Story = {
|
|
91
|
-
args: {
|
|
92
|
-
name: "startDate",
|
|
93
|
-
label: "Start date",
|
|
94
|
-
error: "A start date is required.",
|
|
95
|
-
},
|
|
96
|
-
play: async ({ canvasElement }) => {
|
|
97
|
-
const input = canvasElement.querySelector<HTMLInputElement>('input[name="startDate"]');
|
|
98
|
-
await expect(input?.getAttribute("aria-invalid")).toBe("true");
|
|
99
|
-
await expect(canvasElement.textContent).toContain("A start date is required.");
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export const Range: Story = {
|
|
104
|
-
args: {
|
|
105
|
-
mode: "range",
|
|
106
|
-
startName: "startDate",
|
|
107
|
-
endName: "endDate",
|
|
108
|
-
defaultValue: { from: "2020-01-15", to: "2020-06-30" },
|
|
109
|
-
label: "Active period",
|
|
110
|
-
},
|
|
111
|
-
play: async ({ canvasElement }) => {
|
|
112
|
-
await expect(dateValue(canvasElement, "startDate")).toBe("2020-01-15");
|
|
113
|
-
await expect(dateValue(canvasElement, "endDate")).toBe("2020-06-30");
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
export const RangeEmpty: Story = {
|
|
118
|
-
args: {
|
|
119
|
-
mode: "range",
|
|
120
|
-
startName: "startDate",
|
|
121
|
-
endName: "endDate",
|
|
122
|
-
label: "Active period",
|
|
123
|
-
},
|
|
124
|
-
play: async ({ canvasElement }) => {
|
|
125
|
-
await expect(dateValue(canvasElement, "startDate")).toBe("");
|
|
126
|
-
await expect(dateValue(canvasElement, "endDate")).toBe("");
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
export const RangeControlled: Story = {
|
|
131
|
-
render: () => {
|
|
132
|
-
const [range, setRange] = useState<{ from: string | null; to: string | null }>({
|
|
133
|
-
from: "2026-08-01",
|
|
134
|
-
to: "2026-08-15",
|
|
135
|
-
});
|
|
136
|
-
return (
|
|
137
|
-
<div className="flex flex-col gap-2">
|
|
138
|
-
<DatePicker
|
|
139
|
-
mode="range"
|
|
140
|
-
label="Stay dates"
|
|
141
|
-
startName="stayStart"
|
|
142
|
-
endName="stayEnd"
|
|
143
|
-
value={range}
|
|
144
|
-
onValueChange={setRange}
|
|
145
|
-
fromLabel="Check-in"
|
|
146
|
-
toLabel="Check-out"
|
|
147
|
-
/>
|
|
148
|
-
<p className="text-sm">
|
|
149
|
-
from: {range.from ?? "(none)"} · to: {range.to ?? "(none)"}
|
|
150
|
-
</p>
|
|
151
|
-
</div>
|
|
152
|
-
);
|
|
153
|
-
},
|
|
154
|
-
play: async ({ canvasElement }) => {
|
|
155
|
-
await expect(canvasElement.textContent).toContain("from: 2026-08-01");
|
|
156
|
-
await expect(canvasElement.textContent).toContain("to: 2026-08-15");
|
|
157
|
-
|
|
158
|
-
const end = canvasElement.querySelector<HTMLInputElement>('input[name="stayEnd"]');
|
|
159
|
-
await userEvent.clear(end!);
|
|
160
|
-
await userEvent.type(end!, "0002-08-01");
|
|
161
|
-
await expect(end?.value).toBe("0002-08-01");
|
|
162
|
-
await expect(canvasElement.textContent).toContain("to: 0002-08-01");
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
// Two independent fields cross-wired via min/max: the end field can't go
|
|
167
|
-
// before the start, and the start field can't go after the end.
|
|
168
|
-
export const RangeMinMax: Story = {
|
|
169
|
-
args: {
|
|
170
|
-
mode: "range",
|
|
171
|
-
startName: "startDate",
|
|
172
|
-
endName: "endDate",
|
|
173
|
-
min: "2026-01-01",
|
|
174
|
-
max: "2026-12-31",
|
|
175
|
-
label: "Active period",
|
|
176
|
-
},
|
|
177
|
-
play: async ({ canvasElement }) => {
|
|
178
|
-
const start = canvasElement.querySelector<HTMLInputElement>('input[name="startDate"]');
|
|
179
|
-
const end = canvasElement.querySelector<HTMLInputElement>('input[name="endDate"]');
|
|
180
|
-
await expect(start?.min).toBe("2026-01-01");
|
|
181
|
-
await expect(end?.max).toBe("2026-12-31");
|
|
182
|
-
|
|
183
|
-
await userEvent.type(start!, "2026-03-15");
|
|
184
|
-
await expect(start?.value).toBe("2026-03-15");
|
|
185
|
-
// End field's floor is now the chosen start date.
|
|
186
|
-
await expect(end?.min).toBe("2026-03-15");
|
|
187
|
-
},
|
|
188
|
-
};
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { CalendarIcon } from "lucide-react";
|
|
2
|
-
import { useId, useRef, useState } from "react";
|
|
3
|
-
import type { ChangeEvent, RefObject } from "react";
|
|
4
|
-
|
|
5
|
-
import { cn } from "../../lib/utils";
|
|
6
|
-
import { formatDateInput } from "./date-utils";
|
|
7
|
-
|
|
8
|
-
export interface DateRangeValue {
|
|
9
|
-
from?: Date | string | null;
|
|
10
|
-
to?: Date | string | null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface DatePickerBaseProps {
|
|
14
|
-
label?: string;
|
|
15
|
-
helpText?: string;
|
|
16
|
-
error?: string;
|
|
17
|
-
id?: string;
|
|
18
|
-
disabled?: boolean;
|
|
19
|
-
required?: boolean;
|
|
20
|
-
className?: string;
|
|
21
|
-
/** Earliest selectable date, "YYYY-MM-DD". */
|
|
22
|
-
min?: string;
|
|
23
|
-
/** Latest selectable date, "YYYY-MM-DD". */
|
|
24
|
-
max?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SingleDatePickerProps extends DatePickerBaseProps {
|
|
28
|
-
mode?: "single";
|
|
29
|
-
/** Form field name — the native input submits "YYYY-MM-DD". */
|
|
30
|
-
name?: string;
|
|
31
|
-
value?: Date | string | null;
|
|
32
|
-
defaultValue?: Date | string | null;
|
|
33
|
-
onValueChange?: (value: string | null) => void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface RangeDatePickerProps extends DatePickerBaseProps {
|
|
37
|
-
mode: "range";
|
|
38
|
-
/** Form field name for the range start — submits "YYYY-MM-DD". */
|
|
39
|
-
startName?: string;
|
|
40
|
-
/** Form field name for the range end — submits "YYYY-MM-DD". */
|
|
41
|
-
endName?: string;
|
|
42
|
-
value?: DateRangeValue;
|
|
43
|
-
defaultValue?: DateRangeValue;
|
|
44
|
-
onValueChange?: (value: { from: string | null; to: string | null }) => void;
|
|
45
|
-
fromLabel?: string;
|
|
46
|
-
toLabel?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type DatePickerProps = SingleDatePickerProps | RangeDatePickerProps;
|
|
50
|
-
|
|
51
|
-
const LABEL_CLASS = "text-foreground text-sm font-medium";
|
|
52
|
-
const REQUIRED_CLASS = "after:text-destructive-text after:ml-0.5 after:content-['*']";
|
|
53
|
-
const SUBLABEL_CLASS = "text-muted-foreground text-xs font-medium";
|
|
54
|
-
const FIELD_CLASS =
|
|
55
|
-
"bg-background text-foreground border-input h-10 w-full min-w-0 appearance-none border px-3 py-1 text-sm transition-colors " +
|
|
56
|
-
"[&::-webkit-calendar-picker-indicator]:appearance-none [&::-webkit-calendar-picker-indicator]:opacity-0 " +
|
|
57
|
-
"placeholder:text-muted-foreground " +
|
|
58
|
-
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none " +
|
|
59
|
-
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " +
|
|
60
|
-
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive";
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* DatePickerIcon — the brand calendar glyph. The browser's own picker
|
|
64
|
-
* indicator is hidden, so this is the visible affordance: clicking it calls
|
|
65
|
-
* the input's `showPicker()` (falling back to focus, which opens the native
|
|
66
|
-
* picker on iOS). It is not a tab stop because the input itself is the
|
|
67
|
-
* focusable control and opens the picker on focus.
|
|
68
|
-
*/
|
|
69
|
-
function DatePickerIcon({
|
|
70
|
-
inputRef,
|
|
71
|
-
disabled,
|
|
72
|
-
ariaLabel,
|
|
73
|
-
}: {
|
|
74
|
-
inputRef: RefObject<HTMLInputElement | null>;
|
|
75
|
-
disabled?: boolean;
|
|
76
|
-
ariaLabel: string;
|
|
77
|
-
}) {
|
|
78
|
-
return (
|
|
79
|
-
<button
|
|
80
|
-
type="button"
|
|
81
|
-
tabIndex={-1}
|
|
82
|
-
aria-label={ariaLabel}
|
|
83
|
-
disabled={disabled}
|
|
84
|
-
onClick={() => {
|
|
85
|
-
const input = inputRef.current;
|
|
86
|
-
if (!input) return;
|
|
87
|
-
try {
|
|
88
|
-
input.showPicker?.();
|
|
89
|
-
} catch {
|
|
90
|
-
input.focus();
|
|
91
|
-
}
|
|
92
|
-
}}
|
|
93
|
-
className="border-border bg-background text-foreground flex size-10 shrink-0 items-center justify-center rounded-l-xl border"
|
|
94
|
-
>
|
|
95
|
-
<CalendarIcon className="size-4" aria-hidden />
|
|
96
|
-
</button>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function SingleDatePicker({
|
|
101
|
-
id,
|
|
102
|
-
name,
|
|
103
|
-
label,
|
|
104
|
-
helpText,
|
|
105
|
-
error,
|
|
106
|
-
className,
|
|
107
|
-
disabled,
|
|
108
|
-
required,
|
|
109
|
-
min,
|
|
110
|
-
max,
|
|
111
|
-
value: controlledValue,
|
|
112
|
-
defaultValue,
|
|
113
|
-
onValueChange,
|
|
114
|
-
}: SingleDatePickerProps) {
|
|
115
|
-
const generatedId = useId();
|
|
116
|
-
const inputId = id ?? generatedId;
|
|
117
|
-
const descriptionId = `${inputId}-description`;
|
|
118
|
-
const errorId = `${inputId}-error`;
|
|
119
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
120
|
-
const isControlled = controlledValue !== undefined;
|
|
121
|
-
const [internalValue, setInternalValue] = useState<string>(() =>
|
|
122
|
-
formatDateInput(isControlled ? controlledValue : defaultValue),
|
|
123
|
-
);
|
|
124
|
-
const inputValue = (isControlled ? formatDateInput(controlledValue) : internalValue) || "";
|
|
125
|
-
|
|
126
|
-
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
|
127
|
-
const next = event.target.value || null;
|
|
128
|
-
if (!isControlled) setInternalValue(next ?? "");
|
|
129
|
-
onValueChange?.(next);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<div className={cn("flex flex-col gap-1.5", className)}>
|
|
134
|
-
{label ? (
|
|
135
|
-
<label htmlFor={inputId} className={cn(LABEL_CLASS, required && REQUIRED_CLASS)}>
|
|
136
|
-
{label}
|
|
137
|
-
</label>
|
|
138
|
-
) : null}
|
|
139
|
-
<div className="flex w-full items-center">
|
|
140
|
-
<DatePickerIcon inputRef={inputRef} disabled={disabled} ariaLabel="Open date picker" />
|
|
141
|
-
<input
|
|
142
|
-
ref={inputRef}
|
|
143
|
-
id={inputId}
|
|
144
|
-
name={name}
|
|
145
|
-
type="date"
|
|
146
|
-
value={isControlled ? inputValue : undefined}
|
|
147
|
-
defaultValue={isControlled ? undefined : inputValue}
|
|
148
|
-
onChange={handleChange}
|
|
149
|
-
disabled={disabled}
|
|
150
|
-
required={required}
|
|
151
|
-
min={min}
|
|
152
|
-
max={max}
|
|
153
|
-
aria-invalid={error ? true : undefined}
|
|
154
|
-
aria-describedby={error ? errorId : helpText ? descriptionId : undefined}
|
|
155
|
-
className={cn(FIELD_CLASS, "rounded-l-none rounded-r-xl border-l-0")}
|
|
156
|
-
/>
|
|
157
|
-
</div>
|
|
158
|
-
{error ? (
|
|
159
|
-
<p id={errorId} role="alert" className="text-destructive-text text-sm">
|
|
160
|
-
{error}
|
|
161
|
-
</p>
|
|
162
|
-
) : helpText ? (
|
|
163
|
-
<p id={descriptionId} className="text-muted-foreground text-sm">
|
|
164
|
-
{helpText}
|
|
165
|
-
</p>
|
|
166
|
-
) : null}
|
|
167
|
-
</div>
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function RangeDatePicker({
|
|
172
|
-
id,
|
|
173
|
-
label,
|
|
174
|
-
helpText,
|
|
175
|
-
error,
|
|
176
|
-
className,
|
|
177
|
-
disabled,
|
|
178
|
-
required,
|
|
179
|
-
min,
|
|
180
|
-
max,
|
|
181
|
-
startName,
|
|
182
|
-
endName,
|
|
183
|
-
value: controlledValue,
|
|
184
|
-
defaultValue,
|
|
185
|
-
onValueChange,
|
|
186
|
-
fromLabel = "From",
|
|
187
|
-
toLabel = "To",
|
|
188
|
-
}: RangeDatePickerProps) {
|
|
189
|
-
const generatedId = useId();
|
|
190
|
-
const startId = id ?? generatedId;
|
|
191
|
-
const endId = `${startId}-to`;
|
|
192
|
-
const descriptionId = `${startId}-description`;
|
|
193
|
-
const errorId = `${startId}-error`;
|
|
194
|
-
const startRef = useRef<HTMLInputElement>(null);
|
|
195
|
-
const endRef = useRef<HTMLInputElement>(null);
|
|
196
|
-
const isControlled = controlledValue !== undefined;
|
|
197
|
-
const [internal, setInternal] = useState<{ from: string; to: string }>(() => {
|
|
198
|
-
const initial = (isControlled ? controlledValue : defaultValue) as DateRangeValue | undefined;
|
|
199
|
-
return { from: formatDateInput(initial?.from), to: formatDateInput(initial?.to) };
|
|
200
|
-
});
|
|
201
|
-
const from = (isControlled ? formatDateInput(controlledValue?.from) : internal.from) || "";
|
|
202
|
-
const to = (isControlled ? formatDateInput(controlledValue?.to) : internal.to) || "";
|
|
203
|
-
|
|
204
|
-
function handleChange(which: "from" | "to") {
|
|
205
|
-
return (event: ChangeEvent<HTMLInputElement>) => {
|
|
206
|
-
const nextValue = event.target.value || null;
|
|
207
|
-
const next = which === "from" ? { from: nextValue, to } : { from, to: nextValue };
|
|
208
|
-
if (!isControlled) setInternal({ from: next.from ?? "", to: next.to ?? "" });
|
|
209
|
-
onValueChange?.({ from: next.from, to: next.to });
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return (
|
|
214
|
-
<div className={cn("flex flex-col gap-1.5", className)}>
|
|
215
|
-
{label ? <span className={cn(LABEL_CLASS, required && REQUIRED_CLASS)}>{label}</span> : null}
|
|
216
|
-
<div className="grid gap-3 sm:grid-cols-2">
|
|
217
|
-
<div className="flex min-w-0 flex-col gap-1">
|
|
218
|
-
<label htmlFor={startId} className={SUBLABEL_CLASS}>
|
|
219
|
-
{fromLabel}
|
|
220
|
-
</label>
|
|
221
|
-
<div className="flex w-full items-center">
|
|
222
|
-
<DatePickerIcon inputRef={startRef} disabled={disabled} ariaLabel={fromLabel} />
|
|
223
|
-
<input
|
|
224
|
-
ref={startRef}
|
|
225
|
-
id={startId}
|
|
226
|
-
name={startName}
|
|
227
|
-
type="date"
|
|
228
|
-
value={isControlled ? from : undefined}
|
|
229
|
-
defaultValue={isControlled ? undefined : from}
|
|
230
|
-
onChange={handleChange("from")}
|
|
231
|
-
disabled={disabled}
|
|
232
|
-
required={required}
|
|
233
|
-
min={min}
|
|
234
|
-
max={to || max || undefined}
|
|
235
|
-
aria-invalid={error ? true : undefined}
|
|
236
|
-
aria-describedby={error ? errorId : helpText ? descriptionId : undefined}
|
|
237
|
-
className={cn(FIELD_CLASS, "rounded-l-none rounded-r-xl border-l-0")}
|
|
238
|
-
/>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
<div className="flex min-w-0 flex-col gap-1">
|
|
242
|
-
<label htmlFor={endId} className={SUBLABEL_CLASS}>
|
|
243
|
-
{toLabel}
|
|
244
|
-
</label>
|
|
245
|
-
<div className="flex w-full items-center">
|
|
246
|
-
<DatePickerIcon inputRef={endRef} disabled={disabled} ariaLabel={toLabel} />
|
|
247
|
-
<input
|
|
248
|
-
ref={endRef}
|
|
249
|
-
id={endId}
|
|
250
|
-
name={endName}
|
|
251
|
-
type="date"
|
|
252
|
-
value={isControlled ? to : undefined}
|
|
253
|
-
defaultValue={isControlled ? undefined : to}
|
|
254
|
-
onChange={handleChange("to")}
|
|
255
|
-
disabled={disabled}
|
|
256
|
-
required={required}
|
|
257
|
-
min={from || min || undefined}
|
|
258
|
-
max={max}
|
|
259
|
-
aria-invalid={error ? true : undefined}
|
|
260
|
-
aria-describedby={error ? errorId : helpText ? descriptionId : undefined}
|
|
261
|
-
className={cn(FIELD_CLASS, "rounded-l-none rounded-r-xl border-l-0")}
|
|
262
|
-
/>
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
</div>
|
|
266
|
-
{error ? (
|
|
267
|
-
<p id={errorId} role="alert" className="text-destructive-text text-sm">
|
|
268
|
-
{error}
|
|
269
|
-
</p>
|
|
270
|
-
) : helpText ? (
|
|
271
|
-
<p id={descriptionId} className="text-muted-foreground text-sm">
|
|
272
|
-
{helpText}
|
|
273
|
-
</p>
|
|
274
|
-
) : null}
|
|
275
|
-
</div>
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* DatePicker — native date inputs styled to the design system. The browser
|
|
281
|
-
* owns the picker UI (calendar on desktop, wheel on iOS, etc.) so there is no
|
|
282
|
-
* popover, calendar grid, or positioning layer in the bundle. Both a single
|
|
283
|
-
* date (`name`) and a range (`startName` + `endName`) are supported; the
|
|
284
|
-
* native inputs submit "YYYY-MM-DD" local date strings directly.
|
|
285
|
-
*
|
|
286
|
-
* @example
|
|
287
|
-
* <DatePicker name="startDate" defaultValue="2020-01-15" label="Start date" />
|
|
288
|
-
* <DatePicker
|
|
289
|
-
* mode="range"
|
|
290
|
-
* startName="startDate"
|
|
291
|
-
* endName="endDate"
|
|
292
|
-
* defaultValue={{ from: "2020-01-15", to: "2020-06-30" }}
|
|
293
|
-
* label="Active period"
|
|
294
|
-
* />
|
|
295
|
-
*/
|
|
296
|
-
function DatePicker(props: DatePickerProps) {
|
|
297
|
-
if (props.mode === "range") {
|
|
298
|
-
return <RangeDatePicker {...props} />;
|
|
299
|
-
}
|
|
300
|
-
return <SingleDatePicker {...props} />;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export { DatePicker };
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse a date-only string ("YYYY-MM-DD") as a local calendar day instead of
|
|
3
|
-
* UTC midnight, so negative-UTC-offset timezones don't render the previous day.
|
|
4
|
-
* Accepts a `Date` (returned as-is when valid) and returns `undefined` for
|
|
5
|
-
* null/undefined/invalid input.
|
|
6
|
-
*/
|
|
7
|
-
export function parseDateInput(value: Date | string | null | undefined): Date | undefined {
|
|
8
|
-
if (!value) return undefined;
|
|
9
|
-
if (value instanceof Date) return Number.isNaN(value.getTime()) ? undefined : value;
|
|
10
|
-
const [year, month, day] = value.split("-").map(Number);
|
|
11
|
-
if (!year || !month || !day) return undefined;
|
|
12
|
-
// `new Date(year, month, day)` treats years from 0 through 99 as
|
|
13
|
-
// 1900-based years. Native date inputs can temporarily expose those years
|
|
14
|
-
// while a user is editing the year segment, so construct the date first and
|
|
15
|
-
// set the full year explicitly to avoid turning `0002` into `1902`.
|
|
16
|
-
const date = new Date(0);
|
|
17
|
-
date.setFullYear(year, month - 1, day);
|
|
18
|
-
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Serialize a date to its "YYYY-MM-DD" form using local components, so the
|
|
23
|
-
* value round-trips without a UTC shift. Returns "" for invalid input.
|
|
24
|
-
*/
|
|
25
|
-
export function formatDateInput(value: Date | string | null | undefined): string {
|
|
26
|
-
const date = parseDateInput(value);
|
|
27
|
-
if (!date) return "";
|
|
28
|
-
const year = String(date.getFullYear()).padStart(4, "0");
|
|
29
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
30
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
31
|
-
return `${year}-${month}-${day}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Today's date as a "YYYY-MM-DD" local string. */
|
|
35
|
-
export function todayDateInput(): string {
|
|
36
|
-
return formatDateInput(new Date());
|
|
37
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import { expect, fireEvent, userEvent, within } from "storybook/test";
|
|
4
|
-
|
|
5
|
-
import { DropZone, type DropZoneFileInfo, type DropZoneStatus } from "./drop-zone";
|
|
6
|
-
|
|
7
|
-
const meta = {
|
|
8
|
-
title: "Forms/DropZone",
|
|
9
|
-
component: DropZone,
|
|
10
|
-
tags: ["autodocs"],
|
|
11
|
-
args: {
|
|
12
|
-
status: "empty",
|
|
13
|
-
onFiles: () => {},
|
|
14
|
-
},
|
|
15
|
-
} satisfies Meta<typeof DropZone>;
|
|
16
|
-
|
|
17
|
-
export default meta;
|
|
18
|
-
type Story = StoryObj<typeof meta>;
|
|
19
|
-
|
|
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
|
-
);
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div className="w-96">
|
|
28
|
-
<DropZone
|
|
29
|
-
status={status}
|
|
30
|
-
file={file}
|
|
31
|
-
accept=".csv"
|
|
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
|
-
}}
|
|
48
|
-
/>
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
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
|
-
},
|
|
71
|
-
};
|
|
72
|
-
|
|
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 = {
|
|
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
|
-
},
|
|
115
|
-
};
|
|
116
|
-
|
|
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
|
-
},
|
|
140
|
-
};
|