Skip to content

Commit

Permalink
🪟 🎨 Update toast design (#19980)
Browse files Browse the repository at this point in the history
* feat: changed styling for toast

* fix: made changes requested

* fix: made changes requested

* update Toast design

* update Toast storybook

* replace "hasError" with actual type of Toast

* replace classes with mixins

* fix broken notifications

* change the animation to "ease-out"

* replace Notification type with interface

* make enum const

* use button dark theme

* fixed stretched icon

* move z-index value to css variables file

* fix toast bottom-margin and update animation

* add shadow mixin

* reduce spacing-page-bottom to 88px

* Revert "reduce spacing-page-bottom to 88px"

This reverts commit 6c72ace.

* extend base Button component: do not break the text

* align text to left
set baseline as flex-start

* set max width for notification container

* fix text center alignment

* fix action button margin

Co-authored-by: Harshith Mullapudi <harshithmullapudi@gmail.com>
  • Loading branch information
dizel852 and harshithmullapudi committed Dec 8, 2022
1 parent 50f22cd commit db70435
Show file tree
Hide file tree
Showing 21 changed files with 266 additions and 171 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DataGeographyDropdown } from "components/common/DataGeographyDropdown";
import { ControlLabels } from "components/LabeledControl";
import { Card } from "components/ui/Card";
import { Spinner } from "components/ui/Spinner";
import { ToastType } from "components/ui/Toast";

import { Geography } from "core/request/AirbyteClient";
import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService";
Expand Down Expand Up @@ -32,8 +33,8 @@ export const UpdateConnectionDataResidency: React.FC = () => {
} catch (e) {
registerNotification({
id: "connection.geographyUpdateError",
title: formatMessage({ id: "connection.geographyUpdateError" }),
isError: true,
text: formatMessage({ id: "connection.geographyUpdateError" }),
type: ToastType.ERROR,
});
}
setSelectedValue(undefined);
Expand Down
1 change: 1 addition & 0 deletions airbyte-webapp/src/components/ui/Button/Button.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
font-weight: 600;
cursor: pointer;
transition: 0.2s ease-in;
white-space: nowrap;

&.full {
width: 100%;
Expand Down
28 changes: 0 additions & 28 deletions airbyte-webapp/src/components/ui/Toast/ErrorSign.tsx

This file was deleted.

97 changes: 96 additions & 1 deletion airbyte-webapp/src/components/ui/Toast/Toast.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,98 @@
@use "scss/colors";
@use "scss/variables" as vars;
@use "scss/z-indices";
@use "scss/mixins";

$toast-icon-size: 13px;
$toast-icon-container-size: 34px;
$toast-bottom-margin: 27px;

@keyframes slide-up-animations {
0% {
transform: translate(-50%, -100%);
bottom: -60px;
}

100% {
transform: translate(-50%, 0);
bottom: $toast-bottom-margin;
}
}

@mixin type($name, $color, $background) {
&.#{$name} {
background-color: $background;
border: 1px solid $color;

.iconContainer {
background-color: $color;
}

.toastIcon {
color: $color;
}
}
}

.toastContainer {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: vars.$spacing-md;
max-width: vars.$width-max-notification;
position: fixed;
box-sizing: border-box;
bottom: $toast-bottom-margin;
left: 50%;
transform: translate(-50%, 0);
z-index: z-indices.$notification;
padding: vars.$spacing-md;
border-radius: vars.$border-radius-md;
animation: slide-up-animations 0.25s ease-out;

@include mixins.shadow;

@include type("info", colors.$blue-400, colors.$blue-50);
@include type("warning", colors.$yellow-500, colors.$yellow-50);
@include type("success", colors.$green-200, colors.$green-50);
@include type("error", colors.$red-300, colors.$red-50);
}

.iconContainer {
width: $toast-icon-container-size;
height: $toast-icon-container-size;
max-height: $toast-icon-container-size;
min-width: $toast-icon-container-size;
padding: vars.$border-radius-md;
border-radius: vars.$border-radius-md;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
}

