• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

react-ui-org / react-ui / 13941709617

19 Mar 2025 08:08AM UTC coverage: 89.85% (-2.1%) from 91.956%
13941709617

Pull #601

github

web-flow
Merge fc0e96dd0 into c2b0b44f8
Pull Request #601: Introduce custom design of `FileInputField` (#244)

812 of 912 branches covered (89.04%)

Branch coverage included in aggregate %.

23 of 41 new or added lines in 1 file covered. (56.1%)

3 existing lines in 1 file now uncovered.

746 of 822 relevant lines covered (90.75%)

73.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.08
/src/components/FileInputField/FileInputField.jsx
1
import PropTypes from 'prop-types';
2
import React, {
3
  useContext,
4
  useEffect,
5
  useState,
6
} from 'react';
7
import { withGlobalProps } from '../../providers/globalProps';
8
import { classNames } from '../../helpers/classNames';
9
import { transferProps } from '../../helpers/transferProps';
10
import { TranslationsContext } from '../../providers/translations';
11
import { getRootSizeClassName } from '../_helpers/getRootSizeClassName';
12
import { getRootValidationStateClassName } from '../_helpers/getRootValidationStateClassName';
13
import { resolveContextOrProp } from '../_helpers/resolveContextOrProp';
14
import { InputGroupContext } from '../InputGroup';
15
import { Text } from '../Text';
16
import { FormLayoutContext } from '../FormLayout';
17
import styles from './FileInputField.module.scss';
18

19
export const FileInputField = React.forwardRef((props, ref) => {
4✔
20
  const {
21
    disabled,
22
    fullWidth,
23
    helpText,
24
    id,
25
    isLabelVisible,
26
    label,
27
    layout,
28
    onChange,
29
    onDragEnter,
30
    onDragLeave,
31
    onDragOver,
32
    onDrop,
33
    required,
34
    size,
35
    validationState,
36
    validationText,
37
    ...restProps
38
  } = props;
50✔
39

40
  const formLayoutContext = useContext(FormLayoutContext);
50✔
41
  const inputGroupContext = useContext(InputGroupContext);
50✔
42
  const translations = useContext(TranslationsContext);
50✔
43

44
  const [selectedFileNames, setSelectedFileNames] = useState([]);
50✔
45
  const [isDragAndDropSupported, setIsDragAndDropSupported] = useState(false);
50✔
46
  const [isDragging, setIsDragging] = useState(false);
50✔
47

48
  const handleFileChange = (files) => {
50✔
49
    if (files.length === 0) {
2!
NEW
50
      setSelectedFileNames([]);
×
NEW
51
      return;
×
52
    }
53

54
    const fileNames = [];
2✔
55

56
    [...files].forEach((file) => {
2✔
57
      fileNames.push(file.name);
2✔
58
    });
59

60
    setSelectedFileNames(fileNames);
2✔
61
  };
62

63
  const handleInputChange = (event) => {
50✔
64
    handleFileChange(event.target.files);
2✔
65

66
    if (props?.onChange) {
2!
67
      props.onChange(event);
2✔
68
    }
69
  };
70

71
  const handleClick = (event) => {
50✔
NEW
72
    event.currentTarget.previousElementSibling.click();
×
73
  };
74

75
  const handleDrop = (event) => {
50✔
NEW
76
    event.preventDefault();
×
NEW
77
    handleFileChange(event.dataTransfer.files);
×
NEW
78
    setIsDragging(false);
×
79

NEW
80
    if (props?.onDrop) {
×
NEW
81
      props.onDrop(event);
×
82
    }
83
  };
84

85
  const handleDragOver = (event) => {
50✔
NEW
86
    event.preventDefault();
×
NEW
87
    setIsDragging(true);
×
88

NEW
89
    if (props?.onDragOver) {
×
NEW
90
      props.onDragOver(event);
×
91
    }
92
  };
93

94
  const handleDragEnter = (event) => {
50✔
NEW
95
    setIsDragging(true);
×
96

NEW
UNCOV
97
    if (props?.onDragEnter) {
×
NEW
UNCOV
98
      props.onDragEnter(event);
×
99
    }
100
  };
101

102
  const handleDragLeave = (event) => {
50✔
NEW
103
    setIsDragging(false);
×
104

NEW
105
    if (props?.onDragLeave) {
×
NEW
UNCOV
106
      props.onDragLeave(event);
×
107
    }
108
  };
109

110
  useEffect(() => {
50✔
111
    setIsDragAndDropSupported('draggable' in document.createElement('span'));
46✔
112
  }, []);
113

114
  return (
50✔
115
    <div
116
      className={classNames(
117
        styles.root,
118
        fullWidth && styles.isRootFullWidth,
26✔
119
        formLayoutContext && styles.isRootInFormLayout,
27✔
120
        resolveContextOrProp(formLayoutContext && formLayoutContext.layout, layout) === 'horizontal'
52✔
121
          ? styles.isRootLayoutHorizontal
122
          : styles.isRootLayoutVertical,
123
        resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled) && styles.isRootDisabled,
50!
124
        inputGroupContext && styles.isRootGrouped,
25!
125
        isDragging && styles.isRootDragging,
25!
126
        required && styles.isRootRequired,
26✔
127
        getRootSizeClassName(
128
          resolveContextOrProp(inputGroupContext && inputGroupContext.size, size),
25!
129
          styles,
130
        ),
131
        getRootValidationStateClassName(validationState, styles),
132
      )}
133
      id={id && `${id}__root`}
33✔
134
      onDragEnter={!disabled && isDragAndDropSupported ? handleDragEnter : undefined}
75!
135
      onDragLeave={!disabled && isDragAndDropSupported ? handleDragLeave : undefined}
75!
136
      onDragOver={!disabled && isDragAndDropSupported ? handleDragOver : undefined}
75!
137
      onDrop={!disabled && isDragAndDropSupported ? handleDrop : undefined}
75!
138
    >
139
      <label
140
        className={classNames(
141
          styles.label,
142
          (!isLabelVisible || inputGroupContext) && styles.isLabelHidden,
50✔
143
        )}
144
        htmlFor={id}
145
        id={id && `${id}__labelText`}
33✔
146
      >
147
        {label}
148
      </label>
149
      <div className={styles.field}>
150
        <div className={styles.inputContainer}>
151
          <input
152
            {...transferProps(restProps)}
153
            className={styles.input}
154
            disabled={resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled)}
25!
155
            id={id}
156
            onChange={handleInputChange}
157
            ref={ref}
158
            required={required}
159
            tabIndex={-1}
160
            type="file"
161
          />
162
          <button
163
            className={styles.dropZone}
164
            onClick={handleClick}
165
            type="button"
166
          >
167
            <Text lines={1}>
168
              {!selectedFileNames.length && (
48✔
169
                <>
170
                  <span className={styles.dropZoneLink}>{translations.FileInputField.browse}</span>
171
                  {isDragAndDropSupported && ` ${translations.FileInputField.drop}`}
23!
172
                </>
173
              )}
174
              {selectedFileNames.length === 1 && selectedFileNames[0]}
27✔
175
              {selectedFileNames.length > 1 && (
25!
176
                <>
177
                  {selectedFileNames.length}
178
                  {' '}
179
                  {translations.FileInputField.filesSelected}
180
                </>
181
              )}
182
            </Text>
183
          </button>
184
        </div>
185
        {helpText && (
27✔
186
          <div
187
            className={styles.helpText}
188
            id={id && `${id}__helpText`}
3✔
189
          >
190
            {helpText}
191
          </div>
192
        )}
193
        {validationText && (
27✔
194
          <div
195
            className={styles.validationText}
196
            id={id && `${id}__validationText`}
3✔
197
          >
198
            {validationText}
199
          </div>
200
        )}
201
      </div>
202
    </div>
203
  );
