Skip to content

Commit

Permalink
Merge pull request #217 from US-CBP/Toast
Browse files Browse the repository at this point in the history
Toast component
  • Loading branch information
bagrub authored Oct 25, 2024
2 parents d375f67 + 10c006a commit 4821878
Show file tree
Hide file tree
Showing 5 changed files with 384 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const CbpTab = /*@__PURE__*/createReactComponent<JSX.CbpTab, HTMLCbpTabEl
export const CbpTabPanel = /*@__PURE__*/createReactComponent<JSX.CbpTabPanel, HTMLCbpTabPanelElement>('cbp-tab-panel');
export const CbpTabs = /*@__PURE__*/createReactComponent<JSX.CbpTabs, HTMLCbpTabsElement>('cbp-tabs');
export const CbpTag = /*@__PURE__*/createReactComponent<JSX.CbpTag, HTMLCbpTagElement>('cbp-tag');
export const CbpToast = /*@__PURE__*/createReactComponent<JSX.CbpToast, HTMLCbpToastElement>('cbp-toast');
export const CbpTypography = /*@__PURE__*/createReactComponent<JSX.CbpTypography, HTMLCbpTypographyElement>('cbp-typography');
export const CbpUniversalHeader = /*@__PURE__*/createReactComponent<JSX.CbpUniversalHeader, HTMLCbpUniversalHeaderElement>('cbp-universal-header');
export const CbpUsaBanner = /*@__PURE__*/createReactComponent<JSX.CbpUsaBanner, HTMLCbpUsaBannerElement>('cbp-usa-banner');
152 changes: 152 additions & 0 deletions packages/web-components/src/components/cbp-toast/cbp-toast.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* @Prop --cbp-toast-color-bg: var(--cbp-color-info-dark);
* @Prop --cbp-toast-color-bg-dark: var(--cbp-color-info-lighter);
* @Prop --cbp-toast-color: var(--cbp-color-text-lighter);
* @Prop --cbp-toast-color-dark: var(--cbp-color-text-darker);
* @Prop --cbp-toast-color-content: var(--cbp-color-text-lighter);
* @Prop --cbp-toast-color-content-dark: var(--cbp-color-text-darkest);
* @Prop --cbp-toast-color-bg-sidebar: var(--cbp-color-info-darker);
* @Prop --cbp-toast-color-bg-sidebar-dark: var(--cbp-color-info-light);
* @Prop --cbp-toast-color-icon-sidebar: var(--cbp-color-text-light);
* @Prop --cbp-toast-color-icon-sidebar-dark: var(--cbp-color-text-darker);
*/
:root{

--cbp-toast-color-bg: var(--cbp-color-info-dark);
--cbp-toast-color-bg-dark: var(--cbp-color-info-lighter);
--cbp-toast-color: var(--cbp-color-text-lighter);
--cbp-toast-color-dark: var(--cbp-color-text-darker);
--cbp-toast-color-content: var(--cbp-color-text-lighter);
--cbp-toast-color-content-dark: var(--cbp-color-text-darkest);
--cbp-toast-color-bg-sidebar: var(--cbp-color-info-darker);
--cbp-toast-color-bg-sidebar-dark: var(--cbp-color-info-light);
--cbp-toast-color-icon-sidebar: var(--cbp-color-text-light);
--cbp-toast-color-icon-sidebar-dark: var(--cbp-color-text-darker);

}

[data-cbp-theme=light] cbp-toast[context*=dark]:not([context=light-always]),
[data-cbp-theme=dark] cbp-toast:not([context=dark-inverts]):not([context=light-always]) {

--cbp-toast-color-bg: var(--cbp-toast-color-bg-dark);
--cbp-toast-color: var(--cbp-toast-color-dark);
--cbp-toast-color-content: var(--cbp-toast-color-content-dark);
--cbp-toast-color-bg-sidebar: var(--cbp-toast-color-bg-sidebar-dark);
--cbp-toast-color-icon-sidebar: var(--cbp-toast-color-icon-sidebar-dark);

}