.toastIcon {
width: $toast-icon-size;
height: $toast-icon-size;
background: colors.$white;
border-radius: 50%;
}

.textContainer {
align-self: center;
}

.text {
line-height: 17px;
text-align: left;
}

.actionButton {
margin-top: vars.$spacing-xs;
}

.closeButton {
margin-left: 10px;
svg {
color: colors.$dark-blue-900;
}
}
124 changes: 51 additions & 73 deletions airbyte-webapp/src/components/ui/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,64 @@
import { faTimes } from "@fortawesome/free-solid-svg-icons";
import { faCheck, faExclamation, faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import classNames from "classnames";
import React from "react";
import styled, { keyframes } from "styled-components";

import { H5 } from "components/base/Titles";
import { CrossIcon } from "components/icons/CrossIcon";
import { Text } from "components/ui/Text";

import { Button } from "../Button";
import { ErrorSign } from "./ErrorSign";
import styles from "./Toast.module.scss";

interface ToastProps {
title: string | React.ReactNode;
text?: string | React.ReactNode;
hasError?: boolean;
onClose?: () => void;
export const enum ToastType {
WARNING = "warning",
SUCCESS = "success",
ERROR = "error",
INFO = "info",
}

export const SlideUpAnimation = keyframes`
0% {
translate(-50%, -100%);
bottom: -49px;
}
100% {
translate(-50%, 0);
bottom: 49px;
}
`;

const Singleton = styled.div<{ hasError?: boolean }>`
position: fixed;
bottom: 49px;
left: 50%;
transform: translate(-50%, 0);
z-index: 20;
padding: 25px 25px 22px;
background: ${({ theme, hasError }) => (hasError ? theme.lightDangerColor : theme.lightPrimaryColor)};
border: 1px solid ${({ theme }) => theme.greyColor20};
box-shadow: 0 1px 2px ${({ theme }) => theme.shadowColor};
border-radius: 8px;
display: flex;
flex-direction: row;
align-items: center;
animation: ${SlideUpAnimation} 0.25s linear;
`;

const Title = styled(H5)<{ hasError?: boolean }>`
color: ${({ theme, hasError }) => (hasError ? theme.dangerColor : theme.primaryColor)};
font-style: normal;
font-weight: bold;
font-size: 15px;
line-height: 18px;
`;
export interface ToastProps {
text: string | React.ReactNode;
type?: ToastType;
onAction?: () => void;
actionBtnText?: string;
onClose?: () => void;
}

const Text = styled.div`
color: ${({ theme }) => theme.mediumPrimaryColor};
const ICON_MAPPING = {
[ToastType.WARNING]: faExclamation,
[ToastType.ERROR]: faTimes,
[ToastType.SUCCESS]: faCheck,
[ToastType.INFO]: faExclamation,
};

font-style: normal;
font-weight: normal;
font-size: 14px;
line-height: 17px;
margin-top: 5px;
`;
const STYLES_BY_TYPE: Readonly<Record<ToastType, string>> = {
[ToastType.WARNING]: styles.warning,
[ToastType.ERROR]: styles.error,
[ToastType.SUCCESS]: styles.success,
[ToastType.INFO]: styles.info,
};

export const Toast: React.FC<ToastProps> = (props) => (
<Singleton hasError={props.hasError}>
{props.hasError && <ErrorSign />}
<div>
<Title hasError={props.hasError}>{props.title}</Title>
{props.text && <Text>{props.text}</Text>}
export const Toast: React.FC<ToastProps> = ({ type = ToastType.INFO, onAction, actionBtnText, onClose, text }) => {
return (
<div className={classNames(styles.toastContainer, STYLES_BY_TYPE[type])}>
<div className={classNames(styles.iconContainer)}>
<FontAwesomeIcon icon={ICON_MAPPING[type]} className={styles.toastIcon} />
</div>
<div className={styles.textContainer}>
{text && (
<Text size="lg" className={styles.text}>
{text}
</Text>
)}
</div>
{onAction && (
<Button variant="dark" className={styles.actionButton} onClick={onAction}>
{actionBtnText}
</Button>
)}
{onClose && (
<Button variant="clear" className={styles.closeButton} onClick={onClose} size="sm" icon={<CrossIcon />} />
)}
</div>
{props.onClose && (
<Button
className={styles.closeButton}
variant="clear"
onClick={props.onClose}
icon={<FontAwesomeIcon icon={faTimes} />}
/>
)}
</Singleton>
);
);
};
55 changes: 48 additions & 7 deletions airbyte-webapp/src/components/ui/Toast/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { ComponentStory, ComponentMeta } from "@storybook/react";

import { Toast } from "./Toast";
import { Toast, ToastType } from "./Toast";

export default {
title: "UI/Toast",
component: Toast,
argTypes: {
title: { type: { name: "string", required: false } },
text: { type: { name: "string", required: false } },
type: { type: { name: "string", required: false } },
onAction: { table: { disable: true } },
actionBtnText: { type: { name: "string", required: false } },
onClose: { table: { disable: true } },
},
} as ComponentMeta<typeof Toast>;
Expand All @@ -19,17 +21,56 @@ Basic.args = {
text: "This is a basic card",
};

export const WithTitle = Template.bind({});
WithTitle.args = {
title: "With Title",
text: "This is a card with a title",
export const WithText = Template.bind({});
WithText.args = {
text: "This is a card with a text",
};

export const WithLongText = Template.bind({});
WithLongText.args = {
text: "This is a card with a long text, very very long text message. Just an example how ",
};

export const WithCloseButton = Template.bind({});
WithCloseButton.args = {
title: "With Close button",
text: "This is a card with a close button",
onClose: () => {
console.log("Closed!");
},
};

export const WithActionButton = Template.bind({});
WithActionButton.args = {
text: "This is a card with an action button button",
onAction: () => console.log("Action btn clicked!"),
actionBtnText: "Click me!",
};

export const WithActionAndCloseButton = Template.bind({});
WithActionAndCloseButton.args = {
text: "This is a card with an action button button",
onAction: () => console.log("Action btn clicked!"),
actionBtnText: "Click me!",
onClose: () => console.log("Closed!"),
};

export const WarningToast = Template.bind({});
WarningToast.args = {
text: "This is a card with a close button",
onClose: () => console.log("Closed!"),
type: ToastType.WARNING,
};

export const ErrorToast = Template.bind({});
ErrorToast.args = {
text: "This is a card with a close button",
onClose: () => console.log("Closed!"),
type: ToastType.ERROR,
};

export const SuccessToast = Template.bind({});
SuccessToast.args = {
text: "This is a card with a close button",
onClose: () => console.log("Closed!"),
type: ToastType.SUCCESS,
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { HealthService } from "core/health/HealthService";
import { useGetService } from "core/servicesProvider";
import { useNotificationService } from "hooks/services/Notification/NotificationService";

import { ToastType } from "../../../components/ui/Toast";
import { Notification } from "../Notification";

const HEALTH_NOTIFICATION_ID = "health.error";
const HEALTHCHECK_MAX_COUNT = 3;

Expand All @@ -17,10 +20,10 @@ function useApiHealthPoll(): void {
const { registerNotification, unregisterNotificationById } = useNotificationService();

useEffect(() => {
const errorNotification = {
const errorNotification: Notification = {
id: HEALTH_NOTIFICATION_ID,
title: formatMessage({ id: "notifications.error.health" }),
isError: true,
text: formatMessage({ id: "notifications.error.health" }),
type: ToastType.ERROR,
};

const interval = setInterval(async () => {
Expand Down
Loading

0 comments on commit db70435

Please sign in to comment.