Skip to content

Commit 5ca370b

Browse files
New single drag and drop single FileUploader (#14660)
* feat: added new story * chore: working tooltip without style * chore: working upload file without style * feat: tooltip styling on progress * fix: fixed loading in the state * feat: added style for replace box * chore: improving the code * test: added test to fileUploaderItem * chore: added css to fix layout movement after upload * fix: removed replace functionality * fix: added breakpoint and ellipse to FF * fix: fixed width * fix: fixed styles * fix: fixed styling * fix: change pixel --------- Co-authored-by: Andrea N. Cardona <[email protected]>
1 parent 8b22597 commit 5ca370b

File tree

5 files changed

+325
-29
lines changed

5 files changed

+325
-29
lines changed

packages/react/src/components/FileUploader/FileUploader.stories.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,21 @@ DragAndDropUploadContainerExampleApplication.argTypes = {
154154
onChange: { action: 'onChange' },
155155
};
156156

157+
export const DragAndDropUploadSingleContainerExampleApplication = (args) =>
158+
require('./stories/drag-and-drop-single').default(args);
159+
160+
DragAndDropUploadSingleContainerExampleApplication.args = {
161+
labelText: 'Drag and drop a file here or click to upload',
162+
name: '',
163+
multiple: false,
164+
accept: ['image/jpeg', 'image/png'],
165+
disabled: false,
166+
tabIndex: 0,
167+
};
168+
DragAndDropUploadSingleContainerExampleApplication.argTypes = {
169+
onChange: { action: 'onChange' },
170+
};
171+
157172
export const Skeleton = () => (
158173
<div style={{ width: '500px' }}>
159174
<FileUploaderSkeleton />

packages/react/src/components/FileUploader/FileUploaderItem.tsx

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77

88
import cx from 'classnames';
99
import PropTypes from 'prop-types';
10-
import React, { useRef } from 'react';
10+
import React, { useLayoutEffect, useRef, useState } from 'react';
1111
import Filename from './Filename';
1212
import { keys, matches } from '../../internal/keyboard';
1313
import uid from '../../tools/uniqueId';
1414
import { usePrefix } from '../../internal/usePrefix';
1515
import { ReactAttr } from '../../types/common';
16+
import { Tooltip } from '../Tooltip';
1617

1718
export interface FileUploaderItemProps extends ReactAttr<HTMLSpanElement> {
1819
/**
@@ -40,6 +41,11 @@ export interface FileUploaderItemProps extends ReactAttr<HTMLSpanElement> {
4041
*/
4142
name?: string;
4243

44+
/**
45+
* Event handler that is called after files are added to the uploader
46+
*/
47+
onAddFiles?: (event: React.ChangeEvent<HTMLInputElement>) => void;
48+
4349
/**
4450
* Event handler that is called after removing a file from the file uploader
4551
* The event handler signature looks like `onDelete(evt, { uuid })`
@@ -78,40 +84,76 @@ function FileUploaderItem({
7884
size,
7985
...other
8086
}: FileUploaderItemProps) {
87+
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);
8188
const prefix = usePrefix();
8289
const { current: id } = useRef(uuid || uid());
8390
const classes = cx(`${prefix}--file__selected-file`, {
8491
[`${prefix}--file__selected-file--invalid`]: invalid,
8592
[`${prefix}--file__selected-file--md`]: size === 'md',
8693
[`${prefix}--file__selected-file--sm`]: size === 'sm',
8794
});
95+
const isInvalid = invalid
96+
? `${prefix}--file-filename-container-wrap-invalid`
97+
: `${prefix}--file-filename-container-wrap`;
98+
99+
const isEllipsisActive = (element: any) => {
100+
setIsEllipsisApplied(element.offsetWidth < element.scrollWidth);
101+
return element.offsetWidth < element.scrollWidth;
102+
};
103+
104+
useLayoutEffect(() => {
105+
const element = document.querySelector(`.${prefix}--file-filename`);
106+
isEllipsisActive(element);
107+
}, [prefix, name]);
108+
88109
return (
89110
<span className={classes} {...other}>
90-
<p className={`${prefix}--file-filename`} title={name} id={name}>
91-
{name}
92-
</p>
93-
<span className={`${prefix}--file__state-container`}>
94-
<Filename
95-
name={name}
96-
iconDescription={iconDescription}
97-
status={status}
98-
invalid={invalid}
99-
aria-describedby={`${name}-id-error`}
100-
onKeyDown={(evt) => {
101-
if (matches(evt as unknown as Event, [keys.Enter, keys.Space])) {
111+
{isEllipsisApplied ? (
112+
<div className={isInvalid}>
113+
<Tooltip
114+
label={name}
115+
align="bottom"
116+
className={`${prefix}--file-filename-tooltip`}>
117+
<button className={`${prefix}--file-filename-button`} type="button">
118+
<p
119+
title={name}
120+
className={`${prefix}--file-filename-button`}
121+
id={name}>
122+
{name}
123+
</p>
124+
</button>
125+
</Tooltip>
126+
</div>
127+
) : (
128+
<p title={name} className={`${prefix}--file-filename`} id={name}>
129+
{name}
130+
</p>
131+
)}
132+
133+
<div className={`${prefix}--file-container-item`}>
134+
<span className={`${prefix}--file__state-container`}>
135+
<Filename
136+
name={name}
137+
iconDescription={iconDescription}
138+
status={status}
139+
invalid={invalid}
140+
aria-describedby={`${name}-id-error`}
141+
onKeyDown={(evt) => {
142+
if (matches(evt as unknown as Event, [keys.Enter, keys.Space])) {
143+
if (status === 'edit') {
144+
evt.preventDefault();
145+
onDelete(evt, { uuid: id });
146+
}
147+
}
148+
}}
149+
onClick={(evt) => {
102150
if (status === 'edit') {
103-
evt.preventDefault();
104151
onDelete(evt, { uuid: id });
105152
}
106-
}
107-
}}
108-
onClick={(evt) => {
109-
if (status === 'edit') {
110-
onDelete(evt, { uuid: id });
111-
}
112-
}}
113-
/>
114-
</span>
153+
}}
154+
/>
155+
</span>
156+
</div>
115157
{invalid && errorSubject && (
116158
<div
117159
className={`${prefix}--form-requirement`}

packages/react/src/components/FileUploader/Filename.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ function Filename({
5959
switch (status) {
6060
case 'uploading':
6161
return (
62-
<Loading description={iconDescription} small withOverlay={false} />
62+
<Loading
63+
description={iconDescription}
64+
small
65+
withOverlay={false}
66+
className={`${prefix}--file-loading`}
67+
/>
6368
);
6469
case 'edit':
6570
return (
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* Copyright IBM Corp. 2016, 2023
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import React, { useState, useEffect, useRef } from 'react';
9+
import classnames from 'classnames';
10+
import FileUploaderItem from '../FileUploaderItem';
11+
import FileUploaderDropContainer from '../FileUploaderDropContainer';
12+
import FormItem from '../../FormItem';
13+
14+
// import uid from '../../../tools/uniqueId';
15+
import '../FileUploader-story.scss';
16+
17+
const prefix = 'cds';
18+
19+
// -- copied from internal/tools/uniqueId.js
20+
let lastId = 0;
21+
function uid(prefix = 'id') {
22+
lastId++;
23+
return `${prefix}${lastId}`;
24+
}
25+
// -- end copied
26+
27+
const ExampleDropContainerApp = (props) => {
28+
const [file, setFile] = useState();
29+
const uploaderButton = useRef(null);
30+
const handleDrop = (e) => {
31+
e.preventDefault();
32+
};
33+
34+
const handleDragover = (e) => {
35+
e.preventDefault();
36+
};
37+
38+
useEffect(() => {
39+
document.addEventListener('drop', handleDrop);
40+
document.addEventListener('dragover', handleDragover);
41+
return () => {
42+
document.removeEventListener('drop', handleDrop);
43+
document.removeEventListener('dragover', handleDragover);
44+
};
45+
}, []);
46+
47+
const uploadFile = async (fileToUpload) => {
48+
// file size validation
49+
if (fileToUpload[0].filesize > 512000) {
50+
const updatedFile = {
51+
...fileToUpload[0],
52+
status: 'edit',
53+
iconDescription: 'Delete file',
54+
invalid: true,
55+
errorSubject: 'File size exceeds limit',
56+
errorBody: '500kb max file size. Select a new file and try again.',
57+
};
58+
setFile(updatedFile);
59+
return;
60+
}
61+
62+
// file type validation
63+
if (fileToUpload.invalidFileType) {
64+
const updatedFile = {
65+
...fileToUpload[0],
66+
status: 'edit',
67+
iconDescription: 'Delete file',
68+
invalid: true,
69+
errorSubject: 'Invalid file type',
70+
errorBody: `"${fileToUpload.name}" does not have a valid file type.`,
71+
};
72+
setFile(updatedFile);
73+
return;
74+
}
75+
76+
// simulate network request time
77+
const rand = Math.random() * 1000;
78+
setTimeout(() => {
79+
const updatedFile = {
80+
...fileToUpload[0],
81+
status: 'complete',
82+
iconDescription: 'Upload complete',
83+
};
84+
setFile(updatedFile);
85+
}, rand);
86+
87+
// show x icon after 1 second
88+
setTimeout(() => {
89+
const updatedFile = {
90+
...fileToUpload[0],
91+
status: 'edit',
92+
iconDescription: 'Delete file',
93+
};
94+
setFile(updatedFile);
95+
}, rand + 1000);
96+
};
97+
98+
const onAddFilesButton = (event) => {
99+
const file = event.target.files;
100+
101+
const newFile = [
102+
{
103+
uuid: uid(),
104+
name: file[0].name,
105+
filesize: file[0].size,
106+
status: 'uploading',
107+
iconDescription: 'Uploading',
108+
invalidFileType: file[0].invalidFileType,
109+
},
110+
];
111+
112+
setFile(newFile[0]);
113+
uploadFile([newFile[0]]);
114+
};
115+
116+
const handleFileUploaderItemClick = () => {
117+
setFile();
118+
};
119+
120+
const labelClasses = classnames(`${prefix}--file--label`, {
121+
// eslint-disable-next-line react/prop-types
122+
[`${prefix}--file--label--disabled`]: props.disabled,
123+
});
124+
125+
const helperTextClasses = classnames(`${prefix}--label-description`, {
126+
// eslint-disable-next-line react/prop-types
127+
[`${prefix}--label-description--disabled`]: props.disabled,
128+
});
129+
130+
return (
131+
<FormItem>
132+
<p className={labelClasses}>Upload files</p>
133+
<p className={helperTextClasses}>
134+
Max file size is 500kb. Supported file types are .jpg and .png.
135+
</p>
136+
{file === undefined && (
137+
<FileUploaderDropContainer
138+
{...props}
139+
onAddFiles={onAddFilesButton}
140+
innerRef={uploaderButton}
141+
/>
142+
)}
143+
144+
<div
145+
className={classnames(
146+
`${prefix}--file-container`,
147+
`${prefix}--file-container--drop`
148+
)}>
149+
{file !== undefined && (
150+
<FileUploaderItem
151+
key={uid()}
152+
uuid={file.uuid}
153+
name={file.name}
154+
filesize={file.filesize}
155+
errorSubject="File size exceeds limit"
156+
errorBody="500kb max file size. Select a new file and try again."
157+
// eslint-disable-next-line react/prop-types
158+
size={props.size}
159+
status={file.status}
160+
iconDescription={file.iconDescription}
161+
invalid={file.invalid}
162+
onDelete={handleFileUploaderItemClick}
163+
onAddFiles={onAddFilesButton}
164+
/>
165+
)}
166+
</div>
167+
</FormItem>
168+
);
169+
};
170+
171+
export default ExampleDropContainerApp;

0 commit comments

Comments
 (0)