Skip to content

Commit

Permalink
chore(Upload): add drop zone (#1647)
Browse files Browse the repository at this point in the history
* chore(Upload): add new translation string "errorUnsupportedFile"

* chore(Upload): add drop zone
  • Loading branch information
tujoworker committed Oct 19, 2022
1 parent 32aa0eb commit b74cc49
Show file tree
Hide file tree
Showing 17 changed files with 569 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ showTabs: true
| `buttonText` | _(optional)_ Custom text property. Replaces the default upload button text. |
| `loadingText` | _(optional)_ Custom text property. Replaces the default loading text. |
| `errorLargeFile` | _(optional)_ Custom text property. Replaces the default file size error message. |
| `errorUnsupportedFile` | _(optional)_ Custom text property. Replaces the default file type error message. |
| `deleteButton` | _(optional)_ Custom text property. Replaces the default delete button text. |
| `fileListAriaLabel` | _(optional)_ Custom text property. Replaces the default list aria label. |
| `skeleton` | _(optional)_ Skeleton should be applied when loading content Default: `null`. |
Expand Down
161 changes: 81 additions & 80 deletions packages/dnb-eufemia/src/components/upload/Upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,29 @@ import P from '../../elements/P'
import Dl from '../../elements/Dl'
import Dt from '../../elements/Dt'
import Dd from '../../elements/Dd'
import HeightAnimation from '../height-animation/HeightAnimation'

// Shared
import { createSpacingClasses } from '../space/SpacingHelper'
import Provider from '../../shared/Provider'
import Context from '../../shared/Context'
import { extendPropsWithContext } from '../../shared/component-helper'
import { format } from '../number-format/NumberUtils'
import { LocaleProps, SpacingProps } from '../../shared/types'

// Internal
import UploadFileInput from './UploadFileInput'
import type { UploadFile, UploadProps } from './types'
import UploadFileListCell from './UploadFileListCell'
import useUpload from './useUpload'
import UploadDropzone from './UploadDropzone'
import { UploadContext } from './UploadContext'
import { verifyFiles } from './UploadVerify'

import type { UploadFile, UploadAllProps } from './types'

export const defaultProps = {
fileMaxSize: 5000,
}

const Upload = (localProps: UploadProps & SpacingProps & LocaleProps) => {
const Upload = (localProps: UploadAllProps) => {
const context = React.useContext(Context)

const extendedProps = extendPropsWithContext(
Expand All @@ -53,6 +55,7 @@ const Upload = (localProps: UploadProps & SpacingProps & LocaleProps) => {
buttonText,
loadingText,
errorLargeFile,
errorUnsupportedFile,
deleteButton,
fileListAriaLabel,
...props
Expand All @@ -67,82 +70,76 @@ const Upload = (localProps: UploadProps & SpacingProps & LocaleProps) => {
.toUpperCase()

return (
<HeightAnimation
open
data-testid="upload"
className={classnames('dnb-upload', spacingClasses, className)}
{...props}
<UploadContext.Provider
value={{
id,
acceptedFileTypes,
onInputUpload,
fileMaxSize,
buttonText,
errorLargeFile,
errorUnsupportedFile,
multipleFiles,
}}
>
<Provider skeleton={skeleton}>
<Lead data-testid="upload-title" space="0">
{title}
</Lead>

<P
data-testid="upload-text"
top="xx-small"
className="dnb-upload__text"
>
{text}
</P>

<Dl
top="small"
bottom={0}
direction="horizontal"
className="dnb-upload__condition-list"
<UploadDropzone
data-testid="upload"
className={classnames('dnb-upload', spacingClasses, className)}
{...props}
>
<Dl.Item>
<Dt data-testid="upload-accepted-formats-description">
{fileTypeDescription}
</Dt>
<Dd data-testid="upload-accepted-formats">
{prettyfiedAcceptedFileFormats}
</Dd>
</Dl.Item>

<Dl.Item>
<Dt data-testid="upload-file-size-description">
{fileSizeDescription}
</Dt>
<Dd data-testid="upload-file-size">
{String(fileSizeContent).replace(
'%size',
format(fileMaxSize).toString()
)}
</Dd>
</Dl.Item>
</Dl>

<UploadFileInput
id={id}
acceptedFormats={acceptedFileTypes}
onUpload={onInputUpload}
fileMaxSize={fileMaxSize}
buttonText={buttonText}
errorLargeFile={errorLargeFile}
multipleFiles={multipleFiles}
/>

<UploadFileList />

<svg
className="dnb-upload__outline"
aria-hidden
xmlns="http://www.w3.org/2000/svg"
fill="none"
>
<rect
width="100%"
height="100%"
rx="0.25rem"
ry="0.25rem"
strokeWidth="2.5"
strokeDasharray="7 7"
/>
</svg>
<Lead data-testid="upload-title" space="0">
{title}
</Lead>

<P
data-testid="upload-text"
top="xx-small"
className="dnb-upload__text"
>
{text}
</P>

<Dl
top="small"
bottom={0}
direction="horizontal"
className="dnb-upload__condition-list"
>
<Dl.Item>
<Dt
data-testid="upload-accepted-formats-description"
className="dnb-upload__condition-list__label"
>
{fileTypeDescription}
</Dt>
<Dd data-testid="upload-accepted-formats">
{prettyfiedAcceptedFileFormats}
</Dd>
</Dl.Item>

<Dl.Item>
<Dt
data-testid="upload-file-size-description"
className="dnb-upload__condition-list__label"
>
{fileSizeDescription}
</Dt>
<Dd data-testid="upload-file-size">
{String(fileSizeContent).replace(
'%size',
format(fileMaxSize).toString()
)}
</Dd>
</Dl.Item>
</Dl>

<UploadFileInput />

<UploadFileList />
</UploadDropzone>
</Provider>
</HeightAnimation>
</UploadContext.Provider>
)

function UploadFileList() {
Expand Down Expand Up @@ -175,10 +172,14 @@ const Upload = (localProps: UploadProps & SpacingProps & LocaleProps) => {
)
}

function onInputUpload(addedFiles: UploadFile[]) {
const newFiles = [...files, ...addedFiles]

setFiles(newFiles)
function onInputUpload(newFiles: UploadFile[]) {
const verifiedFiles = verifyFiles([...files, ...newFiles], {
fileMaxSize,
acceptedFileTypes,
errorUnsupportedFile,
errorLargeFile,
})
setFiles(verifiedFiles)
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/dnb-eufemia/src/components/upload/UploadContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createContext } from 'react'
import { UploadContextProps } from './types'

export const UploadContext = createContext<UploadContextProps>(null)
87 changes: 87 additions & 0 deletions packages/dnb-eufemia/src/components/upload/UploadDropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react'
import classnames from 'classnames'

import HeightAnimation from '../height-animation/HeightAnimation'
import { UploadContext } from './UploadContext'
import type { UploadAllProps, UploadFile, UploadProps } from './types'

export default function UploadDropzone({
children,
className,
...rest
}: Partial<UploadAllProps>) {
const props = rest as Omit<UploadProps, 'title'>
const context = React.useContext(UploadContext)
const [hover, setHover] = React.useState(false)
const hoverTimeout = React.useRef<NodeJS.Timer>()

const { onInputUpload, multipleFiles = false } = context

const dropHandler = (event: React.DragEvent) => {
event.preventDefault()

const fileData = event.dataTransfer

const files: UploadFile[] = []

Array.from(fileData.files).forEach((file, i) => {
if (multipleFiles ? i >= 0 : i === 0) {
files.push({ file })
}
})

onInputUpload(files)

setHover(false)
}

const dragOverHandler = (event: React.SyntheticEvent) => {
event.preventDefault()
clearTimers()
setHover(true)
}

const dragLeaveHandler = (event: React.SyntheticEvent) => {
event.preventDefault()
clearTimers()
hoverTimeout.current = setTimeout(() => {
setHover(false)
}, 300) // prevent flickering
}

const clearTimers = () => {
clearTimeout(hoverTimeout.current)
}

React.useEffect(() => {
return clearTimers
}, [])

return (
<HeightAnimation
className={classnames(className, hover && 'dnb-upload--active')}
onDrop={dropHandler}
onDragOver={dragOverHandler}
onDragLeave={dragLeaveHandler}
{...props}
>
{children}

<svg
className="dnb-upload__outline"
aria-hidden
xmlns="http://www.w3.org/2000/svg"
fill="none"
>
<rect
width="100%"
height="100%"
rx="0.25rem"
ry="0.25rem"
strokeWidth="2.5"
strokeDasharray="7 7"
/>
</svg>
</HeightAnimation>
)
}
63 changes: 21 additions & 42 deletions packages/dnb-eufemia/src/components/upload/UploadFileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,26 @@ import Button from '../button/Button'
import { folder as FolderIcon } from '../../icons'

// Shared
import { format } from '../number-format/NumberUtils'
import { makeUniqueId } from '../../shared/component-helper'

// Internal
import { UploadFile } from './types'

export type UploadFileInputProps = {
id?: string
acceptedFormats: string[]
onUpload: (files: UploadFile[]) => void
fileMaxSize: number
buttonText: React.ReactNode
errorLargeFile: React.ReactNode
multipleFiles: boolean
}
import { UploadContextProps } from './types'
import { UploadContext } from './UploadContext'

const BYTES_IN_A_MEGA_BYTE = 1048576

const UploadFileInput = ({
id,
acceptedFormats,
buttonText,
onUpload,
fileMaxSize,
errorLargeFile,
multipleFiles = false,
}: UploadFileInputProps) => {
const UploadFileInput = (props: Partial<UploadContextProps> = null) => {
const fileInput = useRef<HTMLInputElement>(null)

const accept = acceptedFormats.reduce((accept, format, index) => {
const context = React.useContext(UploadContext)

const {
id,
acceptedFileTypes,
buttonText,
onInputUpload,
multipleFiles = false,
} = context || props

const accept = acceptedFileTypes.reduce((accept, format, index) => {
const previus = index === 0 ? '' : `${accept},`
return `${previus} .${format}`
}, '')
Expand Down Expand Up @@ -78,26 +68,15 @@ const UploadFileInput = ({
</div>
)

function handleFileInput({ target: { files } }) {
const uploadFile = [...Array(files.length)].map((_item, index) => {
const file: UploadFile = { file: files[index] }
const errorMessage = getErrorMessage(file.file.size)

if (errorMessage) return { ...file, errorMessage }
return file
})
onUpload(uploadFile)
}
function handleFileInput(event: React.SyntheticEvent) {
const target = event.target as HTMLInputElement
const { files } = target

function getErrorMessage(fileSize: number) {
const errorMessage = String(errorLargeFile).replace(
'%size',
format(fileMaxSize).toString()
onInputUpload(
Array.from(files).map((file) => {
return { file }
})
)
// Converts from b (binary) to MB (decimal)
return fileSize / BYTES_IN_A_MEGA_BYTE > fileMaxSize
? errorMessage
: null
}
}

Expand Down

0 comments on commit b74cc49

Please sign in to comment.