소스 검색

Update fileselectbox

Ensure fileselectbox has proper enhanced and non-enhanced behavior.
master
부모
커밋
7c1928b286
3개의 변경된 파일348개의 추가작업 그리고 253개의 파일을 삭제
  1. +310
    -237
      categories/blob/react/src/components/FileSelectBox/index.tsx
  2. +38
    -0
      showcases/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx
  3. +0
    -16
      showcases/web-kitchensink-reactnext/src/pages/categories/freeform/index.tsx

+ 310
- 237
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<F extends Partial<File> = Partial<File>> {
@@ -14,7 +14,7 @@ export type FileSelectBoxDerivedElement = HTMLInputElement;
export interface FileSelectBoxProps<
F extends Partial<File> = Partial<File>,
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?
*/
@@ -39,264 +39,337 @@ export interface FileSelectBoxProps<
* Is the label hidden?
*/
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());
} 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());
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
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"
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(
'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
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>
)
}
{
!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 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>
</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.defaultProps = {
border: false,
block: false,
enhanced: false,
hiddenLabel: false,
label: undefined,
hint: undefined,
previewComponent: FileSelectBoxDefaultPreviewComponent,
};

+ 38
- 0
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 (
<DefaultLayout title="Blob">
<Section title="FileSelectBox">
<Subsection title="Default">
<TesseractBlob.FileSelectBox
label="File"
name="file"
border
placeholder="Select a file"
previewComponent={() => null}
onChange={(e) => { console.log('change', e.currentTarget.name, e.currentTarget, e.currentTarget.files)}}
/>
</Subsection>
<Subsection title="Enhanced">
<TesseractBlob.FileSelectBox
label="File"
name="file"
border
enhanced
placeholder="Select a file"
style={{
height: 256,
}}
onChange={(e) => { console.log('change', e.currentTarget.name, e.currentTarget, e.currentTarget.files)}}
/>
</Subsection>
</Section>
</DefaultLayout>
)
}

export default BlobPage;

+ 0
- 16
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&hellip;"
indicator="A"
block
/>
</div>
@@ -296,7 +295,6 @@ const FreeformPage: NextPage = () => {
size="small"
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
@@ -304,7 +302,6 @@ const FreeformPage: NextPage = () => {
<Freeform.MaskedTextInput
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
@@ -313,7 +310,6 @@ const FreeformPage: NextPage = () => {
border
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
@@ -322,7 +318,6 @@ const FreeformPage: NextPage = () => {
size="large"
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
@@ -332,7 +327,6 @@ const FreeformPage: NextPage = () => {
size="large"
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
@@ -340,7 +334,6 @@ const FreeformPage: NextPage = () => {
<Freeform.MaskedTextInput
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
disabled
/>
@@ -350,7 +343,6 @@ const FreeformPage: NextPage = () => {
border
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
disabled
/>
@@ -365,7 +357,6 @@ const FreeformPage: NextPage = () => {
size="small"
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
@@ -376,7 +367,6 @@ const FreeformPage: NextPage = () => {
size="small"
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
@@ -385,7 +375,6 @@ const FreeformPage: NextPage = () => {
variant="alternate"
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
@@ -395,7 +384,6 @@ const FreeformPage: NextPage = () => {
variant="alternate"
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
@@ -405,7 +393,6 @@ const FreeformPage: NextPage = () => {
size="large"
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
@@ -417,7 +404,6 @@ const FreeformPage: NextPage = () => {
size="large"
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
/>
</div>
<div>
@@ -425,7 +411,6 @@ const FreeformPage: NextPage = () => {
variant="alternate"
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
disabled
/>
@@ -436,7 +421,6 @@ const FreeformPage: NextPage = () => {
border
label="MaskedTextInput"
hint="Type anything here&hellip;"
indicator="A"
block
disabled
/>


불러오는 중...
취소
저장