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

react-ui-org / react-ui / 14754643663

30 Apr 2025 12:32PM UTC coverage: 90.283% (-0.08%) from 90.36%
14754643663

Pull #631

github

web-flow
Merge 3a48e7562 into 40eaa8d14
Pull Request #631: Allow to reset `FileInputField` internal state by calling `resetState` function on its ref (#630)

807 of 897 branches covered (89.97%)

Branch coverage included in aggregate %.

5 of 6 new or added lines in 1 file covered. (83.33%)

13 existing lines in 1 file now uncovered.

754 of 832 relevant lines covered (90.63%)

73.57 hits per line

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

69.49
/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
  // We need to have a reference to the input element to be able to call its methods,
50
  // but at the same time we want to expose this reference to the parent component for
51
  // case someone wants to call input methods from outside the component.
52
  useImperativeHandle(
52✔
53
    ref,
54
    () => {
55
      const inputEl = internalInputRef?.current ?? {};
2!
56
      inputEl.resetState = () => {
2✔
NEW
57
        setSelectedFileNames([]);
×
58
      };
59
      return inputEl;
2✔
60
    },
61
    [setSelectedFileNames],
62
  );
63

64
  const handleFileChange = (files, event) => {
52✔
65
    if (files.length === 0) {
2!
66
      setSelectedFileNames([]);
×
67
      return;
×
68
    }
69

70
    // Mimic the native behavior of the `input` element: if multiple files are selected and the input
71
    // does not accept multiple files, no files are processed.
72
    if (files.length > 1 && !multiple) {
2!
UNCOV
73
      setSelectedFileNames([]);
×
74
      return;
×
75
    }
76

77
    const fileNames = [];
2✔
78

79
    [...files].forEach((file) => {
2✔
80
      fileNames.push(file.name);
2✔
81
    });
82

83
    setSelectedFileNames(fileNames);
2✔
84
    onFilesChanged(files, event);
2✔
85
  };
86

87
  const handleInputChange = (event) => {
52✔
88
    handleFileChange(event.target.files, event);
2✔
89
  };
90

91
  const handleClick = () => {
52✔
UNCOV
92
    internalInputRef?.current.click();
×
93
  };
94

95
  const handleDrop = (event) => {
52✔
UNCOV
96
    event.preventDefault();
×
UNCOV
97
    handleFileChange(event.dataTransfer.files, event);
×
UNCOV
98
    setIsDragging(false);
×
99
  };
100

101
  const handleDragOver = (event) => {
52✔
UNCOV
102
    if (!isDragging) {
×
UNCOV
103
      setIsDragging(true);
×
104
    }
105
    event.preventDefault();
×
106
  };
107

108
  const handleDragLeave = () => {
52✔
UNCOV
109
    if (isDragging) {
×
110
      setIsDragging(false);
×
111
    }
112
  };
113

114
  const handleReset = useCallback((event) => {
52✔
UNCOV
115
    setSelectedFileNames([]);
×
UNCOV
116
    onFilesChanged([], event);
×
117
  }, [onFilesChanged]);
118

119
  useEffect(() => {
52✔
120
    const inputEl = internalInputRef.current;
50✔
121
    if (!inputEl) {
50✔
UNCOV
122
      return () => {};
×
123
    }
124

125
    const { form } = inputEl;
50✔
126
    if (!form) {
50!
127
      return () => {};
50✔
128
    }
129

UNCOV
130
    form.addEventListener('reset', handleReset);
×
131

UNCOV
132
    return () => {
×
133
      form.removeEventListener('reset', handleReset);
×
134
    };
135
  }, [handleReset]);
136

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

231
FileInputField.defaultProps = {
4✔
232
  disabled: false,
233
  fullWidth: false,
234
  helpText: null,
235
  isLabelVisible: true,
236
  layout: 'vertical',
237
  multiple: false,
238
  required: false,
239
  size: 'medium',
240
  validationState: null,
241
  validationText: null,
242
};
243

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

312
export const FileInputFieldWithGlobalProps = withGlobalProps(FileInputField, 'FileInputField');
4✔
313

314
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