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..8daf670abc --- /dev/null +++ b/packages/smarthr-ui/src/components/InputFile/InputFile.test.tsx @@ -0,0 +1,135 @@ +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() + }) + + // FIXME: DataTransferで落ちるので修正が必要 + it.skip('ファイル選択後、削除するとinputのvalueには存在しないこと', async () => { + await render( + +
+ + + +
+
, + ) + const input: HTMLInputElement = 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) + }) + + // FIXME: DataTransferで落ちるので修正が必要 + it.skip('multipleで複数ファイル選択後、1つ削除すると削除したもののみinputのvalueには存在しないこと', async () => { + await render( + +
+ + + +
+
, + ) + const input: HTMLInputElement = screen.getByLabelText('input file') + await userEvent.upload(input, [file1, file2]) + expect(input.files).toHaveLength(2) + const deleteButton = screen.getAllByRole('button', { name: '削除' })[0] + await userEvent.click(deleteButton) + expect(input.files).toHaveLength(1) + }) + + // FIXME: DataTransferで落ちるので修正が必要 + it.skip('ファイル削除後、ファイルリストに存在しないこと', 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..64b2b7c986 100644 --- a/packages/smarthr-ui/src/components/InputFile/InputFile.tsx +++ b/packages/smarthr-ui/src/components/InputFile/InputFile.tsx @@ -1,256 +1,15 @@ '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 } 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' - -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) - }) +import type { ElementProps, Props } from './types' - 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 + } + 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..817d5319c8 --- /dev/null +++ b/packages/smarthr-ui/src/components/InputFile/InputFileMultiplyAppendable.tsx @@ -0,0 +1,201 @@ +'use client' + +import { + type ChangeEvent, + type MouseEvent, + type ReactNode, + forwardRef, + memo, + useCallback, + useId, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react' + +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' + +import { classNameGenerator } from './style' + +import type { DecoratorKeyTypes, ElementProps, Props } from './types' + +const BASE_COLUMN_PADDING = { block: 0.5, inline: 1 } as const + +export const InputFileMultiplyAppendable = forwardRef< + HTMLInputElement, + Omit & Omit +>( + ( + { + 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 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) + updateInputFiles(newFiles) + setFiles(newFiles) + } + : (newFiles: File[]) => { + setFiles(newFiles) + updateInputFiles(newFiles) + }, + [onChange, updateInputFiles], + ) + + const handleChange = useCallback( + (e: ChangeEvent) => { + // Safari において、input.files への直接代入時はonChangeを発火させない + if (isUpdatingFilesDirectly.current) { + return + } + + const newFiles = Array.from(e.target.files ?? []) + + updateFiles([...files, ...newFiles]) + }, + [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) + + // 削除後、同一ファイルを再選択可能にするためinput.valueをリセット + inputRef.current.value = '' + + updateFiles(newFiles) + }, + [files, updateFiles], + ) + + return ( + + {!disabled && hasFileList && files.length > 0 && ( + + {files.map((file, index) => ( +
  • + {file.name} + +
  • + ))} +
    + )} + + {/* eslint-disable-next-line smarthr/a11y-input-in-form-control */} + + + + +
    + ) + }, +) + +const StyledFaFolderOpenIcon = memo<{ className: string }>(({ className }) => ( + + + +)) + +const LabelRender = memo<{ id: string; label: ReactNode }>(({ id, label }) => ( + +)) diff --git a/packages/smarthr-ui/src/components/InputFile/InputFileNative.tsx b/packages/smarthr-ui/src/components/InputFile/InputFileNative.tsx new file mode 100644 index 0000000000..92e24286fd --- /dev/null +++ b/packages/smarthr-ui/src/components/InputFile/InputFileNative.tsx @@ -0,0 +1,180 @@ +'use client' + +import { + type ChangeEvent, + type MouseEvent, + type ReactNode, + forwardRef, + memo, + useCallback, + useId, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react' + +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' + +import { classNameGenerator } from './style' + +import type { DecoratorKeyTypes, ElementProps, Props } from './types' + +const BASE_COLUMN_PADDING = { block: 0.5, inline: 1 } as const + +export const InputFileNative = 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} + + +
  • + ))} +
    + )} + + {/* eslint-disable-next-line smarthr/a11y-input-in-form-control */} + + + + +
    + ) + }, +) + +const StyledFaFolderOpenIcon = memo<{ className: string }>(({ className }) => ( + + + +)) + +const LabelRender = memo<{ id: string; label: ReactNode }>(({ id, label }) => ( + +)) 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')} + + +
    + ) +} 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..82c7612a42 --- /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' + +export 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>