1
1
import PropTypes from 'prop-types' ;
2
- import React , { useContext } from 'react' ;
2
+ import React , {
3
+ useContext ,
4
+ useImperativeHandle ,
5
+ useRef ,
6
+ useState ,
7
+ } from 'react' ;
3
8
import { withGlobalProps } from '../../providers/globalProps' ;
4
- import { classNames } from '../../helpers/classNames/classNames ' ;
9
+ import { classNames } from '../../helpers/classNames' ;
5
10
import { transferProps } from '../../helpers/transferProps' ;
11
+ import { TranslationsContext } from '../../providers/translations' ;
12
+ import { getRootSizeClassName } from '../_helpers/getRootSizeClassName' ;
6
13
import { getRootValidationStateClassName } from '../_helpers/getRootValidationStateClassName' ;
7
14
import { resolveContextOrProp } from '../_helpers/resolveContextOrProp' ;
15
+ import { InputGroupContext } from '../InputGroup' ;
16
+ import { Text } from '../Text' ;
8
17
import { FormLayoutContext } from '../FormLayout' ;
9
18
import styles from './FileInputField.module.scss' ;
10
19
@@ -17,79 +26,182 @@ export const FileInputField = React.forwardRef((props, ref) => {
17
26
isLabelVisible,
18
27
label,
19
28
layout,
29
+ multiple,
30
+ onFilesChanged,
20
31
required,
32
+ size,
21
33
validationState,
22
34
validationText,
23
35
...restProps
24
36
} = props ;
25
37
26
- const context = useContext ( FormLayoutContext ) ;
38
+ const internalInputRef = useRef ( ) ;
39
+
40
+ // We need to have a reference to the input element to be able to call its methods,
41
+ // but at the same time we want to expose this reference to the parent component for
42
+ // case someone wants to call input methods from outside the component.
43
+ useImperativeHandle ( ref , ( ) => internalInputRef . current ) ;
44
+
45
+ const formLayoutContext = useContext ( FormLayoutContext ) ;
46
+ const inputGroupContext = useContext ( InputGroupContext ) ;
47
+ const translations = useContext ( TranslationsContext ) ;
48
+
49
+ const [ selectedFileNames , setSelectedFileNames ] = useState ( [ ] ) ;
50
+ const [ isDragging , setIsDragging ] = useState ( false ) ;
51
+
52
+ const handleFileChange = ( files , event ) => {
53
+ if ( files . length === 0 ) {
54
+ setSelectedFileNames ( [ ] ) ;
55
+ return ;
56
+ }
57
+
58
+ // Mimic the native behavior of the `input` element: if multiple files are selected and the input
59
+ // does not accept multiple files, no files are processed.
60
+ if ( files . length > 1 && ! multiple ) {
61
+ setSelectedFileNames ( [ ] ) ;
62
+ return ;
63
+ }
64
+
65
+ const fileNames = [ ] ;
66
+
67
+ [ ...files ] . forEach ( ( file ) => {
68
+ fileNames . push ( file . name ) ;
69
+ } ) ;
70
+
71
+ setSelectedFileNames ( fileNames ) ;
72
+ onFilesChanged ( files , event ) ;
73
+ } ;
74
+
75
+ const handleInputChange = ( event ) => {
76
+ handleFileChange ( event . target . files , event ) ;
77
+ } ;
78
+
79
+ const handleClick = ( ) => {
80
+ internalInputRef ?. current . click ( ) ;
81
+ } ;
82
+
83
+ const handleDrop = ( event ) => {
84
+ event . preventDefault ( ) ;
85
+ handleFileChange ( event . dataTransfer . files , event ) ;
86
+ setIsDragging ( false ) ;
87
+ } ;
88
+
89
+ const handleDragOver = ( event ) => {
90
+ if ( ! isDragging ) {
91
+ setIsDragging ( true ) ;
92
+ }
93
+ event . preventDefault ( ) ;
94
+ } ;
95
+
96
+ const handleDragLeave = ( ) => {
97
+ if ( isDragging ) {
98
+ setIsDragging ( false ) ;
99
+ }
100
+ } ;
27
101
28
102
return (
29
- < label
103
+ < div
30
104
className = { classNames (
31
105
styles . root ,
32
106
fullWidth && styles . isRootFullWidth ,
33
- context && styles . isRootInFormLayout ,
34
- resolveContextOrProp ( context && context . layout , layout ) === 'horizontal'
107
+ formLayoutContext && styles . isRootInFormLayout ,
108
+ resolveContextOrProp ( formLayoutContext && formLayoutContext . layout , layout ) === 'horizontal'
35
109
? styles . isRootLayoutHorizontal
36
110
: styles . isRootLayoutVertical ,
37
- disabled && styles . isRootDisabled ,
111
+ resolveContextOrProp ( inputGroupContext && inputGroupContext . disabled , disabled ) && styles . isRootDisabled ,
112
+ inputGroupContext && styles . isRootGrouped ,
113
+ isDragging && styles . isRootDragging ,
38
114
required && styles . isRootRequired ,
115
+ getRootSizeClassName (
116
+ resolveContextOrProp ( inputGroupContext && inputGroupContext . size , size ) ,
117
+ styles ,
118
+ ) ,
39
119
getRootValidationStateClassName ( validationState , styles ) ,
40
120
) }
41
- htmlFor = { id }
42
- id = { id && `${ id } __label` }
121
+ id = { `${ id } __root` }
122
+ onDragLeave = { ! disabled ? handleDragLeave : undefined }
123
+ onDragOver = { ! disabled ? handleDragOver : undefined }
124
+ onDrop = { ! disabled ? handleDrop : undefined }
43
125
>
44
- < div
126
+ < label
45
127
className = { classNames (
46
128
styles . label ,
47
- ! isLabelVisible && styles . isLabelHidden ,
129
+ ( ! isLabelVisible || inputGroupContext ) && styles . isLabelHidden ,
48
130
) }
49
- id = { id && `${ id } __labelText` }
131
+ htmlFor = { id }
132
+ id = { `${ id } __labelText` }
50
133
>
51
134
{ label }
52
- </ div >
135
+ </ label >
53
136
< div className = { styles . field } >
54
137
< div className = { styles . inputContainer } >
55
138
< input
56
139
{ ...transferProps ( restProps ) }
57
- disabled = { disabled }
140
+ className = { styles . input }
141
+ disabled = { resolveContextOrProp ( inputGroupContext && inputGroupContext . disabled , disabled ) }
58
142
id = { id }
59
- ref = { ref }
143
+ multiple = { multiple }
144
+ onChange = { handleInputChange }
145
+ ref = { internalInputRef }
60
146
required = { required }
147
+ tabIndex = { - 1 }
61
148
type = "file"
62
149
/>
150
+ < button
151
+ className = { styles . dropZone }
152
+ disabled = { resolveContextOrProp ( inputGroupContext && inputGroupContext . disabled , disabled ) }
153
+ onClick = { handleClick }
154
+ type = "button"
155
+ >
156
+ < Text lines = { 1 } >
157
+ { ! selectedFileNames . length && (
158
+ < >
159
+ < span className = { styles . dropZoneLink } > { translations . FileInputField . browse } </ span >
160
+ { ' ' }
161
+ { translations . FileInputField . drop }
162
+ </ >
163
+ ) }
164
+ { selectedFileNames . length === 1 && selectedFileNames [ 0 ] }
165
+ { selectedFileNames . length > 1 && (
166
+ < >
167
+ { selectedFileNames . length }
168
+ { ' ' }
169
+ { translations . FileInputField . filesSelected }
170
+ </ >
171
+ ) }
172
+ </ Text >
173
+ </ button >
63
174
</ div >
64
175
{ helpText && (
65
176
< div
66
177
className = { styles . helpText }
67
- id = { id && `${ id } __helpText` }
178
+ id = { `${ id } __helpText` }
68
179
>
69
180
{ helpText }
70
181
</ div >
71
182
) }
72
183
{ validationText && (
73
184
< div
74
185
className = { styles . validationText }
75
- id = { id && `${ id } __validationText` }
186
+ id = { `${ id } __validationText` }
76
187
>
77
188
{ validationText }
78
189
</ div >
79
190
) }
80
191
</ div >
81
- </ label >
192
+ </ div >
82
193
) ;
83
194
} ) ;
84
195
85
196
FileInputField . defaultProps = {
86
197
disabled : false ,
87
198
fullWidth : false ,
88
199
helpText : null ,
89
- id : undefined ,
90
200
isLabelVisible : true ,
91
201
layout : 'vertical' ,
202
+ multiple : false ,
92
203
required : false ,
204
+ size : 'medium' ,
93
205
validationState : null ,
94
206
validationText : null ,
95
207
} ;
@@ -116,7 +228,7 @@ FileInputField.propTypes = {
116
228
* * `<ID>__helpText`
117
229
* * `<ID>__validationText`
118
230
*/
119
- id : PropTypes . string ,
231
+ id : PropTypes . string . isRequired ,
120
232
/**
121
233
* If `false`, the label will be visually hidden (but remains accessible by assistive
122
234
* technologies).
@@ -134,10 +246,24 @@ FileInputField.propTypes = {
134
246
*
135
247
*/
136
248
layout : PropTypes . oneOf ( [ 'horizontal' , 'vertical' ] ) ,
249
+ /**
250
+ * If `true`, the input will accept multiple files.
251
+ */
252
+ multiple : PropTypes . bool ,
253
+ /**
254
+ * Callback fired when the value of the input changes.
255
+ */
256
+ onFilesChanged : PropTypes . func . isRequired ,
137
257
/**
138
258
* If `true`, the input will be required.
139
259
*/
140
260
required : PropTypes . bool ,
261
+ /**
262
+ * Size of the field.
263
+ *
264
+ * Ignored if the component is rendered within `InputGroup` component as the value is inherited in such case.
265
+ */
266
+ size : PropTypes . oneOf ( [ 'small' , 'medium' , 'large' ] ) ,
141
267
/**
142
268
* Alter the field to provide feedback based on validation result.
143
269
*/
0 commit comments