From eb47759bb41be8c3c104b93339225b2fa99e302f Mon Sep 17 00:00:00 2001 From: "Mr.Dr.Professor Patrick" Date: Fri, 14 Apr 2023 18:33:55 +0200 Subject: [PATCH] feat: add useFileInput hook (#624) --- src/components/index.ts | 1 + .../__stories__/UseFileInput.stories.tsx | 20 +++++ .../utils/useFileInput/useFileInput.ts | 74 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 src/components/utils/useFileInput/__stories__/UseFileInput.stories.tsx create mode 100644 src/components/utils/useFileInput/useFileInput.ts diff --git a/src/components/index.ts b/src/components/index.ts index 179b016cd..a2ad8b6f6 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -66,3 +66,4 @@ export * from './utils/useForkRef'; export * from './utils/setRef'; export {useOnFocusOutside} from './utils/useOnFocusOutside'; export * from './utils/xpath'; +export * from './utils/useFileInput/useFileInput'; diff --git a/src/components/utils/useFileInput/__stories__/UseFileInput.stories.tsx b/src/components/utils/useFileInput/__stories__/UseFileInput.stories.tsx new file mode 100644 index 000000000..16c01fce3 --- /dev/null +++ b/src/components/utils/useFileInput/__stories__/UseFileInput.stories.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import {Meta, Story} from '@storybook/react'; +import {Button} from '../../../Button'; +import {useFileInput} from '../useFileInput'; + +export default {title: 'Hooks/useFileInput'} as Meta; + +const DefaultTemplate: Story = () => { + const onUpdate = React.useCallback((files: File[]) => console.log(files), []); + const {controlProps, triggerProps} = useFileInput({onUpdate}); + + return ( + + + + + ); +}; + +export const Default = DefaultTemplate.bind({}); diff --git a/src/components/utils/useFileInput/useFileInput.ts b/src/components/utils/useFileInput/useFileInput.ts new file mode 100644 index 000000000..642eba290 --- /dev/null +++ b/src/components/utils/useFileInput/useFileInput.ts @@ -0,0 +1,74 @@ +/* eslint-disable valid-jsdoc */ +import React from 'react'; + +export type UseFileInputProps = { + onUpdate?: (files: File[]) => void; + onChange?: (event: React.ChangeEvent) => void; +}; + +export type UseFileInputOutput = { + controlProps: React.DetailedHTMLProps< + React.InputHTMLAttributes, + HTMLInputElement + >; + triggerProps: { + onClick: () => void; + }; +}; + +/** + * Used to shape props for input with type "file". + * + * Usage example: + ```tsx + import React from 'react'; + import {Button, useFileInput} from '@gravity-ui/uikit'; + + const Component = () => { + const onUpdate = React.useCallback((files: File[]) => console.log(files), []); + const {controlProps, triggerProps} = useFileInput({onUpdate}); + + return ( + + + + + ); + }; +``` +*/ +export function useFileInput({onUpdate, onChange}: UseFileInputProps): UseFileInputOutput { + const ref = React.useRef(null); + + const handleChange = React.useCallback( + (event: React.ChangeEvent) => { + onChange?.(event); + onUpdate?.(Array.from(event.target.files || [])); + // https://stackoverflow.com/a/12102992 + event.target.value = ''; + }, + [onChange, onUpdate], + ); + + const openDeviceStorage = React.useCallback(() => { + ref.current?.click(); + }, []); + + const result: UseFileInputOutput = React.useMemo( + () => ({ + controlProps: { + ref, + type: 'file', + tabIndex: -1, + style: {opacity: 0, position: 'absolute', width: 1, height: 1}, + onChange: handleChange, + }, + triggerProps: { + onClick: openDeviceStorage, + }, + }), + [handleChange, openDeviceStorage], + ); + + return result; +}