From 3c615bc7baafa431823d93b7e2ef19a787b9c18f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 11 Dec 2019 15:44:09 +0100 Subject: [PATCH] added toast component to display information in an asynchronous manner --- .../.storybook/preview-body.html | 2 + scm-ui/ui-components/src/index.ts | 1 + scm-ui/ui-components/src/toast/Toast.tsx | 61 +++++++++++++++++++ .../ui-components/src/toast/ToastButton.tsx | 37 +++++++++++ .../ui-components/src/toast/ToastButtons.tsx | 20 ++++++ .../ui-components/src/toast/index.stories.tsx | 42 +++++++++++++ scm-ui/ui-components/src/toast/index.ts | 3 + scm-ui/ui-components/src/toast/themes.ts | 53 ++++++++++++++++ scm-ui/ui-webapp/public/index.mustache | 1 + 9 files changed, 220 insertions(+) create mode 100644 scm-ui/ui-components/.storybook/preview-body.html create mode 100644 scm-ui/ui-components/src/toast/Toast.tsx create mode 100644 scm-ui/ui-components/src/toast/ToastButton.tsx create mode 100644 scm-ui/ui-components/src/toast/ToastButtons.tsx create mode 100644 scm-ui/ui-components/src/toast/index.stories.tsx create mode 100644 scm-ui/ui-components/src/toast/index.ts create mode 100644 scm-ui/ui-components/src/toast/themes.ts diff --git a/scm-ui/ui-components/.storybook/preview-body.html b/scm-ui/ui-components/.storybook/preview-body.html new file mode 100644 index 0000000000..d2084fbba6 --- /dev/null +++ b/scm-ui/ui-components/.storybook/preview-body.html @@ -0,0 +1,2 @@ +
+
diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts index e039411842..46ccbb48ec 100644 --- a/scm-ui/ui-components/src/index.ts +++ b/scm-ui/ui-components/src/index.ts @@ -66,6 +66,7 @@ export * from "./modals"; export * from "./navigation"; export * from "./repos"; export * from "./table"; +export * from "./toast"; export { File, diff --git a/scm-ui/ui-components/src/toast/Toast.tsx b/scm-ui/ui-components/src/toast/Toast.tsx new file mode 100644 index 0000000000..cbbb2c58b6 --- /dev/null +++ b/scm-ui/ui-components/src/toast/Toast.tsx @@ -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` + 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` + margin-bottom: 0.25rem; + font-weight: bold; +`; + +const Toast: FC = ({ children, title, type }) => { + if (!rootElement) { + throw new Error("could not find toast container #toastRoot"); + } + + const theme = getTheme(type); + const content = ( + + {title} + {children} + + ); + + return createPortal(content, rootElement); +}; + +export default Toast; diff --git a/scm-ui/ui-components/src/toast/ToastButton.tsx b/scm-ui/ui-components/src/toast/ToastButton.tsx new file mode 100644 index 0000000000..76ae63cd73 --- /dev/null +++ b/scm-ui/ui-components/src/toast/ToastButton.tsx @@ -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" +}))` + 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 = ({ icon, children }) => { + const theme = useContext(ToastThemeContext); + return ( + + {icon && } {children} + + ); +}; + +export default ToastButton; diff --git a/scm-ui/ui-components/src/toast/ToastButtons.tsx b/scm-ui/ui-components/src/toast/ToastButtons.tsx new file mode 100644 index 0000000000..4444a2e14a --- /dev/null +++ b/scm-ui/ui-components/src/toast/ToastButtons.tsx @@ -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 }) => {children}; + +export default ToastButtons; diff --git a/scm-ui/ui-components/src/toast/index.stories.tsx b/scm-ui/ui-components/src/toast/index.stories.tsx new file mode 100644 index 0000000000..af17964d2e --- /dev/null +++ b/scm-ui/ui-components/src/toast/index.stories.tsx @@ -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 = () => ( + + Awesome animated Toast + +); + +const Animator = () => { + const [display, setDisplay] = useState(false); + + return ( +
+ {display && } + +
+ ); +}; + +toastStories.add("Open/Close", () => ); + +types.forEach(type => { + toastStories.add(type.charAt(0).toUpperCase() + type.slice(1), () => ( + +

The underlying Pull-Request has changed. Press reload to see the changes.

+

Warning: Non saved modification will be lost.

+ + Reload + Ignore + +
+ )); +}); diff --git a/scm-ui/ui-components/src/toast/index.ts b/scm-ui/ui-components/src/toast/index.ts new file mode 100644 index 0000000000..f7b618a3ac --- /dev/null +++ b/scm-ui/ui-components/src/toast/index.ts @@ -0,0 +1,3 @@ +export { default as Toast } from "./Toast"; +export { default as ToastButton } from "./ToastButton"; +export { default as ToastButtons } from "./ToastButtons"; diff --git a/scm-ui/ui-components/src/toast/themes.ts b/scm-ui/ui-components/src/toast/themes.ts new file mode 100644 index 0000000000..1b2744870c --- /dev/null +++ b/scm-ui/ui-components/src/toast/themes.ts @@ -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); diff --git a/scm-ui/ui-webapp/public/index.mustache b/scm-ui/ui-webapp/public/index.mustache index d94bc552ee..86193387ce 100644 --- a/scm-ui/ui-webapp/public/index.mustache +++ b/scm-ui/ui-webapp/public/index.mustache @@ -34,6 +34,7 @@
+