Skip to content

Commit

Permalink
feature: toast (#47)
Browse files Browse the repository at this point in the history
* toast component

* added tests

Co-authored-by: Omri Avigdor <[email protected]>
  • Loading branch information
omri-av and Omri Avigdor authored Dec 29, 2020
1 parent 6c64016 commit 3ab99a2
Show file tree
Hide file tree
Showing 9 changed files with 410 additions and 2 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

158 changes: 158 additions & 0 deletions src/components/Toast/Toast.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import NOOP from "lodash/noop";
import React, { useMemo, useCallback, useRef } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { CSSTransition } from "react-transition-group";
import Button from "../Button/Button";
import Icon from "../Icon/Icon";
import Check from "../Icon/Icons/components/Check";
import Alert from "../Icon/Icons/components/Alert";
import Info from "../Icon/Icons/components/Info";
import CloseSmall from "../Icon/Icons/components/CloseSmall";
import { TOAST_TYPES } from "./ToastConstants";
import "./Toast.scss";

const defaultIconMap = {
[TOAST_TYPES.NORMAL]: Info,
[TOAST_TYPES.POSITIVE]: Check,
[TOAST_TYPES.NEGATIVE]: Alert
};

const getIcon = (type, icon) => {
/* icon may be node a may be a string */
if (icon && typeof icon === "object") {
return icon;
}
return icon || defaultIconMap[type] ? (
<Icon
iconType={icon ? Icon.type.ICON_FONT : Icon.type.SVG}
clickable={false}
icon={icon || defaultIconMap[type]}
iconSize="24px"
ignoreFocusStyle
/>
) : null;
};

const Toast = ({
open,
autoHideDuration,
type,
icon,
hideIcon,
action,
children,
closeable,
onClose
}) => {
const classNames = useMemo(
() => cx("monday-style-toast", `monday-style-toast--type-${type}`),
[type]
);
const handleClose = useCallback(() => {
if (onClose) {
onClose();
}
}, [onClose]);
/* Timer */
const timerAutoHide = useRef();
const setAutoHideTimer = useCallback(
duration => {
if (!onClose || duration == null) {
return;
}

clearTimeout(timerAutoHide.current);
timerAutoHide.current = setTimeout(() => {
handleClose(null, "timeout");
}, duration);
},
[handleClose, onClose]
);
React.useEffect(() => {
if (open && autoHideDuration > 0) {
setAutoHideTimer(autoHideDuration);
}

return () => {
clearTimeout(timerAutoHide.current);
};
}, [open, autoHideDuration, setAutoHideTimer]);
const iconElement = !hideIcon && getIcon(type, icon);

return (
<CSSTransition
in={open}
classNames="monday-style-toast-animation"
timeout={400}
unmountOnExit
>
<div className={classNames} role="alert" aria-live="polite">
{iconElement && (
<div className="monday-style-toast-icon">{iconElement}</div>
)}
<div
className={cx("monday-style-toast-content", {
"monday-style-toast-content-no-icon": !iconElement
})}
>
{children}
</div>
{action && <div className="monday-style-toast-action">{action}</div>}
{closeable && (
<Button
onClick={handleClose}
size={Button.sizes.SMALL}
kind={Button.kinds.TERTIARY}
color={Button.colors.ON_PRIMARY_COLOR}
ariaLabel="close-toast"
>
<Icon
iconType={Icon.type.SVG}
clickable={false}
icon={CloseSmall}
iconSize="20px"
ignoreFocusStyle
/>
</Button>
)}
</div>
</CSSTransition>
);
};

Toast.propTypes = {
/** If true, Toast is open (visible) */
open: PropTypes.bool,
type: PropTypes.oneOf([
TOAST_TYPES.NORMAL,
TOAST_TYPES.POSITIVE,
TOAST_TYPES.NEGATIVE
]),
/** Possible to override the dafult icon */
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
/** If true, won't show the icon */
hideIcon: PropTypes.bool,
/** The action to display */
action: PropTypes.element,
/** If false, won't show the close button */
closeable: PropTypes.bool,
onClose: PropTypes.func,
/** The number of milliseconds to wait before
* automatically closing the Toast
* (0 or null cancels this behaviour) */
autoHideDuration: PropTypes.number
};

Toast.defaultProps = {
type: TOAST_TYPES.NORMAL,
open: false,
action: null,
hideIcon: false,
icon: null,
closeable: true,
autoHideDuration: null,
onClose: NOOP
};

export default Toast;
103 changes: 103 additions & 0 deletions src/components/Toast/Toast.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
@import "../../styles/global-css-settings.scss";
@import "../../styles/typography.scss";
@import "../../styles/themes.scss";

.monday-style-toast {
@include font-paragraph();
@include box-shadow-medium();
@include theme-prop(color, text-color-on-primary);
margin: $spacing-medium;
position: fixed;
top: 0;
left: 50%;
right: auto;
transform: translate(-50%,0);
padding: $spacing-small;
align-items: center;
display: flex;
min-width: 390px;
border-radius: $border-radius-small;

&-icon {
display: flex;
padding-left: $spacing-small;
padding-right: $spacing-small;
}

&-content {
line-height: 22px;
font-size: 14px;
flex-grow: 1;
&-no-icon {
padding-left: $spacing-small;
}
}

&-action {
padding-right: $spacing-small;
}

&--type {
&-normal {
@include theme-prop(background-color, primary-color);
}
&-positive {
@include theme-prop(background-color, positive-color);
}
&-negative {
@include theme-prop(background-color, negative-color);
}
}

&-animation {
&-enter-active, &-exit-active {
animation-iteration-count: 1;
animation-direction: forwards;
animation-fill-mode: forwards;
}
&-enter-active {
animation-duration: 400ms;
animation-name: toast-pop-in;
}

&-exit-active {
animation-duration: 160ms;
animation-name: toast-pop-out;
}
}
}

@keyframes toast-pop-in {
0% {
transform: translate(-50%,-115px);
}
25% {
// down 130 px
transform: translate(-50%,15px);
animation-timing-function: cubic-bezier(0,0,0.35,1);
}

66% {
// up 20 px
transform: translate(-50%,-5px);
animation-timing-function: cubic-bezier(0.4,0,1,1);
}

100% {
// down 5 px
transform: translate(-50%,0px);
animation-timing-function: cubic-bezier(0.4,0,1,1);
}
}


@keyframes toast-pop-out {
0% {
transform: translate(-50%,0);
}
100% {
// up 130 px
transform: translate(-50%,-130px);
animation-timing-function: cubic-bezier(0,0,0.35,1);
}
}
5 changes: 5 additions & 0 deletions src/components/Toast/ToastConstants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const TOAST_TYPES = {
NORMAL: "normal",
POSITIVE: "positive",
NEGATIVE: "negative"
};
76 changes: 76 additions & 0 deletions src/components/Toast/__stories__/toast.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useState } from "react";
import { number, select, boolean } from "@storybook/addon-knobs";
import { TOAST_TYPES } from "../ToastConstants";
import Toast from "../Toast";
import Button from "../../Button/Button";
import Icon from "../../Icon/Icon";
import Send from "../../Icon/Icons/components/Send";
import { StoryStateColumn, StoryStateRow } from "../../storybook-helpers";