@keyframes show {
0%{
opacity: 1;
transform: translateX(100%);
}

}

@keyframes dismiss {
99%{
transform: translateX(200%);
}
100%{
visibility: hidden;
}
}

cbp-toast {
display: flex;
margin-inline-start: auto;
margin-block-end: var(--cbp-space-6x);
width: min(100%, 30rem);
background-color: var(--cbp-toast-color-bg);
color: var(--cbp-toast-color);
box-shadow: 0px 0px 18px 2px rgba(0, 0, 0, 0.3);
border-radius: var(--cbp-border-radius-softer);

animation: show 600ms 100ms ease-in-out forwards;

&:not([open]){
animation: dismiss 1s ease-in-out forwards;
}

.cbp-toast-sidebar{
display: flex;
align-items: center;
padding: var(--cbp-space-4x);
background-color: var(--cbp-toast-color-bg-sidebar);
border-top-left-radius: var(--cbp-border-radius-softer);;
border-bottom-left-radius: var(--cbp-border-radius-softer);;

[slot="cbp-toast-icon"]{
color: var(--cbp-toast-color-icon-sidebar);
}
}

.cbp-toast-title{
font-weight: var(--cbp-font-weight-medium);
font-size: var(--cbp-font-size-heading-sm);
line-height: var(--cbp-line-height-sm);
}

.cbp-toast-container{
padding: var(--cbp-space-3x);
}

.cbp-toast-content{
font-weight: var(--cbp-font-weight-regular);
font-size: var(--cbp-font-size-body);
line-height: var(--cbp-line-height-xs);
color: var(--cbp-toast-color-content);
}

.cbp-toast-button-bar{
display: flex;
justify-content: center;

& > div{
display: flex;
gap: var(--cbp-space-4x);
}

cbp-button[fill=ghost][color=secondary]{ //match button color to text color for toast
--cbp-button-color: var(--cbp-toast-color);
--cbp-button-color-dark: var(--cbp-toast-color-dark);
}
}

&[color='info']{
--cbp-toast-color-bg: var(--cbp-color-info-dark);
--cbp-toast-color-bg-dark: var(--cbp-color-info-lighter);
--cbp-toast-color-bg-sidebar: var(--cbp-color-info-darker);
--cbp-toast-color-bg-sidebar-dark: var(--cbp-color-info-light);
}

&[color='success']{
--cbp-toast-color-bg: var(--cbp-color-success-base);
--cbp-toast-color-bg-dark: var(--cbp-color-success-lighter);
--cbp-toast-color-bg-sidebar: var(--cbp-color-success-darker);
--cbp-toast-color-bg-sidebar-dark: var(--cbp-color-success-light);
}

&[color='warning']{
--cbp-toast-color-bg: var(--cbp-color-warning-base);
--cbp-toast-color-bg-dark: var(--cbp-color-warning-base);
--cbp-toast-color-bg-sidebar: var(--cbp-color-warning-darker);
--cbp-toast-color-bg-sidebar-dark: var(--cbp-color-warning-darker);
--cbp-toast-color: var(--cbp-color-text-darker);
--cbp-toast-color-dark: var(--cbp-color-text-darker);
--cbp-toast-color-content: var(--cbp-color-text-darker);
--cbp-toast-color-content-dark: var(--cbp-color-text-darker);
--cbp-toast-color-icon-sidebar-dark: var(--cbp-color-text-light);
}

&[color='danger']{

--cbp-toast-color-bg: var(--cbp-color-danger-dark);
--cbp-toast-color-bg-dark: var(--cbp-color-danger-lighter);
--cbp-toast-color-bg-sidebar: var(--cbp-color-danger-darker);
--cbp-toast-color-bg-sidebar-dark: var(--cbp-color-danger-light);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Meta } from '@storybook/addon-docs';

<Meta title="Components/Toast/Specifications" />

# cbp-toast

## Purpose

* Toast is intended to temporarialy display messages to users such as error or warning messages about an form input, or confrimation of a file upload. It is the intent of the designer that for things
like error messages to also be populated elsewhere in app so that they don't get 'lost'.

## Functional Requirements

* Toast expects for users to define the below props/slots
*Props:
*Icon: sets the name prop for the icon in the sidebar. default value of 'user'
*timer: provides 3 options (3sec, 5sec, or 10sec) which determines how long the toast will be visible. if this is not set the toast will persist, which is intended for development, not final
User experience
*color: provides 4 options (info, success, warning, & danger) defaulting to info. determines the color palette used for the background & text color for the toast
*Slots:
*title: Slot for the title section of the Toast, intended for text or typography tags
*content: Slot for the content of the toast, used for description of error or progress bar for example.
*buttons: Slot intended for 1 or 2 buttons, UX design intended for these to be a dismiss (of the toast) and/or a button to link to log/more detailed section explaining the toast

## Technical Specifications

### User Interactions

* Toast is intended to temporarialy draw attention to items/actions for the user as such they are meant to have an internal timer to display & have the displayed content in a log elsewhere for user to
review after the toast is gone.

### Responsiveness

* Toast's width is set to the max-content of the toast, with a max-width on the toast of 50% of the viewportS

### Accessibility

* Toast being on a timer is an Accessibility concern, which is why it is advised to have a log of toasts perserved elsewhere in the app as mentioned above
* Slotted content is Accessibile via keyboard, primarily use being the slotted buttons

### Additional Notes and Considerations
121 changes: 121 additions & 0 deletions packages/web-components/src/components/cbp-toast/cbp-toast.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
export default {
title: 'Components/Toast',
tags: ['autodocs'],
argTypes: {

duration: {
control: 'select',
options: [3, 5, 10]
},
color: {
control: 'select',
options: ['info', 'danger', 'success', 'warning']
},
context : {
control: 'select',
options: [ "light-inverts", "light-always", "dark-inverts", "dark-always"]
},
sx: {
description: 'Supports adding inline styles as an object of key-value pairs comprised of CSS properties and values. Values should reference design tokens when possible.',
control: 'object',
},
},
};

const Template = ({ open, icon, title, content, buttons, duration, color, context, sx }) => {
return `
<cbp-toast
${open ? `open` : ''}
${color ? `color=${color}` : ''}
${duration ? `duration=${duration}` : ''}
${icon ? `icon=${icon}` : ''}
${context && context != 'light-inverts' ? `context=${context}` : ''}
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<div slot="cbp-toast-icon"><cbp-icon size='2rem' name=${icon}></cbp-icon></div>
<div slot="cbp-toast-title">${title}</div>
${content}
<div slot="cbp-toast-buttons">${buttons}</div>
</cbp-toast>
`;
};

export const Toast = Template.bind({});

Toast.args = {
open: true,
icon: `user`,
title: 'Test Toast Title',
content: 'Notification Description - A rule you are following just fired.',
buttons: `<cbp-button type="button" fill="ghost" color="secondary"> Dismiss </cbp-button> <cbp-button type="button" fill="ghost" color="secondary"> Default 2</cbp-button>`
}

const MultiTemplate = ({ open, icon, title, content, buttons, duration, color, context, sx }) => {
return `
<cbp-toast
${open ? `open=${open}` : ''}
color=${color}
duration=${duration}
${icon ? `icon=${icon}` : ''}
${context && context != 'light-inverts' ? `context=${context}` : ''}
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<div slot="cbp-toast-icon"><cbp-icon size='2rem' name=${icon}></cbp-icon></div>
<div slot="cbp-toast-title">${title}</div>
${content}
<div slot="cbp-toast-buttons">${buttons}</div>
</cbp-toast>
<cbp-toast
${open ? `open=${open}` : ''}
color=${color}
duration=${duration}
${icon ? `icon=${icon}` : ''}
${context && context != 'light-inverts' ? `context=${context}` : ''}
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<div slot="cbp-toast-icon"><cbp-icon size='2rem' name=${icon}></cbp-icon></div>
<div slot="cbp-toast-title">${title}</div>
${content}
<div slot="cbp-toast-buttons">${buttons}</div>
</cbp-toast>
<cbp-toast
${open ? `open=${open}` : ''}
color=${color}
duration=${duration}
${icon ? `icon=${icon}` : ''}
${context && context != 'light-inverts' ? `context=${context}` : ''}
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<div slot="cbp-toast-icon"><cbp-icon size='2rem' name=${icon}></cbp-icon></div>
<div slot="cbp-toast-title">${title}</div>
${content}
<div slot="cbp-toast-buttons">${buttons}</div>
</cbp-toast>
<cbp-toast
${open ? `open=${open}` : ''}
color=${color}
duration=${duration}
${icon ? `icon=${icon}` : ''}
${context && context != 'light-inverts' ? `context=${context}` : ''}
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<div slot="cbp-toast-icon"><cbp-icon size='2rem' name=${icon}></cbp-icon></div>
<div slot="cbp-toast-title">${title}</div>
${content}
<div slot="cbp-toast-buttons">${buttons}</div>
</cbp-toast>
`;
};

export const MultipleToast = MultiTemplate.bind({});

MultipleToast.args = {
open: true,
icon: `user`,
title: 'Test Toast Title',
content: 'Notification Description - A rule you are following just fired.',
buttons: '<cbp-button type="button" fill="ghost" color="secondary"> Dismiss </cbp-button> <cbp-button type="button" fill="ghost" color="secondary"> Default 2</cbp-button>'
}
69 changes: 69 additions & 0 deletions packages/web-components/src/components/cbp-toast/cbp-toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Component, Prop, Element, Host, Watch, h } from '@stencil/core';
import { setCSSProps } from '../../utils/utils';

