|
- import * as React from 'react';
- import { delegateTriggerEvent, useClientSide } from '@modal-sh/react-utils';
- import clsx from 'clsx';
-
- export interface CommonPreviewProps<F extends Partial<File> = Partial<File>> {
- file?: F;
- disabled?: boolean;
- enhanced?: boolean;
- mini?: boolean;
- }
-
- 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'> {
- /**
- * Should the component display a border?
- */
- border?: boolean,
- /**
- * Should the component occupy the whole width of its parent?
- */
- block?: boolean,
- /**
- * Short textual description indicating the nature of the component's value.
- */
- label?: React.ReactNode,
- /**
- * Short textual description as guidelines for valid input values.
- */
- hint?: React.ReactNode,
- enhanced?: boolean,
- /**
- * Is the label hidden?
- */
- hiddenLabel?: boolean,
- 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 defaultId = React.useId();
- const id = idProp ?? defaultId;
-
- const doSetFileList: React.ChangeEventHandler<HTMLInputElement> = (e) => {
- if (enhancedProp) {
- setFileList(e.currentTarget.files as FileList);
- 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;
- }
-
- current.value = '';
- setFileList(undefined);
- setLastUpdated(Date.now());
- setTimeout(() => {
- delegateTriggerEvent('change', current);
- });
- };
-
- const cancelEvent = (e: React.DragEvent) => {
- e.stopPropagation();
- e.preventDefault();
- };
-
- 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);
- setLastUpdated(Date.now());
- setTimeout(() => {
- delegateTriggerEvent('change', current);
- });
- };
-
- const filesCount = fileList?.length ?? 0;
-
- 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"
- >
- <label
- className="block absolute top-0 left-0 w-full h-full cursor-pointer"
- data-testid="clickArea"
- htmlFor={id}
- />
- <input
- {...etcProps}
- id={id}
- disabled={disabled}
- ref={ref}
- type="file"
- className={clsx(
- 'peer',
- {
- 'sr-only': clientSide,
- }
- )}
- 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
- 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
- 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>
- </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 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"
- >
- <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';
|