Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deep-chairs-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ultraviolet/ui": minor
---

New component `FileInput`
21 changes: 21 additions & 0 deletions packages/ui/src/components/FileInput/FileInputProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createContext, useContext } from 'react'

type FileInputContextType =
| {
disabled?: boolean
inputId: string
}
| undefined
export const FileInputContext = createContext<FileInputContextType>(undefined)

export const useFileInput = () => {
const context = useContext(FileInputContext)

if (!context) {
throw new Error(
'FileInputContext should be inside FileInput to work properly.',
)
}

return context
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { StoryFn } from '@storybook/react-vite'
import { Stack } from '../../Stack'
import { Text } from '../../Text'
import { FileInput } from '..'
import { hereText } from './styles.css'

export const Children: StoryFn<typeof FileInput> = args => (
<Stack direction="column" gap={2}>
<FileInput
aria-label="label"
disabled={args.disabled}
list
title={inputId => (
<label htmlFor={inputId}>Click here to add a file (title)</label>
)}
variant="dropzone"
>
{inputId => (
<Stack>
<Text
as="label"
htmlFor={inputId}
sentiment="primary"
variant="bodyStrong"
>
You can also click here (children)
</Text>
But not here
</Stack>
)}
</FileInput>
<FileInput
aria-label="label-2"
disabled={args.disabled}
list
title="drag here"
variant="overlay"
>
{inputId => (
<>
Drag an drop on me or click{' '}
<Text
as="label"
className={hereText}
htmlFor={inputId}
sentiment="info"
variant="body"
>
here
</Text>{' '}
to add a file
</>
)}
</FileInput>
</Stack>
)

Children.parameters = {
docs: {
description: {
story:
'You can get the input id from the component if you want to use it on some other components, not just `FileInput.Button`. Simply use a label and `htmlFor`. You can also do it with the title.',
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { StoryFn } from '@storybook/react-vite'
import { useState } from 'react'
import { Stack } from '../../Stack'
import { Text } from '../../Text'
import { FileInput } from '..'
import type { FilesType } from '../types'

export const Controlled: StoryFn<typeof FileInput> = args => {
const [files, setFiles] = useState<FilesType[]>([])
const onChange = (f: FilesType[]) => setFiles(f)

return (
<Stack direction="column" gap={3}>
<FileInput
defaultFiles={[]}
disabled={args.disabled}
label="type='dropzone'"
multiple
onChangeFiles={onChange}
size="small"
title="Click or drag file here"
variant="dropzone"
/>
<Text as="div" variant="body">
Files:
<ul>
{files.map(file => (
<li key={file.fileName}>{file.fileName}</li>
))}
</ul>
</Text>
</Stack>
)
}

Controlled.parameters = {
docs: {
description: {
story:
'The component can be controlled two ways: with the `onDrop` prop to catch the event or with `onChangeFiles` to get a more polished list of the added files.',
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { StoryFn } from '@storybook/react-vite'
import { PlusIcon, UploadIcon } from '@ultraviolet/icons'
import { Stack } from '../../Stack'
import { FileInput } from '..'

export const Disabled: StoryFn<typeof FileInput> = args => (
<Stack direction="column" gap={2}>
<FileInput
accept={args.accept}
disabled
label="medium disabled"
title="Drag and drop files to get started"
variant="dropzone"
>
Only pay for the storage you use. For example, storing 100 GB of data will
cost use monthly less than a cup of coffee.
<Stack direction="row" gap="2" justifyContent="center">
<FileInput.Button sentiment="neutral" variant="outlined">
<PlusIcon />
Add folder
</FileInput.Button>
<FileInput.Button sentiment="primary" variant="filled">
<UploadIcon />
Upload
</FileInput.Button>
</Stack>
</FileInput>
<FileInput
disabled
label="small disabled"
size="small"
title="Click or drag file to this area to upload"
variant="dropzone"
/>
<FileInput
aria-label="label"
disabled
title={
<Stack direction="row" gap={1}>
<UploadIcon />
Click or drag file to this area to upload (disabled)
</Stack>
}
variant="overlay"
>
Some content (drag on me)
</FileInput>
</Stack>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { StoryFn } from '@storybook/react-vite'
import { PlusIcon, UploadIcon } from '@ultraviolet/icons'
import { Link } from '../../Link'
import { Stack } from '../../Stack'
import { FileInput } from '..'

export const DropzoneSize: StoryFn<typeof FileInput> = args => (
<Stack direction="column" gap={2}>
<FileInput
disabled={args.disabled}
label="medium"
title="Drag and drop files to get started"
variant="dropzone"
>
Only pay for the storage you use. For example, storing 100 GB of data will
cost use monthly less than a cup of coffee.
<Link href="" target="_blank">
Object Storage pricing
</Link>
<Stack direction="row" gap="2" justifyContent="center">
<FileInput.Button sentiment="neutral" variant="outlined">
<PlusIcon />
Add folder
</FileInput.Button>
<FileInput.Button sentiment="primary" variant="filled">
<UploadIcon />
Upload
</FileInput.Button>
</Stack>
</FileInput>
<FileInput
disabled={args.disabled}
label="small"
size="small"
title="Click or drag file to this area to upload"
variant="dropzone"
/>
</Stack>
)

DropzoneSize.parameters = {
docs: {
description: {
story:
'There are two sizes for the fileinput when variant="dropdzone" (default value). <br/> <strong>⚠️ When size="medium" (default value), do not forget to use `FileInput.Button` in order to add button to open the file explorer ! ⚠️</strong>',
},
},
}
112 changes: 112 additions & 0 deletions packages/ui/src/components/FileInput/__stories__/List.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { StoryFn } from '@storybook/react-vite'
import { Separator } from '../../Separator'
import { Stack } from '../../Stack'
import { FileInput } from '..'

const defaultFile = [
{
file: 'https://upload.wikimedia.org/wikipedia/commons/4/41/Photo_Chat_Noir_et_blanc.jpg',
fileName: 'cat.png',
lastModified: 1,
size: 30460,
type: 'image/png',
},
{
file: 'sound.mp3',
fileName: 'sound.mp3',
lastModified: 1,
size: 30460,
type: 'audio/mp3',
},
{
file: 'doc.pdf',
fileName: 'doc.pdf',
lastModified: 1,
size: 304600,
type: 'application/pdf',
},
{
file: 'video.mp4',
fileName: 'video.mp4',
lastModified: 1,
size: 40460000,
type: 'video/mp4',
},
{
file: 'loading.pdf',
fileName: 'loading_example.pdf',
lastModified: 1,
loading: true,
size: 40460000,
type: 'application/pdf',
},
{
error: 'Maximum file size exceeded',
file: 'error.png',
fileName: 'error_example.png',
lastModified: 1,
size: 4046000000,
type: 'image/png',
},
]
export const List: StoryFn<typeof FileInput> = args => (
<Stack direction="column" gap={3}>
<FileInput
defaultFiles={defaultFile}
disabled={args.disabled}
label="type='dropzone'"
list
multiple
size="small"
title="Click or drag file here"
variant="dropzone"
/>
<Separator />
<FileInput
aria-label="label"
defaultFiles={defaultFile}
disabled={args.disabled}
list
listPosition="top"
multiple
title="dnd here"
variant="overlay"
>
Type &quot;ovelay&quot; (listPosition=&quot;top&quot;)
</FileInput>
<Separator />
<FileInput
aria-label="label"
defaultFiles={defaultFile}
disabled={args.disabled}
list
listPosition="bottom"
multiple
title="dnd here"
variant="overlay"
>
Type &quot;ovelay&quot; (listPosition=&quot;bottom&quot;)
</FileInput>
<Separator />
<FileInput
defaultFiles={defaultFile}
disabled={args.disabled}
label="With prop listLimit"
list
listLimit={{ limit: 3, overflowText: 'See all' }}
multiple
size="small"
title="Click or drag file here"
variant="dropzone"
/>
</Stack>
)

List.parameters = {
docs: {
description: {
story:
'With prop `list` it is possible to display all the drag&drop files added to the input. When using the FileInput as an overlay, it is possible to add the list on top or beneath the content using prop `listPosition`. Size is displayed and computed automatically from file.sie (number, in byte). It is also possible to add a limit to the number of visible files in the list using prop `listLimit`.',
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { StoryFn } from '@storybook/react-vite'
import { Stack } from '../../Stack'
import { FileInput } from '..'

const defaultFile = [
{
file: 'https://upload.wikimedia.org/wikipedia/commons/4/41/Photo_Chat_Noir_et_blanc.jpg',
fileName: 'cat.png',
lastModified: 1,
size: 30460,
type: 'image/png',
},
]
export const Multiple: StoryFn<typeof FileInput> = args => (
<Stack direction="column" gap={3}>
<FileInput
defaultFiles={defaultFile}
disabled={args.disabled}
label="Multiple"
list
multiple
size="small"
title="Click or drag file here"
variant="dropzone"
/>
<FileInput
defaultFiles={defaultFile}
disabled={args.disabled}
label="Not multiple (default behavior)"
list
size="small"
title="Click or drag file here"
variant="dropzone"
/>
</Stack>
)

Multiple.parameters = {
docs: {
description: {
story: 'It is possible to add mutliple files when using prop `multiple`.',
},
},
}
Loading
Loading