From 7c1928b286978bd12b42c6727eb70546518d2bb4 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Sun, 23 Jul 2023 12:23:00 +0800 Subject: [PATCH] Update fileselectbox Ensure fileselectbox has proper enhanced and non-enhanced behavior. --- .../src/components/FileSelectBox/index.tsx | 547 ++++++++++-------- .../src/pages/categories/blob/index.tsx | 38 ++ .../src/pages/categories/freeform/index.tsx | 16 - 3 files changed, 348 insertions(+), 253 deletions(-) create mode 100644 showcases/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx diff --git a/categories/blob/react/src/components/FileSelectBox/index.tsx b/categories/blob/react/src/components/FileSelectBox/index.tsx index 4a3b9c9..e284d30 100644 --- a/categories/blob/react/src/components/FileSelectBox/index.tsx +++ b/categories/blob/react/src/components/FileSelectBox/index.tsx @@ -1,5 +1,5 @@ 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'; export interface CommonPreviewProps = Partial> { @@ -14,7 +14,7 @@ export type FileSelectBoxDerivedElement = HTMLInputElement; export interface FileSelectBoxProps< F extends Partial = Partial, P extends CommonPreviewProps = CommonPreviewProps -> extends Omit, 'size' | 'type' | 'style' | 'label' | 'list'> { +> extends Omit, 'size' | 'type' | 'label' | 'list'> { /** * Should the component display a border? */ @@ -39,264 +39,337 @@ export interface FileSelectBoxProps< * Is the label hidden? */ hiddenLabel?: boolean, - previewComponent: React.ElementType

, + previewComponent?: React.ElementType

