Skip to content

Commit

Permalink
feat: Toast (#138)
Browse files Browse the repository at this point in the history
* feat: Toast

* perf(toast): Clear timeout before creating

* build: Update packages

* chore: Fix warning in story

* fix(text): Add semicolon after textTransform to prevent invalid 'space' styles

Co-authored-by: RichardPK <r.phillips.kerr@gmail.com>
  • Loading branch information
hachiojidev and RichardPK authored Jan 19, 2021
1 parent 8d2cb19 commit ed5faeb
Show file tree
Hide file tree
Showing 9 changed files with 1,216 additions and 1,004 deletions.
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,17 @@
"@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^11.0.0",
"@pancakeswap-libs/eslint-config-pancake": "0.1.0",
"@rollup/plugin-typescript": "^6.0.0",
"@rollup/plugin-typescript": "^8.1.0",
"@storybook/addon-a11y": "^6.1.5",
"@storybook/addon-actions": "^6.1.5",
"@storybook/addon-essentials": "^6.1.5",
"@storybook/addon-links": "^6.1.5",
"@storybook/react": "^6.1.5",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2",
"@types/react": "^16.9.52",
"@types/react": "^17.0.0",
"@types/react-router-dom": "^5.1.6",
"@types/react-transition-group": "^4.4.0",
"@types/styled-components": "^5.1.4",
"@typescript-eslint/eslint-plugin": "^4.4.1",
"@typescript-eslint/parser": "^4.4.1",
Expand All @@ -55,12 +56,13 @@
"husky": "^4.3.0",
"jest": "^26.6.3",
"jest-styled-components": "^7.0.3",
"np": "^6.5.0",
"np": "^7.2.0",
"prettier": "^2.1.2",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-is": "^16.13.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-is": "^17.0.1",
"react-router-dom": "^5.2.0",
"react-transition-group": "^4.4.1",
"rollup": "^2.35.0",
"styled-components": "^5.2.0",
"themeprovider-storybook": "^1.6.4",
Expand All @@ -72,6 +74,7 @@
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-router-dom": "^5.2.0",
"react-transition-group": "^4.4.1",
"styled-components": "^5.2.0"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Text/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const Text = styled.div<TextProps>`
font-size: ${getFontSize};
font-weight: ${({ bold }) => (bold ? 600 : 400)};
line-height: 1.5;
${({ textTransform }) => textTransform && `text-transform: ${textTransform}`}
${({ textTransform }) => textTransform && `text-transform: ${textTransform};`}
${space}
`;

Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ export * from "./components/Spinner";
export * from "./components/Skeleton";
export * from "./components/Toggle";
export * from "./components/Table";

// Hooks
export * from "./hooks";

// Widgets
export * from "./widgets/Modal";
export * from "./widgets/Menu";
export * from "./widgets/Toast";
export * from "./widgets/WalletModal";

// Theme
Expand Down
64 changes: 64 additions & 0 deletions src/widgets/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useCallback, useEffect, useRef } from "react";
import { CSSTransition } from "react-transition-group";
import styled from "styled-components";
import { Alert } from "../../components/Alert";
import { ToastProps } from "./types";

const StyledToast = styled.div`
right: 16px;
position: fixed;
max-width: calc(100% - 32px);
transition: all 250ms ease-in;
width: 100%;
${({ theme }) => theme.mediaQueries.sm} {
max-width: 400px;
}
`;

