cozy-ui 60.4.0 → 60.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # [60.5.0](https://github.com/cozy/cozy-ui/compare/v60.4.0...v60.5.0) (2022-01-20)
2
+
3
+
4
+ ### Features
5
+
6
+ * Add FilePicker component ([288d047](https://github.com/cozy/cozy-ui/commit/288d047))
7
+ * Add filesize package ([9286dcd](https://github.com/cozy/cozy-ui/commit/9286dcd))
8
+
1
9
  # [60.4.0](https://github.com/cozy/cozy-ui/compare/v60.3.0...v60.4.0) (2022-01-20)
2
10
 
3
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-ui",
3
- "version": "60.4.0",
3
+ "version": "60.5.0",
4
4
  "description": "Cozy apps UI SDK",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -153,6 +153,7 @@
153
153
  "classnames": "^2.2.5",
154
154
  "cozy-interapp": "^0.5.4",
155
155
  "date-fns": "^1.28.5",
156
+ "filesize": "8.0.7",
156
157
  "hammerjs": "^2.0.8",
157
158
  "intersection-observer": "0.11.0",
158
159
  "mui-bottom-sheet": "https://github.com/cozy/mui-bottom-sheet.git#v1.0.6",
@@ -0,0 +1,112 @@
1
+ import React, { useCallback, memo } from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import { models, useQuery } from 'cozy-client'
5
+ import List from '../MuiCozyTheme/List'
6
+ import LoadMore from '../LoadMore'
7
+
8
+ import { buildContentFolderQuery } from './queries'
9
+ import FilePickerBodyItem from './FilePickerBodyItem'
10
+
11
+ const {
12
+ file: { isDirectory, isFile }
13
+ } = models
14
+
15
+ const FilePickerBody = ({
16
+ navigateTo,
17
+ folderId,
18
+ onSelectFileId,
19
+ filesIdsSelected,
20
+ fileTypesAccepted,
21
+ multiple
22
+ }) => {
23
+ const contentFolderQuery = buildContentFolderQuery(folderId)
24
+ const { data: contentFolder, hasMore, fetchMore } = useQuery(
25
+ contentFolderQuery.definition,
26
+ contentFolderQuery.options
27
+ )
28
+
29
+ const onCheck = useCallback(
30
+ fileId => {
31
+ const isChecked = filesIdsSelected.some(
32
+ fileIdSelected => fileIdSelected === fileId
33
+ )
34
+ if (isChecked) {
35
+ onSelectFileId(
36
+ filesIdsSelected.filter(fileIdSelected => fileIdSelected !== fileId)
37
+ )
38
+ } else onSelectFileId(prev => [...prev, fileId])
39
+ },
40
+ [filesIdsSelected, onSelectFileId]
41
+ )
42
+
43
+ // When click on checkbox/radio area...
44
+ const handleChoiceClick = useCallback(
45
+ file => () => {
46
+ if (multiple) onCheck(file._id)
47
+ else onSelectFileId(file._id)
48
+ },
49
+ [multiple, onCheck, onSelectFileId]
50
+ )
51
+
52
+ // ...when click anywhere on the rest of the line
53
+ const handleListItemClick = useCallback(
54
+ file => () => {
55
+ if (isDirectory(file)) {
56
+ navigateTo(contentFolder.find(f => f._id === file._id))
57
+ }
58
+
59
+ if (isFile(file) && fileTypesAccepted.file) {
60
+ if (multiple) onCheck(file._id)
61
+ else onSelectFileId(file._id)
62
+ }
63
+ },
64
+ [
65
+ contentFolder,
66
+ fileTypesAccepted.file,
67
+ multiple,
68
+ navigateTo,
69
+ onCheck,
70
+ onSelectFileId
71
+ ]
72
+ )
73
+
74
+ return (
75
+ <List>
76
+ {contentFolder &&
77
+ contentFolder.map((file, idx) => {
78
+ const hasDivider = contentFolder
79
+ ? idx !== contentFolder.length - 1
80
+ : false
81
+
82
+ return (
83
+ <FilePickerBodyItem
84
+ key={file._id}
85
+ file={file}
86
+ fileTypesAccepted={fileTypesAccepted}
87
+ multiple={multiple}
88
+ handleChoiceClick={handleChoiceClick}
89
+ handleListItemClick={handleListItemClick}
90
+ onCheck={onCheck}
91
+ filesIdsSelected={filesIdsSelected}
92
+ hasDivider={hasDivider}
93
+ />
94
+ )
95
+ })}
96
+ {hasMore && <LoadMore label={'loadMore'} fetchMore={fetchMore} />}
97
+ </List>
98
+ )
99
+ }
100
+
101
+ FilePickerBody.propTypes = {
102
+ onSelectFileId: PropTypes.func.isRequired,
103
+ filesIdsSelected: PropTypes.arrayOf(PropTypes.string).isRequired,
104
+ folderId: PropTypes.string.isRequired,
105
+ navigateTo: PropTypes.func.isRequired,
106
+ fileTypesAccepted: PropTypes.exact({
107
+ file: PropTypes.bool,
108
+ folder: PropTypes.bool
109
+ })
110
+ }
111
+
112
+ export default memo(FilePickerBody)
@@ -0,0 +1,129 @@
1
+ import React, { memo } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import cx from 'classnames'
4
+ import filesize from 'filesize'
5
+ import { makeStyles } from '@material-ui/core/styles'
6
+
7
+ import { models } from 'cozy-client'
8
+
9
+ import ListItem from '../MuiCozyTheme/ListItem'
10
+ import ListItemIcon from '../MuiCozyTheme/ListItemIcon'
11
+ import ListItemText from '../ListItemText'
12
+ import Divider from '../MuiCozyTheme/Divider'
13
+ import Icon from '../Icon'
14
+ import FileTypeText from '../Icons/FileTypeText'
15
+ import FileTypeFolder from '../Icons/FileTypeFolder'
16
+ import Checkbox from '../Checkbox'
17
+ import Radio from '../Radio'
18
+ import { useI18n } from '../I18n'
19
+
20
+ import styles from './styles.styl'
21
+
22
+ const {
23
+ file: { isDirectory, isFile }
24
+ } = models
25
+
26
+ const useStyles = makeStyles(() => ({
27
+ verticalDivider: {
28
+ height: '2rem',
29
+ display: 'flex',
30
+ alignSelf: 'auto',
31
+ alignItems: 'center',
32
+ marginLeft: '0.5rem'
33
+ },
34
+ listItemIcon: {
35
+ marginLeft: '1rem'
36
+ }
37
+ }))
38
+
39
+ const FilePickerBodyItem = ({
40
+ file,
41
+ fileTypesAccepted,
42
+ multiple,
43
+ handleChoiceClick,
44
+ handleListItemClick,
45
+ filesIdsSelected,
46
+ hasDivider
47
+ }) => {
48
+ const classes = useStyles()
49
+ const { f } = useI18n()
50
+ const hasChoice =
51
+ (fileTypesAccepted.file && isFile(file)) ||
52
+ (fileTypesAccepted.folder && isDirectory(file))
53
+
54
+ const Input = multiple ? Checkbox : Radio
55
+
56
+ const listItemSecondaryContent = isFile(file)
57
+ ? `${f(file.attributes.updated_at, 'DD MMM YYYY')} - ${filesize(
58
+ file.attributes.size,
59
+ { base: 10 }
60
+ )}`
61
+ : null
62
+
63
+ return (
64
+ <>
65
+ <ListItem button className="u-p-0">
66
+ <div
67
+ data-testid="listitem-onclick"
68
+ className={styles['filePickerBreadcrumb-wrapper']}
69
+ onClick={handleListItemClick(file)}
70
+ >
71
+ <ListItemIcon className={classes.listItemIcon}>
72
+ <Icon
73
+ icon={isDirectory(file) ? FileTypeFolder : FileTypeText}
74
+ width="32"
75
+ height="32"
76
+ />
77
+ </ListItemIcon>
78
+ <ListItemText
79
+ primary={file.name}
80
+ secondary={listItemSecondaryContent}
81
+ />
82
+ </div>
83
+ {isDirectory(file) && hasChoice && (
84
+ <Divider
85
+ orientation="vertical"
86
+ flexItem
87
+ className={classes.verticalDivider}
88
+ />
89
+ )}
90
+ <div
91
+ data-testid="choice-onclick"
92
+ className="u-ph-1 u-pv-half u-h-2 u-flex u-flex-items-center"
93
+ onClick={hasChoice ? handleChoiceClick(file) : undefined}
94
+ >
95
+ <Input
96
+ data-testid={multiple ? 'checkbox-btn' : 'radio-btn'}
97
+ gutter={false}
98
+ onChange={() => {
99
+ // handled by onClick on the container
100
+ }}
101
+ checked={filesIdsSelected.includes(file._id)}
102
+ value={file._id}
103
+ className={cx('u-p-0', {
104
+ 'u-o-100': hasChoice,
105
+ 'u-o-0': !hasChoice
106
+ })}
107
+ disabled={!hasChoice}
108
+ />
109
+ </div>
110
+ </ListItem>
111
+ {hasDivider && <Divider component="li" />}
112
+ </>
113
+ )
114
+ }
115
+
116
+ FilePickerBodyItem.propTypes = {
117
+ file: PropTypes.object.isRequired,
118
+ fileTypesAccepted: PropTypes.exact({
119
+ file: PropTypes.bool,
120
+ folder: PropTypes.bool
121
+ }),
122
+ multiple: PropTypes.bool,
123
+ handleChoiceClick: PropTypes.func.isRequired,
124
+ handleListItemClick: PropTypes.func.isRequired,
125
+ filesIdsSelected: PropTypes.arrayOf(PropTypes.string).isRequired,
126
+ hasDivider: PropTypes.bool.isRequired
127
+ }
128
+
129
+ export default memo(FilePickerBodyItem)
@@ -0,0 +1,131 @@
1
+ import React from 'react'
2
+ import { render, fireEvent } from '@testing-library/react'
3
+ import filesize from 'filesize'
4
+
5
+ import DemoProvider from './docs/DemoProvider'
6
+ import FilePickerBodyItem from './FilePickerBodyItem'
7
+
8
+ const mockFile01 = {
9
+ _id: '001',
10
+ type: 'file',
11
+ name: 'Filename',
12
+ attributes: { updated_at: '2021-01-01T12:00:00.000000+01:00' }
13
+ }
14
+ const mockFolder01 = {
15
+ _id: '002',
16
+ type: 'directory',
17
+ name: 'Foldername',
18
+ attributes: { updated_at: '2021-01-01T12:00:00.000000+01:00' }
19
+ }
20
+
21
+ jest.mock('filesize', () => jest.fn())
22
+
23
+ describe('FilePickerBodyItem components:', () => {
24
+ const mockHandleChoiceClick = jest.fn()
25
+ const mockHandleListItemClick = jest.fn()
26
+ filesize.mockReturnValue('111Ko')
27
+
28
+ const setup = ({
29
+ file = mockFile01,
30
+ multiple = false,
31
+ fileTypesAccepted = { file: true, folder: false }
32
+ }) => {
33
+ return render(
34
+ <DemoProvider>
35
+ <FilePickerBodyItem
36
+ file={file}
37
+ fileTypesAccepted={fileTypesAccepted}
38
+ multiple={multiple}
39
+ handleChoiceClick={mockHandleChoiceClick}
40
+ handleListItemClick={mockHandleListItemClick}
41
+ filesIdsSelected={[]}
42
+ hasDivider={false}
43
+ />
44
+ </DemoProvider>
45
+ )
46
+ }
47
+
48
+ afterEach(() => {
49
+ jest.clearAllMocks()
50
+ })
51
+
52
+ it('should be rendered correctly', () => {
53
+ const { container } = setup({})
54
+
55
+ expect(container).toBeDefined()
56
+ })
57
+
58
+ it('should display filename', () => {
59
+ const { getByText } = setup({})
60
+
61
+ expect(getByText('Filename'))
62
+ })
63
+
64
+ it('should display foldername', () => {
65
+ const { getByText } = setup({ file: mockFolder01 })
66
+
67
+ expect(getByText('Foldername'))
68
+ })
69
+
70
+ describe('Functions called', () => {
71
+ it('should call "handleChoiceClick" function when click on checkbox/radio area', () => {
72
+ const { getByTestId } = setup({})
73
+ fireEvent.click(getByTestId('choice-onclick'))
74
+
75
+ expect(mockHandleChoiceClick).toHaveBeenCalled()
76
+ })
77
+
78
+ it('should NOT call "handleChoiceClick" function when click on checkbox/radio area, if is Folder & not accepted', () => {
79
+ const { getByTestId } = setup({ file: mockFolder01 })
80
+ fireEvent.click(getByTestId('choice-onclick'))
81
+
82
+ expect(mockHandleChoiceClick).not.toHaveBeenCalled()
83
+ })
84
+ it('should NOT call "handleChoiceClick" function when click on checkbox/radio area, if is File & not accepted', () => {
85
+ const { getByTestId } = setup({
86
+ fileTypesAccepted: { file: false, folder: true }
87
+ })
88
+ fireEvent.click(getByTestId('choice-onclick'))
89
+
90
+ expect(mockHandleChoiceClick).not.toHaveBeenCalled()
91
+ })
92
+
93
+ it('should call "handleListItemClick" function when click on ListItem node', () => {
94
+ const { getByTestId } = setup({})
95
+ fireEvent.click(getByTestId('listitem-onclick'))
96
+
97
+ expect(mockHandleListItemClick).toHaveBeenCalled()
98
+ })
99
+ })
100
+
101
+ describe('Attribute "multiple"', () => {
102
+ it('should radio button exists if "multiple" atribute is False', () => {
103
+ const { getByTestId } = setup({})
104
+ const radioBtn = getByTestId('radio-btn')
105
+ expect(radioBtn).not.toBeNull()
106
+ })
107
+
108
+ it('should checkbox button exists if "multiple" atribute is True', () => {
109
+ const { getByTestId } = setup({ multiple: true })
110
+ const checkboxBtn = getByTestId('checkbox-btn')
111
+ expect(checkboxBtn).not.toBeNull()
112
+ })
113
+ })
114
+
115
+ describe('Radio/Checkbox button', () => {
116
+ it('should disable and not display the Radio button if it is a File and is not accepted', () => {
117
+ const { getByTestId } = setup({
118
+ fileTypesAccepted: { file: false }
119
+ })
120
+ const radioBtn = getByTestId('radio-btn')
121
+
122
+ expect(radioBtn.getAttribute('disabled')).toBe('')
123
+ })
124
+ it('should disable and not display the Radio button if it is a Folder and is not accepted', () => {
125
+ const { getByTestId } = setup({ file: mockFolder01 })
126
+ const radioBtn = getByTestId('radio-btn')
127
+
128
+ expect(radioBtn.getAttribute('disabled')).toBe('')
129
+ })
130
+ })
131
+ })
@@ -0,0 +1,54 @@
1
+ import React, { Fragment, useCallback, memo } from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import Typography from '../Typography'
5
+ import Icon from '../Icon'
6
+ import RightIcon from '../Icons/Right'
7
+ import useBreakpoints from '../hooks/useBreakpoints'
8
+
9
+ import styles from './styles.styl'
10
+
11
+ const FilePickerBreadcrumb = ({ path, onBreadcrumbClick }) => {
12
+ const { isMobile } = useBreakpoints()
13
+ const hasPath = path && path.length > 0
14
+
15
+ const navigateTo = useCallback(folder => () => onBreadcrumbClick(folder), [
16
+ onBreadcrumbClick
17
+ ])
18
+
19
+ return (
20
+ <Typography variant="h4" className="u-flex u-flex-items-center">
21
+ {hasPath
22
+ ? isMobile
23
+ ? path[path.length - 1].name
24
+ : path.map((folder, idx) => {
25
+ if (idx < path.length - 1) {
26
+ return (
27
+ <Fragment key={idx}>
28
+ <span
29
+ className={styles['filePickerBreadcrumb-previousPath']}
30
+ onClick={navigateTo(folder)}
31
+ >
32
+ {folder.name}
33
+ </span>
34
+ <Icon
35
+ icon={RightIcon}
36
+ className={styles['filePickerBreadcrumb-icon']}
37
+ />
38
+ </Fragment>
39
+ )
40
+ } else {
41
+ return <span key={idx}>{folder.name}</span>
42
+ }
43
+ })
44
+ : null}
45
+ </Typography>
46
+ )
47
+ }
48
+
49
+ FilePickerBreadcrumb.propTypes = {
50
+ path: PropTypes.array,
51
+ onBreadcrumbClick: PropTypes.func
52
+ }
53
+
54
+ export default memo(FilePickerBreadcrumb)
@@ -0,0 +1,40 @@
1
+ import React, { memo } from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import Button from '../Button'
5
+ import { createUseI18n } from '../I18n'
6
+
7
+ import en from './locales/en.json'
8
+ import fr from './locales/fr.json'
9
+
10
+ const locales = { en, fr }
11
+ const useI18n = createUseI18n(locales)
12
+
13
+ const FilePickerFooter = ({ onConfirm, onClose, disabledConfirm }) => {
14
+ const { t } = useI18n()
15
+
16
+ return (
17
+ <>
18
+ <Button
19
+ data-testid="close-btn"
20
+ label={t('footer.buttons.cancel')}
21
+ theme="secondary"
22
+ onClick={onClose}
23
+ />
24
+ <Button
25
+ data-testid="confirm-btn"
26
+ label={t('footer.buttons.confirm')}
27
+ onClick={onConfirm}
28
+ disabled={disabledConfirm}
29
+ />
30
+ </>
31
+ )
32
+ }
33
+
34
+ FilePickerFooter.propTypes = {
35
+ onConfirm: PropTypes.func.isRequired,
36
+ onClose: PropTypes.func.isRequired,
37
+ disabledConfirm: PropTypes.bool.isRequired
38
+ }
39
+
40
+ export default memo(FilePickerFooter)
@@ -0,0 +1,41 @@
1
+ import React from 'react'
2
+ import { render, fireEvent } from '@testing-library/react'
3
+
4
+ import DemoProvider from './docs/DemoProvider'
5
+ import FilePickerFooter from './FilePickerFooter'
6
+
7
+ describe('FilePickerFooter components:', () => {
8
+ const mockOnConfirm = jest.fn()
9
+ const mockOnClose = jest.fn()
10
+
11
+ const setup = (disabledConfirm = false) => {
12
+ return render(
13
+ <DemoProvider>
14
+ <FilePickerFooter
15
+ onConfirm={mockOnConfirm}
16
+ onClose={mockOnClose}
17
+ disabledConfirm={disabledConfirm}
18
+ />
19
+ </DemoProvider>
20
+ )
21
+ }
22
+ it('should be rendered correctly', () => {
23
+ const { container } = setup()
24
+
25
+ expect(container).toBeDefined()
26
+ })
27
+
28
+ it('should confirm button have been called', () => {
29
+ const { getByTestId } = setup()
30
+ fireEvent.click(getByTestId('confirm-btn'))
31
+
32
+ expect(mockOnConfirm).toHaveBeenCalled()
33
+ })
34
+
35
+ it('should close button have been called', () => {
36
+ const { getByTestId } = setup()
37
+ fireEvent.click(getByTestId('close-btn'))
38
+
39
+ expect(mockOnClose).toHaveBeenCalled()
40
+ })
41
+ })
@@ -0,0 +1,85 @@
1
+ import React, { useCallback, memo } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import uniqBy from 'lodash/uniqBy'
4
+ import get from 'lodash/get'
5
+
6
+ import { useQuery, hasQueryBeenLoaded } from 'cozy-client'
7
+
8
+ import useBreakpoints from '../hooks/useBreakpoints'
9
+ import IconButton from '../IconButton'
10
+ import Icon from '../Icon'
11
+ import Previous from '../Icons/Previous'
12
+
13
+ import FilePickerBreadcrumb from './FilePickerBreadcrumb'
14
+ import { buildCurrentFolderQuery } from './queries'
15
+ import { ROOT_DIR_ID } from './index'
16
+
17
+ /**
18
+ * @param {IOCozyFolder} displayedFolder - An io.cozy.files folder
19
+ * @returns {{id: string, name: string}[]}
20
+ */
21
+ const getBreadcrumbPath = displayedFolder => {
22
+ return uniqBy(
23
+ [
24
+ {
25
+ id: ROOT_DIR_ID
26
+ },
27
+ {
28
+ id: get(displayedFolder, 'dir_id')
29
+ },
30
+ {
31
+ id: displayedFolder.id,
32
+ name: displayedFolder.name
33
+ }
34
+ ],
35
+ 'id'
36
+ )
37
+ .filter(({ id }) => Boolean(id))
38
+ .map(breadcrumb => ({
39
+ id: breadcrumb.id,
40
+ name: breadcrumb.name || (breadcrumb.id === ROOT_DIR_ID ? 'Drive' : '…')
41
+ }))
42
+ }
43
+
44
+ const FilePickerHeader = ({ navigateTo, folderId, onClose }) => {
45
+ const { isMobile } = useBreakpoints()
46
+
47
+ const currentFolderQuery = buildCurrentFolderQuery(folderId)
48
+ const { data: currentFolder, ...restCurrentFolder } = useQuery(
49
+ currentFolderQuery.definition,
50
+ currentFolderQuery.options
51
+ )
52
+
53
+ const path = hasQueryBeenLoaded(restCurrentFolder)
54
+ ? getBreadcrumbPath(currentFolder[0])
55
+ : []
56
+
57
+ const onBack = useCallback(path => navigateTo(path), [navigateTo])
58
+
59
+ const handleClick = useCallback(() => {
60
+ path.length > 1 && isMobile ? onBack(path[path.length - 2]) : onClose()
61
+ }, [isMobile, path, onBack, onClose])
62
+
63
+ return (
64
+ <div className="u-flex u-flex-items-center">
65
+ {isMobile && (
66
+ <IconButton onClick={handleClick} className="u-p-0 u-pr-1">
67
+ <Icon icon={Previous} />
68
+ </IconButton>
69
+ )}
70
+ <FilePickerBreadcrumb
71
+ path={path}
72
+ onBreadcrumbClick={navigateTo}
73
+ opening={false}
74
+ inlined
75
+ />
76
+ </div>
77
+ )
78
+ }
79
+
80
+ FilePickerHeader.propTypes = {
81
+ folderId: PropTypes.string.isRequired,
82
+ navigateTo: PropTypes.func.isRequired
83
+ }
84
+
85
+ export default memo(FilePickerHeader)
@@ -0,0 +1,53 @@
1
+ FilePicker allows you to select a file or folder in Drive
2
+
3
+ ```jsx
4
+ import CozyIcon from 'cozy-ui/transpiled/react/Icons/Cozy'
5
+ import Button from 'cozy-ui/transpiled/react/Button'
6
+ import FilePicker from 'cozy-ui/transpiled/react/FilePicker'
7
+ import Variants from 'cozy-ui/docs/components/Variants'
8
+ import DemoProvider from './docs/DemoProvider';
9
+
10
+ initialState = {
11
+ filePickerOpened: isTesting()
12
+ };
13
+
14
+ const initialVariants = [
15
+ { acceptFile: true, acceptFolder: false, multiple: false }
16
+ ];
17
+
18
+ const toggleFilePicker = () => setState({ filePickerOpened: !state.filePickerOpened });
19
+ const onChange = (fileId) => alert(`ID of file selected : [${fileId}]`);
20
+ <DemoProvider>
21
+ <Variants initialVariants={initialVariants} screenshotAllVariants>
22
+ {variant => {
23
+ let acceptRule = ''
24
+ if (variant.acceptFile) {
25
+ acceptRule = 'file'
26
+ if (variant.acceptFolder) {
27
+ acceptRule = 'file,folder'
28
+ }
29
+ }
30
+ if (variant.acceptFolder) {
31
+ acceptRule = 'folder'
32
+ if (variant.acceptFile) {
33
+ acceptRule = 'folder,file'
34
+ }
35
+ }
36
+
37
+ return (
38
+ <>
39
+ <button onClick={toggleFilePicker}>Open FilePicker</button>
40
+ {state.filePickerOpened && (
41
+ <FilePicker
42
+ onClose={toggleFilePicker}
43
+ onChange={onChange}
44
+ accept={acceptRule}
45
+ multiple={variant.multiple}
46
+ />
47
+ )}
48
+ </>
49
+ )
50
+ }}
51
+ </Variants>
52
+ </DemoProvider>
53
+ ```