added toast component to display information in an asynchronous manner

This commit is contained in:
Sebastian Sdorra
2019-12-11 15:44:09 +01:00
parent dd3b02bbff
commit 3c615bc7ba
9 changed files with 220 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
<div id="modalRoot"></div>
<div id="toastRoot"></div>

View File

@@ -66,6 +66,7 @@ export * from "./modals";
export * from "./navigation"; export * from "./navigation";
export * from "./repos"; export * from "./repos";
export * from "./table"; export * from "./table";
export * from "./toast";
export { export {
File, File,

View File

@@ -0,0 +1,61 @@
import React, { FC } from "react";
import { createPortal } from "react-dom";
import styled from "styled-components";
import { getTheme, Themeable, ToastThemeContext, Type } from "./themes";
type Props = {
type: Type;
title: string;
};
const rootElement = document.getElementById("toastRoot");
const Container = styled.div<Themeable>`
z-index: 99999;
position: fixed;
padding: 1.5rem;
right: 1.5rem;
bottom: 1.5rem;
color: ${props => props.theme.primary};
background-color: ${props => props.theme.secondary};
max-width: 18rem;
font-size: 0.75rem;
border-radius: 5px;
animation: 0.5s slide-up;
& > p {
margin-bottom: 0.5rem;
}
@keyframes slide-up {
from {
bottom: -10rem;
}
to {
bottom: 1.5rem;
}
}
`;
const Title = styled.h1<Themeable>`
margin-bottom: 0.25rem;
font-weight: bold;
`;
const Toast: FC<Props> = ({ children, title, type }) => {
if (!rootElement) {
throw new Error("could not find toast container #toastRoot");
}
const theme = getTheme(type);
const content = (
<Container theme={theme}>
<Title theme={theme}>{title}</Title>
<ToastThemeContext.Provider value={theme}>{children}</ToastThemeContext.Provider>
</Container>
);
return createPortal(content, rootElement);
};
export default Toast;

View File

@@ -0,0 +1,37 @@
import React, { FC, useContext } from "react";
import { ToastThemeContext, Themeable } from "./themes";
import styled from "styled-components";
type Props = {
icon?: string;
};
const ThemedButton = styled.div.attrs(props => ({
className: "button"
}))<Themeable>`
color: ${props => props.theme.primary};
border-color: ${props => props.theme.primary};
background-color: ${props => props.theme.secondary};
font-size: 0.75rem;
&:hover {
color: ${props => props.theme.primary};
border-color: ${props => props.theme.tertiary};
background-color: ${props => props.theme.tertiary};
}
`;
const ToastButtonIcon = styled.i`
margin-right: 0.25rem;
`;
const ToastButton: FC<Props> = ({ icon, children }) => {
const theme = useContext(ToastThemeContext);
return (
<ThemedButton theme={theme}>
{icon && <ToastButtonIcon className={`fas fa-fw fa-${icon}`} />} {children}
</ThemedButton>
);
};
export default ToastButton;

View File

@@ -0,0 +1,20 @@
import React, { FC } from "react";
import styled from "styled-components";
const Buttons = styled.div`
display: flex;
padding-top: 0.5rem;
width: 100%;
& > * {
flex-grow: 1;
}
& > *:not(:last-child) {
margin-right: 0.5rem;
}
`;
const ToastButtons: FC = ({ children }) => <Buttons>{children}</Buttons>;
export default ToastButtons;

View File

@@ -0,0 +1,42 @@
import React, { useState } from "react";
import { storiesOf } from "@storybook/react";
import Toast from "./Toast";
import ToastButtons from "./ToastButtons";
import ToastButton from "./ToastButton";
import { types } from "./themes";
const toastStories = storiesOf("Toast", module);
const AnimatedToast = () => (
<Toast type="primary" title="Animated">
Awesome animated Toast
</Toast>
);
const Animator = () => {
const [display, setDisplay] = useState(false);
return (
<div style={{ padding: "2rem" }}>
{display && <AnimatedToast />}
<button className="button is-primary" onClick={() => setDisplay(!display)}>
{display ? "Close" : "Open"} Toast
</button>
</div>
);
};
toastStories.add("Open/Close", () => <Animator />);
types.forEach(type => {
toastStories.add(type.charAt(0).toUpperCase() + type.slice(1), () => (
<Toast type={type} title="New Changes">
<p>The underlying Pull-Request has changed. Press reload to see the changes.</p>
<p>Warning: Non saved modification will be lost.</p>
<ToastButtons>
<ToastButton icon="redo">Reload</ToastButton>
<ToastButton icon="times">Ignore</ToastButton>
</ToastButtons>
</Toast>
));
});

View File

@@ -0,0 +1,3 @@
export { default as Toast } from "./Toast";
export { default as ToastButton } from "./ToastButton";
export { default as ToastButtons } from "./ToastButtons";

View File

@@ -0,0 +1,53 @@
import * as React from "react";
export type ToastTheme = {
primary: string;
secondary: string;
tertiary: string;
};
export type Themeable = {
theme: ToastTheme;
};
export type Type = "info" | "primary" | "success" | "warning" | "danger";
export const types: Type[] = ["info", "primary", "success", "warning", "danger"];
const themes: { [name in Type]: ToastTheme } = {
info: {
primary: "#363636",
secondary: "#99d8f3",
tertiary: "white"
},
primary: {
primary: "#363636",
secondary: "#7fe8ef",
tertiary: "white"
},
success: {
primary: "#363636",
secondary: "#7fe3cd",
tertiary: "white"
},
warning: {
primary: "#905515",
secondary: "#ffeeab",
tertiary: "white"
},
danger: {
primary: "#363636",
secondary: "#ff9baf",
tertiary: "white"
}
};
export const getTheme = (name: Type) => {
const theme = themes[name];
if (!theme) {
throw new Error(`could not find theme with name ${name}`);
}
return theme;
};
export const ToastThemeContext = React.createContext(themes.warning);

View File

@@ -34,6 +34,7 @@
</noscript> </noscript>
<div id="root"></div> <div id="root"></div>
<div id="modalRoot"></div> <div id="modalRoot"></div>
<div id="toastRoot"></div>
<!-- <!--
This HTML file is a template. This HTML file is a template.