Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Toast component #217

Merged
merged 6 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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');
149 changes: 149 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,149 @@
/**
* @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%, 500px);
bagrub marked this conversation as resolved.
Show resolved Hide resolved
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: 5px;

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

&:not([open]){
bagrub marked this conversation as resolved.
Show resolved Hide resolved
animation: dismiss 1s ease-in-out forwards;
}

.cbp-toast-sidebar{
display: flex;
padding: var(--cbp-space-4x);
color: var(--cbp-toast-color-icon-sidebar);
background-color: var(--cbp-toast-color-bg-sidebar);
border-top-left-radius: 5px;
bagrub marked this conversation as resolved.
Show resolved Hide resolved
bagrub marked this conversation as resolved.
Show resolved Hide resolved
border-bottom-left-radius: 5px;
}

.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{
width: max-content;
padding: var(--cbp-space-3x);
}

.cbp-toast-content{
font-weight: var(--cbp-font-weight-regular);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is all slotted content, you shouldn't need to explicitly define values that are the default text values.

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);
bagrub marked this conversation as resolved.
Show resolved Hide resolved
--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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to refine this. You should never use toast for form validation. Confirmation misspelled.

like error messages to also be populated elsewhere in app so that they don't get 'lost'.

## Functional Requirements

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to explicitly list props (more maintenance tying it to the implementation). Just defined things it does.

* 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a user interaction. User interaction would be dismissing a toast or acting on other slotted links/buttons.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant to mention this previously, but toast should use aria-live. They are typically out of the flow of the page and would take forever to keyboard navigate to. You don't want to send focus and interrupt what the user is doing, but screen readers should read the toast automoatically.


### Additional Notes and Considerations
114 changes: 114 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,114 @@
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, content, buttons, duration, color, context, sx }) => {
return `
<cbp-toast
${open ? `open=${open}` : ''}
bagrub marked this conversation as resolved.
Show resolved Hide resolved
color=${color}
duration=${duration}
bagrub marked this conversation as resolved.
Show resolved Hide resolved
${icon ? `icon=${icon}` : ''}
${context && context != 'light-inverts' ? `context=${context}` : ''}
${sx ? `sx=${JSON.stringify(sx)}` : ''}
>
<div slot="cbp-toast-title"> Test Toast Title</div>
${content}
<div slot="cbp-toast-buttons">${buttons}</div>
</cbp-toast>
`;
};

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

Toast.args = {
open: true,
icon: 'user',
content: 'Notification Description - A rule you are following just fired.',
bagrub marked this conversation as resolved.
Show resolved Hide resolved
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, 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-title"> Test Toast 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-title"> Test Toast 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-title"> Test Toast 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-title"> Test Toast Title</div>
${content}
<div slot="cbp-toast-buttons">${buttons}</div>
</cbp-toast>
`;
};

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

MultipleToast.args = {
open: true,
icon: 'user',
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>'
}
75 changes: 75 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,75 @@
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 icon loaded into the sidebar */
@Prop() icon: string = 'user';

/** 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 = {};
bagrub marked this conversation as resolved.
Show resolved Hide resolved

@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'>
<cbp-icon
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This locks them in to our icons. I would just provide a named slot so they can slot an icon component or include an external icon if needed.

size='2rem'
name={this.icon}
/>
bagrub marked this conversation as resolved.
Show resolved Hide resolved
</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>
);
}
}