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);
+ });
+});