|
|
@@ -2,6 +2,10 @@ import * as React from 'react'; |
|
|
|
import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-utils'; |
|
|
|
import clsx from 'clsx'; |
|
|
|
|
|
|
|
const DEFAULT_ENHANCED_HEIGHT_PX = 64 as const; |
|
|
|
|
|
|
|
const DEFAULT_NON_ENHANCED_SIDE_HEIGHT_PX = 256 as const; |
|
|
|
|
|
|
|
/** |
|
|
|
* Common props for the {@link FileSelectBoxProps.previewComponent|previewComponent prop} of the |
|
|
|
* {@link FileSelectBox} component. |
|
|
@@ -65,6 +69,14 @@ export interface FileSelectBoxProps< |
|
|
|
* Preview component for the selected file(s). |
|
|
|
*/ |
|
|
|
previewComponent?: React.ElementType<P>, |
|
|
|
/** |
|
|
|
* Reselect label. |
|
|
|
*/ |
|
|
|
reselectLabel?: string, |
|
|
|
/** |
|
|
|
* Clear label. |
|
|
|
*/ |
|
|
|
clearLabel?: string, |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
@@ -127,6 +139,16 @@ export const FileSelectBoxDefaultPreviewComponent = React.forwardRef< |
|
|
|
</div> |
|
|
|
)); |
|
|
|
|
|
|
|
const isButtonElement = (el: HTMLElement): el is HTMLButtonElement => el.tagName === 'BUTTON'; |
|
|
|
|
|
|
|
const isInputElement = (el: HTMLElement): el is HTMLInputElement => el.tagName === 'INPUT'; |
|
|
|
|
|
|
|
const isKeyUpEvent = (e: React.SyntheticEvent): e is React.KeyboardEvent => e.type === 'keyup'; |
|
|
|
|
|
|
|
const isMouseUpEvent = (e: React.SyntheticEvent): e is React.MouseEvent => e.type === 'mouseup'; |
|
|
|
|
|
|
|
const isDropEvent = (e: React.SyntheticEvent): e is React.DragEvent => e.type === 'drop'; |
|
|
|
|
|
|
|
/** |
|
|
|
* Component for selecting files. |
|
|
|
*/ |
|
|
@@ -147,6 +169,8 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
previewComponent: FilePreviewComponent = FileSelectBoxDefaultPreviewComponent, |
|
|
|
style, |
|
|
|
onBlur, |
|
|
|
reselectLabel = 'Reselect', |
|
|
|
clearLabel = 'Clear', |
|
|
|
...etcProps |
|
|
|
}, |
|
|
|
forwardedRef, |
|
|
@@ -157,6 +181,21 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
const clearFileListRef = React.useRef(false); |
|
|
|
const [deleteKeyPressed, setDeleteKeyPressed] = React.useState(false); |
|
|
|
const [aboutToSelect, setAboutToSelect] = React.useState(false); |
|
|
|
|
|
|
|
const clearFiles = ( |
|
|
|
fileInput: FileSelectBoxDerivedElement, |
|
|
|
clearRef: React.MutableRefObject<boolean>, |
|
|
|
) => { |
|
|
|
const clearRefMut = clearRef; |
|
|
|
clearRefMut.current = true; |
|
|
|
|
|
|
|
const fileInputMut = fileInput; |
|
|
|
fileInputMut.value = ''; |
|
|
|
|
|
|
|
setFileList(undefined); |
|
|
|
setLastUpdated(Date.now()); |
|
|
|
}; |
|
|
|
|
|
|
|
const { defaultRef, handleChange: doSetFileList } = useProxyInput< |
|
|
|
React.ChangeEvent<FileSelectBoxDerivedElement> |
|
|
|
| React.MouseEvent<HTMLButtonElement> |
|
|
@@ -166,34 +205,36 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
>({ |
|
|
|
forwardedRef, |
|
|
|
valueSetterFn: (e) => { |
|
|
|
if (e.type === 'mouseup') { |
|
|
|
// delete |
|
|
|
const fileInput = defaultRef.current as FileSelectBoxDerivedElement; |
|
|
|
clearFileListRef.current = true; |
|
|
|
fileInput.value = ''; |
|
|
|
setFileList(undefined); |
|
|
|
setLastUpdated(Date.now()); |
|
|
|
} else if (e.type === 'drop') { |
|
|
|
const fileInput = defaultRef.current; |
|
|
|
if (!fileInput) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if (isButtonElement(e.currentTarget) && isMouseUpEvent(e)) { |
|
|
|
clearFiles(fileInput, clearFileListRef); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (isInputElement(e.currentTarget) && isKeyUpEvent(e)) { |
|
|
|
// delete via keyboard |
|
|
|
const { code } = e; |
|
|
|
|
|
|
|
if (!(code === 'Backspace' || code === 'Delete')) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
clearFiles(e.currentTarget, clearFileListRef); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (isDropEvent(e)) { |
|
|
|
// drop file |
|
|
|
const fileInput = defaultRef.current as FileSelectBoxDerivedElement; |
|
|
|
const { dataTransfer } = e as React.DragEvent<HTMLDivElement>; |
|
|
|
const { dataTransfer } = e; |
|
|
|
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()); |
|
|
|
} |
|
|
|
return; |
|
|
|
} |
|
|
|
console.warn('Unhandled event', e); |
|
|
|
}, |
|
|
|
}); |
|
|
|
const labelId = React.useId(); |
|
|
@@ -316,7 +357,7 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
onDrop={clientSide ? handleDrop : undefined} |
|
|
|
data-testid="root" |
|
|
|
style={{ |
|
|
|
height: clientSide ? 64 : undefined, |
|
|
|
height: clientSide ? DEFAULT_ENHANCED_HEIGHT_PX : undefined, |
|
|
|
...(style ?? {}), |
|
|
|
}} |
|
|
|
> |
|
|
@@ -374,7 +415,7 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
data-testid="input" |
|
|
|
aria-labelledby={label ? `${labelId}` : undefined} |
|
|
|
style={{ |
|
|
|
height: clientSide ? undefined : 256, |
|
|
|
height: clientSide ? undefined : DEFAULT_NON_ENHANCED_SIDE_HEIGHT_PX, |
|
|
|
}} |
|
|
|
/> |
|
|
|
{filesCount < 1 |
|
|
@@ -459,7 +500,7 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
<span |
|
|
|
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" |
|
|
|
> |
|
|
|
Reselect |
|
|
|
{reselectLabel} |
|
|
|
</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
@@ -484,7 +525,7 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS |
|
|
|
<span |
|
|
|
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" |
|
|
|
> |
|
|
|
Clear |
|
|
|
{clearLabel} |
|
|
|
</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
@@ -511,6 +552,8 @@ FileSelectBox.defaultProps = { |
|
|
|
label: undefined, |
|
|
|
hint: undefined, |
|
|
|
previewComponent: FileSelectBoxDefaultPreviewComponent, |
|
|
|
reselectLabel: 'Reselect' as const, |
|
|
|
clearLabel: 'Clear' as const, |
|
|
|
}; |
|
|
|
|
|
|
|
FileSelectBoxDefaultPreviewComponent.defaultProps = { |
|
|
|