Skip to content

Commit 041cab7

Browse files
authored
feat(FileDropZone): add file drop zone component
1 parent 11be372 commit 041cab7

File tree

19 files changed

+380
-2
lines changed

19 files changed

+380
-2
lines changed

packages/components/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@
145145
"types": "./dist/js/types/components/FileCardList/index.d.ts",
146146
"import": "./dist/js/FileCardList.js"
147147
},
148+
"./FileDropZone/styles.css": "./dist/css/FileDropZone.css",
149+
"./FileDropZone": {
150+
"types": "./dist/js/types/components/FileDropZone/index.d.ts",
151+
"import": "./dist/js/FileDropZone.js"
152+
},
148153
"./FileField/styles.css": "./dist/css/FileField.css",
149154
"./FileField": {
150155
"types": "./dist/js/types/components/FileField/index.d.ts",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.fileDropZone {
2+
border-style: var(--file-drop-zone--border-style--default);
3+
border-width: var(--file-drop-zone--border-width);
4+
border-color: var(--file-drop-zone--border-color--default);
5+
background-color: var(--file-drop-zone--background-color--default);
6+
border-radius: var(--file-drop-zone--corner-radius);
7+
padding: var(--file-drop-zone--padding);
8+
9+
&[data-focus-visible],
10+
&[data-drop-target] {
11+
border-color: var(--file-drop-zone--border-color--target);
12+
border-style: var(--file-drop-zone--border-style--target);
13+
}
14+
15+
&[data-drop-target] {
16+
background-color: var(--file-drop-zone--background-color--target);
17+
}
18+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { FC, PropsWithChildren } from "react";
2+
import React from "react";
3+
import * as Aria from "react-aria-components";
4+
import { IllustratedMessage } from "@/components/IllustratedMessage";
5+
import type { PropsWithClassName } from "@/lib/types/props";
6+
import styles from "./FileDropZone.module.scss";
7+
import clsx from "clsx";
8+
import type { FileInputOnChangeHandler } from "@/components/FileField/components/FileInput";
9+
import type { PropsContext } from "@/lib/propsContext";
10+
import { PropsContextProvider } from "@/lib/propsContext";
11+
12+
export interface FileDropZoneProps
13+
extends PropsWithClassName,
14+
PropsWithChildren,
15+
Pick<Aria.InputProps, "accept" | "multiple"> {
16+
onChange?: FileInputOnChangeHandler;
17+
}
18+
19+
export const FileDropZone: FC<FileDropZoneProps> = (props) => {
20+
const { multiple, accept, className, onChange, children } = props;
21+
22+
const rootClassName = clsx(styles.fileDropZone, className);
23+
24+
const propsContext: PropsContext = {
25+
FileField: {
26+
accept: accept,
27+
multiple: multiple,
28+
Button: { variant: "outline", color: "dark" },
29+
},
30+
};
31+
32+
return (
33+
<Aria.DropZone
34+
className={rootClassName}
35+
onDrop={async (e) => {
36+
{
37+
const fileDropItems = e.items.filter(
38+
(file) => file.kind === "file",
39+
) as Aria.FileDropItem[];
40+
41+
const files = await Promise.all(
42+
fileDropItems
43+
.filter((f) => !accept || accept?.includes(f.type))
44+
.map(async (f) => {
45+
return await f.getFile();
46+
}),
47+
);
48+
49+
if (files.length > 0) {
50+
onChange?.((multiple ? files : [files[0]]) as unknown as FileList);
51+
}
52+
}
53+
}}
54+
>
55+
<IllustratedMessage color="dark">
56+
<PropsContextProvider props={propsContext} mergeInParentContext>
57+
{children}
58+
</PropsContextProvider>
59+
</IllustratedMessage>
60+
</Aria.DropZone>
61+
);
62+
};
63+
64+
export default FileDropZone;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { FileDropZone } from "./FileDropZone";
2+
3+
export { type FileDropZoneProps, FileDropZone } from "./FileDropZone";
4+
export default FileDropZone;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import React, { useState } from "react";
3+
import { FileDropZone } from "@/components/FileDropZone";
4+
import { Section } from "@/components/Section";
5+
import { FileCardList } from "@/components/FileCardList";
6+
import { FileCard } from "@/components/FileCard";
7+
import { Form, typedField } from "@/integrations/react-hook-form";
8+
import { useForm } from "react-hook-form";
9+
import { action } from "@storybook/addon-actions";
10+
import { Button } from "@/components/Button";
11+
import { ActionGroup } from "@/components/ActionGroup";
12+
import { IconImage, IconUpload } from "@/components/Icon/components/icons";
13+
import { Heading } from "@/components/Heading";
14+
import { FileField } from "@/components/FileField";
15+
import { Text } from "@/components/Text";
16+
17+
const meta: Meta<typeof FileDropZone> = {
18+
title: "Upload/FileDropZone",
19+
component: FileDropZone,
20+
parameters: {
21+
controls: { exclude: ["className", "controller", "onChange"] },
22+
},
23+
render: (props) => {
24+
const [files, setFiles] = useState<FileList | null>(null);
25+
26+
return (
27+
<Section>
28+
<FileDropZone {...props} onChange={setFiles}>
29+
<IconUpload />
30+
<Heading>Drop file</Heading>
31+
<FileField name="file" onChange={setFiles}>
32+
<Button>Select file</Button>
33+
</FileField>
34+
</FileDropZone>
35+
<FileCardList>
36+
{[...(files ?? [])].map((f) => (
37+
<FileCard name={f.name} key={f.name} />
38+
))}
39+
</FileCardList>
40+
</Section>
41+
);
42+
},
43+
};
44+
export default meta;
45+
46+
type Story = StoryObj<typeof FileDropZone>;
47+
48+
const submitAction = action("submit");
49+
50+
export const Default: Story = {};
51+
52+
export const WithAcceptedTypes: Story = {
53+
args: { accept: "image/png" },
54+
render: (props) => {
55+
const [files, setFiles] = useState<FileList | null>(null);
56+
57+
return (
58+
<Section>
59+
<FileDropZone {...props} onChange={setFiles}>
60+
<IconImage />
61+
<Heading>Drop image</Heading>
62+
<Text>Only image/png images are allowed.</Text>
63+
<FileField name="file" onChange={setFiles}>
64+
<Button>Select image</Button>
65+
</FileField>
66+
</FileDropZone>
67+
<FileCardList>
68+
{[...(files ?? [])].map((f) => (
69+
<FileCard name={f.name} key={f.name} />
70+
))}
71+
</FileCardList>
72+
</Section>
73+
);
74+
},
75+
};
76+
77+
export const Multiple: Story = {
78+
args: { multiple: true },
79+
render: (props) => {
80+
const [files, setFiles] = useState<FileList | null>(null);
81+
82+
return (
83+
<Section>
84+
<FileDropZone {...props} onChange={setFiles}>
85+
<IconUpload />
86+
<Heading>Drop files</Heading>
87+
<FileField name="file" onChange={setFiles}>
88+
<Button>Select files</Button>
89+
</FileField>
90+
</FileDropZone>
91+
<FileCardList>
92+
{[...(files ?? [])].map((f) => (
93+
<FileCard name={f.name} key={f.name} />
94+
))}
95+
</FileCardList>
96+
</Section>
97+
);
98+
},
99+
};
100+
101+
export const WithReactHookForm: Story = {
102+
render: (props) => {
103+
const form = useForm<{
104+
file: FileList | null;
105+
}>();
106+
107+
const Field = typedField(form);
108+
109+
return (
110+
<Form form={form} onSubmit={submitAction}>
111+
<Section>
112+
<FileDropZone {...props} onChange={(f) => form.setValue("file", f)}>
113+
<IconUpload />
114+
<Heading>Drop file</Heading>
115+
<Field name="file" rules={{ required: "Please choose a file" }}>
116+
<FileField>
117+
<Button variant="outline" color="dark">
118+
Select file
119+
</Button>
120+
</FileField>
121+
</Field>
122+
</FileDropZone>
123+
124+
<FileCardList>
125+
{[...(form.watch("file") ?? [])].map((f) => (
126+
<FileCard name={f.name} key={f.name} />
127+
))}
128+
</FileCardList>
129+
</Section>
130+
<ActionGroup>
131+
<Button type="submit">Upload</Button>
132+
</ActionGroup>
133+
</Form>
134+
);
135+
},
136+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* eslint-disable */
2+
/* auto-generated file */
3+
import React, { ComponentProps, FC } from "react";
4+
import { IconPhoto as Tabler } from "@tabler/icons-react";
5+
import { Icon } from "@/components/Icon";
6+
7+
export const IconPicture: FC<Omit<ComponentProps<typeof Icon>, "children">> = (
8+
props,
9+
) => (
10+
<Icon {...props}>
11+
<Tabler />
12+
</Icon>
13+
);
14+
15+
export default IconPicture;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* eslint-disable */
2+
/* auto-generated file */
3+
import React, { ComponentProps, FC } from "react";
4+
import { IconUpload as Tabler } from "@tabler/icons-react";
5+
import { Icon } from "@/components/Icon";
6+
7+
export const IconUpload: FC<Omit<ComponentProps<typeof Icon>, "children">> = (
8+
props,
9+
) => (
10+
<Icon {...props}>
11+
<Tabler />
12+
</Icon>
13+
);
14+
15+
export default IconUpload;

packages/components/src/components/Icon/components/icons/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export { IconPassword } from "./IconPassword";
6868
export { IconPayment } from "./IconPayment";
6969
export { IconPending } from "./IconPending";
7070
export { IconPerformance } from "./IconPerformance";
71+
export { IconPicture } from "./IconPicture";
7172
export { IconPlus } from "./IconPlus";
7273
export { IconProject } from "./IconProject";
7374
export { IconRadioOff } from "./IconRadioOff";
@@ -97,6 +98,7 @@ export { IconSupport } from "./IconSupport";
9798
export { IconTerminate } from "./IconTerminate";
9899
export { IconTicket } from "./IconTicket";
99100
export { IconUndo } from "./IconUndo";
101+
export { IconUpload } from "./IconUpload";
100102
export { IconUser } from "./IconUser";
101103
export { IconView } from "./IconView";
102104
export { IconVhost } from "./IconVhost";

packages/components/src/components/Icon/icons.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Password: Lock
6666
Payment: CreditCard
6767
Pending: Loader2
6868
Performance: ChartHistogram
69+
Picture: Photo
6970
Plus: Plus
7071
Project: Archive
7172
RadioOff: Circle
@@ -95,6 +96,7 @@ Support: Headset
9596
Terminate: FileX
9697
Ticket: Ticket
9798
Undo: ArrowBackUp
99+
Upload: Upload
98100
User: User
99101
View: List
100102
Vhost: WorldShare

packages/components/vite.build.config.base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const buildConfig = (opts: Options) => {
6161
FileField: "./src/components/FileField/index.ts",
6262
FileCard: "./src/components/FileCard/index.ts",
6363
FileCardList: "./src/components/FileCardList/index.ts",
64+
FileDropZone: "./src/components/FileDropZone/index.ts",
6465
Header: "./src/components/Header/index.ts",
6566
HeaderNavigation: "./src/components/HeaderNavigation/index.ts",
6667
Heading: "./src/components/Heading/index.ts",

0 commit comments

Comments
 (0)