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

react-ui-org / react-ui / 15508993375

07 Jun 2025 03:01PM UTC coverage: 90.023% (-0.3%) from 90.283%
15508993375

Pull #642

github

web-flow
Merge d78e740f0 into 980056e7e
Pull Request #642: Reset the internal input value when the reset button is clicked in FileInputField (#641)

807 of 900 branches covered (89.67%)

Branch coverage included in aggregate %.

0 of 2 new or added lines in 1 file covered. (0.0%)

17 existing lines in 1 file now uncovered.

754 of 834 relevant lines covered (90.41%)

73.39 hits per line

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

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

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

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

44
  const [selectedFileNames, setSelectedFileNames] = useState([]);
52✔
45
  const [isDragging, setIsDragging] = useState(false);
52✔
46

47
  const internalInputRef = useRef();
52✔
48

49
  const handleReset = useCallback((event) => {
52✔
NEW
UNCOV
50
    if (internalInputRef.current) {
×
NEW
UNCOV
51
      internalInputRef.current.value = '';
×
52
    }
53

54
    setSelectedFileNames([]);
×
55
    onFilesChanged([], event);
×
56
  }, [onFilesChanged]);
57

58
  // We need to have a reference to the input element to be able to call its methods,
59
  // but at the same time we want to expose this reference to the parent component in
60
  // case someone wants to call input methods from outside the component.
61
  useImperativeHandle(
52✔
62
    ref,
63
    () => {
64
      // The reason of extending object instead of using spread operator is that
65
      // if it is transformed to the object, it changes the reference of the object
66
      // and its prototype chain.
67
      const inputEl = internalInputRef?.current ?? {};
2!
68
      inputEl.resetState = () => {
2✔
UNCOV
69
        handleReset(null);
×
70
      };
71
      return inputEl;
2✔
72
    },
73
    [handleReset],
74
  );
75

76
  const handleFileChange = (files, event) => {
52✔
77
    if (files.length === 0) {
2!
UNCOV
78
      setSelectedFileNames([]);
×
UNCOV
79
      return;
×
80
    }
81

82
    // Mimic the native behavior of the `input` element: if multiple files are selected and the input
83
    // does not accept multiple files, no files are processed.
84
    if (files.length > 1 && !multiple) {
2!
UNCOV
85
      setSelectedFileNames([]);
×
UNCOV
86
      return;
×
87
    }
88

89
    const fileNames = [];
2✔
90

91
    [...files].forEach((file) => {
2✔
92
      fileNames.push(file.name);
2✔
93
    });
94

95
    setSelectedFileNames(fileNames);
2✔
96
    onFilesChanged(files, event);
2✔
97
  };
98

99
  const handleInputChange = (event) => {
52✔
100
    handleFileChange(event.target.files, event);
2✔
101
  };
102

103
  const handleClick = () => {
52✔
UNCOV
104
    internalInputRef?.current.click();
×
105
  };
106

107
  const handleDrop = (event) => {
52✔
UNCOV
108
    event.preventDefault();
×
UNCOV
109
    handleFileChange(event.dataTransfer.files, event);
×
110
    setIsDragging(false);
×
111
  };
112

113
  const handleDragOver = (event) => {
52✔
UNCOV
114
    if (!isDragging) {
×
UNCOV
115
      setIsDragging(true);
×
116
    }
117
    event.preventDefault();
×
118
  };
119

120
  const handleDragLeave = () => {
52✔
UNCOV
121
    if (isDragging) {
×
UNCOV
122
      setIsDragging(false);
×
123
    }
124
  };
125

126
  useEffect(() => {
52✔
127
    const inputEl = internalInputRef.current;
50✔
128
    if (!inputEl) {
50✔
UNCOV
129
      return () => {};
×
130
    }
131

132
    const { form } = inputEl;
50✔
133
    if (!form) {
50!
134
      return () => {};
50✔
135
    }
136

UNCOV
137
    form.addEventListener('reset', handleReset);
×
138

139
    return () => {
×
UNCOV
140
      form.removeEventListener('reset', handleReset);
×
141
    };
142
  }, [handleReset]);
143

144
  return (
52✔
145
    <div
146
      className={classNames(
147
        styles.root,
148
        fullWidth && styles.isRootFullWidth,
27✔
149
        formLayoutContext && styles.isRootInFormLayout,
28✔
150
        resolveContextOrProp(formLayoutContext && formLayoutContext.layout, layout) === 'horizontal'
54✔
151
          ? styles.isRootLayoutHorizontal
152
          : styles.isRootLayoutVertical,
153
        resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled) && styles.isRootDisabled,
53!
154
        inputGroupContext && styles.isRootGrouped,
26!
155
        isDragging && styles.isRootDragging,
26!
156
        required && styles.isRootRequired,
27✔
157
        getRootSizeClassName(
158
          resolveContextOrProp(inputGroupContext && inputGroupContext.size, size),
26!
159
          styles,
160
        ),
161
        getRootValidationStateClassName(validationState, styles),
162
      )}