const Toast: React.FC<ToastProps> = ({ alert, onRemove, style, ttl, ...props }) => {
const timer = useRef<number>();
const ref = useRef(null);
const removeHandler = useRef(onRemove);
const { id, title, description, type } = alert;

const handleRemove = useCallback(() => removeHandler.current(id), [id, removeHandler]);

const handleMouseEnter = () => {
clearTimeout(timer.current);
};

const handleMouseLeave = () => {
if (timer.current) {
clearTimeout(timer.current);
}

timer.current = window.setTimeout(() => {
handleRemove();
}, ttl);
};

useEffect(() => {
if (timer.current) {
clearTimeout(timer.current);
}

timer.current = window.setTimeout(() => {
handleRemove();
}, ttl);

return () => {
clearTimeout(timer.current);
};
}, [timer, ttl, handleRemove]);

return (
<CSSTransition nodeRef={ref} timeout={250} style={style} {...props}>
<StyledToast ref={ref} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<Alert title={title} description={description} variant={type} onClick={handleRemove} />
</StyledToast>
</CSSTransition>
);
};

export default Toast;
49 changes: 49 additions & 0 deletions src/widgets/Toast/ToastContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";
import { TransitionGroup } from "react-transition-group";
import styled from "styled-components";
import Toast from "./Toast";
import { ToastContainerProps } from "./types";

const ZINDEX = 1000;
const TOP_POSITION = 80; // Initial position from the top

const StyledToastContainer = styled.div`
.enter,
.appear {
opacity: 0.01;
}
.enter.enter-active,
.appear.appear-active {
opacity: 1;
transition: opacity 250ms ease-in;
}
.exit {
opacity: 1;
}
.exit.exit-active {
opacity: 0.01;
transition: opacity 250ms ease-out;
}
`;

const ToastContainer: React.FC<ToastContainerProps> = ({ alerts, onRemove, ttl = 6000, stackSpacing = 24 }) => {
return (
<StyledToastContainer>
<TransitionGroup>
{alerts.map((alert, index) => {
const zIndex = (ZINDEX - index).toString();
const top = TOP_POSITION + index * stackSpacing;

return (
<Toast key={alert.id} alert={alert} onRemove={onRemove} ttl={ttl} style={{ top: `${top}px`, zIndex }} />
);
})}
</TransitionGroup>
</StyledToastContainer>
);
};

export default ToastContainer;
48 changes: 48 additions & 0 deletions src/widgets/Toast/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useState } from "react";
import { sample } from "lodash";
import { alertVariants } from "../../components/Alert";
import Button from "../../components/Button/Button";
import ToastContainer from "./ToastContainer";

export default {
title: "Widgets/Toast",
component: ToastContainer,
argTypes: {},
};

export const Default: React.FC = () => {
const [alerts, setAlerts] = useState([]);

const handleClick = (description = "") => {
const now = Date.now();
const randomAlert = {
id: `id-${now}`,
title: `Title: ${now}`,
description,
type: alertVariants[sample(Object.keys(alertVariants))],
};

setAlerts((prevAlerts) => [randomAlert, ...prevAlerts]);
};

const handleRemove = (id: string) => {
setAlerts((prevAlerts) => prevAlerts.filter((prevAlert) => prevAlert.id !== id));
};

return (
<div>
<Button type="button" variant="secondary" onClick={() => handleClick()}>
Random Toast
</Button>
<Button
type="button"
variant="secondary"
ml="8px"
onClick={() => handleClick("This is a description to explain more about the alert")}
>
Random Toast with Description
</Button>
<ToastContainer alerts={alerts} onRemove={handleRemove} />
</div>
);
};
2 changes: 2 additions & 0 deletions src/widgets/Toast/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as ToastContainer } from "./ToastContainer";
export type { ToastContainerProps } from "./types";
27 changes: 27 additions & 0 deletions src/widgets/Toast/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export enum AlertType {
SUCCESS = "success",
DANGER = "danger",
WARNING = "warning",
INFO = "info",
}

export interface Alert {
id: string;
type: AlertType;
title: string;
description?: string;
}

export interface ToastContainerProps {
alerts: Alert[];
stackSpacing?: number;
ttl?: number;
onRemove: (id: string) => void;
}

export interface ToastProps {
alert: Alert;
onRemove: ToastContainerProps["onRemove"];
ttl: number;
style: Partial<CSSStyleDeclaration>;
}
Loading

0 comments on commit ed5faeb

Please sign in to comment.