204
});
205

206
FileInputField.defaultProps = {
4✔
207
  disabled: false,
208
  fullWidth: false,
209
  helpText: null,
210
  id: undefined,
211
  isLabelVisible: true,
212
  layout: 'vertical',
213
  onChange: () => {},
214
  onDragEnter: () => {},
215
  onDragLeave: () => {},
216
  onDragOver: () => {},
217
  onDrop: () => {},
218
  required: false,
219
  size: 'medium',
220
  validationState: null,
221
  validationText: null,
222
};
223

224
FileInputField.propTypes = {
4✔
225
  /**
226
   * If `true`, the input will be disabled.
227
   */
228
  disabled: PropTypes.bool,
229
  /**
230
   * If `true`, the field will span the full width of its parent.
231
   */
232
  fullWidth: PropTypes.bool,
233
  /**
234
   * Optional help text.
235
   */
236
  helpText: PropTypes.node,
237
  /**
238
   * ID of the `<input>` HTML element.
239
   *
240
   * Also serves as base for ids of nested elements:
241
   * * `<ID>__label`
242
   * * `<ID>__labelText`
243
   * * `<ID>__helpText`
244
   * * `<ID>__validationText`
245
   */
246
  id: PropTypes.string,
247
  /**
248
   * If `false`, the label will be visually hidden (but remains accessible by assistive
249
   * technologies).
250
   */
251
  isLabelVisible: PropTypes.bool,
252
  /**
253
   * File input field label.
254
   */
255
  label: PropTypes.node.isRequired,
256
  /**
257
   * Layout of the field.
258
   *
259
   * Ignored if the component is rendered within `FormLayout` component
260
   * as the value is inherited in such case.
261
   *
262
   */
263
  layout: PropTypes.oneOf(['horizontal', 'vertical']),
264
  /**
265
   * Callback fired when the value of the input changes.
266
   */
267
  onChange: PropTypes.func,
268
  /**
269
   * Callback fired when a drag event enters the field.
270
   */
271
  onDragEnter: PropTypes.func,
272
  /**
273
   * Callback fired when a drag event leaves the field.
274
   */
275
  onDragLeave: PropTypes.func,
276
  /**
277
   * Callback fired when a drag event is over the field.
278
   */
279
  onDragOver: PropTypes.func,
280
  /**
281
   * Callback fired when a file is dropped onto the field.
282
   */
283
  onDrop: PropTypes.func,
284
  /**
285
   * If `true`, the input will be required.
286
   */
287
  required: PropTypes.bool,
288
  /**
289
   * Size of the field.
290
   *
291
   * Ignored if the component is rendered within `InputGroup` component as the value is inherited in such case.
292
   */
293
  size: PropTypes.oneOf(['small', 'medium', 'large']),
294
  /**
295
   * Alter the field to provide feedback based on validation result.
296
   */
297
  validationState: PropTypes.oneOf(['invalid', 'valid', 'warning']),
298
  /**
299
   * Validation message to be displayed.
300
   */
301
  validationText: PropTypes.node,
302
};
303

304
export const FileInputFieldWithGlobalProps = withGlobalProps(FileInputField, 'FileInputField');
4✔
305

306
export default FileInputFieldWithGlobalProps;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc