Skip to content

Commit

Permalink
New single drag and drop single FileUploader (carbon-design-system#…
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
guidari and andreancardona authored Oct 3, 2023
1 parent 8b22597 commit 5ca370b
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 29 deletions.
15 changes: 15 additions & 0 deletions packages/react/src/components/FileUploader/FileUploader.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,21 @@ DragAndDropUploadContainerExampleApplication.argTypes = {
onChange: { action: 'onChange' },
};

export const DragAndDropUploadSingleContainerExampleApplication = (args) =>
require('./stories/drag-and-drop-single').default(args);

DragAndDropUploadSingleContainerExampleApplication.args = {
labelText: 'Drag and drop a file here or click to upload',
name: '',
multiple: false,
accept: ['image/jpeg', 'image/png'],
disabled: false,
tabIndex: 0,
};
DragAndDropUploadSingleContainerExampleApplication.argTypes = {
onChange: { action: 'onChange' },
};

export const Skeleton = () => (
<div style={{ width: '500px' }}>
<FileUploaderSkeleton />
Expand Down
88 changes: 65 additions & 23 deletions packages/react/src/components/FileUploader/FileUploaderItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@

import cx from 'classnames';
import PropTypes from 'prop-types';
import React, { useRef } from 'react';
import React, { useLayoutEffect, useRef, useState } from 'react';
import Filename from './Filename';
import { keys, matches } from '../../internal/keyboard';
import uid from '../../tools/uniqueId';
import { usePrefix } from '../../internal/usePrefix';
import { ReactAttr } from '../../types/common';
import { Tooltip } from '../Tooltip';

export interface FileUploaderItemProps extends ReactAttr<HTMLSpanElement> {
/**
Expand Down Expand Up @@ -40,6 +41,11 @@ export interface FileUploaderItemProps extends ReactAttr<HTMLSpanElement> {
*/
name?: string;

/**
* Event handler that is called after files are added to the uploader
*/
onAddFiles?: (event: React.ChangeEvent<HTMLInputElement>) => void;

/**
* Event handler that is called after removing a file from the file uploader
* The event handler signature looks like `onDelete(evt, { uuid })`
Expand Down Expand Up @@ -78,40 +84,76 @@ function FileUploaderItem({
size,
...other
}: FileUploaderItemProps) {
const [isEllipsisApplied, setIsEllipsisApplied] = useState(false);
const prefix = usePrefix();
const { current: id } = useRef(uuid || uid());
const classes = cx(`${prefix}--file__selected-file`, {
[`${prefix}--file__selected-file--invalid`]: invalid,
[`${prefix}--file__selected-file--md`]: size === 'md',
[`${prefix}--file__selected-file--sm`]: size === 'sm',
});
const isInvalid = invalid
? `${prefix}--file-filename-container-wrap-invalid`
: `${prefix}--file-filename-container-wrap`;

const isEllipsisActive = (element: any) => {
setIsEllipsisApplied(element.offsetWidth < element.scrollWidth);
return element.offsetWidth < element.scrollWidth;
};

useLayoutEffect(() => {
const element = document.querySelector(`.${prefix}--file-filename`);
isEllipsisActive(element);
}, [prefix, name]);

return (
<span className={classes} {...other}>
<p className={`${prefix}--file-filename`} title={name} id={name}>
{name}
</p>
<span className={`${prefix}--file__state-container`}>
<Filename
name={name}
iconDescription={iconDescription}
status={status}
invalid={invalid}
aria-describedby={`${name}-id-error`}
onKeyDown={(evt) => {
if (matches(evt as unknown as Event, [keys.Enter, keys.Space])) {
{isEllipsisApplied ? (
<div className={isInvalid}>
<Tooltip
label={name}
align="bottom"
className={`${prefix}--file-filename-tooltip`}>
<button className={`${prefix}--file-filename-button`} type="button">
<p
title={name}
className={`${prefix}--file-filename-button`}
id={name}>
{name}
</p>
</button>
</Tooltip>
</div>
) : (
<p title={name} className={`${prefix}--file-filename`} id={name}>
{name}
</p>
)}

<div className={`${prefix}--file-container-item`}>
<span className={`${prefix}--file__state-container`}>
<Filename
name={name}
iconDescription={iconDescription}
status={status}
invalid={invalid}
aria-describedby={`${name}-id-error`}
onKeyDown={(evt) => {
if (matches(evt as unknown as Event, [keys.Enter, keys.Space])) {
if (status === 'edit') {
evt.preventDefault();
onDelete(evt, { uuid: id });
}
}
}}
onClick={(evt) => {
if (status === 'edit') {
evt.preventDefault();
onDelete(evt, { uuid: id });
}
}
}}
onClick={(evt) => {
if (status === 'edit') {
onDelete(evt, { uuid: id });
}
}}
/>
</span>
}}
/>
</span>
</div>
{invalid && errorSubject && (
<div
className={`${prefix}--form-requirement`}
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/components/FileUploader/Filename.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ function Filename({
switch (status) {
case 'uploading':
return (
<Loading description={iconDescription} small withOverlay={false} />
<Loading
description={iconDescription}
small
withOverlay={false}
className={`${prefix}--file-loading`}
/>
);
case 'edit':
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* Copyright IBM Corp. 2016, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import React, { useState, useEffect, useRef } from 'react';
import classnames from 'classnames';
import FileUploaderItem from '../FileUploaderItem';
import FileUploaderDropContainer from '../FileUploaderDropContainer';
import FormItem from '../../FormItem';

// import uid from '../../../tools/uniqueId';
import '../FileUploader-story.scss';

const prefix = 'cds';

// -- copied from internal/tools/uniqueId.js
let lastId = 0;
function uid(prefix = 'id') {
lastId++;
return `${prefix}${lastId}`;
}
// -- end copied

const ExampleDropContainerApp = (props) => {
const [file, setFile] = useState();
const uploaderButton = useRef(null);
const handleDrop = (e) => {
e.preventDefault();
};

const handleDragover = (e) => {
e.preventDefault();
};

useEffect(() => {
document.addEventListener('drop', handleDrop);
document.addEventListener('dragover', handleDragover);
return () => {
document.removeEventListener('drop', handleDrop);
document.removeEventListener('dragover', handleDragover);
};
}, []);

const uploadFile = async (fileToUpload) => {
// file size validation
if (fileToUpload[0].filesize > 512000) {
const updatedFile = {
...fileToUpload[0],
status: 'edit',
iconDescription: 'Delete file',
invalid: true,
errorSubject: 'File size exceeds limit',
errorBody: '500kb max file size. Select a new file and try again.',
};
setFile(updatedFile);
return;
}

// file type validation
if (fileToUpload.invalidFileType) {
const updatedFile = {
...fileToUpload[0],
status: 'edit',
iconDescription: 'Delete file',
invalid: true,
errorSubject: 'Invalid file type',
errorBody: `"${fileToUpload.name}" does not have a valid file type.`,
};
setFile(updatedFile);
return;
}

// simulate network request time
const rand = Math.random() * 1000;
setTimeout(() => {
const updatedFile = {
...fileToUpload[0],
status: 'complete',
iconDescription: 'Upload complete',
};
setFile(updatedFile);
}, rand);

// show x icon after 1 second
setTimeout(() => {
const updatedFile = {
...fileToUpload[0],
status: 'edit',
iconDescription: 'Delete file',
};
setFile(updatedFile);
}, rand + 1000);
};

const onAddFilesButton = (event) => {
const file = event.target.files;

const newFile = [
{
uuid: uid(),
name: file[0].name,
filesize: file[0].size,
status: 'uploading',
iconDescription: 'Uploading',
invalidFileType: file[0].invalidFileType,
},
];

setFile(newFile[0]);
uploadFile([newFile[0]]);
};

const handleFileUploaderItemClick = () => {
setFile();
};

const labelClasses = classnames(`${prefix}--file--label`, {
// eslint-disable-next-line react/prop-types
[`${prefix}--file--label--disabled`]: props.disabled,
});

const helperTextClasses = classnames(`${prefix}--label-description`, {
// eslint-disable-next-line react/prop-types
[`${prefix}--label-description--disabled`]: props.disabled,
});

return (
<FormItem>
<p className={labelClasses}>Upload files</p>
<p className={helperTextClasses}>
Max file size is 500kb. Supported file types are .jpg and .png.
</p>
{file === undefined && (
<FileUploaderDropContainer
{...props}
onAddFiles={onAddFilesButton}
innerRef={uploaderButton}
/>
)}

<div
className={classnames(
`${prefix}--file-container`,
`${prefix}--file-container--drop`
)}>
{file !== undefined && (
<FileUploaderItem
key={uid()}
uuid={file.uuid}
name={file.name}
filesize={file.filesize}
errorSubject="File size exceeds limit"
errorBody="500kb max file size. Select a new file and try again."
// eslint-disable-next-line react/prop-types
size={props.size}
status={file.status}
iconDescription={file.iconDescription}
invalid={file.invalid}
onDelete={handleFileUploaderItemClick}
onAddFiles={onAddFilesButton}
/>
)}
</div>
</FormItem>
);
};

export default ExampleDropContainerApp;
Loading

0 comments on commit 5ca370b

Please sign in to comment.