, } -export const FileSelectBox = React.forwardRef( - ( - { - 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(); - const [lastUpdated, setLastUpdated] = React.useState(); - const defaultRef = React.useRef(null); - const ref = forwardedRef ?? defaultRef; - const labelId = React.useId(); - const id = useFallbackId(idProp); +export const FileSelectBoxDefaultPreviewComponent: React.FC = ({ + file, + mini, +}) => ( + <> +

+ {file?.name ?? ( + + File + + )} +
+ {!mini && ( + <> + {typeof file?.type === 'string' && ( +
{file?.type}
+ )} + {typeof file?.size === 'number' && ( +
+ {new Intl.NumberFormat(undefined, { + style: 'unit', + unit: 'byte', + unitDisplay: 'long', + }).format(file.size ?? 0)} +
+ )} + {typeof file?.lastModified === 'number' && ( +
+ +
+ )} + + )} + +); - const doSetFileList: React.ChangeEventHandler = (e) => { - if (enhancedProp) { - setFileList(e.currentTarget.files as FileList); +export const FileSelectBox = React.forwardRef(( + { + 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(); + const [lastUpdated, setLastUpdated] = React.useState(); + const clearFileListRef = React.useRef(false); + const { defaultRef, handleChange: doSetFileList } = useProxyInput< + React.ChangeEvent + | React.MouseEvent + | React.DragEvent + | React.KeyboardEvent, + 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()); + } else if (e.type === 'drop') { + // drop file + const fileInput = defaultRef.current as FileSelectBoxDerivedElement; + const { dataTransfer } = e as React.DragEvent; + 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; + if (code === 'Backspace' || code === 'Delete') { + clearFileListRef.current = true; + fileInput.value = ''; + setFileList(undefined); + setLastUpdated(Date.now()); + } } - onChange?.(e); - }; - - const doClearFileList: React.MouseEventHandler = (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 = 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 = (e) => { + const { currentTarget } = e; + if (clientSide && currentTarget.files && currentTarget.files.length > 0) { + setFileList(currentTarget.files); 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 = (e) => { + const { code } = e; + if (code === 'Backspace' || code === 'Delete') { + doSetFileList(e); + } + }; - return ( -
+ return ( +
{ + cancelEvent(e); + doSetFileList(e); + } : undefined} + data-testid="root" + style={{ + height: clientSide ? 64 : undefined, + ...(style ?? {}), + }} + > + {clientSide && ( + )} + + {label && ( +
- { - label && ( -
-
- {label} -
-
- ) - } - { - filesCount < 1 - && clientSide - && hint - && ( + > +
+ {label} +
+
+ )} + {filesCount < 1 + && clientSide + && hint + && ( +
+
+ {hint} +
+
+ )} + {filesCount > 0 + && clientSide + && ( + +
-
- {hint} -
-
- ) - } - { - filesCount > 0 - && clientSide - && ( - -
-
- { - multiple - && ( -
-
- {Array.from(fileList ?? []).map((file, i) => { - return ( -
- -
- ); - })} -
+ {multiple + && ( +
+
+ {Array.from(fileList ?? []).map((file, i) => ( +
+
- ) - } - { - !multiple - && Array.from(fileList ?? []).map((file, i) => { - return ( -
-
-
- -
-
-
- ) - }) - } -
-
-
-
- + ))} +
-
- + +
+
-
-
- ) - } - { - border && ( - - ) - } -
- ); - } -); + ))} +
+
+
+
+ +
+
+ +
+
+ + )} + {border && ( + + )} + + ); +}); FileSelectBox.displayName = 'FileSelectBox'; + +FileSelectBox.defaultProps = { + border: false, + block: false, + enhanced: false, + hiddenLabel: false, + label: undefined, + hint: undefined, + previewComponent: FileSelectBoxDefaultPreviewComponent, +}; diff --git a/showcases/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx b/showcases/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx new file mode 100644 index 0000000..65c780e --- /dev/null +++ b/showcases/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx @@ -0,0 +1,38 @@ +import {NextPage} from 'next'; +import {DefaultLayout} from '@/components/DefaultLayout'; +import {Section, Subsection} from '@/components/Section'; +import * as TesseractBlob from '@tesseract-design/web-blob-react'; + +const BlobPage: NextPage = () => { + return ( + +
+ + null} + onChange={(e) => { console.log('change', e.currentTarget.name, e.currentTarget, e.currentTarget.files)}} + /> + + + { console.log('change', e.currentTarget.name, e.currentTarget, e.currentTarget.files)}} + /> + +
+
+ ) +} + +export default BlobPage; diff --git a/showcases/web-kitchensink-reactnext/src/pages/categories/freeform/index.tsx b/showcases/web-kitchensink-reactnext/src/pages/categories/freeform/index.tsx index 9205d2f..3cb6882 100644 --- a/showcases/web-kitchensink-reactnext/src/pages/categories/freeform/index.tsx +++ b/showcases/web-kitchensink-reactnext/src/pages/categories/freeform/index.tsx @@ -286,7 +286,6 @@ const FreeformPage: NextPage = () => { size="small" label="MaskedTextInput" hint="Type anything here…" - indicator="A" block /> @@ -296,7 +295,6 @@ const FreeformPage: NextPage = () => { size="small" label="MaskedTextInput" hint="Type anything here…" - indicator="A" block /> @@ -304,7 +302,6 @@ const FreeformPage: NextPage = () => { @@ -313,7 +310,6 @@ const FreeformPage: NextPage = () => { border label="MaskedTextInput" hint="Type anything here…" - indicator="A" block /> @@ -322,7 +318,6 @@ const FreeformPage: NextPage = () => { size="large" label="MaskedTextInput" hint="Type anything here…" - indicator="A" block /> @@ -332,7 +327,6 @@ const FreeformPage: NextPage = () => { size="large" label="MaskedTextInput" hint="Type anything here…" - indicator="A" block /> @@ -340,7 +334,6 @@ const FreeformPage: NextPage = () => { @@ -350,7 +343,6 @@ const FreeformPage: NextPage = () => { border label="MaskedTextInput" hint="Type anything here…" - indicator="A" block disabled /> @@ -365,7 +357,6 @@ const FreeformPage: NextPage = () => { size="small" label="MaskedTextInput" hint="Type anything here…" - indicator="A" block /> @@ -376,7 +367,6 @@ const FreeformPage: NextPage = () => { size="small" label="MaskedTextInput" hint="Type anything here…" - indicator="A" block /> @@ -385,7 +375,6 @@ const FreeformPage: NextPage = () => { variant="alternate" label="MaskedTextInput" hint="Type anything here…" - indicator="A" block /> @@ -395,7 +384,6 @@ const FreeformPage: NextPage = () => { variant="alternate" label="MaskedTextInput" hint="Type anything here…" - indicator="A" block /> @@ -405,7 +393,6 @@ const FreeformPage: NextPage = () => { size="large" label="MaskedTextInput" hint="Type anything here…" - indicator="A" block /> @@ -417,7 +404,6 @@ const FreeformPage: NextPage = () => { size="large" label="MaskedTextInput" hint="Type anything here…" - indicator="A" />
@@ -425,7 +411,6 @@ const FreeformPage: NextPage = () => { variant="alternate" label="MaskedTextInput" hint="Type anything here…" - indicator="A" block disabled /> @@ -436,7 +421,6 @@ const FreeformPage: NextPage = () => { border label="MaskedTextInput" hint="Type anything here…" - indicator="A" block disabled />