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

react-ui-org / react-ui / 14411418965

11 Apr 2025 08:23PM UTC coverage: 90.674%. First build
14411418965

Pull #621

github

web-flow
Merge 11caa4328 into 9f2f1998b
Pull Request #621: Bump version to v0.59.0

802 of 890 branches covered (90.11%)

Branch coverage included in aggregate %.

744 of 815 relevant lines covered (91.29%)

74.66 hits per line

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

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

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

38
  const internalInputRef = useRef();
52✔
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);
52✔
44

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

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

52
  const handleFileChange = (files, event) => {
52✔
53
    if (files.length === 0) {
2!
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) {
2!
61
      setSelectedFileNames([]);
×
62
      return;
×
63
    }
64

65
    const fileNames = [];
2✔
66

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

71
    setSelectedFileNames(fileNames);
2✔
72
    onFilesChanged(files, event);
2✔
73
  };
74

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

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

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

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

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

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

196
FileInputField.defaultProps = {
4✔
197
  disabled: false,
198
  fullWidth: false,
199
  helpText: null,
200
  isLabelVisible: true,
201
  layout: 'vertical',
202
  multiple: false,
203
  required: false,
204
  size: 'medium',
205
  validationState: null,
206
  validationText: null,
207
};
208

209
FileInputField.propTypes = {
4✔
210
  /**
211
   * If `true`, the input will be disabled.
212
   */
213
  disabled: PropTypes.bool,
214
  /**
215
   * If `true`, the field will span the full width of its parent.
216
   */
217
  fullWidth: PropTypes.bool,
218
  /**
219
   * Optional help text.
220
   */
221
  helpText: PropTypes.node,
222
  /**
223
   * ID of the `<input>` HTML element.
224
   *
225
   * Also serves as base for ids of nested elements:
226
   * * `<ID>__label`
227
   * * `<ID>__labelText`
228
   * * `<ID>__helpText`
229
   * * `<ID>__validationText`
230
   */
231
  id: PropTypes.string.isRequired,
232
  /**
233
   * If `false`, the label will be visually hidden (but remains accessible by assistive
234
   * technologies).
235
   */
236
  isLabelVisible: PropTypes.bool,
237
  /**
238
   * File input field label.
239
   */
240
  label: PropTypes.node.isRequired,
241
  /**
242
   * Layout of the field.
243
   *
244
   * Ignored if the component is rendered within `FormLayout` component
245
   * as the value is inherited in such case.
246
   *
247
   */
248
  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,
257
  /**
258
   * If `true`, the input will be required.
259
   */
260
  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']),
267
  /**
268
   * Alter the field to provide feedback based on validation result.
269
   */
270
  validationState: PropTypes.oneOf(['invalid', 'valid', 'warning']),
271
  /**
272
   * Validation message to be displayed.
273
   */
274
  validationText: PropTypes.node,
275
};
276

277
export const FileInputFieldWithGlobalProps = withGlobalProps(FileInputField, 'FileInputField');
4✔
278

279
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