|
|
@@ -153,6 +153,7 @@ export interface FileSelectBoxProps< |
|
|
|
* Clear label. |
|
|
|
*/ |
|
|
|
clearLabel?: string, |
|
|
|
resize?: boolean, |
|
|
|
} |
|
|
|
|
|
|
|
const isButtonElement = (el: HTMLElement): el is FileSelectBoxActionElement => el.tagName === 'BUTTON'; |
|
|
@@ -180,14 +181,14 @@ type SelectKey = typeof SELECT_KEYS[number]; |
|
|
|
*/ |
|
|
|
export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileSelectBoxProps>(( |
|
|
|
{ |
|
|
|
label = '', |
|
|
|
hint = '', |
|
|
|
border = false, |
|
|
|
block = false, |
|
|
|
enhanced: enhancedProp = false, |
|
|
|
hiddenLabel = false, |
|
|
|
multiple = false, |
|
|
|
disabled = false, |
|
|
|
label = '' as const, |
|
|
|
hint = '' as const, |
|
|
|
border = false as const, |
|
|
|
block = false as const, |
|
|
|
enhanced: enhancedProp = false as const, |
|
|
|
hiddenLabel = false as const, |
|
|
|
multiple = false as const, |
|
|
|
disabled = false as const, |
|
|
|
className, |
|
|
|
onChange, |
|
|
|
id: idProp, |
|
|
@@ -195,9 +196,10 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
previewComponent: FilePreviewComponent = FileSelectBoxDefaultPreviewComponent, |
|
|
|
style, |
|
|
|
onBlur, |
|
|
|
reselectLabel = 'Reselect', |
|
|
|
clearLabel = 'Clear', |
|
|
|
reselectLabel = 'Reselect' as const, |
|
|
|
clearLabel = 'Clear' as const, |
|
|
|
onPaste, |
|
|
|
resize = false as const, |
|
|
|
...etcProps |
|
|
|
}, |
|
|
|
forwardedRef, |
|
|
@@ -302,12 +304,16 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
setFileList(currentTarget.files); |
|
|
|
setLastUpdated(Date.now()); |
|
|
|
setAboutToSelect(false); |
|
|
|
} else if (clearFileListRef.current) { |
|
|
|
onChange?.(e); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (clearFileListRef.current) { |
|
|
|
clearFileListRef.current = false; |
|
|
|
setFileList(undefined); |
|
|
|
setLastUpdated(Date.now()); |
|
|
|
onChange?.(e); |
|
|
|
} |
|
|
|
onChange?.(e); |
|
|
|
// prevent triggering onChange when the user cancels the file picker. |
|
|
|
}, [clearFileListRef, onChange]); |
|
|
|
|
|
|
|
const handleKeyDown: React.KeyboardEventHandler< |
|
|
@@ -328,10 +334,12 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
const handleReselectMouseDown: React.MouseEventHandler< |
|
|
|
FileSelectBoxActionElement |
|
|
|
> = React.useCallback(() => { |
|
|
|
setAboutToSelect(true); |
|
|
|
setTimeout(() => { |
|
|
|
const fileInput = defaultRef.current as FileSelectBoxDerivedElement; |
|
|
|
fileInput.focus(); |
|
|
|
setAboutToSelect(true); |
|
|
|
setTimeout(() => { |
|
|
|
const fileInput = defaultRef.current as FileSelectBoxDerivedElement; |
|
|
|
fileInput.focus(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}, [defaultRef]); |
|
|
|
|
|
|
@@ -349,11 +357,13 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
const handleDeleteMouseDown: React.MouseEventHandler< |
|
|
|
FileSelectBoxActionElement |
|
|
|
> = React.useCallback(() => { |
|
|
|
setAboutToClear(true); |
|
|
|
setTimeout(() => { |
|
|
|
const fileInput = defaultRef.current as FileSelectBoxDerivedElement; |
|
|
|
fileInput.focus(); |
|
|
|
}); |
|
|
|
setTimeout(() => { |
|
|
|
setAboutToClear(true); |
|
|
|
setTimeout(() => { |
|
|
|
const fileInput = defaultRef.current as FileSelectBoxDerivedElement; |
|
|
|
fileInput.focus(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}, [defaultRef]); |
|
|
|
|
|
|
|
const handleBlur: React.FocusEventHandler< |
|
|
@@ -385,204 +395,211 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
'relative rounded ring-secondary/50 group file-select-box', |
|
|
|
'focus-within:ring-4', |
|
|
|
block && 'flex w-full', |
|
|
|
!block && 'inline-flex w-64 min-h-16 justify-center items-center', |
|
|
|
!block && 'inline-flex align-top w-64 justify-center items-center', |
|
|
|
className, |
|
|
|
)} |
|
|
|
onDragEnter={clientSide ? cancelEvent : undefined} |
|
|
|
onDragOver={clientSide ? cancelEvent : undefined} |
|
|
|
onDrop={clientSide ? handleDrop : undefined} |
|
|
|
data-testid="root" |
|
|
|
style={{ |
|
|
|
height: clientSide ? DEFAULT_ENHANCED_HEIGHT_PX : undefined, |
|
|
|
...(style ?? {}), |
|
|
|
}} |
|
|
|
style={style} |
|
|
|
> |
|
|
|
{label && ( |
|
|
|
<label |
|
|
|
data-testid="label" |
|
|
|
id={labelId} |
|
|
|
htmlFor={id} |
|
|
|
className={clsx( |
|
|
|
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none select-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> |
|
|
|
</label> |
|
|
|
)} |
|
|
|
{clientSide && ( |
|
|
|
<FileSelectBoxActionElementComponent |
|
|
|
type="button" |
|
|
|
disabled={disabled} |
|
|
|
tabIndex={-1} |
|
|
|
className={clsx( |
|
|
|
'flex items-center focus:outline-0 justify-center absolute top-0 left-0 w-full h-full cursor-pointer select-none', |
|
|
|
(fileList?.length ?? 0) > 0 && 'opacity-0', |
|
|
|
)} |
|
|
|
data-testid="clickArea" |
|
|
|
onMouseDown={handleReselectMouseDown} |
|
|
|
onMouseUp={handleReselectMouseUp} |
|
|
|
> |
|
|
|
{placeholder} |
|
|
|
</FileSelectBoxActionElementComponent> |
|
|
|
)} |
|
|
|
<FileSelectBoxDerivedElementComponent |
|
|
|
{...etcProps} |
|
|
|
id={id} |
|
|
|
disabled={disabled} |
|
|
|
ref={defaultRef} |
|
|
|
type="file" |
|
|
|
onBlur={handleBlur} |
|
|
|
onKeyDown={clientSide ? handleKeyDown : undefined} |
|
|
|
onKeyUp={clientSide ? doSetFileList : 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} |
|
|
|
onPaste={clientSide ? doSetFileList : onPaste} |
|
|
|
multiple={multiple} |
|
|
|
data-testid="input" |
|
|
|
aria-labelledby={label ? `${labelId}` : undefined} |
|
|
|
style={{ |
|
|
|
height: clientSide ? undefined : DEFAULT_NON_ENHANCED_SIDE_HEIGHT_PX, |
|
|
|
}} |
|
|
|
/> |
|
|
|
{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" |
|
|
|
data-testid="preview" |
|
|
|
> |
|
|
|
{multiple |
|
|
|
&& ( |
|
|
|
<div className="pointer-events-auto w-full h-full overflow-auto px-4 box-border"> |
|
|
|
<div className="w-full grid gap-2 grid-cols-3"> |
|
|
|
{fileListArray.map((file, i) => ( |
|
|
|
<div |
|
|
|
key={file.name ?? i} |
|
|
|
data-testid="selectedFileItem" |
|
|
|
className="w-full p-2 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> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
{!multiple |
|
|
|
&& fileListArray.map((file, i) => ( |
|
|
|
<div |
|
|
|
key={file.name ?? i} |
|
|
|
className="pointer-events-auto w-full h-full px-4 box-border" |
|
|
|
> |
|
|
|
<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 |
|
|
|
data-testid="actions" |
|
|
|
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"> |
|
|
|
<FileSelectBoxActionElementComponent |
|
|
|
type="button" |
|
|
|
data-testid="reselect" |
|
|
|
disabled={disabled} |
|
|
|
onMouseDown={handleReselectMouseDown} |
|
|
|
onMouseUp={handleReselectMouseUp} |
|
|
|
tabIndex={-1} |
|
|
|
className={clsx( |
|
|
|
'flex w-full h-full focus:outline-0 bg-negative text-primary cursor-pointer items-center justify-center leading-none gap-4 select-none', |
|
|
|
{ |
|
|
|
'group-focus-within:text-secondary group-focus-within:active:text-tertiary': !aboutToSelect, |
|
|
|
'text-tertiary': aboutToSelect, |
|
|
|
}, |
|
|
|
)} |
|
|
|
> |
|
|
|
<div |
|
|
|
className={clsx( |
|
|
|
'w-full overflow-hidden min-h-16 relative pt-4 min-h-16 rounded-inherit', |
|
|
|
{ |
|
|
|
'h-4': clientSide, |
|
|
|
'h-16': !clientSide, |
|
|
|
'resize-y': resize, |
|
|
|
'pb-4': clientSide && filesCount > 0, |
|
|
|
}, |
|
|
|
)} |
|
|
|
> |
|
|
|
{label && ( |
|
|
|
<label |
|
|
|
data-testid="label" |
|
|
|
id={labelId} |
|
|
|
htmlFor={id} |
|
|
|
className={clsx( |
|
|
|
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none select-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> |
|
|
|
</label> |
|
|
|
)} |
|
|
|
{clientSide && filesCount <= 0 && ( |
|
|
|
<FileSelectBoxActionElementComponent |
|
|
|
type="button" |
|
|
|
disabled={disabled} |
|
|
|
tabIndex={-1} |
|
|
|
className={clsx( |
|
|
|
'flex items-center focus:outline-0 justify-center absolute top-0 left-0 w-full h-full cursor-pointer select-none', |
|
|
|
(fileList?.length ?? 0) > 0 && 'opacity-0', |
|
|
|
)} |
|
|
|
data-testid="clickArea" |
|
|
|
onMouseDown={handleReselectMouseDown} |
|
|
|
onMouseUp={handleReselectMouseUp} |
|
|
|
> |
|
|
|
{placeholder} |
|
|
|
</FileSelectBoxActionElementComponent> |
|
|
|
)} |
|
|
|
<FileSelectBoxDerivedElementComponent |
|
|
|
{...etcProps} |
|
|
|
id={id} |
|
|
|
disabled={disabled} |
|
|
|
ref={defaultRef} |
|
|
|
type="file" |
|
|
|
onBlur={handleBlur} |
|
|
|
onKeyDown={clientSide ? handleKeyDown : undefined} |
|
|
|
onKeyUp={clientSide ? doSetFileList : undefined} |
|
|
|
className={clsx( |
|
|
|
'peer box-border focus:outline-0 px-4 file:cursor-pointer disabled:file:cursor-pointer block 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} |
|
|
|
onPaste={clientSide ? doSetFileList : onPaste} |
|
|
|
multiple={multiple} |
|
|
|
data-testid="input" |
|
|
|
aria-labelledby={label ? `${labelId}` : undefined} |
|
|
|
/> |
|
|
|
{filesCount <= 0 |
|
|
|
&& clientSide |
|
|
|
&& hint |
|
|
|
&& ( |
|
|
|
<div |
|
|
|
data-testid="hint" |
|
|
|
className="w-full h-full pointer-events-none box-border overflow-hidden" |
|
|
|
> |
|
|
|
<div className="flex items-center justify-center w-full h-full px-4"> |
|
|
|
{hint} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
{filesCount > 0 |
|
|
|
&& clientSide |
|
|
|
&& ( |
|
|
|
<div |
|
|
|
key={lastUpdated} |
|
|
|
className="flex flex-col h-full gap-4" |
|
|
|
> |
|
|
|
<div |
|
|
|
className="flex-auto pointer-events-none overflow-hidden" |
|
|
|
data-testid="preview" |
|
|
|
> |
|
|
|
{multiple |
|
|
|
&& ( |
|
|
|
<div className="pointer-events-auto w-full h-full overflow-auto px-4 box-border"> |
|
|
|
<div className="w-full grid gap-2 grid-cols-3"> |
|
|
|
{fileListArray.map((file, i) => ( |
|
|
|
<div |
|
|
|
key={file.name ?? i} |
|
|
|
data-testid="selectedFileItem" |
|
|
|
className="w-full p-2 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> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
{!multiple |
|
|
|
&& fileListArray.map((file, i) => ( |
|
|
|
<div |
|
|
|
key={file.name ?? i} |
|
|
|
className="pointer-events-auto w-full h-full px-4 box-border" |
|
|
|
> |
|
|
|
<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 |
|
|
|
data-testid="actions" |
|
|
|
className="w-full text-center h-8 flex" |
|
|
|
> |
|
|
|
<div className="w-0 flex-auto flex flex-col items-center justify-center h-full"> |
|
|
|
<FileSelectBoxActionElementComponent |
|
|
|
type="button" |
|
|
|
data-testid="reselect" |
|
|
|
disabled={disabled} |
|
|
|
onMouseDown={handleReselectMouseDown} |
|
|
|
onMouseUp={handleReselectMouseUp} |
|
|
|
tabIndex={-1} |
|
|
|
className={clsx( |
|
|
|
'flex w-full h-full focus:outline-0 bg-negative text-primary cursor-pointer items-center justify-center leading-none gap-4 select-none', |
|
|
|
{ |
|
|
|
'group-focus-within:text-secondary group-focus-within:active:text-tertiary': !aboutToSelect, |
|
|
|
'text-tertiary': aboutToSelect, |
|
|
|
}, |
|
|
|
)} |
|
|
|
> |
|
|
|
<span |
|
|
|
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" |
|
|
|
> |
|
|
|
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" |
|
|
|
> |
|
|
|
{reselectLabel} |
|
|
|
</span> |
|
|
|
</FileSelectBoxActionElementComponent> |
|
|
|
</div> |
|
|
|
<div className="w-0 flex-auto flex flex-col items-center justify-center h-full"> |
|
|
|
<FileSelectBoxActionElementComponent |
|
|
|
disabled={disabled} |
|
|
|
data-testid="clear" |
|
|
|
type="button" |
|
|
|
name="action" |
|
|
|
value="clear" |
|
|
|
onMouseDown={handleDeleteMouseDown} |
|
|
|
onMouseUp={doSetFileList} |
|
|
|
tabIndex={-1} |
|
|
|
className={clsx( |
|
|
|
'flex w-full h-full bg-negative text-primary disabled:text-primary items-center justify-center leading-none gap-4 select-none focus:outline-0', |
|
|
|
{ |
|
|
|
'group-focus-within:text-secondary group-focus-within:active:text-tertiary': !aboutToClear, |
|
|
|
'text-tertiary': aboutToClear, |
|
|
|
}, |
|
|
|
)} |
|
|
|
> |
|
|
|
</FileSelectBoxActionElementComponent> |
|
|
|
</div> |
|
|
|
<div className="w-0 flex-auto flex flex-col items-center justify-center h-full"> |
|
|
|
<FileSelectBoxActionElementComponent |
|
|
|
disabled={disabled} |
|
|
|
data-testid="clear" |
|
|
|
type="button" |
|
|
|
name="action" |
|
|
|
value="clear" |
|
|
|
onMouseDown={handleDeleteMouseDown} |
|
|
|
onMouseUp={doSetFileList} |
|
|
|
tabIndex={-1} |
|
|
|
className={clsx( |
|
|
|
'flex w-full h-full bg-negative text-primary disabled:text-primary items-center justify-center leading-none gap-4 select-none focus:outline-0', |
|
|
|
{ |
|
|
|
'group-focus-within:text-secondary group-focus-within:active:text-tertiary': !aboutToClear, |
|
|
|
'text-tertiary': aboutToClear, |
|
|
|
}, |
|
|
|
)} |
|
|
|
> |
|
|
|
<span |
|
|
|
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" |
|
|
|
> |
|
|
|
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" |
|
|
|
> |
|
|
|
{clearLabel} |
|
|
|
</span> |
|
|
|
</FileSelectBoxActionElementComponent> |
|
|
|
</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" |
|
|
|
/> |
|
|
|
)} |
|
|
|
</FileSelectBoxActionElementComponent> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
{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> |
|
|
|
</FileSelectBoxRootElementComponent> |
|
|
|
); |
|
|
|
}); |
|
|
@@ -599,6 +616,7 @@ FileSelectBox.defaultProps = { |
|
|
|
previewComponent: FileSelectBoxDefaultPreviewComponent, |
|
|
|
reselectLabel: 'Reselect' as const, |
|
|
|
clearLabel: 'Clear' as const, |
|
|
|
resize: false as const, |
|
|
|
}; |
|
|
|
|
|
|
|
FileSelectBoxDefaultPreviewComponent.defaultProps = { |
|
|
|