|
@@ -1,5 +1,5 @@ |
|
|
import * as React from 'react'; |
|
|
import * as React from 'react'; |
|
|
import { useClientSide, useFallbackId } from '@modal-sh/react-utils'; |
|
|
|
|
|
|
|
|
import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-utils'; |
|
|
import clsx from 'clsx'; |
|
|
import clsx from 'clsx'; |
|
|
|
|
|
|
|
|
export interface CommonPreviewProps<F extends Partial<File> = Partial<File>> { |
|
|
export interface CommonPreviewProps<F extends Partial<File> = Partial<File>> { |
|
@@ -14,7 +14,7 @@ export type FileSelectBoxDerivedElement = HTMLInputElement; |
|
|
export interface FileSelectBoxProps< |
|
|
export interface FileSelectBoxProps< |
|
|
F extends Partial<File> = Partial<File>, |
|
|
F extends Partial<File> = Partial<File>, |
|
|
P extends CommonPreviewProps<F> = CommonPreviewProps<F> |
|
|
P extends CommonPreviewProps<F> = CommonPreviewProps<F> |
|
|
> extends Omit<React.HTMLProps<FileSelectBoxDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { |
|
|
|
|
|
|
|
|
> extends Omit<React.HTMLProps<FileSelectBoxDerivedElement>, 'size' | 'type' | 'label' | 'list'> { |
|
|
/** |
|
|
/** |
|
|
* Should the component display a border? |
|
|
* Should the component display a border? |
|
|
*/ |
|
|
*/ |
|
@@ -39,264 +39,337 @@ export interface FileSelectBoxProps< |
|
|
* Is the label hidden? |
|
|
* Is the label hidden? |
|
|
*/ |
|
|
*/ |
|
|
hiddenLabel?: boolean, |
|
|
hiddenLabel?: boolean, |
|
|
previewComponent: React.ElementType<P>, |
|
|
|
|
|
|
|
|
previewComponent?: React.ElementType<P>, |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileSelectBoxProps>( |
|
|
|
|
|
( |
|
|
|
|
|
{ |
|
|
|
|
|
label = '', |
|
|
|
|
|
hint = '', |
|
|
|
|
|
border = false, |
|
|
|
|
|
block = false, |
|
|
|
|
|
enhanced: enhancedProp = false, |
|
|
|
|
|
hiddenLabel = false, |
|
|
|
|
|
multiple = false, |
|
|
|
|
|
onChange, |
|
|
|
|
|
disabled = false, |
|
|
|
|
|
className, |
|
|
|
|
|
id: idProp, |
|
|
|
|
|
previewComponent: FilePreviewComponent, |
|
|
|
|
|
...etcProps |
|
|
|
|
|
}: FileSelectBoxProps, |
|
|
|
|
|
forwardedRef, |
|
|
|
|
|
) => { |
|
|
|
|
|
const { clientSide } = useClientSide({ clientSide: enhancedProp }); |
|
|
|
|
|
const [fileList, setFileList] = React.useState<FileList>(); |
|
|
|
|
|
const [lastUpdated, setLastUpdated] = React.useState<number>(); |
|
|
|
|
|
const defaultRef = React.useRef<HTMLInputElement>(null); |
|
|
|
|
|
const ref = forwardedRef ?? defaultRef; |
|
|
|
|
|
const labelId = React.useId(); |
|
|
|
|
|
const id = useFallbackId(idProp); |
|
|
|
|
|
|
|
|
export const FileSelectBoxDefaultPreviewComponent: React.FC<CommonPreviewProps> = ({ |
|
|
|
|
|
file, |
|
|
|
|
|
mini, |
|
|
|
|
|
}) => ( |
|
|
|
|
|
<> |
|
|
|
|
|
<div className="w-full whitespace-nowrap overflow-hidden text-ellipsis"> |
|
|
|
|
|
{file?.name ?? ( |
|
|
|
|
|
<span className="opacity-50"> |
|
|
|
|
|
File |
|
|
|
|
|
</span> |
|
|
|
|
|
)} |
|
|
|
|
|
</div> |
|
|
|
|
|
{!mini && ( |
|
|
|
|
|
<> |
|
|
|
|
|
{typeof file?.type === 'string' && ( |
|
|
|
|
|
<div className="w-full whitespace-nowrap overflow-hidden text-ellipsis">{file?.type}</div> |
|
|
|
|
|
)} |
|
|
|
|
|
{typeof file?.size === 'number' && ( |
|
|
|
|
|
<div className="w-full whitespace-nowrap overflow-hidden text-ellipsis"> |
|
|
|
|
|
{new Intl.NumberFormat(undefined, { |
|
|
|
|
|
style: 'unit', |
|
|
|
|
|
unit: 'byte', |
|
|
|
|
|
unitDisplay: 'long', |
|
|
|
|
|
}).format(file.size ?? 0)} |
|
|
|
|
|
</div> |
|
|
|
|
|
)} |
|
|
|
|
|
{typeof file?.lastModified === 'number' && ( |
|
|
|
|
|
<div className="w-full whitespace-nowrap overflow-hidden text-ellipsis"> |
|
|
|
|
|
<time dateTime={new Date(file.lastModified).toISOString()}> |
|
|
|
|
|
{new Date(file.lastModified).toDateString()} |
|
|
|
|
|
</time> |
|
|
|
|
|
</div> |
|
|
|
|
|
)} |
|
|
|
|
|
</> |
|
|
|
|
|
)} |
|
|
|
|
|
</> |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const doSetFileList: React.ChangeEventHandler<HTMLInputElement> = (e) => { |
|
|
|
|
|
if (enhancedProp) { |
|
|
|
|
|
setFileList(e.currentTarget.files as FileList); |
|
|
|
|
|
|
|
|
export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileSelectBoxProps>(( |
|
|
|
|
|
{ |
|
|
|
|
|
label = '', |
|
|
|
|
|
hint = '', |
|
|
|
|
|
border = false, |
|
|
|
|
|
block = false, |
|
|
|
|
|
enhanced: enhancedProp = false, |
|
|
|
|
|
hiddenLabel = false, |
|
|
|
|
|
multiple = false, |
|
|
|
|
|
disabled = false, |
|
|
|
|
|
className, |
|
|
|
|
|
onChange, |
|
|
|
|
|
id: idProp, |
|
|
|
|
|
placeholder, |
|
|
|
|
|
previewComponent: FilePreviewComponent = FileSelectBoxDefaultPreviewComponent, |
|
|
|
|
|
style, |
|
|
|
|
|
...etcProps |
|
|
|
|
|
}, |
|
|
|
|
|
forwardedRef, |
|
|
|
|
|
) => { |
|
|
|
|
|
const { clientSide } = useClientSide({ clientSide: enhancedProp }); |
|
|
|
|
|
const [fileList, setFileList] = React.useState<FileList>(); |
|
|
|
|
|
const [lastUpdated, setLastUpdated] = React.useState<number>(); |
|
|
|
|
|
const clearFileListRef = React.useRef(false); |
|
|
|
|
|
const { defaultRef, handleChange: doSetFileList } = useProxyInput< |
|
|
|
|
|
React.ChangeEvent<FileSelectBoxDerivedElement> |
|
|
|
|
|
| React.MouseEvent<HTMLButtonElement> |
|
|
|
|
|
| React.DragEvent<HTMLDivElement> |
|
|
|
|
|
| React.KeyboardEvent<FileSelectBoxDerivedElement>, |
|
|
|
|
|
FileSelectBoxDerivedElement |
|
|
|
|
|
>({ |
|
|
|
|
|
forwardedRef, |
|
|
|
|
|
valueSetterFn: (e) => { |
|
|
|
|
|
if (e.type === 'click') { |
|
|
|
|
|
// delete |
|
|
|
|
|
const fileInput = defaultRef.current as FileSelectBoxDerivedElement; |
|
|
|
|
|
clearFileListRef.current = true; |
|
|
|
|
|
fileInput.value = ''; |
|
|
|
|
|
setFileList(undefined); |
|
|
setLastUpdated(Date.now()); |
|
|
setLastUpdated(Date.now()); |
|
|
|
|
|
} else if (e.type === 'drop') { |
|
|
|
|
|
// drop file |
|
|
|
|
|
const fileInput = defaultRef.current as FileSelectBoxDerivedElement; |
|
|
|
|
|
const { dataTransfer } = e as React.DragEvent<HTMLDivElement>; |
|
|
|
|
|
const { files } = dataTransfer; |
|
|
|
|
|
if (files && files.length > 0) { |
|
|
|
|
|
setFileList(fileInput.files = files); |
|
|
|
|
|
setLastUpdated(Date.now()); |
|
|
|
|
|
} |
|
|
|
|
|
} else if (e.type === 'keyup') { |
|
|
|
|
|
const { |
|
|
|
|
|
currentTarget: fileInput, |
|
|
|
|
|
code, |
|
|
|
|
|
} = e as React.KeyboardEvent<FileSelectBoxDerivedElement>; |
|
|
|
|
|
if (code === 'Backspace' || code === 'Delete') { |
|
|
|
|
|
clearFileListRef.current = true; |
|
|
|
|
|
fileInput.value = ''; |
|
|
|
|
|
setFileList(undefined); |
|
|
|
|
|
setLastUpdated(Date.now()); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
onChange?.(e); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const doClearFileList: React.MouseEventHandler<HTMLButtonElement> = (e) => { |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
if (!(typeof ref === 'object' && ref)) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const { current } = ref; |
|
|
|
|
|
if (!current) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
}, |
|
|
|
|
|
}); |
|
|
|
|
|
const ref = forwardedRef ?? defaultRef; |
|
|
|
|
|
const labelId = React.useId(); |
|
|
|
|
|
const id = useFallbackId(idProp); |
|
|
|
|
|
|
|
|
current.value = ''; |
|
|
|
|
|
setFileList(undefined); |
|
|
|
|
|
setLastUpdated(Date.now()); |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
delegateTriggerEvent('change', current); |
|
|
|
|
|
}); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const cancelEvent = (e: React.DragEvent) => { |
|
|
|
|
|
e.stopPropagation(); |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const cancelEvent = (e: React.DragEvent) => { |
|
|
|
|
|
e.stopPropagation(); |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const filesCount = fileList?.length ?? 0; |
|
|
|
|
|
|
|
|
const handleDropZone: React.DragEventHandler<HTMLDivElement> = async (e) => { |
|
|
|
|
|
cancelEvent(e); |
|
|
|
|
|
if (!(typeof ref === 'object' && ref)) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const { current } = ref; |
|
|
|
|
|
if (!current) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const { dataTransfer } = e; |
|
|
|
|
|
const { files } = dataTransfer; |
|
|
|
|
|
if (!(files && files.length > 0)) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
setFileList(current.files = files); |
|
|
|
|
|
|
|
|
const handleFileChange: React.ChangeEventHandler<FileSelectBoxDerivedElement> = (e) => { |
|
|
|
|
|
const { currentTarget } = e; |
|
|
|
|
|
if (clientSide && currentTarget.files && currentTarget.files.length > 0) { |
|
|
|
|
|
setFileList(currentTarget.files); |
|
|
setLastUpdated(Date.now()); |
|
|
setLastUpdated(Date.now()); |
|
|
setTimeout(() => { |
|
|
|
|
|
delegateTriggerEvent('change', current); |
|
|
|
|
|
}); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
onChange?.(e); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
if (clientSide && clearFileListRef.current) { |
|
|
|
|
|
clearFileListRef.current = false; |
|
|
|
|
|
setFileList(undefined); |
|
|
|
|
|
setLastUpdated(Date.now()); |
|
|
|
|
|
onChange?.(e); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
e.currentTarget.files = fileList ?? null; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const filesCount = fileList?.length ?? 0; |
|
|
|
|
|
|
|
|
const handleKeyUp: React.KeyboardEventHandler<FileSelectBoxDerivedElement> = (e) => { |
|
|
|
|
|
const { code } = e; |
|
|
|
|
|
if (code === 'Backspace' || code === 'Delete') { |
|
|
|
|
|
doSetFileList(e); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<div |
|
|
|
|
|
className={clsx( |
|
|
|
|
|
'relative rounded ring-secondary/50 group', |
|
|
|
|
|
'focus-within:ring-4', |
|
|
|
|
|
block && 'w-full', |
|
|
|
|
|
!block && 'inline-block min-w-64', |
|
|
|
|
|
className, |
|
|
|
|
|
)} |
|
|
|
|
|
onDragEnter={cancelEvent} |
|
|
|
|
|
onDragOver={cancelEvent} |
|
|
|
|
|
onDrop={handleDropZone} |
|
|
|
|
|
data-testid="root" |
|
|
|
|
|
> |
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<div |
|
|
|
|
|
className={clsx( |
|
|
|
|
|
'relative rounded ring-secondary/50 group', |
|
|
|
|
|
'focus-within:ring-4', |
|
|
|
|
|
block && 'flex w-full', |
|
|
|
|
|
!block && 'inline-flex w-64 min-h-16 justify-center items-center', |
|
|
|
|
|
className, |
|
|
|
|
|
clientSide && 'resize-y', |
|
|
|
|
|
)} |
|
|
|
|
|
onDragEnter={clientSide ? cancelEvent : undefined} |
|
|
|
|
|
onDragOver={clientSide ? cancelEvent : undefined} |
|
|
|
|
|
onDrop={clientSide ? (e) => { |
|
|
|
|
|
cancelEvent(e); |
|
|
|
|
|
doSetFileList(e); |
|
|
|
|
|
} : undefined} |
|
|
|
|
|
data-testid="root" |
|
|
|
|
|
style={{ |
|
|
|
|
|
height: clientSide ? 64 : undefined, |
|
|
|
|
|
...(style ?? {}), |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{clientSide && ( |
|
|
<label |
|
|
<label |
|
|
className="block absolute top-0 left-0 w-full h-full cursor-pointer" |
|
|
|
|
|
|
|
|
className={clsx( |
|
|
|
|
|
'flex items-center justify-center absolute top-0 left-0 w-full h-full cursor-pointer', |
|
|
|
|
|
(fileList?.length ?? 0) > 0 && 'opacity-0', |
|
|
|
|
|
)} |
|
|
data-testid="clickArea" |
|
|
data-testid="clickArea" |
|
|
htmlFor={id} |
|
|
htmlFor={id} |
|
|
/> |
|
|
|
|
|
<input |
|
|
|
|
|
{...etcProps} |
|
|
|
|
|
id={id} |
|
|
|
|
|
disabled={disabled} |
|
|
|
|
|
ref={ref} |
|
|
|
|
|
type="file" |
|
|
|
|
|
|
|
|
> |
|
|
|
|
|
{placeholder} |
|
|
|
|
|
</label> |
|
|
|
|
|
)} |
|
|
|
|
|
<input |
|
|
|
|
|
{...etcProps} |
|
|
|
|
|
id={id} |
|
|
|
|
|
disabled={disabled} |
|
|
|
|
|
ref={typeof ref === 'function' ? defaultRef : ref} |
|
|
|
|
|
type="file" |
|
|
|
|
|
onKeyUp={clientSide ? handleKeyUp : undefined} |
|
|
|
|
|
className={clsx( |
|
|
|
|
|
'peer box-border focus:outline-0 px-4 pt-2 block resize-y min-h-16 cursor-pointer disabled:cursor-not-allowed file:bg-transparent file:text-primary file:block file:font-bold file:font-semi-expanded file:uppercase file:p-0 file:border-0 group-focus-within:file:text-secondary', |
|
|
|
|
|
{ |
|
|
|
|
|
'sr-only': clientSide, |
|
|
|
|
|
'h-full w-full': !clientSide, |
|
|
|
|
|
}, |
|
|
|
|
|
)} |
|
|
|
|
|
onChange={clientSide ? handleFileChange : onChange} |
|
|
|
|
|
multiple={multiple} |
|
|
|
|
|
data-testid="input" |
|
|
|
|
|
aria-labelledby={label ? `${labelId}` : undefined} |
|
|
|
|
|
style={{ |
|
|
|
|
|
height: clientSide ? undefined : 256, |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
{label && ( |
|
|
|
|
|
<div |
|
|
|
|
|
data-testid="label" |
|
|
|
|
|
id={labelId} |
|
|
className={clsx( |
|
|
className={clsx( |
|
|
'peer', |
|
|
|
|
|
|
|
|
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 group-focus-within:text-secondary text-primary leading-none bg-negative', |
|
|
{ |
|
|
{ |
|
|
'sr-only': clientSide, |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
'sr-only': hiddenLabel, |
|
|
|
|
|
}, |
|
|
)} |
|
|
)} |
|
|
onChange={doSetFileList} |
|
|
|
|
|
multiple={multiple} |
|
|
|
|
|
data-testid="input" |
|
|
|
|
|
aria-labelledby={label ? `${labelId}` : undefined} |
|
|
|
|
|
/> |
|
|
|
|
|
{ |
|
|
|
|
|
label && ( |
|
|
|
|
|
<div |
|
|
|
|
|
data-testid="label" |
|
|
|
|
|
id={labelId} |
|
|
|
|
|
className={clsx( |
|
|
|
|
|
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 group-focus-within:text-secondary text-primary leading-none bg-negative', |
|
|
|
|
|
{ |
|
|
|
|
|
'sr-only': hiddenLabel, |
|
|
|
|
|
}, |
|
|
|
|
|
)} |
|
|
|
|
|
> |
|
|
|
|
|
<div className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> |
|
|
|
|
|
{label} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
{ |
|
|
|
|
|
filesCount < 1 |
|
|
|
|
|
&& clientSide |
|
|
|
|
|
&& hint |
|
|
|
|
|
&& ( |
|
|
|
|
|
|
|
|
> |
|
|
|
|
|
<div className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> |
|
|
|
|
|
{label} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
)} |
|
|
|
|
|
{filesCount < 1 |
|
|
|
|
|
&& clientSide |
|
|
|
|
|
&& hint |
|
|
|
|
|
&& ( |
|
|
|
|
|
<div |
|
|
|
|
|
data-testid="hint" |
|
|
|
|
|
className="absolute top-0 left-0 w-full h-full pointer-events-none box-border overflow-hidden pt-4" |
|
|
|
|
|
> |
|
|
|
|
|
<div className="flex items-center justify-center w-full h-full"> |
|
|
|
|
|
{hint} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
)} |
|
|
|
|
|
{filesCount > 0 |
|
|
|
|
|
&& clientSide |
|
|
|
|
|
&& ( |
|
|
|
|
|
<React.Fragment key={lastUpdated}> |
|
|
|
|
|
<div className="sm:absolute top-0 left-0 w-full h-full pointer-events-none pb-12 box-border overflow-hidden pt-8"> |
|
|
<div |
|
|
<div |
|
|
data-testid="hint" |
|
|
|
|
|
className="absolute top-0 left-0 w-full h-full pointer-events-none box-border overflow-hidden pt-4" |
|
|
|
|
|
|
|
|
className="pointer-events-auto w-full h-full px-4 pb-4 box-border" |
|
|
> |
|
|
> |
|
|
<div className="flex items-center justify-center w-full h-full"> |
|
|
|
|
|
{hint} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
{ |
|
|
|
|
|
filesCount > 0 |
|
|
|
|
|
&& clientSide |
|
|
|
|
|
&& ( |
|
|
|
|
|
<React.Fragment key={lastUpdated}> |
|
|
|
|
|
<div className={`sm:absolute top-0 left-0 w-full h-full pointer-events-none pb-12 box-border overflow-hidden pt-8`}> |
|
|
|
|
|
<div |
|
|
|
|
|
className={`pointer-events-auto w-full h-full px-4 pb-4 box-border`} |
|
|
|
|
|
> |
|
|
|
|
|
{ |
|
|
|
|
|
multiple |
|
|
|
|
|
&& ( |
|
|
|
|
|
<div className="w-full h-full overflow-auto -mx-4 px-4"> |
|
|
|
|
|
<div className="w-full grid gap-4 grid-cols-3"> |
|
|
|
|
|
{Array.from(fileList ?? []).map((file, i) => { |
|
|
|
|
|
return ( |
|
|
|
|
|
<div |
|
|
|
|
|
key={`${file.name}:${i}`} |
|
|
|
|
|
data-testid="selectedFileItem" |
|
|
|
|
|
className={`w-full aspect-square rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10`} |
|
|
|
|
|
> |
|
|
|
|
|
<FilePreviewComponent |
|
|
|
|
|
file={file} |
|
|
|
|
|
enhanced={clientSide} |
|
|
|
|
|
disabled={disabled} |
|
|
|
|
|
mini |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
); |
|
|
|
|
|
})} |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
{multiple |
|
|
|
|
|
&& ( |
|
|
|
|
|
<div className="w-full h-full overflow-auto -mx-4 px-4"> |
|
|
|
|
|
<div className="w-full grid gap-4 grid-cols-3"> |
|
|
|
|
|
{Array.from(fileList ?? []).map((file, i) => ( |
|
|
|
|
|
<div |
|
|
|
|
|
key={`${file.name}:${i}`} |
|
|
|
|
|
data-testid="selectedFileItem" |
|
|
|
|
|
className="w-full aspect-square rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10" |
|
|
|
|
|
> |
|
|
|
|
|
<FilePreviewComponent |
|
|
|
|
|
file={file} |
|
|
|
|
|
enhanced={clientSide} |
|
|
|
|
|
disabled={disabled} |
|
|
|
|
|
mini |
|
|
|
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
{ |
|
|
|
|
|
!multiple |
|
|
|
|
|
&& Array.from(fileList ?? []).map((file, i) => { |
|
|
|
|
|
return ( |
|
|
|
|
|
<div |
|
|
|
|
|
key={`${file.name}:${i}`} |
|
|
|
|
|
className="w-full h-full" |
|
|
|
|
|
> |
|
|
|
|
|
<div |
|
|
|
|
|
data-testid="selectedFileItem" |
|
|
|
|
|
className="h-full w-full p-4 box-border rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10" |
|
|
|
|
|
> |
|
|
|
|
|
<div |
|
|
|
|
|
className="w-full h-full relative" |
|
|
|
|
|
> |
|
|
|
|
|
<FilePreviewComponent |
|
|
|
|
|
file={file} |
|
|
|
|
|
enhanced={clientSide} |
|
|
|
|
|
disabled={disabled} |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
) |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className="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"> |
|
|
|
|
|
<label |
|
|
|
|
|
data-testid="reselect" |
|
|
|
|
|
htmlFor={id} |
|
|
|
|
|
className="flex w-full h-full bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none" |
|
|
|
|
|
> |
|
|
|
|
|
<span |
|
|
|
|
|
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" |
|
|
|
|
|
> |
|
|
|
|
|
Reselect |
|
|
|
|
|
</span> |
|
|
|
|
|
</label> |
|
|
|
|
|
|
|
|
))} |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div className="w-0 flex-auto flex flex-col items-center justify-center h-full"> |
|
|
|
|
|
<button |
|
|
|
|
|
data-testid="clear" |
|
|
|
|
|
type="button" |
|
|
|
|
|
onClick={doClearFileList} |
|
|
|
|
|
className="flex w-full h-full bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none focus:outline-0" |
|
|
|
|
|
|
|
|
)} |
|
|
|
|
|
{!multiple |
|
|
|
|
|
&& Array.from(fileList ?? []).map((file, i) => ( |
|
|
|
|
|
<div |
|
|
|
|
|
key={`${file.name}:${i}`} |
|
|
|
|
|
className="w-full h-full" |
|
|
|
|
|
> |
|
|
|
|
|
<div |
|
|
|
|
|
data-testid="selectedFileItem" |
|
|
|
|
|
className="h-full w-full p-4 box-border rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10" |
|
|
> |
|
|
> |
|
|
<span |
|
|
|
|
|
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" |
|
|
|
|
|
|
|
|
<div |
|
|
|
|
|
className="w-full h-full relative" |
|
|
> |
|
|
> |
|
|
Clear |
|
|
|
|
|
</span> |
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
|
<FilePreviewComponent |
|
|
|
|
|
file={file} |
|
|
|
|
|
enhanced={clientSide} |
|
|
|
|
|
disabled={disabled} |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
</React.Fragment> |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
{ |
|
|
|
|
|
border && ( |
|
|
|
|
|
<span |
|
|
|
|
|
data-testid="border" |
|
|
|
|
|
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary" |
|
|
|
|
|
/> |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
</div> |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
))} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className="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"> |
|
|
|
|
|
<label |
|
|
|
|
|
data-testid="reselect" |
|
|
|
|
|
htmlFor={id} |
|
|
|
|
|
className="flex w-full h-full bg-negative text-primary disabled:text-primary cursor-pointer group-focus-within:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none" |
|
|
|
|
|
> |
|
|
|
|
|
<span |
|
|
|
|
|
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" |
|
|
|
|
|
> |
|
|
|
|
|
Reselect |
|
|
|
|
|
</span> |
|
|
|
|
|
</label> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className="w-0 flex-auto flex flex-col items-center justify-center h-full"> |
|
|
|
|
|
<button |
|
|
|
|
|
data-testid="clear" |
|
|
|
|
|
type="button" |
|
|
|
|
|
name="action" |
|
|
|
|
|
value="clear" |
|
|
|
|
|
onClick={doSetFileList} |
|
|
|
|
|
tabIndex={-1} |
|
|
|
|
|
className="flex w-full h-full bg-negative text-primary disabled:text-primary group-focus-within:text-secondary group-focus-within:active:text-tertiary items-center justify-center leading-none gap-4 select-none focus:outline-0" |
|
|
|
|
|
> |
|
|
|
|
|
<span |
|
|
|
|
|
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" |
|
|
|
|
|
> |
|
|
|
|
|
Clear |
|
|
|
|
|
</span> |
|
|
|
|
|
</button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</React.Fragment> |
|
|
|
|
|
)} |
|
|
|
|
|
{border && ( |
|
|
|
|
|
<span |
|
|
|
|
|
data-testid="border" |
|
|
|
|
|
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary" |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
</div> |
|
|
|
|
|
); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
FileSelectBox.displayName = 'FileSelectBox'; |
|
|
FileSelectBox.displayName = 'FileSelectBox'; |
|
|
|
|
|
|
|
|
|
|
|
FileSelectBox.defaultProps = { |
|
|
|
|
|
border: false, |
|
|
|
|
|
block: false, |
|
|
|
|
|
enhanced: false, |
|
|
|
|
|
hiddenLabel: false, |
|
|
|
|
|
label: undefined, |
|
|
|
|
|
hint: undefined, |
|
|
|
|
|
previewComponent: FileSelectBoxDefaultPreviewComponent, |
|
|
|
|
|
}; |