From a5c06d32a9b47af3bea74a1337b6a90665d498eb Mon Sep 17 00:00:00 2001 From: Qs-F Date: Tue, 7 Oct 2025 13:02:36 +0900 Subject: [PATCH 01/10] =?UTF-8?q?chore(InputFile):=20InputFile=E3=82=92Nat?= =?UTF-8?q?ive=E3=81=A8=E7=8B=AC=E8=87=AA=E3=81=AB=E5=88=86=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/InputFile/NativeInputFile.tsx | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 packages/smarthr-ui/src/components/InputFile/NativeInputFile.tsx diff --git a/packages/smarthr-ui/src/components/InputFile/NativeInputFile.tsx b/packages/smarthr-ui/src/components/InputFile/NativeInputFile.tsx new file mode 100644 index 0000000000..f4e557642c --- /dev/null +++ b/packages/smarthr-ui/src/components/InputFile/NativeInputFile.tsx @@ -0,0 +1,233 @@ +'use client' + +import { + type ChangeEvent, + type ComponentPropsWithRef, + type MouseEvent, + type ReactNode, + forwardRef, + memo, + useCallback, + useId, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react' +import { type VariantProps, tv } from 'tailwind-variants' + +import { type DecoratorsType, useDecorators } from '../../hooks/useDecorators' +import { useIntl } from '../../intl' +import { BaseColumn } from '../Base' +import { Button } from '../Button' +import { FaFolderOpenIcon, FaTrashCanIcon } from '../Icon' +import { Stack } from '../Layout' + +const classNameGenerator = tv({ + slots: { + wrapper: 'smarthr-ui-InputFile shr-block', + fileList: ['smarthr-ui-InputFile-fileList', 'shr-list-none shr-self-stretch shr-text-base'], + fileItem: 'shr-flex shr-items-center', + inputWrapper: [ + 'shr-border-shorthand shr-relative shr-inline-flex shr-rounded-m shr-bg-white shr-font-bold shr-leading-none', + 'contrast-more:shr-border-high-contrast', + 'focus-within:shr-focus-indicator', + 'has-[[aria-invalid]]:shr-border-danger', + ], + input: [ + 'smarthr-ui-InputFile-input', + 'shr-absolute shr-left-0 shr-top-0 shr-h-full shr-w-full shr-opacity-0', + 'file:shr-h-full file:shr-w-full file:shr-cursor-pointer', + 'file:disabled:shr-cursor-not-allowed', + ], + prefix: 'shr-me-0.5 shr-inline-flex', + }, + variants: { + size: { + default: { + inputWrapper: 'shr-px-1 shr-py-0.75 shr-text-base', + }, + s: { + inputWrapper: 'shr-p-0.5 shr-text-sm', + }, + }, + disabled: { + true: { + inputWrapper: 'shr-border-disabled shr-bg-white-darken shr-text-disabled', + }, + false: { + inputWrapper: 'hover:shr-border-darken hover:shr-bg-white-darken hover:shr-text-black', + }, + }, + }, + defaultVariants: { + size: 'default', + }, +}) + +export type Props = VariantProps & { + /** フォームのラベル */ + label: ReactNode + /** ファイルの選択に変更があったときに発火するコールバック関数 */ + onChange?: (files: File[]) => void + /** ファイルリストを表示するかどうか */ + hasFileList?: boolean + /** コンポーネント内のテキストを変更する関数 */ + decorators?: DecoratorsType + error?: boolean +} +type ElementProps = Omit, keyof Props> +type DecoratorKeyTypes = 'destroy' + +const BASE_COLUMN_PADDING = { block: 0.5, inline: 1 } as const + +export const InputFile = forwardRef( + ( + { + className, + size, + label, + hasFileList = true, + onChange, + disabled = false, + error, + decorators, + ...props + }, + ref, + ) => { + const [files, setFiles] = useState([]) + const labelId = useId() + const { localize } = useIntl() + + const decoratorDefaultTexts = useMemo( + () => ({ + destroy: localize({ + id: 'smarthr-ui/InputFile/destroy', + defaultText: '削除', + }), + }), + [localize], + ) + + const decorated = useDecorators(decoratorDefaultTexts, decorators) + + const classNames = useMemo(() => { + const { wrapper, fileList, fileItem, inputWrapper, input, prefix } = classNameGenerator() + + return { + wrapper: wrapper({ className }), + inputWrapper: inputWrapper({ size, disabled }), + fileList: fileList(), + fileItem: fileItem(), + input: input(), + prefix: prefix(), + } + }, [disabled, size, className]) + + // Safari において、input.files への直接代入時に onChange が発火することを防ぐためのフラグ + const isUpdatingFilesDirectly = useRef(false) + + const inputRef = useRef(null) + useImperativeHandle( + ref, + () => inputRef.current, + ) + + const updateFiles = useMemo( + () => + onChange + ? (newFiles: File[]) => { + onChange(newFiles) + setFiles(newFiles) + } + : setFiles, + [onChange], + ) + + const handleChange = useCallback( + (e: ChangeEvent) => { + if (!isUpdatingFilesDirectly.current) { + updateFiles(Array.from(e.target.files ?? [])) + } + }, + [isUpdatingFilesDirectly, updateFiles], + ) + + const handleDelete = useCallback( + (e: MouseEvent) => { + if (!inputRef.current) { + return + } + + const index = parseInt(e.currentTarget.value, 10) + const newFiles = files.filter((_, i) => index !== i) + + updateFiles(newFiles) + + const buff = new DataTransfer() + + newFiles.forEach((file) => { + buff.items.add(file) + }) + + isUpdatingFilesDirectly.current = true + inputRef.current.files = buff.files + isUpdatingFilesDirectly.current = false + }, + [files, isUpdatingFilesDirectly, inputRef, updateFiles], + ) + + return ( + + {!disabled && hasFileList && files.length > 0 && ( + + {files.map((file, index) => ( +
  • + + {file.name} + + +
  • + ))} +
    + )} + + + + + +
    + ) + }, +) + +const StyledFaFolderOpenIcon = memo<{ className: string }>(({ className }) => ( + + + +)) + +const LabelRender = memo<{ id: string; label: ReactNode }>(({ id, label }) => ( + +)) From 4ef4b915a3dd8e824cb9a3e66d9a677611d1d26d Mon Sep 17 00:00:00 2001 From: Qs-F Date: Tue, 7 Oct 2025 13:17:10 +0900 Subject: [PATCH 02/10] fix: Divide InputFile into two compoennts internally --- .../components/InputFile/InputFile.test.tsx | 132 +++++++++ .../src/components/InputFile/InputFile.tsx | 181 +----------- .../InputFile/InputFileMultiplyAppendable.tsx | 260 ++++++++++++++++++ ...ativeInputFile.tsx => InputFileNative.tsx} | 2 +- 4 files changed, 401 insertions(+), 174 deletions(-) create mode 100644 packages/smarthr-ui/src/components/InputFile/InputFile.test.tsx create mode 100644 packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx rename packages/smarthr-ui/src/components/InputFile/{NativeInputFile.tsx => InputFileNative.tsx} (98%) diff --git a/packages/smarthr-ui/src/components/InputFile/InputFile.test.tsx b/packages/smarthr-ui/src/components/InputFile/InputFile.test.tsx new file mode 100644 index 0000000000..343241249c --- /dev/null +++ b/packages/smarthr-ui/src/components/InputFile/InputFile.test.tsx @@ -0,0 +1,132 @@ +import { render, screen } from '@testing-library/react' +import { userEvent } from 'storybook/test' + +import { IntlProvider } from '../../intl' +import { FormControl } from '../FormControl' + +import { InputFile } from './InputFile' + +describe('InputFile', () => { + const file1 = new File(['foo'], 'foo.txt', { type: 'text/plain' }) + const file2 = new File(['bar'], 'bar.txt', { type: 'text/plain' }) + + it('ファイル選択時にonChangeが発火すること', async () => { + const onChange = vi.fn() + await render( + +
    + + + +
    +
    , + ) + const input = screen.getByLabelText('input file') + await userEvent.upload(input, file1) + expect(onChange).toHaveBeenCalledOnce() + expect(onChange).toHaveBeenCalledWith([file1]) + }) + + it('multipleでファイル選択時にonChangeが発火すること', async () => { + const onChange = vi.fn() + await render( + +
    + + + +
    +
    , + ) + const input = screen.getByLabelText('input file') + await userEvent.upload(input, [file1, file2]) + expect(onChange).toHaveBeenCalledOnce() + expect(onChange).toHaveBeenCalledWith([file1, file2]) + }) + + it('multiplyAppendableでファイル選択時にonChangeが発火すること', async () => { + const onChange = vi.fn() + await render( + +
    + + + +
    +
    , + ) + const input = screen.getByLabelText('input file') + await userEvent.upload(input, file1) + expect(onChange).toHaveBeenCalledOnce() + expect(onChange).toHaveBeenCalledWith([file1]) + }) + + it('ファイル選択後、ファイルリストが表示されること', async () => { + await render( + +
    + + + +
    +
    , + ) + const input = screen.getByLabelText('input file') + await userEvent.upload(input, file1) + expect(screen.getByText(file1.name)).toBeInTheDocument() + }) + + it('ファイル選択後、削除するとinputのvalueには存在しないこと', async () => { + await render( + +
    + + + +
    +
    , + ) + const input = screen.getByLabelText('input file') + await userEvent.upload(input, file1) + expect(input.files).toHaveLength(1) + const deleteButton = screen.getByRole('button', { name: '削除' }) + await userEvent.click(deleteButton) + expect(input.files).toHaveLength(0) + }) + + it('multipleで複数ファイル選択後、1つ削除すると削除したもののみinputのvalueには存在しないこと', async () => { + await render( + +
    + + + +
    +
    , + ) + const input = screen.getByLabelText('input file') + await userEvent.upload(input, [file1, file2]) + expect(input.files).toHaveLength(2) + const deleteButton = screen.getByRole('button', { name: '削除' }) + await userEvent.click(deleteButton) + expect(input.files).toHaveLength(1) + }) + + it('ファイル削除後、ファイルリストに存在しないこと', async () => { + await render( + +
    + + + +
    +
    , + ) + const input = screen.getByLabelText('input file') + await userEvent.upload(input, file1) + expect(screen.getByText(file1.name)).toBeInTheDocument() + const deleteButton = screen.getByRole('button', { name: '削除' }) + await userEvent.click(deleteButton) + expect(screen.queryByText(file1.name)).not.toBeInTheDocument() + }) +}) diff --git a/packages/smarthr-ui/src/components/InputFile/InputFile.tsx b/packages/smarthr-ui/src/components/InputFile/InputFile.tsx index d3f1e2048a..60ae55320f 100644 --- a/packages/smarthr-ui/src/components/InputFile/InputFile.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFile.tsx @@ -23,6 +23,9 @@ import { Button } from '../Button' import { FaFolderOpenIcon, FaTrashCanIcon } from '../Icon' import { Stack } from '../Layout' +import { InputFileMultiplyAppendable } from './InputFileMultiplyAppendable' +import { InputFileNative } from './InputFileNative' + const classNameGenerator = tv({ slots: { wrapper: 'smarthr-ui-InputFile shr-block', @@ -81,176 +84,8 @@ export type Props = VariantProps & { type ElementProps = Omit, keyof Props> type DecoratorKeyTypes = 'destroy' -const BASE_COLUMN_PADDING = { block: 0.5, inline: 1 } as const - -export const InputFile = forwardRef( - ( - { - className, - size, - label, - hasFileList = true, - onChange, - disabled = false, - multiple, - multiplyAppendable = false, - error, - decorators, - ...props - }, - ref, - ) => { - const [files, setFiles] = useState([]) - const labelId = useId() - const { localize } = useIntl() - - const decoratorDefaultTexts = useMemo( - () => ({ - destroy: localize({ - id: 'smarthr-ui/InputFile/destroy', - defaultText: '削除', - }), - }), - [localize], - ) - - const decorated = useDecorators(decoratorDefaultTexts, decorators) - - const classNames = useMemo(() => { - const { wrapper, fileList, fileItem, inputWrapper, input, prefix } = classNameGenerator() - - return { - wrapper: wrapper({ className }), - inputWrapper: inputWrapper({ size, disabled }), - fileList: fileList(), - fileItem: fileItem(), - input: input(), - prefix: prefix(), - } - }, [disabled, size, className]) - - // Safari において、input.files への直接代入時に onChange が発火することを防ぐためのフラグ - const isUpdatingFilesDirectly = useRef(false) - - const inputRef = useRef(null) - useImperativeHandle( - ref, - () => inputRef.current, - ) - - const updateInputValue = useCallback( - (newFiles: File[]) => { - if (!inputRef.current) { - return - } - const buff = new DataTransfer() - newFiles.forEach((file) => { - buff.items.add(file) - }) - - isUpdatingFilesDirectly.current = true - inputRef.current.files = buff.files - isUpdatingFilesDirectly.current = false - }, - [inputRef], - ) - - const updateFiles = useMemo( - () => - onChange - ? (newFiles: File[]) => { - onChange(newFiles) - updateInputValue(newFiles) - setFiles(newFiles) - } - : setFiles, - [onChange, updateInputValue], - ) - - const handleChange = useCallback( - (e: ChangeEvent) => { - // Safari において、input.files への直接代入時はonChangeを発火させない - if (isUpdatingFilesDirectly.current) { - return - } - - const newFiles = Array.from(e.target.files ?? []) - - if (multiplyAppendable) { - // multiplyAppendableの場合、すでに選択済みのファイルと結合する - updateFiles([...files, ...newFiles]) - } else { - updateFiles(newFiles) - } - }, - [files, isUpdatingFilesDirectly, updateFiles, multiplyAppendable], - ) - - const handleDelete = useCallback( - (e: MouseEvent) => { - if (!inputRef.current) { - return - } - - const index = parseInt(e.currentTarget.value, 10) - const newFiles = files.filter((_, i) => index !== i) - - updateFiles(newFiles) - }, - [files, inputRef, updateFiles], - ) - - return ( - - {!disabled && hasFileList && files.length > 0 && ( - - {files.map((file, index) => ( -
  • - - {file.name} - - -
  • - ))} -
    - )} - - - - - -
    - ) - }, -) - -const StyledFaFolderOpenIcon = memo<{ className: string }>(({ className }) => ( - - - -)) - -const LabelRender = memo<{ id: string; label: ReactNode }>(({ id, label }) => ( - -)) +export const InputFile = forwardRef((props, ref) => { + if (props.multiple && props.multiplyAppendable) { + return + } +}) diff --git a/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx new file mode 100644 index 0000000000..c51b7dfb41 --- /dev/null +++ b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx @@ -0,0 +1,260 @@ +'use client' + +import { + type ChangeEvent, + type ComponentPropsWithRef, + type MouseEvent, + type ReactNode, + forwardRef, + memo, + useCallback, + useId, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react' +import { type VariantProps, tv } from 'tailwind-variants' + +import { type DecoratorsType, useDecorators } from '../../hooks/useDecorators' +import { useIntl } from '../../intl' +import { BaseColumn } from '../Base' +import { Button } from '../Button' +import { FaFolderOpenIcon, FaTrashCanIcon } from '../Icon' +import { Stack } from '../Layout' + +const classNameGenerator = tv({ + slots: { + wrapper: 'smarthr-ui-InputFile shr-block', + fileList: ['smarthr-ui-InputFile-fileList', 'shr-list-none shr-self-stretch shr-text-base'], + fileItem: 'shr-flex shr-items-center', + inputWrapper: [ + 'shr-border-shorthand shr-relative shr-inline-flex shr-rounded-m shr-bg-white shr-font-bold shr-leading-none', + 'contrast-more:shr-border-high-contrast', + 'focus-within:shr-focus-indicator', + 'has-[[aria-invalid]]:shr-border-danger', + ], + input: [ + 'smarthr-ui-InputFile-input', + 'shr-absolute shr-left-0 shr-top-0 shr-h-full shr-w-full shr-opacity-0', + 'file:shr-h-full file:shr-w-full file:shr-cursor-pointer', + 'file:disabled:shr-cursor-not-allowed', + ], + prefix: 'shr-me-0.5 shr-inline-flex', + }, + variants: { + size: { + default: { + inputWrapper: 'shr-px-1 shr-py-0.75 shr-text-base', + }, + s: { + inputWrapper: 'shr-p-0.5 shr-text-sm', + }, + }, + disabled: { + true: { + inputWrapper: 'shr-border-disabled shr-bg-white-darken shr-text-disabled', + }, + false: { + inputWrapper: 'hover:shr-border-darken hover:shr-bg-white-darken hover:shr-text-black', + }, + }, + }, + defaultVariants: { + size: 'default', + }, +}) + +export type Props = VariantProps & { + /** フォームのラベル */ + label: ReactNode + /** ファイルの選択に変更があったときに発火するコールバック関数 */ + onChange?: (files: File[]) => void + /** ファイルリストを表示するかどうか */ + hasFileList?: boolean + /** コンポーネント内のテキストを変更する関数 */ + decorators?: DecoratorsType + error?: boolean + /** ファイル複数選択の際に、選択済みのファイルと結合するかどうか */ + multiplyAppendable?: boolean +} +type ElementProps = Omit, keyof Props> +type DecoratorKeyTypes = 'destroy' + +const BASE_COLUMN_PADDING = { block: 0.5, inline: 1 } as const + +export const InputFileMultiplyAppendable = forwardRef( + ( + { + className, + size, + label, + hasFileList = true, + onChange, + disabled = false, + multiple, + multiplyAppendable = false, + error, + decorators, + ...props + }, + ref, + ) => { + const [files, setFiles] = useState([]) + const labelId = useId() + const { localize } = useIntl() + + const decoratorDefaultTexts = useMemo( + () => ({ + destroy: localize({ + id: 'smarthr-ui/InputFile/destroy', + defaultText: '削除', + }), + }), + [localize], + ) + + const decorated = useDecorators(decoratorDefaultTexts, decorators) + + const classNames = useMemo(() => { + const { wrapper, fileList, fileItem, inputWrapper, input, prefix } = classNameGenerator() + + return { + wrapper: wrapper({ className }), + inputWrapper: inputWrapper({ size, disabled }), + fileList: fileList(), + fileItem: fileItem(), + input: input(), + prefix: prefix(), + } + }, [disabled, size, className]) + + // Safari において、input.files への直接代入時に onChange が発火することを防ぐためのフラグ + const isUpdatingFilesDirectly = useRef(false) + + const inputRef = useRef(null) + useImperativeHandle( + ref, + () => inputRef.current, + ) + + const updateInputFiles = useCallback( + (newFiles: File[]) => { + if (!inputRef.current) { + return + } + const buff = new DataTransfer() + newFiles.forEach((file) => { + buff.items.add(file) + }) + + isUpdatingFilesDirectly.current = true + inputRef.current.files = buff.files + isUpdatingFilesDirectly.current = false + }, + [inputRef], + ) + + const updateFiles = useMemo( + () => + onChange + ? (newFiles: File[]) => { + onChange(newFiles) + if (multiplyAppendable) { + // multiplyAppendable以外ではinput要素を直接弄る必要がないので、updateInputFilesを呼ばない + updateInputFiles(newFiles) + } + setFiles(newFiles) + } + : setFiles, + [onChange, updateInputFiles, multiplyAppendable], + ) + + const handleChange = useCallback( + (e: ChangeEvent) => { + // Safari において、input.files への直接代入時はonChangeを発火させない + if (isUpdatingFilesDirectly.current) { + return + } + + const newFiles = Array.from(e.target.files ?? []) + + if (multiplyAppendable) { + // multiplyAppendableの場合、すでに選択済みのファイルと結合する + updateFiles([...files, ...newFiles]) + } else { + updateFiles(newFiles) + } + }, + [files, isUpdatingFilesDirectly, updateFiles, multiplyAppendable], + ) + + const handleDelete = useCallback( + (e: MouseEvent) => { + if (!inputRef.current) { + return + } + + const index = parseInt(e.currentTarget.value, 10) + const newFiles = files.filter((_, i) => index !== i) + + updateFiles(newFiles) + + // 削除後、同一ファイルを再選択可能にするためinput.valueをリセット + inputRef.current.value = '' + }, + [files, updateFiles], + ) + + return ( + + {!disabled && hasFileList && files.length > 0 && ( + + {files.map((file, index) => ( +
  • + {file.name} + +
  • + ))} +
    + )} + + + + + +
    + ) + }, +) + +const StyledFaFolderOpenIcon = memo<{ className: string }>(({ className }) => ( + + + +)) + +const LabelRender = memo<{ id: string; label: ReactNode }>(({ id, label }) => ( + +)) diff --git a/packages/smarthr-ui/src/components/InputFile/NativeInputFile.tsx b/packages/smarthr-ui/src/components/InputFile/InputFileNative.tsx similarity index 98% rename from packages/smarthr-ui/src/components/InputFile/NativeInputFile.tsx rename to packages/smarthr-ui/src/components/InputFile/InputFileNative.tsx index f4e557642c..8420f6fa14 100644 --- a/packages/smarthr-ui/src/components/InputFile/NativeInputFile.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFileNative.tsx @@ -81,7 +81,7 @@ type DecoratorKeyTypes = 'destroy' const BASE_COLUMN_PADDING = { block: 0.5, inline: 1 } as const -export const InputFile = forwardRef( +export const InputFileNative = forwardRef( ( { className, From 69b6008337548ae0ac5c3a1479aec15ca2f72e22 Mon Sep 17 00:00:00 2001 From: Misako Tateiwa Date: Tue, 7 Oct 2025 13:23:41 +0900 Subject: [PATCH 03/10] =?UTF-8?q?fix:=20value=E3=81=AE=E5=88=9D=E6=9C=9F?= =?UTF-8?q?=E5=8C=96=E4=BD=8D=E7=BD=AE=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/InputFile/InputFileMultiplyAppendable.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx index c51b7dfb41..c46beb3946 100644 --- a/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx @@ -198,10 +198,10 @@ export const InputFileMultiplyAppendable = forwardRef index !== i) - updateFiles(newFiles) - // 削除後、同一ファイルを再選択可能にするためinput.valueをリセット inputRef.current.value = '' + + updateFiles(newFiles) }, [files, updateFiles], ) @@ -227,6 +227,7 @@ export const InputFileMultiplyAppendable = forwardRef )} + {/* eslint-disable-next-line smarthr/a11y-input-in-form-control */} Date: Tue, 7 Oct 2025 13:25:09 +0900 Subject: [PATCH 04/10] =?UTF-8?q?fix:=20onChange=E3=81=8C=E3=81=AA?= =?UTF-8?q?=E3=81=8F=E3=81=A6=E3=82=82=E8=A4=87=E6=95=B0=E5=9B=9E=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E3=82=A2=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=89=E3=81=97=E3=81=9F=E3=81=A8=E3=81=8D?= =?UTF-8?q?=E3=81=AB=E3=83=87=E3=83=BC=E3=82=BF=E3=81=8C=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/InputFile/InputFileMultiplyAppendable.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx index c46beb3946..374ef0afdf 100644 --- a/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx @@ -166,7 +166,13 @@ export const InputFileMultiplyAppendable = forwardRef { + setFiles(newFiles) + if (multiplyAppendable) { + // multiplyAppendable以外ではinput要素を直接弄る必要がないので、updateInputFilesを呼ばない + updateInputFiles(newFiles) + } + }, [onChange, updateInputFiles, multiplyAppendable], ) From 6ec054653ef71b4cef800f9146a9cd86476d26b5 Mon Sep 17 00:00:00 2001 From: Misako Tateiwa Date: Tue, 7 Oct 2025 13:32:23 +0900 Subject: [PATCH 05/10] =?UTF-8?q?refactor:=20InputFile=E5=91=A8=E3=82=8A?= =?UTF-8?q?=E3=81=AE=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/InputFile/InputFile.tsx | 84 +------------------ .../src/components/InputFile/style.ts | 43 ++++++++++ .../src/components/InputFile/types.ts | 22 +++++ 3 files changed, 68 insertions(+), 81 deletions(-) create mode 100644 packages/smarthr-ui/src/components/InputFile/style.ts create mode 100644 packages/smarthr-ui/src/components/InputFile/types.ts diff --git a/packages/smarthr-ui/src/components/InputFile/InputFile.tsx b/packages/smarthr-ui/src/components/InputFile/InputFile.tsx index 60ae55320f..9e8e2e06f2 100644 --- a/packages/smarthr-ui/src/components/InputFile/InputFile.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFile.tsx @@ -1,88 +1,10 @@ 'use client' -import { - type ChangeEvent, - type ComponentPropsWithRef, - type MouseEvent, - type ReactNode, - forwardRef, - memo, - useCallback, - useId, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react' -import { type VariantProps, tv } from 'tailwind-variants' +import { forwardRef } from 'react' -import { type DecoratorsType, useDecorators } from '../../hooks/useDecorators' -import { useIntl } from '../../intl' -import { BaseColumn } from '../Base' -import { Button } from '../Button' -import { FaFolderOpenIcon, FaTrashCanIcon } from '../Icon' -import { Stack } from '../Layout' +import { InputFileMultiplyAppendable, type Props } from './InputFileMultiplyAppendable' -import { InputFileMultiplyAppendable } from './InputFileMultiplyAppendable' -import { InputFileNative } from './InputFileNative' - -const classNameGenerator = tv({ - slots: { - wrapper: 'smarthr-ui-InputFile shr-block', - fileList: ['smarthr-ui-InputFile-fileList', 'shr-list-none shr-self-stretch shr-text-base'], - fileItem: 'shr-flex shr-items-center', - inputWrapper: [ - 'shr-border-shorthand shr-relative shr-inline-flex shr-rounded-m shr-bg-white shr-font-bold shr-leading-none', - 'contrast-more:shr-border-high-contrast', - 'focus-within:shr-focus-indicator', - 'has-[[aria-invalid]]:shr-border-danger', - ], - input: [ - 'smarthr-ui-InputFile-input', - 'shr-absolute shr-left-0 shr-top-0 shr-h-full shr-w-full shr-opacity-0', - 'file:shr-h-full file:shr-w-full file:shr-cursor-pointer', - 'file:disabled:shr-cursor-not-allowed', - ], - prefix: 'shr-me-0.5 shr-inline-flex', - }, - variants: { - size: { - default: { - inputWrapper: 'shr-px-1 shr-py-0.75 shr-text-base', - }, - s: { - inputWrapper: 'shr-p-0.5 shr-text-sm', - }, - }, - disabled: { - true: { - inputWrapper: 'shr-border-disabled shr-bg-white-darken shr-text-disabled', - }, - false: { - inputWrapper: 'hover:shr-border-darken hover:shr-bg-white-darken hover:shr-text-black', - }, - }, - }, - defaultVariants: { - size: 'default', - }, -}) - -export type Props = VariantProps & { - /** フォームのラベル */ - label: ReactNode - /** ファイルの選択に変更があったときに発火するコールバック関数 */ - onChange?: (files: File[]) => void - /** ファイルリストを表示するかどうか */ - hasFileList?: boolean - /** コンポーネント内のテキストを変更する関数 */ - decorators?: DecoratorsType - error?: boolean - /** ファイル複数選択の際に、選択済みのファイルと結合するかどうか */ - multiplyAppendable?: boolean -} -type ElementProps = Omit, keyof Props> -type DecoratorKeyTypes = 'destroy' +import type { ElementProps } from './types' export const InputFile = forwardRef((props, ref) => { if (props.multiple && props.multiplyAppendable) { diff --git a/packages/smarthr-ui/src/components/InputFile/style.ts b/packages/smarthr-ui/src/components/InputFile/style.ts new file mode 100644 index 0000000000..ca4f35aa50 --- /dev/null +++ b/packages/smarthr-ui/src/components/InputFile/style.ts @@ -0,0 +1,43 @@ +import { tv } from 'tailwind-variants' + +export const classNameGenerator = tv({ + slots: { + wrapper: 'smarthr-ui-InputFile shr-block', + fileList: ['smarthr-ui-InputFile-fileList', 'shr-list-none shr-self-stretch shr-text-base'], + fileItem: 'shr-flex shr-items-center', + inputWrapper: [ + 'shr-border-shorthand shr-relative shr-inline-flex shr-rounded-m shr-bg-white shr-font-bold shr-leading-none', + 'contrast-more:shr-border-high-contrast', + 'focus-within:shr-focus-indicator', + 'has-[[aria-invalid]]:shr-border-danger', + ], + input: [ + 'smarthr-ui-InputFile-input', + 'shr-absolute shr-left-0 shr-top-0 shr-h-full shr-w-full shr-opacity-0', + 'file:shr-h-full file:shr-w-full file:shr-cursor-pointer', + 'file:disabled:shr-cursor-not-allowed', + ], + prefix: 'shr-me-0.5 shr-inline-flex', + }, + variants: { + size: { + default: { + inputWrapper: 'shr-px-1 shr-py-0.75 shr-text-base', + }, + s: { + inputWrapper: 'shr-p-0.5 shr-text-sm', + }, + }, + disabled: { + true: { + inputWrapper: 'shr-border-disabled shr-bg-white-darken shr-text-disabled', + }, + false: { + inputWrapper: 'hover:shr-border-darken hover:shr-bg-white-darken hover:shr-text-black', + }, + }, + }, + defaultVariants: { + size: 'default', + }, +}) diff --git a/packages/smarthr-ui/src/components/InputFile/types.ts b/packages/smarthr-ui/src/components/InputFile/types.ts new file mode 100644 index 0000000000..c9fe47fd16 --- /dev/null +++ b/packages/smarthr-ui/src/components/InputFile/types.ts @@ -0,0 +1,22 @@ +import type { classNameGenerator } from './style' +import type { DecoratorsType } from '../../hooks/useDecorators' +import type { ComponentPropsWithRef, ReactNode } from 'react' +import type { VariantProps } from 'tailwind-variants' + +type DecoratorKeyTypes = 'destroy' + +export type Props = VariantProps & { + /** フォームのラベル */ + label: ReactNode + /** ファイルの選択に変更があったときに発火するコールバック関数 */ + onChange?: (files: File[]) => void + /** ファイルリストを表示するかどうか */ + hasFileList?: boolean + /** コンポーネント内のテキストを変更する関数 */ + decorators?: DecoratorsType + error?: boolean + /** ファイル複数選択の際に、選択済みのファイルと結合するかどうか */ + multiplyAppendable?: boolean +} + +export type ElementProps = Omit, keyof Props> From 9ed52d7b8a29fbade15b578a75b2fb47fedb7f96 Mon Sep 17 00:00:00 2001 From: Misako Tateiwa Date: Tue, 7 Oct 2025 13:33:50 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20InputFileNative=E3=82=92=E8=BF=94?= =?UTF-8?q?=E3=81=9B=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/smarthr-ui/src/components/InputFile/InputFile.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/smarthr-ui/src/components/InputFile/InputFile.tsx b/packages/smarthr-ui/src/components/InputFile/InputFile.tsx index 9e8e2e06f2..64b2b7c986 100644 --- a/packages/smarthr-ui/src/components/InputFile/InputFile.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFile.tsx @@ -2,12 +2,14 @@ import { forwardRef } from 'react' -import { InputFileMultiplyAppendable, type Props } from './InputFileMultiplyAppendable' +import { InputFileMultiplyAppendable } from './InputFileMultiplyAppendable' +import { InputFileNative } from './InputFileNative' -import type { ElementProps } from './types' +import type { ElementProps, Props } from './types' export const InputFile = forwardRef((props, ref) => { if (props.multiple && props.multiplyAppendable) { return } + return }) From 64864f78321959abb3ada4e1ca56defc0c8c6ebf Mon Sep 17 00:00:00 2001 From: Misako Tateiwa Date: Tue, 7 Oct 2025 13:36:51 +0900 Subject: [PATCH 07/10] =?UTF-8?q?refactor:=20import=E5=91=A8=E3=82=8A?= =?UTF-8?q?=E3=82=92=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InputFile/InputFileMultiplyAppendable.tsx | 62 +----------------- .../components/InputFile/InputFileNative.tsx | 63 ++----------------- .../src/components/InputFile/types.ts | 2 +- 3 files changed, 9 insertions(+), 118 deletions(-) diff --git a/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx index 374ef0afdf..e73159d57f 100644 --- a/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx @@ -2,7 +2,6 @@ import { type ChangeEvent, - type ComponentPropsWithRef, type MouseEvent, type ReactNode, forwardRef, @@ -14,72 +13,17 @@ import { useRef, useState, } from 'react' -import { type VariantProps, tv } from 'tailwind-variants' -import { type DecoratorsType, useDecorators } from '../../hooks/useDecorators' +import { useDecorators } from '../../hooks/useDecorators' import { useIntl } from '../../intl' import { BaseColumn } from '../Base' import { Button } from '../Button' import { FaFolderOpenIcon, FaTrashCanIcon } from '../Icon' import { Stack } from '../Layout' -const classNameGenerator = tv({ - slots: { - wrapper: 'smarthr-ui-InputFile shr-block', - fileList: ['smarthr-ui-InputFile-fileList', 'shr-list-none shr-self-stretch shr-text-base'], - fileItem: 'shr-flex shr-items-center', - inputWrapper: [ - 'shr-border-shorthand shr-relative shr-inline-flex shr-rounded-m shr-bg-white shr-font-bold shr-leading-none', - 'contrast-more:shr-border-high-contrast', - 'focus-within:shr-focus-indicator', - 'has-[[aria-invalid]]:shr-border-danger', - ], - input: [ - 'smarthr-ui-InputFile-input', - 'shr-absolute shr-left-0 shr-top-0 shr-h-full shr-w-full shr-opacity-0', - 'file:shr-h-full file:shr-w-full file:shr-cursor-pointer', - 'file:disabled:shr-cursor-not-allowed', - ], - prefix: 'shr-me-0.5 shr-inline-flex', - }, - variants: { - size: { - default: { - inputWrapper: 'shr-px-1 shr-py-0.75 shr-text-base', - }, - s: { - inputWrapper: 'shr-p-0.5 shr-text-sm', - }, - }, - disabled: { - true: { - inputWrapper: 'shr-border-disabled shr-bg-white-darken shr-text-disabled', - }, - false: { - inputWrapper: 'hover:shr-border-darken hover:shr-bg-white-darken hover:shr-text-black', - }, - }, - }, - defaultVariants: { - size: 'default', - }, -}) +import { classNameGenerator } from './style' -export type Props = VariantProps & { - /** フォームのラベル */ - label: ReactNode - /** ファイルの選択に変更があったときに発火するコールバック関数 */ - onChange?: (files: File[]) => void - /** ファイルリストを表示するかどうか */ - hasFileList?: boolean - /** コンポーネント内のテキストを変更する関数 */ - decorators?: DecoratorsType - error?: boolean - /** ファイル複数選択の際に、選択済みのファイルと結合するかどうか */ - multiplyAppendable?: boolean -} -type ElementProps = Omit, keyof Props> -type DecoratorKeyTypes = 'destroy' +import type { DecoratorKeyTypes, ElementProps, Props } from './types' const BASE_COLUMN_PADDING = { block: 0.5, inline: 1 } as const diff --git a/packages/smarthr-ui/src/components/InputFile/InputFileNative.tsx b/packages/smarthr-ui/src/components/InputFile/InputFileNative.tsx index 8420f6fa14..92e24286fd 100644 --- a/packages/smarthr-ui/src/components/InputFile/InputFileNative.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFileNative.tsx @@ -2,7 +2,6 @@ import { type ChangeEvent, - type ComponentPropsWithRef, type MouseEvent, type ReactNode, forwardRef, @@ -14,70 +13,17 @@ import { useRef, useState, } from 'react' -import { type VariantProps, tv } from 'tailwind-variants' -import { type DecoratorsType, useDecorators } from '../../hooks/useDecorators' +import { useDecorators } from '../../hooks/useDecorators' import { useIntl } from '../../intl' import { BaseColumn } from '../Base' import { Button } from '../Button' import { FaFolderOpenIcon, FaTrashCanIcon } from '../Icon' import { Stack } from '../Layout' -const classNameGenerator = tv({ - slots: { - wrapper: 'smarthr-ui-InputFile shr-block', - fileList: ['smarthr-ui-InputFile-fileList', 'shr-list-none shr-self-stretch shr-text-base'], - fileItem: 'shr-flex shr-items-center', - inputWrapper: [ - 'shr-border-shorthand shr-relative shr-inline-flex shr-rounded-m shr-bg-white shr-font-bold shr-leading-none', - 'contrast-more:shr-border-high-contrast', - 'focus-within:shr-focus-indicator', - 'has-[[aria-invalid]]:shr-border-danger', - ], - input: [ - 'smarthr-ui-InputFile-input', - 'shr-absolute shr-left-0 shr-top-0 shr-h-full shr-w-full shr-opacity-0', - 'file:shr-h-full file:shr-w-full file:shr-cursor-pointer', - 'file:disabled:shr-cursor-not-allowed', - ], - prefix: 'shr-me-0.5 shr-inline-flex', - }, - variants: { - size: { - default: { - inputWrapper: 'shr-px-1 shr-py-0.75 shr-text-base', - }, - s: { - inputWrapper: 'shr-p-0.5 shr-text-sm', - }, - }, - disabled: { - true: { - inputWrapper: 'shr-border-disabled shr-bg-white-darken shr-text-disabled', - }, - false: { - inputWrapper: 'hover:shr-border-darken hover:shr-bg-white-darken hover:shr-text-black', - }, - }, - }, - defaultVariants: { - size: 'default', - }, -}) - -export type Props = VariantProps & { - /** フォームのラベル */ - label: ReactNode - /** ファイルの選択に変更があったときに発火するコールバック関数 */ - onChange?: (files: File[]) => void - /** ファイルリストを表示するかどうか */ - hasFileList?: boolean - /** コンポーネント内のテキストを変更する関数 */ - decorators?: DecoratorsType - error?: boolean -} -type ElementProps = Omit, keyof Props> -type DecoratorKeyTypes = 'destroy' +import { classNameGenerator } from './style' + +import type { DecoratorKeyTypes, ElementProps, Props } from './types' const BASE_COLUMN_PADDING = { block: 0.5, inline: 1 } as const @@ -201,6 +147,7 @@ export const InputFileNative = forwardRef )} + {/* eslint-disable-next-line smarthr/a11y-input-in-form-control */} & { /** フォームのラベル */ From 448943e9c5958899040fa9cdba472443f9fd408c Mon Sep 17 00:00:00 2001 From: Misako Tateiwa Date: Tue, 7 Oct 2025 13:41:48 +0900 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=E8=A4=87=E6=95=B0=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AB=E7=89=B9=E5=8C=96=E3=81=95?= =?UTF-8?q?=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InputFile/InputFileMultiplyAppendable.tsx | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx index e73159d57f..817d5319c8 100644 --- a/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx @@ -27,7 +27,10 @@ import type { DecoratorKeyTypes, ElementProps, Props } from './types' const BASE_COLUMN_PADDING = { block: 0.5, inline: 1 } as const -export const InputFileMultiplyAppendable = forwardRef( +export const InputFileMultiplyAppendable = forwardRef< + HTMLInputElement, + Omit & Omit +>( ( { className, @@ -36,8 +39,6 @@ export const InputFileMultiplyAppendable = forwardRef { onChange(newFiles) - if (multiplyAppendable) { - // multiplyAppendable以外ではinput要素を直接弄る必要がないので、updateInputFilesを呼ばない - updateInputFiles(newFiles) - } + updateInputFiles(newFiles) setFiles(newFiles) } : (newFiles: File[]) => { setFiles(newFiles) - if (multiplyAppendable) { - // multiplyAppendable以外ではinput要素を直接弄る必要がないので、updateInputFilesを呼ばない - updateInputFiles(newFiles) - } + updateInputFiles(newFiles) }, - [onChange, updateInputFiles, multiplyAppendable], + [onChange, updateInputFiles], ) const handleChange = useCallback( @@ -129,14 +124,9 @@ export const InputFileMultiplyAppendable = forwardRef Date: Tue, 7 Oct 2025 13:51:07 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20=E8=87=AA=E5=8B=95=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84=E3=82=82?= =?UTF-8?q?=E3=81=AE=E3=81=AF=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E3=82=A2?= =?UTF-8?q?=E3=82=A6=E3=83=88=EF=BC=86=E5=9E=8B=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/InputFile/InputFile.test.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/smarthr-ui/src/components/InputFile/InputFile.test.tsx b/packages/smarthr-ui/src/components/InputFile/InputFile.test.tsx index 343241249c..8daf670abc 100644 --- a/packages/smarthr-ui/src/components/InputFile/InputFile.test.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFile.test.tsx @@ -76,7 +76,8 @@ describe('InputFile', () => { expect(screen.getByText(file1.name)).toBeInTheDocument() }) - it('ファイル選択後、削除するとinputのvalueには存在しないこと', async () => { + // FIXME: DataTransferで落ちるので修正が必要 + it.skip('ファイル選択後、削除するとinputのvalueには存在しないこと', async () => { await render(
    @@ -86,7 +87,7 @@ describe('InputFile', () => {
    , ) - const input = screen.getByLabelText('input file') + const input: HTMLInputElement = screen.getByLabelText('input file') await userEvent.upload(input, file1) expect(input.files).toHaveLength(1) const deleteButton = screen.getByRole('button', { name: '削除' }) @@ -94,7 +95,8 @@ describe('InputFile', () => { expect(input.files).toHaveLength(0) }) - it('multipleで複数ファイル選択後、1つ削除すると削除したもののみinputのvalueには存在しないこと', async () => { + // FIXME: DataTransferで落ちるので修正が必要 + it.skip('multipleで複数ファイル選択後、1つ削除すると削除したもののみinputのvalueには存在しないこと', async () => { await render(
    @@ -104,15 +106,16 @@ describe('InputFile', () => {
    , ) - const input = screen.getByLabelText('input file') + const input: HTMLInputElement = screen.getByLabelText('input file') await userEvent.upload(input, [file1, file2]) expect(input.files).toHaveLength(2) - const deleteButton = screen.getByRole('button', { name: '削除' }) + const deleteButton = screen.getAllByRole('button', { name: '削除' })[0] await userEvent.click(deleteButton) expect(input.files).toHaveLength(1) }) - it('ファイル削除後、ファイルリストに存在しないこと', async () => { + // FIXME: DataTransferで落ちるので修正が必要 + it.skip('ファイル削除後、ファイルリストに存在しないこと', async () => { await render(
    From f54355de7defb9d974405e25bf6bb1b3410b836f Mon Sep 17 00:00:00 2001 From: Qs-F Date: Tue, 7 Oct 2025 21:40:27 +0900 Subject: [PATCH 10/10] test: Add manual test Story --- .../stories/TestInputFile.stories.tsx | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 packages/smarthr-ui/src/components/InputFile/stories/TestInputFile.stories.tsx diff --git a/packages/smarthr-ui/src/components/InputFile/stories/TestInputFile.stories.tsx b/packages/smarthr-ui/src/components/InputFile/stories/TestInputFile.stories.tsx new file mode 100644 index 0000000000..9dc3e83579 --- /dev/null +++ b/packages/smarthr-ui/src/components/InputFile/stories/TestInputFile.stories.tsx @@ -0,0 +1,201 @@ +import { useState } from 'react' +import { BaseColumn } from '../../Base' +import { Button } from '../../Button' +import { FormControl } from '../../FormControl' +import { Stack } from '../../Layout' +import { Text } from '../../Text' +import { InputFile } from '../InputFile' +import { InformationPanel } from '../../InformationPanel' + +import type { Meta } from '@storybook/react' +import { Heading } from '../../Heading' + +export default { + title: 'Components/InputFile/Test', + component: InputFile, + render: (args) => , + parameters: { + chromatic: { disableSnapshot: true }, + }, + tags: ['!autodocs'], +} satisfies Meta + +export const ManualTest1 = () => { + const [result, setResult] = useState('') + return ( + + +
      +
    1. 「ファイルを選択」ボタンを押して、複数のファイルを選択してください。
    2. +
    3. 「送信」ボタンを押してください。
    4. +
    5. 送信結果に選択したファイル名が表示されていることを確認してください。
    6. +
    7. ファイルを1つ削除してください。
    8. +
    9. 「送信」ボタンを押してください。
    10. +
    11. 送信結果に削除したファイル名が表示されていないことを確認してください。
    12. +
    +
    + { + e.preventDefault() + const data = new FormData(e.currentTarget) + setResult( + data + .getAll('files') + .map((file) => (file instanceof File ? file.name : '')) + .join('\n'), + ) + }} + > + + + + + + + + 送信結果 + + + {result} + + +
    + ) +} + +export const ManualTest2 = () => { + const [result, setResult] = useState('') + return ( + + +
      +
    1. 「ファイルを選択」ボタンを押して、複数のファイルを選択してください。
    2. +
    3. 「送信」ボタンを押してください。
    4. +
    5. 再度「ファイルを選択」ボタンを押して、複数のファイルを選択してください。
    6. +
    7. 「送信」ボタンを押してください。
    8. +
    9. 送信結果に選択したファイル名が追加されていることを確認してください。
    10. +
    11. ファイルを1つ削除してください。
    12. +
    13. 「送信」ボタンを押してください。
    14. +
    15. 送信結果に削除したファイル名が表示されていないことを確認してください。
    16. +
    +
    +
    { + e.preventDefault() + const data = new FormData(e.currentTarget) + setResult( + data + .getAll('files') + .map((file) => (file instanceof File ? file.name : '')) + .join('\n'), + ) + }} + > + + + + + + +
    + 送信結果 + + + {result} + + +
    + ) +} + +export const ManualTest3 = () => { + const [value, setValue] = useState([]) + return ( + + +
      +
    1. 「ファイルを選択」ボタンを押して、複数のファイルを選択してください。
    2. +
    3. 送信結果に選択したファイル名が追加されていることを確認してください。
    4. +
    5. ファイルを1つ削除してください。
    6. +
    7. 送信結果に削除したファイル名が表示されていないことを確認してください。
    8. +
    +
    +
    { + e.preventDefault() + }} + > + + + { + setValue(files) + }} + label="ファイルを選択" + name="files" + multiple + hasFileList + /> + + +
    + onChange + + + {value.map((file) => file.name).join('\n')} + + +
    + ) +} + +export const ManualTest4 = () => { + const [value, setValue] = useState([]) + return ( + + +
      +
    1. 「ファイルを選択」ボタンを押して、複数のファイルを選択してください。
    2. +
    3. 送信結果に選択したファイル名が追加されていることを確認してください。
    4. +
    5. 再度「ファイルを選択」ボタンを押して、複数のファイルを選択してください。
    6. +
    7. ファイルを1つ削除してください。
    8. +
    9. 送信結果に削除したファイル名が表示されていないことを確認してください。
    10. +
    11. 消したファイルを再度選択してください。
    12. +
    13. 送信結果に再度選択したファイル名が追加されていることを確認してください。
    14. +
    +
    +
    { + e.preventDefault() + }} + > + + + { + setValue(files) + }} + label="ファイルを選択" + name="files" + multiple + multiplyAppendable + hasFileList + /> + + +
    + onChange + + + {value.map((file) => file.name).join('\n')} + + +
    + ) +}