@Component({
tag: 'cbp-toast',
styleUrl: 'cbp-toast.scss'
})
export class CbpToast {

@Element() host: HTMLElement;

/** specifies the color for the toast */
@Prop({ reflect: true }) color: 'info' | 'danger' | 'success' | 'warning' = 'info';

/** specifies time in seconds for the toast to be displayed */
@Prop() duration: 3 | 5 | 10;

/** When set, specifies that the toast is open */
@Prop({ reflect: true }) open: boolean;

/** Specifies the context of the component as it applies to the visual design and whether it inverts when light/dark mode is toggled. Default behavior is "light-inverts" and does not have to be specified. */
@Prop({ reflect: true }) context: "light-inverts" | "light-always" | "dark-inverts" | "dark-always";

/** Supports adding inline styles as an object */
@Prop() sx: any = {};

@Watch('open')
watchOpenHandler(newValue: boolean){
console.log('watchOpenHandler check');
if(!newValue) {
console.log('dismiss toast!');
}
}

componentWillLoad() {
if (typeof this.sx == 'string') {
this.sx = JSON.parse(this.sx) || {};
}
setCSSProps(this.host, {
...this.sx,
});
}

render() {

if(this.open && this.duration){
setTimeout(() => { this.open = false }, this.duration * 1000)
}

return (
<Host>
<div class='cbp-toast-sidebar'>
<slot name='cbp-toast-icon'></slot>
</div>
<div class='cbp-toast-container'>
<div class='cbp-toast-title'>
<slot name='cbp-toast-title'></slot>
</div>
<div class='cbp-toast-content'>
<slot></slot>
</div>
<div class='cbp-toast-button-bar'>
<slot name='cbp-toast-buttons'></slot>
</div>
</div>
</Host>
);
}
}

0 comments on commit 4821878

Please sign in to comment.