Browse Source

Improve FileSelectBox key behavior

Properly highlight button with current action.
master
TheoryOfNekomata 1 year ago
parent
commit
e1be590f1d
6 changed files with 154 additions and 52 deletions
  1. +144
    -42
      categories/blob/react/src/components/FileSelectBox/index.tsx
  2. +2
    -2
      packages/react-blob-previews/src/components/AudioFilePreview/index.tsx
  3. +2
    -2
      packages/react-blob-previews/src/components/BinaryFilePreview/index.tsx
  4. +2
    -2
      packages/react-blob-previews/src/components/ImageFilePreview/index.tsx
  5. +2
    -2
      packages/react-blob-previews/src/components/TextFilePreview/index.tsx
  6. +2
    -2
      packages/react-blob-previews/src/components/VideoFilePreview/index.tsx

+ 144
- 42
categories/blob/react/src/components/FileSelectBox/index.tsx View File

@@ -2,10 +2,22 @@ import * as React from 'react';
import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-utils'; import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-utils';
import clsx from 'clsx'; import clsx from 'clsx';


export interface CommonPreviewProps<F extends Partial<File> = Partial<File>> {
export interface CommonPreviewComponentProps<F extends Partial<File> = Partial<File>> {
/**
* The file to preview.
*/
file?: F; file?: F;
/**
* Is the component disabled?
*/
disabled?: boolean; disabled?: boolean;
/**
* Should the component be enhanced?
*/
enhanced?: boolean; enhanced?: boolean;
/**
* Should the component use minimal space?
*/
mini?: boolean; mini?: boolean;
} }


@@ -13,7 +25,7 @@ export type FileSelectBoxDerivedElement = HTMLInputElement;


export interface FileSelectBoxProps< export interface FileSelectBoxProps<
F extends Partial<File> = Partial<File>, F extends Partial<File> = Partial<File>,
P extends CommonPreviewProps<F> = CommonPreviewProps<F>
P extends CommonPreviewComponentProps<F> = CommonPreviewComponentProps<F>
> extends Omit<React.HTMLProps<FileSelectBoxDerivedElement>, 'size' | 'type' | 'label' | 'list'> { > extends Omit<React.HTMLProps<FileSelectBoxDerivedElement>, 'size' | 'type' | 'label' | 'list'> {
/** /**
* Should the component display a border? * Should the component display a border?
@@ -39,46 +51,67 @@ export interface FileSelectBoxProps<
* Is the label hidden? * Is the label hidden?
*/ */
hiddenLabel?: boolean, hiddenLabel?: boolean,
/**
* Preview component for the selected file(s).
*/
previewComponent?: React.ElementType<P>, previewComponent?: React.ElementType<P>,
} }


export const FileSelectBoxDefaultPreviewComponent: React.FC<CommonPreviewProps> = ({
export const FileSelectBoxDefaultPreviewComponent = React.forwardRef<
HTMLDivElement,
CommonPreviewComponentProps
>(({
file, file,
mini, mini,
}) => (
<>
enhanced,
disabled,
}, forwardedRef) => (
<div
data-enhanced={enhanced}
className={clsx({
'opacity-50': disabled,
})}
ref={forwardedRef}
>
<div className="w-full whitespace-nowrap overflow-hidden text-ellipsis"> <div className="w-full whitespace-nowrap overflow-hidden text-ellipsis">
{file?.name ?? ( {file?.name ?? (
<span className="opacity-50">
File
</span>
<span className="opacity-50">
File
</span>
)} )}
</div> </div>
{!mini && ( {!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>
)}
</>
<>
{typeof file?.type === 'string' && (
<div className="w-full whitespace-nowrap overflow-hidden text-ellipsis">{file?.type}</div>
)}
{typeof file?.size === 'number' && (
<div
title={new Intl.NumberFormat(undefined, {
style: 'unit',
unit: 'byte',
unitDisplay: 'long',
}).format(file.size ?? 0)}
className="w-full whitespace-nowrap overflow-hidden text-ellipsis tabular-nums"
>
{new Intl.NumberFormat(undefined, {
style: 'unit',
unit: 'kilobyte',
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>
)}
</>
)} )}
</>
);
</div>
));


