11import PropTypes from 'prop-types' ;
2- import React , { useContext } from 'react' ;
2+ import React , {
3+ useContext ,
4+ useImperativeHandle ,
5+ useRef ,
6+ useState ,
7+ } from 'react' ;
38import { withGlobalProps } from '../../providers/globalProps' ;
4- import { classNames } from '../../helpers/classNames/classNames ' ;
9+ import { classNames } from '../../helpers/classNames' ;
510import { transferProps } from '../../helpers/transferProps' ;
11+ import { TranslationsContext } from '../../providers/translations' ;
12+ import { getRootSizeClassName } from '../_helpers/getRootSizeClassName' ;
613import { getRootValidationStateClassName } from '../_helpers/getRootValidationStateClassName' ;
714import { resolveContextOrProp } from '../_helpers/resolveContextOrProp' ;
15+ import { InputGroupContext } from '../InputGroup' ;
16+ import { Text } from '../Text' ;
817import { FormLayoutContext } from '../FormLayout' ;
918import styles from './FileInputField.module.scss' ;
1019
@@ -17,79 +26,182 @@ export const FileInputField = React.forwardRef((props, ref) => {
1726 isLabelVisible,
1827 label,
1928 layout,
29+ multiple,
30+ onFilesChanged,
2031 required,
32+ size,
2133 validationState,
2234 validationText,
2335 ...restProps
2436 } = props ;
2537
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+ } ;
27101
28102 return (
29- < label
103+ < div
30104 className = { classNames (
31105 styles . root ,
32106 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'
35109 ? styles . isRootLayoutHorizontal
36110 : styles . isRootLayoutVertical ,
37- disabled && styles . isRootDisabled ,
111+ resolveContextOrProp ( inputGroupContext && inputGroupContext . disabled , disabled ) && styles . isRootDisabled ,
112+ inputGroupContext && styles . isRootGrouped ,
113+ isDragging && styles . isRootDragging ,
38114 required && styles . isRootRequired ,
115+ getRootSizeClassName (
116+ resolveContextOrProp ( inputGroupContext && inputGroupContext . size , size ) ,
117+ styles ,
118+ ) ,
39119 getRootValidationStateClassName ( validationState , styles ) ,
40120 ) }
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 }
43125 >
44- < div
126+ < label
45127 className = { classNames (
46128 styles . label ,
47- ! isLabelVisible && styles . isLabelHidden ,
129+ ( ! isLabelVisible || inputGroupContext ) && styles . isLabelHidden ,
48130 ) }
49- id = { id && `${ id } __labelText` }
131+ htmlFor = { id }
132+ id = { `${ id } __labelText` }
50133 >
51134 { label }
52- </ div >
135+ </ label >
53136 < div className = { styles . field } >
54137 < div className = { styles . inputContainer } >
55138 < input
56139 { ...transferProps ( restProps ) }
57- disabled = { disabled }
140+ className = { styles . input }
141+ disabled = { resolveContextOrProp ( inputGroupContext && inputGroupContext . disabled , disabled ) }
58142 id = { id }
59- ref = { ref }
143+ multiple = { multiple }
144+ onChange = { handleInputChange }
145+ ref = { internalInputRef }
60146 required = { required }
147+ tabIndex = { - 1 }
61148 type = "file"
62149 />
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 >
63174 </ div >
64175 { helpText && (
65176 < div
66177 className = { styles . helpText }
67- id = { id && `${ id } __helpText` }
178+ id = { `${ id } __helpText` }
68179 >
69180 { helpText }
70181 </ div >
71182 ) }
72183 { validationText && (
73184 < div
74185 className = { styles . validationText }
75- id = { id && `${ id } __validationText` }
186+ id = { `${ id } __validationText` }
76187 >
77188 { validationText }
78189 </ div >
79190 ) }
80191 </ div >
81- </ label >
192+ </ div >
82193 ) ;
83194} ) ;
84195
85196FileInputField . defaultProps = {
86197 disabled : false ,
87198 fullWidth : false ,
88199 helpText : null ,
89- id : undefined ,
90200 isLabelVisible : true ,
91201 layout : 'vertical' ,
202+ multiple : false ,
92203 required : false ,
204+ size : 'medium' ,
93205 validationState : null ,
94206 validationText : null ,
95207} ;
@@ -116,7 +228,7 @@ FileInputField.propTypes = {
116228 * * `<ID>__helpText`
117229 * * `<ID>__validationText`
118230 */
119- id : PropTypes . string ,
231+ id : PropTypes . string . isRequired ,
120232 /**
121233 * If `false`, the label will be visually hidden (but remains accessible by assistive
122234 * technologies).
@@ -134,10 +246,24 @@ FileInputField.propTypes = {
134246 *
135247 */
136248 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 ,
137257 /**
138258 * If `true`, the input will be required.
139259 */
140260 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' ] ) ,
141267 /**
142268 * Alter the field to provide feedback based on validation result.
143269 */
0 commit comments