diff --git a/categories/web/blob/react/src/components/FileSelectBox/FileSelectBox.test.tsx b/categories/web/blob/react/src/components/FileSelectBox/FileSelectBox.test.tsx new file mode 100644 index 0000000..40e96d6 --- /dev/null +++ b/categories/web/blob/react/src/components/FileSelectBox/FileSelectBox.test.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { + render, + screen, + cleanup, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { + vi, + describe, + it, + expect, + afterEach, +} from 'vitest'; +import matchers from '@testing-library/jest-dom/matchers'; +import { + FileSelectBox, + FileSelectBoxDerivedElement, +} from '.'; + +expect.extend(matchers); + +describe('FileSelectBox', () => { + afterEach(() => { + cleanup(); + }); + + it('renders a file input', () => { + render( + , + ); + + const input = screen.getByTestId('input'); + expect(input).toBeInTheDocument(); + }); + + it('renders a border', () => { + render( + , + ); + const border = screen.getByTestId('border'); + expect(border).toBeInTheDocument(); + }); + + it('renders a label', () => { + render( + , + ); + const textbox = screen.getByLabelText('foo'); + expect(textbox).toBeInTheDocument(); + const label = screen.getByTestId('label'); + expect(label).toHaveTextContent('foo'); + }); + + it('renders a hidden label', () => { + render( + , + ); + const textbox = screen.getByLabelText('foo'); + expect(textbox).toBeInTheDocument(); + const label = screen.queryByTestId('label'); + expect(label).toBeInTheDocument(); + expect(label).toHaveClass('sr-only'); + }); + + describe('enhanced', () => { + it('renders a hint', () => { + render( + , + ); + const hint = screen.getByTestId('hint'); + expect(hint).toBeInTheDocument(); + }); + }); +}); diff --git a/categories/web/blob/react/src/components/FileSelectBox/index.tsx b/categories/web/blob/react/src/components/FileSelectBox/index.tsx index 1c961b0..9ebd6be 100644 --- a/categories/web/blob/react/src/components/FileSelectBox/index.tsx +++ b/categories/web/blob/react/src/components/FileSelectBox/index.tsx @@ -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

, + /** + * Reselect label. + */ + reselectLabel?: string, + /** + * Clear label. + */ + clearLabel?: string, } /** @@ -127,6 +139,16 @@ export const FileSelectBoxDefaultPreviewComponent = React.forwardRef< )); +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, + ) => { + const clearRefMut = clearRef; + clearRefMut.current = true; + + const fileInputMut = fileInput; + fileInputMut.value = ''; + + setFileList(undefined); + setLastUpdated(Date.now()); + }; + const { defaultRef, handleChange: doSetFileList } = useProxyInput< React.ChangeEvent | React.MouseEvent @@ -166,34 +205,36 @@ export const FileSelectBox = React.forwardRef({ 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; + 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; - 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 @@ -374,7 +415,7 @@ export const FileSelectBox = React.forwardRef {filesCount < 1 @@ -459,7 +500,7 @@ export const FileSelectBox = React.forwardRef - Reselect + {reselectLabel} @@ -484,7 +525,7 @@ export const FileSelectBox = React.forwardRef - Clear + {clearLabel} @@ -511,6 +552,8 @@ FileSelectBox.defaultProps = { label: undefined, hint: undefined, previewComponent: FileSelectBoxDefaultPreviewComponent, + reselectLabel: 'Reselect' as const, + clearLabel: 'Clear' as const, }; FileSelectBoxDefaultPreviewComponent.defaultProps = { diff --git a/categories/web/blob/react/src/web-blob-react.test.ts b/categories/web/blob/react/src/web-blob-react.test.ts new file mode 100644 index 0000000..9fbfb31 --- /dev/null +++ b/categories/web/blob/react/src/web-blob-react.test.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from 'vitest'; +import * as WebBlobReact from '.'; + +describe('web-action-react', () => { + it.each([ + 'FileSelectBox', + ])('exports %s', (namedExport) => { + expect(WebBlobReact).toHaveProperty(namedExport); + }); +});