Skip to content

Commit

Permalink
#178 Add spinner and upload percentage to DragZone
Browse files Browse the repository at this point in the history
  • Loading branch information
Felix Beceic authored and jtomic-croz committed Jan 29, 2024
1 parent 05ecdae commit f7e9dae
Show file tree
Hide file tree
Showing 5 changed files with 369 additions and 72 deletions.
8 changes: 6 additions & 2 deletions libs/theme/src/defaultTheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -862,9 +862,13 @@ const defaultComponentConfig = {
},
iconSize: 7,
iconColor: `text-slate-400 group-hover:text-primary`,
customUploadDropZoneDescriptionContainer: "text-center space-y-1",
customUploadDropZoneDescriptionContainer: "flex flex-col text-center items-center space-y-1",
loading: {
master: "relative flex justify-center items-center text-slate-600 mx-auto my-px",
percentage: "absolute top-1.5 text-body text-xs",
},
customUploadDropZoneTitle: {
master: "flex",
master: "flex cursor-pointer",
fontSize: "text-base",
fontWeight: "font-medium",
color: "text-body-light",
Expand Down
103 changes: 85 additions & 18 deletions libs/upload/src/DragZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@

import React from "react";

import {UploadyContext, useItemErrorListener, useItemFinishListener, useItemStartListener} from "@rpldy/uploady";
import { UploadyContext, useItemProgressListener, useItemFinishListener, useItemStartListener } from "@rpldy/uploady";
import UploadDropZone from "@rpldy/upload-drop-zone";

import { Field, FieldProps } from "@tiller-ds/form-elements";
import { LoadingIcon } from "@tiller-ds/icons";
import { ComponentTokens, cx, TokenProps, useIcon, useTokens } from "@tiller-ds/theme";

import UploadyWrapper, { UploadyWrapperProps } from "./UploadyWrapper";

import { UseFileUpload, File, defaultUploadResponseMapper } from "./useFileUpload";
import { BatchItem } from "@rpldy/shared";
import {isObject} from "lodash";

export type DragZoneProps<T extends File> = {
/**
Expand Down Expand Up @@ -77,7 +77,18 @@ export type DragZoneProps<T extends File> = {
* Function fired when the component is clicked.
*/
onClick?: () => void;
/**
* Custom upload icon that overrides the default icon.
*/
uploadIcon?: React.ReactElement;
/**
* Enables loading via a custom loader.
*
* If set to `null` no loader will be displayed on file upload.
*
* @param {number} uploadPercentage Percentage retrieved from the loader on file upload.
*/
loader?: ((uploadPercentage: number) => React.ReactNode) | null;
} & Omit<UploadyWrapperProps, "children"> &
Omit<FieldProps, "children"> &
DragZoneTokensProps;
Expand Down Expand Up @@ -115,12 +126,20 @@ type CustomUploadDropZoneContainerProps<T extends File> = {
/**
* `withCredentials` flag on fetch requests for uploading
*/
withCredentials?: boolean
withCredentials?: boolean;

/**
* Custom additional styling applied to the component.
*/
className?: string;
/**
* Enables loading via a custom loader.
*
* If set to `null` no loader will be displayed on file upload.
*
* @param {number} uploadPercentage Percentage retrieved from the loader on file upload.
*/
loader?: ((uploadPercentage: number) => React.ReactNode) | null;
};

type CustomUploadDropZoneProps = {
Expand All @@ -131,6 +150,8 @@ type CustomUploadDropZoneProps = {
uploadActive?: boolean;
uploadIcon?: React.ReactElement;
className?: string;
uploadPercentage?: number;
loader?: ((uploadPercentage: number) => React.ReactNode) | null;
} & TokenProps<"DragZone">;

export default function DragZone<T extends File>({
Expand All @@ -148,6 +169,7 @@ export default function DragZone<T extends File>({
uploadIcon,
withCredentials,
className,
loader = (uploadPercentage: number) => <DragZoneLoader uploadPercentage={uploadPercentage} />,
...props
}: DragZoneProps<T>) {
const tokens = useTokens("DragZone", props.tokens);
Expand All @@ -173,6 +195,7 @@ export default function DragZone<T extends File>({
mapUploadResponse={mapUploadResponse}
uploadIcon={uploadIcon}
className={className}
loader={loader}
/>
</UploadDropZone>
</UploadyWrapper>
Expand All @@ -183,31 +206,45 @@ export default function DragZone<T extends File>({
);
}

function CustomUploadDropZoneContainer<T extends File>({ hook, ...props }: CustomUploadDropZoneContainerProps<T>) {
function CustomUploadDropZoneContainer<T extends File>({
hook,
loader,
...props
}: CustomUploadDropZoneContainerProps<T>) {
const uploady = React.useContext(UploadyContext);
const [originalFileName, setOriginalFileName] = React.useState<string>("");
const [uploadActive, setUploadActive] = React.useState<boolean>(false);
const [uploadPercentage, setUploadPercentage] = React.useState<number>(0);

const onClick = React.useCallback(async () => {
uploady.showFileUpload();
}, [uploady]);

useItemStartListener((item) => {
setUploadActive(true);
setUploadPercentage(0);
setOriginalFileName(item.file.name);
});

useItemProgressListener((item) => {
setUploadPercentage(item.completed);
});

useItemFinishListener((item) => {
const file: T = props.mapUploadResponse(item, originalFileName);
setUploadActive(true);
hook?.onUploadedFiles(file);
setUploadActive(false);
});

React.useEffect(() => {
if (hook?.uploadedFiles.length === 0) setUploadActive(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hook.uploadedFiles]);

return <CustomUploadDropZone onClick={onClick} uploadActive={uploadActive} {...props} />;
return (
<CustomUploadDropZone
onClick={onClick}
uploadActive={uploadActive}
uploadPercentage={uploadPercentage}
loader={loader}
{...props}
/>
);
}

function CustomUploadDropZone({
Expand All @@ -218,6 +255,8 @@ function CustomUploadDropZone({
onClick,
uploadIcon,
className,
loader,
uploadPercentage,
...props
}: CustomUploadDropZoneProps) {
const tokens = useTokens("DragZone", props.tokens);
Expand All @@ -239,25 +278,53 @@ function CustomUploadDropZone({
tokens.customUploadDropZoneTitle.master,
tokens.customUploadDropZoneTitle.fontSize,
tokens.customUploadDropZoneTitle.fontWeight,
tokens.customUploadDropZoneTitle.color
tokens.customUploadDropZoneTitle.color,
);

const customUploadDropZoneSubtitleClassName = cx(
tokens.customUploadDropZoneSubtitle.fontSize,
tokens.customUploadDropZoneSubtitle.color
tokens.customUploadDropZoneSubtitle.color,
);

const uploadZoneIcon = useIcon("file", uploadIcon, { size: tokens.iconSize, className: `mx-auto ${tokens.iconColor}`})
const uploadZoneIcon = useIcon("file", uploadIcon, {
size: tokens.iconSize,
className: `mx-auto ${tokens.iconColor}`,
});

return (
<div className={customUploadDropZoneContainerClassName} onClick={onClick}>
<div className={tokens.customUploadDropZoneDescriptionContainer}>
{uploadZoneIcon}
<div className={customUploadDropZoneTitleClassName}>
<label>{title}</label>
</div>
{!uploadActive || !loader
? uploadZoneIcon
: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
loader(uploadPercentage!)}
<label className={customUploadDropZoneTitleClassName}>{title}</label>
<p className={customUploadDropZoneSubtitleClassName}>{subtitle}</p>
</div>
</div>
);
}

type DragZoneLoaderProps = {
uploadPercentage?: number;
spinner?: boolean;
percentage?: boolean;
} & TokenProps<"DragZone">;

export function DragZoneLoader({ uploadPercentage, spinner = true, percentage = true, ...props }: DragZoneLoaderProps) {
const tokens = useTokens("DragZone", props.tokens);

if (!spinner && !percentage) {
console.warn(
"DragZoneLoader is not configured to show spinner and/or percentage. " +
"At least one of these props needs to be enabled in order for the component to display something.",
);
}

return (
<div className={tokens.loading.master}>
{spinner && <LoadingIcon size={tokens.iconSize} />}
{percentage && <span className={tokens.loading.percentage}>{uploadPercentage}%</span>}
</div>
);
}
2 changes: 1 addition & 1 deletion libs/upload/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ export type File = InternalFile;

export { default as useFileUpload } from "./useFileUpload";

export { default as DragZone } from "./DragZone";
export { default as DragZone, DragZoneLoader } from "./DragZone";
export { default as FileList } from "./FileList";
export { default as UploadButton } from "./UploadButton";
74 changes: 73 additions & 1 deletion storybook/src/formik-elements/DragZoneField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { withDesign } from "storybook-addon-designs";
import { IconButton } from "@tiller-ds/core";
import { DragZoneField } from "@tiller-ds/formik-elements";
import { Intl } from "@tiller-ds/intl";
import { FileList, useFileUpload } from "@tiller-ds/upload";
import { DragZoneLoader, FileList, useFileUpload } from "@tiller-ds/upload";
import { Icon } from "@tiller-ds/icons";

import { UPLOADER_EVENTS } from "@rpldy/uploader";
Expand Down Expand Up @@ -83,6 +83,40 @@ export const Simple = () => {
);
};

export const WithSpinnerOnly = () => {
// incl-code
// hook initialization
const useFileUploadHook = useFileUpload();

return (
<DragZoneField
name={name}
hook={useFileUploadHook}
url={useMockSender.destination.url}
send={useMockSender.send}
title={<Intl name="dragZoneTitle" />}
loader={() => <DragZoneLoader percentage={false} />}
/>
);
};

export const WithNoLoader = () => {
// incl-code
// hook initialization
const useFileUploadHook = useFileUpload();

return (
<DragZoneField
name={name}
hook={useFileUploadHook}
url={useMockSender.destination.url}
send={useMockSender.send}
title={<Intl name="dragZoneTitle" />}
loader={null}
/>
);
};

export const WithSubtitle = () => {
// incl-code
const useFileUploadHook = useFileUpload();
Expand Down Expand Up @@ -149,6 +183,44 @@ export const WithFileList = () => {
);
};

export const WithFileListAndCustomLoader = () => {
// incl-code
// hook initialization
const useFileUploadHook = useFileUpload();

return (
<div>
<DragZoneField
name={name}
hook={useFileUploadHook}
url={useMockSender.destination.url}
send={useMockSender.send}
allowMultiple={true}
title={<Intl name="dragZoneTitle" />}
loader={(percentage) => (
<span className="animate-pulse text-body-light h-7 my-px ">Uploading... {percentage}%</span>
)}
/>
<FileList hook={useFileUploadHook}>
{(file, helpers) => (
<FileList.Header>
<FileList.Header.Name>{file.name}</FileList.Header.Name>
<FileList.Header.Action>
<IconButton
icon={<Icon type="trash" />}
label={<Intl name="delete" />}
onClick={() => {
return helpers.deleteFile(file);
}}
/>
</FileList.Header.Action>
</FileList.Header>
)}
</FileList>
</div>
);
};

export const WithSingleFileUpload = () => {
// incl-code
const useFileUploadHook = useFileUpload();
Expand Down
Loading

0 comments on commit f7e9dae

Please sign in to comment.