Skip to content

Commit 67dd040

Browse files
committed
fixup! fixup! Introduce custom design of FileInputField (#244)
1 parent 1e94192 commit 67dd040

File tree

5 files changed

+111
-26
lines changed

5 files changed

+111
-26
lines changed

src/components/FileInputField/FileInputField.jsx

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import PropTypes from 'prop-types';
22
import React, {
33
useContext,
4+
useEffect,
45
useState,
56
} from 'react';
67
import { withGlobalProps } from '../../providers/globalProps';
@@ -24,6 +25,11 @@ export const FileInputField = React.forwardRef((props, ref) => {
2425
isLabelVisible,
2526
label,
2627
layout,
28+
onChange,
29+
onDragEnter,
30+
onDragLeave,
31+
onDragOver,
32+
onDrop,
2733
required,
2834
size,
2935
validationState,
@@ -36,10 +42,10 @@ export const FileInputField = React.forwardRef((props, ref) => {
3642
const translations = useContext(TranslationsContext);
3743

3844
const [selectedFileNames, setSelectedFileNames] = useState([]);
45+
const [isDragAndDropSupported, setIsDragAndDropSupported] = useState(false);
46+
const [isDragging, setIsDragging] = useState(false);
3947

40-
const handleFileChange = (event) => {
41-
const { files } = event.target;
42-
48+
const handleFileChange = (files) => {
4349
if (files.length === 0) {
4450
setSelectedFileNames([]);
4551
return;
@@ -54,6 +60,53 @@ export const FileInputField = React.forwardRef((props, ref) => {
5460
setSelectedFileNames(fileNames);
5561
};
5662

63+
const handleInputChange = (event) => {
64+
handleFileChange(event.target.files);
65+
66+
if (props?.onChange) {
67+
props.onChange(event);
68+
}
69+
};
70+
71+
const handleDrop = (event) => {
72+
event.preventDefault();
73+
handleFileChange(event.dataTransfer.files);
74+
setIsDragging(false);
75+
76+
if (props?.onDrop) {
77+
props.onDrop(event);
78+
}
79+
};
80+
81+
const handleDragOver = (event) => {
82+
event.preventDefault();
83+
setIsDragging(true);
84+
85+
if (props?.onDragOver) {
86+
props.onDragOver(event);
87+
}
88+
};
89+
90+
const handleDragEnter = (event) => {
91+
setIsDragging(true);
92+
93+
if (props?.onDragEnter) {
94+
props.onDragEnter(event);
95+
}
96+
};
97+
98+
const handleDragLeave = (event) => {
99+
setIsDragging(false);
100+
101+
if (props?.onDragLeave) {
102+
props.onDragLeave(event);
103+
}
104+
};
105+
106+
useEffect(() => {
107+
setIsDragAndDropSupported('draggable' in document.createElement('span'));
108+
}, []);
109+
57110
return (
58111
<label
59112
className={classNames(
@@ -65,6 +118,7 @@ export const FileInputField = React.forwardRef((props, ref) => {
65118
: styles.isRootLayoutVertical,
66119
resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled) && styles.isRootDisabled,
67120
inputGroupContext && styles.isRootGrouped,
121+
isDragging && styles.isRootDragging,
68122
required && styles.isRootRequired,
69123
getRootSizeClassName(
70124
resolveContextOrProp(inputGroupContext && inputGroupContext.size, size),
@@ -74,6 +128,10 @@ export const FileInputField = React.forwardRef((props, ref) => {
74128
)}
75129
htmlFor={id}
76130
id={id && `${id}__label`}
131+
onDragEnter={!disabled && isDragAndDropSupported ? handleDragEnter : undefined}
132+
onDragLeave={!disabled && isDragAndDropSupported ? handleDragLeave : undefined}
133+
onDragOver={!disabled && isDragAndDropSupported ? handleDragOver : undefined}
134+
onDrop={!disabled && isDragAndDropSupported ? handleDrop : undefined}
77135
>
78136
<div
79137
className={classNames(
@@ -91,7 +149,7 @@ export const FileInputField = React.forwardRef((props, ref) => {
91149
className={styles.input}
92150
disabled={resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled)}
93151
id={id}
94-
onChange={handleFileChange}
152+
onChange={handleInputChange}
95153
ref={ref}
96154
required={required}
97155
type="file"
@@ -100,9 +158,8 @@ export const FileInputField = React.forwardRef((props, ref) => {
100158
<Text lines={1}>
101159
{!selectedFileNames.length && (
102160
<>
103-
{translations.FileInputField.drop}
104-
{' '}
105161
<span className={styles.dropZoneLink}>{translations.FileInputField.browse}</span>
162+
{isDragAndDropSupported && ` ${translations.FileInputField.drop}`}
106163
</>
107164
)}
108165
{selectedFileNames.length === 1 && selectedFileNames[0]}
@@ -144,6 +201,11 @@ FileInputField.defaultProps = {
144201
id: undefined,
145202
isLabelVisible: true,
146203
layout: 'vertical',
204+
onChange: () => {},
205+
onDragEnter: () => {},
206+
onDragLeave: () => {},
207+
onDragOver: () => {},
208+
onDrop: () => {},
147209
required: false,
148210
size: 'medium',
149211
validationState: null,
@@ -190,6 +252,26 @@ FileInputField.propTypes = {
190252
*
191253
*/
192254
layout: PropTypes.oneOf(['horizontal', 'vertical']),
255+
/**
256+
* Callback fired when the value of the input changes.
257+
*/
258+
onChange: PropTypes.func,
259+
/**
260+
* Callback fired when a drag event enters the field.
261+
*/
262+
onDragEnter: PropTypes.func,
263+
/**
264+
* Callback fired when a drag event leaves the field.
265+
*/
266+
onDragLeave: PropTypes.func,
267+
/**
268+
* Callback fired when a drag event is over the field.
269+
*/
270+
onDragOver: PropTypes.func,
271+
/**
272+
* Callback fired when a file is dropped onto the field.
273+
*/
274+
onDrop: PropTypes.func,
193275
/**
194276
* If `true`, the input will be required.
195277
*/

src/components/FileInputField/FileInputField.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
border-style: dashed;
4343
}
4444

45+
.isRootDragging input + .dropZone {
46+
--rui-local-border-color: #{settings.$drop-zone-dragging-border-color};
47+
}
48+
4549
.root:not(.isRootDisabled) .dropZone:hover {
4650
--rui-local-border-color: #{settings.$drop-zone-hover-border-color};
4751
}

src/components/FileInputField/__tests__/FileInputField.test.jsx

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React from 'react';
22
import {
33
render,
4-
// screen,
4+
screen,
55
within,
66
} from '@testing-library/react';
7+
import userEvent from '@testing-library/user-event';
78
// Intentionally commented out until the disabledPropTest issue is fixed.
8-
// import userEvent from '@testing-library/user-event';
99
// import { disabledPropTest } from '../../../../tests/propTests/disabledPropTest';
1010
import { refPropTest } from '../../../../tests/propTests/refPropTest';
1111
import { fullWidthPropTest } from '../../../../tests/propTests/fullWidthPropTest';
@@ -69,20 +69,18 @@ describe('rendering', () => {
6969
});
7070

7171
describe('functionality', () => {
72-
// TODO by adam@adamkundrna.cz on 2025-03-03
73-
// Figure out how to test file upload with the onChange handler inside.
74-
// it('calls synthetic event onChange()', async () => {
75-
// const spy = jest.fn();
76-
// render((
77-
// <FileInputField
78-
// {...mandatoryProps}
79-
// id="id"
80-
// onChange={spy}
81-
// />
82-
// ));
83-
//
84-
// const file = new File(['hello'], 'hello.png', { type: 'image/png' });
85-
// await userEvent.upload(screen.getByTestId('id'), file);
86-
// expect(spy).toHaveBeenCalled();
87-
// });
72+
it('calls synthetic event onChange()', async () => {
73+
const spy = jest.fn();
74+
render((
75+
<FileInputField
76+
{...mandatoryProps}
77+
id="id"
78+
onChange={spy}
79+
/>
80+
));
81+
82+
const file = new File(['hello'], 'hello.png', { type: 'image/png' });
83+
await userEvent.upload(screen.getByTestId('id'), file);
84+
expect(spy).toHaveBeenCalled();
85+
});
8886
});

src/components/FileInputField/_settings.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ $drop-zone-disabled-color: var(--rui-color-text-primary-disabled);
55
$drop-zone-border-color: var(--rui-color-border-primary);
66
$drop-zone-hover-border-color: var(--rui-color-border-primary-hover);
77
$drop-zone-active-border-color: var(--rui-color-border-primary-active);
8+
$drop-zone-dragging-border-color: var(--rui-color-border-primary-active);
89
$drop-zone-disabled-border-color: var(--rui-color-border-primary);
910
$drop-zone-background-color: var(--rui-color-background-basic);
1011
$drop-zone-disabled-background-color: var(--rui-color-background-disabled);

src/translations/en.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ export default {
33
close: 'Close',
44
},
55
FileInputField: {
6-
browse: 'browse',
7-
drop: 'Drop file or',
6+
browse: 'Browse',
7+
drop: 'or drop file here',
88
filesSelected: 'files selected',
99
},
1010
ModalCloseButton: {

0 commit comments

Comments
 (0)