163
      id={`${id}__root`}
164
      onDragLeave={!disabled ? handleDragLeave : undefined}
26✔
165
      onDragOver={!disabled ? handleDragOver : undefined}
26✔
166
      onDrop={!disabled ? handleDrop : undefined}
26✔
167
    >
168
      <label
169
        className={classNames(
170
          styles.label,
171
          (!isLabelVisible || inputGroupContext) && styles.isLabelHidden,
52✔
172
        )}
173
        htmlFor={id}
174
        id={`${id}__labelText`}
175
      >
176
        {label}
177
      </label>
178
      <div className={styles.field}>
179
        <div className={styles.inputContainer}>
180
          <input
181
            {...transferProps(restProps)}
182
            className={styles.input}
183
            disabled={resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled)}
26!
184
            id={id}
185
            multiple={multiple}
186
            onChange={handleInputChange}
187
            ref={internalInputRef}
188
            required={required}
189
            tabIndex={-1}
190
            type="file"
191
          />
192
          <button
193
            className={styles.dropZone}
194
            disabled={resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled)}
26!
195
            onClick={handleClick}
196
            type="button"
197
          >
198
            <Text lines={1}>
199
              {!selectedFileNames.length && (
51✔
200
                <>
201
                  <span className={styles.dropZoneLink}>{translations.FileInputField.browse}</span>
202
                  {' '}
203
                  {translations.FileInputField.drop}
204
                </>
205
              )}
206
              {selectedFileNames.length === 1 && selectedFileNames[0]}
27✔
207
              {selectedFileNames.length > 1 && (
26!
208
                <>
209
                  {selectedFileNames.length}
210
                  {' '}
211
                  {translations.FileInputField.filesSelected}
212
                </>
213
              )}
214
            </Text>
215
          </button>
216
        </div>
217
        {helpText && (
28✔
218
          <div
219
            className={styles.helpText}
220
            id={`${id}__helpText`}
221
          >
222
            {helpText}
223
          </div>
224
        )}
225
        {validationText && (
28✔
226
          <div
227
            className={styles.validationText}
228
            id={`${id}__validationText`}
229
          >
230
            {validationText}
231
          </div>
232
        )}
233
      </div>
234
    </div>
235
  );
236
});
237

238
FileInputField.defaultProps = {
4✔
239
  disabled: false,
240
  fullWidth: false,
241
  helpText: null,
242
  isLabelVisible: true,
243
  layout: 'vertical',
244
  multiple: false,
245
  required: false,
246
  size: 'medium',
247
  validationState: null,
248
  validationText: null,
249
};
250

251
FileInputField.propTypes = {
4✔
252
  /**
253
   * If `true`, the input will be disabled.
254
   */
255
  disabled: PropTypes.bool,
256
  /**
257
   * If `true`, the field will span the full width of its parent.
258
   */
259
  fullWidth: PropTypes.bool,
260
  /**
261
   * Optional help text.
262
   */
263
  helpText: PropTypes.node,
264
  /**
265
   * ID of the `<input>` HTML element.
266
   *
267
   * Also serves as base for ids of nested elements:
268
   * * `<ID>__label`
269
   * * `<ID>__labelText`
270
   * * `<ID>__helpText`
271
   * * `<ID>__validationText`
272
   */
273
  id: PropTypes.string.isRequired,
274
  /**
275
   * If `false`, the label will be visually hidden (but remains accessible by assistive
276
   * technologies).
277
   */
278
  isLabelVisible: PropTypes.bool,
279
  /**
280
   * File input field label.
281
   */
282
  label: PropTypes.node.isRequired,
283
  /**
284
   * Layout of the field.
285
   *
286
   * Ignored if the component is rendered within `FormLayout` component
287
   * as the value is inherited in such case.
288
   *
289
   */
290
  layout: PropTypes.oneOf(['horizontal', 'vertical']),
291
  /**
292
   * If `true`, the input will accept multiple files.
293
   */
294
  multiple: PropTypes.bool,
295
  /**
296
   * Callback fired when the value of the input changes.
297
   */
298
  onFilesChanged: PropTypes.func.isRequired,
299
  /**
300
   * If `true`, the input will be required.
301
   */
302
  required: PropTypes.bool,
303
  /**
304
   * Size of the field.
305
   *
306
   * Ignored if the component is rendered within `InputGroup` component as the value is inherited in such case.
307
   */
308
  size: PropTypes.oneOf(['small', 'medium', 'large']),
309
  /**
310
   * Alter the field to provide feedback based on validation result.
311
   */
312
  validationState: PropTypes.oneOf(['invalid', 'valid', 'warning']),
313
  /**
314
   * Validation message to be displayed.
315
   */
316
  validationText: PropTypes.node,
317
};
318

319
export const FileInputFieldWithGlobalProps = withGlobalProps(FileInputField, 'FileInputField');
4✔
320

321
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