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

react-ui-org / react-ui / 14771691801

01 May 2025 07:23AM UTC coverage: 90.36%. Remained the same
14771691801

Pull #633

github

web-flow
Merge 07c60f8fe into 40eaa8d14
Pull Request #633: Partially disable code owners to enable merge during someone's time off

805 of 894 branches covered (90.04%)

Branch coverage included in aggregate %.

751 of 828 relevant lines covered (90.7%)

73.92 hits per line

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

69.37
/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 internalInputRef = useRef();
52✔
41

42
  // We need to have a reference to the input element to be able to call its methods,
43
  // but at the same time we want to expose this reference to the parent component for
44
  // case someone wants to call input methods from outside the component.
45
  useImperativeHandle(ref, () => internalInputRef.current);
52✔
46

47
  const formLayoutContext = useContext(FormLayoutContext);
52✔
48
  const inputGroupContext = useContext(InputGroupContext);
52✔
49
  const translations = useContext(TranslationsContext);
52✔
50

51
  const [selectedFileNames, setSelectedFileNames] = useState([]);
52✔
52
  const [isDragging, setIsDragging] = useState(false);
52✔
53

54
  const handleFileChange = (files, event) => {
52✔
55
    if (files.length === 0) {
2!
56
      setSelectedFileNames([]);
×
57
      return;
×
58
    }
59

60
    // Mimic the native behavior of the `input` element: if multiple files are selected and the input
61
    // does not accept multiple files, no files are processed.
62
    if (files.length > 1 && !multiple) {
2!
63
      setSelectedFileNames([]);
×
64
      return;
×
65
    }
66

67
    const fileNames = [];
2✔
68

69
    [...files].forEach((file) => {
2✔
70
      fileNames.push(file.name);
2✔
71
    });
72

73
    setSelectedFileNames(fileNames);
2✔
74
    onFilesChanged(files, event);
2✔
75
  };
76

77
  const handleInputChange = (event) => {
52✔
78
    handleFileChange(event.target.files, event);
2✔
79
  };
80

81
  const handleClick = () => {
52✔
82
    internalInputRef?.current.click();
×
83
  };
84

85
  const handleDrop = (event) => {
52✔
86
    event.preventDefault();
×
87
    handleFileChange(event.dataTransfer.files, event);
×
88
    setIsDragging(false);
×
89
  };
90

91
  const handleDragOver = (event) => {
52✔
92
    if (!isDragging) {
×
93
      setIsDragging(true);
×
94
    }
95
    event.preventDefault();
×
96
  };
97

98
  const handleDragLeave = () => {
52✔
99
    if (isDragging) {
×
100
      setIsDragging(false);
×
101
    }
102
  };
103

104
  const handleReset = useCallback((event) => {
52✔
105
    setSelectedFileNames([]);
×
106
    onFilesChanged([], event);
×
107
  }, [onFilesChanged]);
108

109
  useEffect(() => {
52✔
110
    const inputEl = internalInputRef.current;
50✔
111
    if (!inputEl) {
50✔
112
      return () => {};
×
113
    }
114

115
    const { form } = inputEl;
50✔
116
    if (!form) {
50!
117
      return () => {};
50✔
118
    }
119

120
    form.addEventListener('reset', handleReset);
×
121

122
    return () => {
×
123
      form.removeEventListener('reset', handleReset);
×
124
    };
125
  }, [handleReset]);
126

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

221
FileInputField.defaultProps = {
4✔
222
  disabled: false,
223
  fullWidth: false,
224
  helpText: null,
225
  isLabelVisible: true,
226
  layout: 'vertical',
227
  multiple: false,
228
  required: false,
229
  size: 'medium',
230
  validationState: null,
231
  validationText: null,
232
};
233

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

302
export const FileInputFieldWithGlobalProps = withGlobalProps(FileInputField, 'FileInputField');
4✔
303

304
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

© 2025 Coveralls, Inc