export const Toasts = () => {
const sendIconElement = (
<Icon
iconType={Icon.type.SVG}
clickable={false}
icon={Send}
iconSize="20px"
ignoreFocusStyle
/>
);
const knobs = {
type: select("type", {
Normal: TOAST_TYPES.NORMAL,
Positive: TOAST_TYPES.POSITIVE,
Negative: TOAST_TYPES.NEGATIVE,
}),
closeable: boolean("closeable", true),
autoHideDuration: number("autoHideDuration", 0),
icon: select("icon", {
default: null,
star: "fa fa-star",
send: "send"
}),
hideIcon: boolean("hideIcon", false)
};
const [toastOpen, setToastOpen] = useState(false);
const toggleToast = () => setToastOpen(open => !open);
const closeToast = () => setToastOpen(false);
let icon = knobs.icon;
if (knobs.icon === "send") {
icon = sendIconElement;
}
return (
<section>
<StoryStateRow>
<StoryStateColumn>
<Button onClick={() => toggleToast()}>Toggle Toast!</Button>
<Toast
open={toastOpen}
onClose={() => closeToast()}
action={(
<Button
size={Button.sizes.SMALL}
kind={Button.kinds.SECONDARY}
color={Button.colors.ON_PRIMARY_COLOR}
>
Undo 5
</Button>
)}
type={knobs.type}
icon={icon}
closeable={knobs.closeable}
autoHideDuration={knobs.autoHideDuration}
hideIcon={knobs.hideIcon}
>
Something Happened
</Toast>
</StoryStateColumn>
</StoryStateRow>
</section>
);
};

export default {
title: "Components|Toast",
component: Toast
};
Loading

0 comments on commit 3ab99a2

Please sign in to comment.