export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileSelectBoxProps>(( export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileSelectBoxProps>((
{ {
@@ -104,6 +137,8 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS
const [fileList, setFileList] = React.useState<FileList>(); const [fileList, setFileList] = React.useState<FileList>();
const [lastUpdated, setLastUpdated] = React.useState<number>(); const [lastUpdated, setLastUpdated] = React.useState<number>();
const clearFileListRef = React.useRef(false); const clearFileListRef = React.useRef(false);
const [deleteKeyPressed, setDeleteKeyPressed] = React.useState(false);
const [aboutToSelect, setAboutToSelect] = React.useState(false);
const { defaultRef, handleChange: doSetFileList } = useProxyInput< const { defaultRef, handleChange: doSetFileList } = useProxyInput<
React.ChangeEvent<FileSelectBoxDerivedElement> React.ChangeEvent<FileSelectBoxDerivedElement>
| React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>
@@ -113,7 +148,7 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS
>({ >({
forwardedRef, forwardedRef,
valueSetterFn: (e) => { valueSetterFn: (e) => {
if (e.type === 'click') {
if (e.type === 'mouseup') {
// delete // delete
const fileInput = defaultRef.current as FileSelectBoxDerivedElement; const fileInput = defaultRef.current as FileSelectBoxDerivedElement;
clearFileListRef.current = true; clearFileListRef.current = true;
@@ -143,7 +178,6 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS
} }
}, },
}); });
const ref = forwardedRef ?? defaultRef;
const labelId = React.useId(); const labelId = React.useId();
const id = useFallbackId(idProp); const id = useFallbackId(idProp);


@@ -173,17 +207,61 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS
e.currentTarget.files = fileList ?? null; e.currentTarget.files = fileList ?? null;
}; };


const handleKeyDown: React.KeyboardEventHandler<FileSelectBoxDerivedElement> = (e) => {
const { code } = e;
if (code === 'Backspace' || code === 'Delete') {
setDeleteKeyPressed(true);
} else if (code === 'Enter' || code === 'Space' || code === 'Return') {
setAboutToSelect(true);
}
};

const handleKeyUp: React.KeyboardEventHandler<FileSelectBoxDerivedElement> = (e) => { const handleKeyUp: React.KeyboardEventHandler<FileSelectBoxDerivedElement> = (e) => {
const { code } = e; const { code } = e;
if (code === 'Backspace' || code === 'Delete') { if (code === 'Backspace' || code === 'Delete') {
doSetFileList(e); doSetFileList(e);
setDeleteKeyPressed(false);
} else if (code === 'Enter' || code === 'Space' || code === 'Return') {
setAboutToSelect(false);
} }
}; };


const handleReselectMouseDown: React.MouseEventHandler<
HTMLLabelElement
> = React.useCallback(() => {
setAboutToSelect(true);
setTimeout(() => {
const fileInput = defaultRef.current as FileSelectBoxDerivedElement;
fileInput.focus();
});
}, [defaultRef]);

const handleDeleteMouseDown: React.MouseEventHandler<
HTMLButtonElement
> = React.useCallback(() => {
setDeleteKeyPressed(true);
setTimeout(() => {
const fileInput = defaultRef.current as FileSelectBoxDerivedElement;
fileInput.focus();
});
}, [defaultRef]);

React.useEffect(() => {
const handleLabelMouseUp = () => {
setAboutToSelect(false);
setDeleteKeyPressed(false);
};

window.addEventListener('mouseup', handleLabelMouseUp, { capture: true });
return () => {
window.removeEventListener('mouseup', handleLabelMouseUp, { capture: true });
};
}, []);

return ( return (
<div <div
className={clsx( className={clsx(
'relative rounded ring-secondary/50 group',
'relative rounded ring-secondary/50 group file-select-box',
'focus-within:ring-4', 'focus-within:ring-4',
block && 'flex w-full', block && 'flex w-full',
!block && 'inline-flex w-64 min-h-16 justify-center items-center', !block && 'inline-flex w-64 min-h-16 justify-center items-center',
@@ -210,6 +288,7 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS
)} )}
data-testid="clickArea" data-testid="clickArea"
htmlFor={id} htmlFor={id}
onMouseDown={handleReselectMouseDown}
> >
{placeholder} {placeholder}
</label> </label>
@@ -218,8 +297,9 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS
{...etcProps} {...etcProps}
id={id} id={id}
disabled={disabled} disabled={disabled}
ref={typeof ref === 'function' ? defaultRef : ref}
ref={defaultRef}
type="file" type="file"
onKeyDown={clientSide ? handleKeyDown : undefined}
onKeyUp={clientSide ? handleKeyUp : undefined} onKeyUp={clientSide ? handleKeyUp : undefined}
className={clsx( 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', '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',
@@ -323,7 +403,14 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS
<label <label
data-testid="reselect" data-testid="reselect"
htmlFor={id} 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"
onMouseDown={handleReselectMouseDown}
className={clsx(
'flex w-full h-full 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 <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"
@@ -334,13 +421,21 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS
</div> </div>
<div className="w-0 flex-auto flex flex-col items-center justify-center h-full"> <div className="w-0 flex-auto flex flex-col items-center justify-center h-full">
<button <button
disabled={disabled}
data-testid="clear" data-testid="clear"
type="button" type="button"
name="action" name="action"
value="clear" value="clear"
onClick={doSetFileList}
onMouseDown={handleDeleteMouseDown}
onMouseUp={doSetFileList}
tabIndex={-1} 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"
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': !deleteKeyPressed,
'text-tertiary': deleteKeyPressed,
},
)}
> >
<span <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"
@@ -365,11 +460,18 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS
FileSelectBox.displayName = 'FileSelectBox'; FileSelectBox.displayName = 'FileSelectBox';


FileSelectBox.defaultProps = { FileSelectBox.defaultProps = {
border: false,
block: false,
enhanced: false,
hiddenLabel: false,
border: false as const,
block: false as const,
enhanced: false as const,
hiddenLabel: false as const,
label: undefined, label: undefined,
hint: undefined, hint: undefined,
previewComponent: FileSelectBoxDefaultPreviewComponent, previewComponent: FileSelectBoxDefaultPreviewComponent,
}; };

FileSelectBoxDefaultPreviewComponent.defaultProps = {
file: undefined,
mini: false,
disabled: FileSelectBox.defaultProps.disabled,
enhanced: FileSelectBox.defaultProps.enhanced,
};

+ 2
- 2
packages/react-blob-previews/src/components/AudioFilePreview/index.tsx View File

@@ -15,12 +15,12 @@ import {SpectrogramCanvas, WaveformCanvas} from 'packages/web-kitchensink-reactn
import {Slider} from 'categories/number/react'; import {Slider} from 'categories/number/react';
import {KeyValueTable} from 'categories/information/react'; import {KeyValueTable} from 'categories/information/react';
import {useClientSide} from 'packages/react-utils'; import {useClientSide} from 'packages/react-utils';
import type {CommonPreviewProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';
import type {CommonPreviewComponentProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';


export type AudioFilePreviewDerivedElement = HTMLAudioElement; export type AudioFilePreviewDerivedElement = HTMLAudioElement;


export interface AudioFilePreviewProps<F extends Partial<File> = Partial<File>> export interface AudioFilePreviewProps<F extends Partial<File> = Partial<File>>
extends Omit<React.HTMLProps<AudioFilePreviewDerivedElement>, 'controls'>, CommonPreviewProps<F> {}
extends Omit<React.HTMLProps<AudioFilePreviewDerivedElement>, 'controls'>, CommonPreviewComponentProps<F> {}


export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedElement, AudioFilePreviewProps>(({ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedElement, AudioFilePreviewProps>(({
file, file,


+ 2
- 2
packages/react-blob-previews/src/components/BinaryFilePreview/index.tsx View File

@@ -6,12 +6,12 @@ import clsx from 'clsx';
import {KeyValueTable} from 'categories/information/react'; import {KeyValueTable} from 'categories/information/react';
import {BinaryDataCanvas} from 'packages/react-binary-data-canvas'; import {BinaryDataCanvas} from 'packages/react-binary-data-canvas';
import {useClientSide} from 'packages/react-utils'; import {useClientSide} from 'packages/react-utils';
import type {CommonPreviewProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';
import type {CommonPreviewComponentProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';


export type BinaryFilePreviewDerivedElement = HTMLDivElement; export type BinaryFilePreviewDerivedElement = HTMLDivElement;


export interface BinaryFilePreviewProps<F extends Partial<File> = Partial<File>> export interface BinaryFilePreviewProps<F extends Partial<File> = Partial<File>>
extends React.HTMLProps<BinaryFilePreviewDerivedElement>, CommonPreviewProps<F> {}
extends React.HTMLProps<BinaryFilePreviewDerivedElement>, CommonPreviewComponentProps<F> {}


export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedElement, BinaryFilePreviewProps>(({ export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedElement, BinaryFilePreviewProps>(({
file, file,


+ 2
- 2
packages/react-blob-previews/src/components/ImageFilePreview/index.tsx View File

@@ -5,13 +5,13 @@ import clsx from 'clsx';
import {useFileMetadata, useFileUrl, useImageControls} from 'src/index'; import {useFileMetadata, useFileUrl, useImageControls} from 'src/index';
import {KeyValueTable} from 'categories/information/react'; import {KeyValueTable} from 'categories/information/react';
import {useClientSide} from 'packages/react-utils'; import {useClientSide} from 'packages/react-utils';
import type {CommonPreviewProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';
import type {CommonPreviewComponentProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';
import {Swatch} from 'categories/color/react'; import {Swatch} from 'categories/color/react';


export type ImageFilePreviewDerivedElement = HTMLImageElement; export type ImageFilePreviewDerivedElement = HTMLImageElement;


export interface ImageFilePreviewProps<F extends Partial<File> = Partial<File>> export interface ImageFilePreviewProps<F extends Partial<File> = Partial<File>>
extends Omit<React.HTMLProps<ImageFilePreviewDerivedElement>, 'src' | 'alt'>, CommonPreviewProps<F> {}
extends Omit<React.HTMLProps<ImageFilePreviewDerivedElement>, 'src' | 'alt'>, CommonPreviewComponentProps<F> {}


export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedElement, ImageFilePreviewProps>(({ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedElement, ImageFilePreviewProps>(({
file, file,


+ 2
- 2
packages/react-blob-previews/src/components/TextFilePreview/index.tsx View File

@@ -6,12 +6,12 @@ import clsx from 'clsx';
import {KeyValueTable} from 'categories/information/react'; import {KeyValueTable} from 'categories/information/react';
import {Refractor} from 'packages/web-kitchensink-reactnext/src/packages/react-refractor'; import {Refractor} from 'packages/web-kitchensink-reactnext/src/packages/react-refractor';
import {useClientSide} from 'packages/react-utils'; import {useClientSide} from 'packages/react-utils';
import type {CommonPreviewProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';
import type {CommonPreviewComponentProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';


type TextFilePreviewDerivedComponent = HTMLDivElement; type TextFilePreviewDerivedComponent = HTMLDivElement;


export interface TextFilePreviewProps<F extends Partial<File> = Partial<File>> export interface TextFilePreviewProps<F extends Partial<File> = Partial<File>>
extends React.HTMLProps<TextFilePreviewDerivedComponent>, CommonPreviewProps<F> {}
extends React.HTMLProps<TextFilePreviewDerivedComponent>, CommonPreviewComponentProps<F> {}


export const TextFilePreview = React.forwardRef<TextFilePreviewDerivedComponent, TextFilePreviewProps>(({ export const TextFilePreview = React.forwardRef<TextFilePreviewDerivedComponent, TextFilePreviewProps>(({
file, file,


+ 2
- 2
packages/react-blob-previews/src/components/VideoFilePreview/index.tsx View File

@@ -6,12 +6,12 @@ import clsx from 'clsx';
import {Slider} from 'categories/number/react'; import {Slider} from 'categories/number/react';
import {KeyValueTable} from 'categories/information/react'; import {KeyValueTable} from 'categories/information/react';
import {useClientSide} from 'packages/react-utils'; import {useClientSide} from 'packages/react-utils';
import type {CommonPreviewProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';
import type {CommonPreviewComponentProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';


export type VideoFilePreviewDerivedComponent = HTMLVideoElement; export type VideoFilePreviewDerivedComponent = HTMLVideoElement;


export interface VideoFilePreviewProps<F extends Partial<File> = Partial<File>> export interface VideoFilePreviewProps<F extends Partial<File> = Partial<File>>
extends Omit<React.HTMLProps<VideoFilePreviewDerivedComponent>, 'controls'>, CommonPreviewProps<F> {}
extends Omit<React.HTMLProps<VideoFilePreviewDerivedComponent>, 'controls'>, CommonPreviewComponentProps<F> {}


export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponent, VideoFilePreviewProps>(({ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponent, VideoFilePreviewProps>(({
file, file,


Loading…
Cancel
Save