|
@@ -1,4 +1,9 @@ |
|
|
import * as React from 'react'; |
|
|
import * as React from 'react'; |
|
|
|
|
|
import * as ButtonBase from '@tesseract-design/web-base-button'; |
|
|
|
|
|
|
|
|
|
|
|
interface FileWithPreview extends File { |
|
|
|
|
|
previewUrl?: string; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
export interface FileButtonProps extends Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { |
|
|
export interface FileButtonProps extends Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { |
|
|
/** |
|
|
/** |
|
@@ -33,6 +38,9 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> |
|
|
block = false, |
|
|
block = false, |
|
|
enhanced = false, |
|
|
enhanced = false, |
|
|
hiddenLabel = false, |
|
|
hiddenLabel = false, |
|
|
|
|
|
multiple = false, |
|
|
|
|
|
onChange, |
|
|
|
|
|
disabled = false, |
|
|
className: _className, |
|
|
className: _className, |
|
|
placeholder: _placeholder, |
|
|
placeholder: _placeholder, |
|
|
as: _as, |
|
|
as: _as, |
|
@@ -40,44 +48,250 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> |
|
|
}: FileButtonProps, |
|
|
}: FileButtonProps, |
|
|
forwardedRef, |
|
|
forwardedRef, |
|
|
) => { |
|
|
) => { |
|
|
|
|
|
const [isEnhanced, setIsEnhanced] = React.useState(false); |
|
|
|
|
|
const [selectedFiles, setSelectedFiles] = React.useState([] as Partial<FileWithPreview>[]); |
|
|
const defaultRef = React.useRef<HTMLInputElement>(null); |
|
|
const defaultRef = React.useRef<HTMLInputElement>(null); |
|
|
const ref = forwardedRef ?? defaultRef; |
|
|
const ref = forwardedRef ?? defaultRef; |
|
|
|
|
|
|
|
|
|
|
|
const handleAddFile = async (fileList: FileList) => { |
|
|
|
|
|
const files = Array.from(fileList); |
|
|
|
|
|
const fileResult = await Promise.all( |
|
|
|
|
|
files.map((f) => new Promise<Partial<FileWithPreview>>((resolve, reject) => { |
|
|
|
|
|
if (f.type.startsWith('image/')) { |
|
|
|
|
|
const reader = new FileReader(); |
|
|
|
|
|
reader.addEventListener('error', () => { |
|
|
|
|
|
reject(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
reader.addEventListener('load', (loadEvent) => { |
|
|
|
|
|
const target = loadEvent.target as FileReader; |
|
|
|
|
|
resolve({ |
|
|
|
|
|
...f, |
|
|
|
|
|
name: f.name, |
|
|
|
|
|
type: f.type, |
|
|
|
|
|
size: f.size, |
|
|
|
|
|
lastModified: f.lastModified, |
|
|
|
|
|
previewUrl: target.result as string, |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
reader.readAsDataURL(f); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
resolve(f); |
|
|
|
|
|
})) |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
setSelectedFiles(fileResult); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const addFile: React.ChangeEventHandler<HTMLInputElement> = async (e) => { |
|
|
|
|
|
if (!enhanced) { |
|
|
|
|
|
onChange?.(e); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const currentTarget = e.currentTarget; |
|
|
|
|
|
const fileList = currentTarget.files as FileList; |
|
|
|
|
|
await handleAddFile(fileList); |
|
|
|
|
|
onChange?.(e); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const deleteFiles: React.MouseEventHandler<HTMLButtonElement> = () => { |
|
|
|
|
|
if (typeof ref === 'object' && ref.current) { |
|
|
|
|
|
ref.current.value = ''; |
|
|
|
|
|
setSelectedFiles([]); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const cancelEvent = (e: React.DragEvent) => { |
|
|
|
|
|
e.stopPropagation(); |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const handleDropZone: React.DragEventHandler<HTMLDivElement> = async (e) => { |
|
|
|
|
|
cancelEvent(e); |
|
|
|
|
|
const { dataTransfer } = e; |
|
|
|
|
|
if (typeof ref === 'object' && ref.current) { |
|
|
|
|
|
const { files } = dataTransfer; |
|
|
|
|
|
ref.current.files = files; |
|
|
|
|
|
await handleAddFile(files); |
|
|
|
|
|
ref.current.dispatchEvent(new Event('change')); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => { |
|
|
|
|
|
setIsEnhanced(enhanced); |
|
|
|
|
|
}, [enhanced]); |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<div> |
|
|
|
|
|
<label> |
|
|
|
|
|
|
|
|
<div |
|
|
|
|
|
className="rounded relative min-h-[300px] overflow-hidden" |
|
|
|
|
|
onDragEnter={cancelEvent} |
|
|
|
|
|
onDragOver={cancelEvent} |
|
|
|
|
|
onDrop={handleDropZone} |
|
|
|
|
|
data-testid="root" |
|
|
|
|
|
> |
|
|
|
|
|
<label |
|
|
|
|
|
className="block absolute top-0 left-0 w-full h-full cursor-pointer" |
|
|
|
|
|
data-testid="clickArea" |
|
|
|
|
|
> |
|
|
<input |
|
|
<input |
|
|
{...etcProps} |
|
|
{...etcProps} |
|
|
|
|
|
disabled={disabled} |
|
|
ref={ref} |
|
|
ref={ref} |
|
|
type="file" |
|
|
type="file" |
|
|
|
|
|
className={`${enhanced ? 'sr-only' : ''}`} |
|
|
|
|
|
onChange={addFile} |
|
|
|
|
|
multiple={multiple} |
|
|
|
|
|
data-testid="input" |
|
|
/> |
|
|
/> |
|
|
</label> |
|
|
</label> |
|
|
{ |
|
|
{ |
|
|
border && ( |
|
|
border && ( |
|
|
<span |
|
|
<span |
|
|
data-testid="border" |
|
|
data-testid="border" |
|
|
|
|
|
className={ |
|
|
|
|
|
ButtonBase.Border({ |
|
|
|
|
|
size: ButtonBase.ButtonSize.MEDIUM, |
|
|
|
|
|
border: true, |
|
|
|
|
|
variant: ButtonBase.ButtonVariant.OUTLINE, |
|
|
|
|
|
disabled, |
|
|
|
|
|
compact: false, |
|
|
|
|
|
menuItem: false, |
|
|
|
|
|
block: true, |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
/> |
|
|
/> |
|
|
) |
|
|
) |
|
|
} |
|
|
} |
|
|
{ |
|
|
{ |
|
|
label && !hiddenLabel && ( |
|
|
|
|
|
|
|
|
label |
|
|
|
|
|
&& !hiddenLabel |
|
|
|
|
|
&& ( |
|
|
<div |
|
|
<div |
|
|
data-testid="label" |
|
|
data-testid="label" |
|
|
|
|
|
className="absolute top-0 left-0" |
|
|
> |
|
|
> |
|
|
{label} |
|
|
{label} |
|
|
</div> |
|
|
</div> |
|
|
) |
|
|
) |
|
|
} |
|
|
} |
|
|
{hint && ( |
|
|
|
|
|
<div |
|
|
|
|
|
data-testid="hint" |
|
|
|
|
|
> |
|
|
|
|
|
|
|
|
{ |
|
|
|
|
|
selectedFiles.length < 1 |
|
|
|
|
|
&& isEnhanced |
|
|
|
|
|
&& hint |
|
|
|
|
|
&& ( |
|
|
<div |
|
|
<div |
|
|
|
|
|
data-testid="hint" |
|
|
|
|
|
className="absolute top-0 left-0 w-full h-full pointer-events-none box-border overflow-hidden pt-4" |
|
|
> |
|
|
> |
|
|
{hint} |
|
|
|
|
|
|
|
|
<div className="flex items-center justify-center w-full h-full"> |
|
|
|
|
|
{hint} |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
{ |
|
|
|
|
|
selectedFiles.length > 0 |
|
|
|
|
|
&& isEnhanced |
|
|
|
|
|
&& ( |
|
|
|
|
|
<> |
|
|
|
|
|
<div className={`absolute top-0 left-0 w-full h-full pointer-events-none pb-12 box-border overflow-hidden ${multiple ? 'pt-8' : 'pt-4'}`}> |
|
|
|
|
|
<div className={`pointer-events-auto w-full h-full overflow-auto ${multiple ? '' : 'pt-4 box-border'}`}> |
|
|
|
|
|
<div |
|
|
|
|
|
className={`w-full px-4 box-border ${multiple ? 'grid gap-4 grid-cols-3 pb-4' : 'h-full'}`} |
|
|
|
|
|
> |
|
|
|
|
|
{selectedFiles.map((f, i) => ( |
|
|
|
|
|
<div data-testid="selectedFileItem" key={i} className={`${!multiple ? 'h-full flex gap-4 w-full' : 'w-full aspect-square'}`}> |
|
|
|
|
|
<div className={`h-full ${multiple ? 'w-full' : 'w-1/3 flex-shrink-0'}`}> |
|
|
|
|
|
{ |
|
|
|
|
|
f.previewUrl |
|
|
|
|
|
&& ( |
|
|
|
|
|
<img |
|
|
|
|
|
className="block w-full h-full object-center object-cover" |
|
|
|
|
|
src={f.previewUrl} |
|
|
|
|
|
alt={f.name} |
|
|
|
|
|
data-testid="preview" |
|
|
|
|
|
/> |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
</div> |
|
|
|
|
|
{ |
|
|
|
|
|
!multiple |
|
|
|
|
|
&& ( |
|
|
|
|
|
<dl className="w-2/3 flex-shrink-0 m-0" data-testid="infoBox"> |
|
|
|
|
|
<div className="w-full"> |
|
|
|
|
|
<dt className="sr-only"> |
|
|
|
|
|
Name |
|
|
|
|
|
</dt> |
|
|
|
|
|
<dd className="m-0 w-full text-ellipsis overflow-hidden"> |
|
|
|
|
|
{f.name} |
|
|
|
|
|
</dd> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className="w-full"> |
|
|
|
|
|
<dt className="sr-only"> |
|
|
|
|
|
Size |
|
|
|
|
|
</dt> |
|
|
|
|
|
<dd className="m-0 w-full text-ellipsis overflow-hidden"> |
|
|
|
|
|
{f.size} |
|
|
|
|
|
</dd> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className="w-full"> |
|
|
|
|
|
<dt className="sr-only"> |
|
|
|
|
|
Type |
|
|
|
|
|
</dt> |
|
|
|
|
|
<dd className="m-0 w-full text-ellipsis overflow-hidden"> |
|
|
|
|
|
{f.type} |
|
|
|
|
|
</dd> |
|
|
|
|
|
</div> |
|
|
|
|
|
</dl> |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
</div> |
|
|
|
|
|
))} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className="pointer-events-none absolute bottom-0 left-0 w-full text-center h-12 box-border flex"> |
|
|
|
|
|
<div className="w-0 flex-auto flex flex-col items-center justify-center h-full"> |
|
|
|
|
|
<span |
|
|
|
|
|
className={ButtonBase.Button({ |
|
|
|
|
|
size: ButtonBase.ButtonSize.MEDIUM, |
|
|
|
|
|
border: false, |
|
|
|
|
|
block: true, |
|
|
|
|
|
variant: ButtonBase.ButtonVariant.OUTLINE, |
|
|
|
|
|
disabled, |
|
|
|
|
|
compact: false, |
|
|
|
|
|
menuItem: false, |
|
|
|
|
|
})} |
|
|
|
|
|
> |
|
|
|
|
|
Reselect Files |
|
|
|
|
|
</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className="pointer-events-auto w-0 flex-auto flex flex-col items-center justify-center h-full"> |
|
|
|
|
|
<button |
|
|
|
|
|
data-testid="clear" |
|
|
|
|
|
type="button" |
|
|
|
|
|
onClick={deleteFiles} |
|
|
|
|
|
className={ButtonBase.Button({ |
|
|
|
|
|
size: ButtonBase.ButtonSize.MEDIUM, |
|
|
|
|
|
border: false, |
|
|
|
|
|
block: true, |
|
|
|
|
|
variant: ButtonBase.ButtonVariant.OUTLINE, |
|
|
|
|
|
disabled, |
|
|
|
|
|
compact: false, |
|
|
|
|
|
menuItem: false, |
|
|
|
|
|
})} |
|
|
|
|
|
> |
|
|
|
|
|
{multiple ? 'Clear Files' : 'Delete File'} |
|
|
|
|
|
</button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</> |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|