Use toast() method instead of prop

This commit is contained in:
Cory LaViska
2020-09-17 16:27:11 -04:00
parent 5b4424143b
commit af0ade31fd
5 changed files with 152 additions and 158 deletions

View File

@@ -85,118 +85,15 @@ Icons are optional. Simply omit the `icon` slot if you don't want them.
</sl-alert>
```
### Toast Notifications
When the `toast` prop is used, the alert will be displayed as a toast notification. To facilitate this, the alert is appended to the toast stack the first time it is shown and removed from the DOM when dismissed. By storing a reference to the alert, you can use it again later as shown in this example.
```html preview
<div class="alert-toast">
<sl-button type="primary">Primary</sl-button>
<sl-button type="success">Success</sl-button>
<sl-button type="info">Info</sl-button>
<sl-button type="warning">Warning</sl-button>
<sl-button type="danger">Danger</sl-button>
<sl-alert type="primary" toast duration="3000" closable>
<sl-icon slot="icon" name="info-circle"></sl-icon>
<strong>This is super informative</strong><br>
You can tell by how pretty the alert is.
</sl-alert>
<sl-alert type="success" toast duration="3000" closable>
<sl-icon slot="icon" name="check2-circle"></sl-icon>
<strong>Your changes have been saved</strong><br>
You can safely exit the app now.
</sl-alert>
<sl-alert type="info" toast duration="3000" closable>
<sl-icon slot="icon" name="gear"></sl-icon>
<strong>Your settings have been updated</strong><br>
Settings will take affect on next login.
</sl-alert>
<sl-alert type="warning" toast duration="3000" closable>
<sl-icon slot="icon" name="exclamation-triangle"></sl-icon>
<strong>Your session has ended</strong><br>
Please login again to continue.
</sl-alert>
<sl-alert type="danger" toast duration="3000" closable>
<sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
<strong>Your account has been deleted</strong><br>
We're very sorry to see you go!
</sl-alert>
</div>
<script>
const container = document.querySelector('.alert-toast');
['primary', 'success', 'info', 'warning', 'danger'].map(type => {
const button = container.querySelector(`sl-button[type="${type}"]`);
const alert = container.querySelector(`sl-alert[type="${type}"]`);
button.addEventListener('click', () => alert.show());
});
</script>
```
The toast stack is a fixed position singleton element created and managed internally by the alert component. It will be added and removed from the DOM as needed when toast alerts are shown. By default, the toast stack is positioned at the top-right of the viewport. When more than one alert is visible, they will stack vertically.
You can change the toast stack's position and behavior in your stylesheet. To make toasts appear at the top-left of the viewport, for example, add the following to your stylesheet.
```css
.sl-toast-stack {
left: 0;
right: auto;
}
```
?> By design, toasts cannot be shown in more than one stack. That would be distracting and confusing to users, which makes for a poor experience.
### Creating Toasts Dynamically
Toast alerts can be created declaratively, but you can create them imperatively as well. Just make sure to append them to the DOM before calling `show()`.
```html preview
<div class="alert-toast-dynamic">
<sl-button type="primary">Create New Toast</sl-button>
</div>
<script>
const container = document.querySelector('.alert-toast-dynamic');
const button = container.querySelector('sl-button');
let count = 0;
function createAlert(message, duration) {
return Object.assign(document.createElement('sl-alert'), {
type: 'primary',
toast: true,
duration: duration,
closable: true,
innerHTML: `
<sl-icon slot="icon" name="info-circle"></sl-icon>
${message}
`
});
}
button.addEventListener('click', () => {
const alert = createAlert(`This is alert #${++count}`, 3000);
document.body.append(alert);
alert.show();
});
</script>
```
### Duration
Set the `duration` prop to automatically hide an alert after a period of time. This is useful for alerts that don't require user acknowledgement and works especially well with the `toast` prop.
Set the `duration` prop to automatically hide an alert after a period of time. This is useful for alerts that don't require acknowledgement.
```html preview
<div class="alert-duration">
<sl-button type="primary">Show Alert</sl-button>
<sl-alert type="primary" toast duration="3000" closable>
<sl-alert type="primary" duration="3000" closable style="margin-top: var(--sl-spacing-medium);">
<sl-icon slot="icon" name="info-circle"></sl-icon>
This alert will automatically hide itself after three seconds, unless you interact with it.
</sl-alert>
@@ -211,4 +108,114 @@ Set the `duration` prop to automatically hide an alert after a period of time. T
</script>
```
### Toast Notifications
To display an alert as a toast notification, call its `toast()` method. This will move the alert out of its position in the DOM and into the [toast stack](#toast-stack) where it will be shown. Once dismissed, it will be removed from the DOM completely. To reuse an alert, store a reference to it and call `toast()` again the next time you want it to appear.
You should always use the `closable` prop so users can dismiss the notification. It's also common to set a reasonable `duration` when the notification doesn't require acknowledgement.
```html preview
<div class="alert-toast">
<sl-button type="primary">Primary</sl-button>
<sl-button type="success">Success</sl-button>
<sl-button type="info">Info</sl-button>
<sl-button type="warning">Warning</sl-button>
<sl-button type="danger">Danger</sl-button>
<sl-alert type="primary" duration="3000" closable>
<sl-icon slot="icon" name="info-circle"></sl-icon>
<strong>This is super informative</strong><br>
You can tell by how pretty the alert is.
</sl-alert>
<sl-alert type="success" duration="3000" closable>
<sl-icon slot="icon" name="check2-circle"></sl-icon>
<strong>Your changes have been saved</strong><br>
You can safely exit the app now.
</sl-alert>
<sl-alert type="info" duration="3000" closable>
<sl-icon slot="icon" name="gear"></sl-icon>
<strong>Your settings have been updated</strong><br>
Settings will take affect on next login.
</sl-alert>
<sl-alert type="warning" duration="3000" closable>
<sl-icon slot="icon" name="exclamation-triangle"></sl-icon>
<strong>Your session has ended</strong><br>
Please login again to continue.
</sl-alert>
<sl-alert type="danger" duration="3000" closable>
<sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
<strong>Your account has been deleted</strong><br>
We're very sorry to see you go!
</sl-alert>
</div>
<script>
const container = document.querySelector('.alert-toast');
['primary', 'success', 'info', 'warning', 'danger'].map(type => {
const button = container.querySelector(`sl-button[type="${type}"]`);
const alert = container.querySelector(`sl-alert[type="${type}"]`);
button.addEventListener('click', () => alert.toast());
});
</script>
```
For convenience, you can create a utility that emits toast notifications with a single function call. To do this, generate the alert dynamically, append it to the body, and call its `toast()` method as shown in the example below.
```html preview
<div class="alert-toast-wrapper">
<sl-button type="primary">Create Toast</sl-button>
</div>
<script>
const container = document.querySelector('.alert-toast-wrapper');
const button = container.querySelector('sl-button');
let count = 0;
function escapeHtml(html) {
return document.createElement('div').appendChild(document.createTextNode(html)).parentNode.innerHTML;
}
// Custom function to emit toast notifications on the fly
function notify(message, type = 'primary', icon = 'info-circle', duration = 3000) {
const alert = Object.assign(document.createElement('sl-alert'), {
type: type,
closable: true,
duration: duration,
innerHTML: `
<sl-icon name="${icon}" slot="icon"></sl-icon>
${escapeHtml(message)}
`
});
document.body.append(alert);
return alert.toast();
}
button.addEventListener('click', () => {
notify(`This is custom toast #${++count}`);
});
</script>
```
### Toast Stack
The toast stack is a fixed position singleton element created and managed by the alert component. It will be added and removed from the DOM as needed when toast alerts are shown. By default, the toast stack is positioned at the top-right of the viewport. When more than one alert is visible, they will stack vertically.
You can change the toast stack's position and behavior in your stylesheet. To make toasts appear at the top-left of the viewport, for example, add the following CSS.
```css
.sl-toast-stack {
left: 0;
right: auto;
}
```
?> By design, toasts cannot be shown in more than one stack. This makes for a poor user experience, as it distracts and confuses users.
[component-metadata:sl-alert]

8
src/components.d.ts vendored
View File

@@ -28,9 +28,9 @@ export namespace Components {
*/
"show": () => Promise<void>;
/**
* When true, the alert will be shown as a toast notification. To facilitate this, the alert is appended to the toast stack the first time it is shown and removed from the DOM when dismissed.
* Displays the alert as a toast notification. This will move the alert out of its position in the DOM and, when dismissed, it will be removed from the DOM completely. By storing a reference to the alert, you can reuse it by calling this method again. The returned promise resolves when the alert is hidden.
*/
"toast": boolean;
"toast": () => Promise<unknown>;
/**
* The type of alert.
*/
@@ -1450,10 +1450,6 @@ declare namespace LocalJSX {
* Indicates whether or not the alert is open. You can use this in lieu of the show/hide methods.
*/
"open"?: boolean;
/**
* When true, the alert will be shown as a toast notification. To facilitate this, the alert is appended to the toast stack the first time it is shown and removed from the DOM when dismissed.
*/
"toast"?: boolean;
/**
* The type of alert.
*/

View File

@@ -3,7 +3,13 @@
top: 0;
right: 0;
z-index: var(--sl-z-index-toast);
width: 28rem;
max-width: 100%;
max-height: 100%;
overflow: auto;
sl-alert {
--box-shadow: var(--sl-shadow-large);
margin: var(--sl-spacing-medium);
}
}

View File

@@ -2,13 +2,9 @@
/**
* @prop --box-shadow: The alert's box shadow.
* @prop --toast-spacing: The spacing to use when the alert is shown as a toast notification.
* @prop --toast-width: The width of the alert when shown as a toast notification.
*/
:host {
--box-shadow: none;
--toast-spacing: var(--sl-spacing-medium);
--toast-width: 28rem;
display: block;
@@ -36,13 +32,6 @@
transition: var(--sl-transition-medium) opacity ease, var(--sl-transition-medium) transform ease;
}
.alert--toast {
width: var(--toast-width);
max-width: calc(100% - var(--toast-spacing) * 2);
box-shadow: var(--sl-shadow-large);
margin: var(--toast-spacing);
}
.alert--open {
opacity: 1;
transform: scale(1);

View File

@@ -36,12 +36,6 @@ export class Alert {
/** The type of alert. */
@Prop({ reflect: true }) type: 'primary' | 'success' | 'info' | 'warning' | 'danger' = 'primary';
/**
* When true, the alert will be shown as a toast notification. To facilitate this, the alert is appended to the toast
* stack the first time it is shown and removed from the DOM when dismissed.
*/
@Prop({ reflect: true }) toast = false;
/**
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with the
* alert before it closes (e.g. moves the mouse over it), the duration will restart.
@@ -91,16 +85,9 @@ export class Alert {
return;
}
if (this.toast) {
this.appendToStack();
}
const slShow = this.slShow.emit();
if (slShow.defaultPrevented) {
this.open = false;
if (this.toast) {
this.removeFromStack();
}
return;
}
@@ -133,6 +120,38 @@ export class Alert {
this.open = false;
}
/**
* Displays the alert as a toast notification. This will move the alert out of its position in the DOM and, when
* dismissed, it will be removed from the DOM completely. By storing a reference to the alert, you can reuse it by
* calling this method again. The returned promise resolves when the alert is hidden.
*/
@Method()
async toast() {
return new Promise(resolve => {
if (!stack.parentElement) {
document.body.append(stack);
}
stack.clientWidth; // force a reflow
stack.append(this.host);
this.show();
this.host.addEventListener(
'slAfterHide',
() => {
this.host.remove();
resolve();
// Remove the stack from the DOM when there are no more alerts
if (stack.querySelector('sl-alert') === null) {
stack.remove();
}
},
{ once: true }
);
});
}
handleCloseClick() {
this.hide();
}
@@ -148,28 +167,6 @@ export class Alert {
if (event.propertyName === 'opacity' && target.classList.contains('alert')) {
this.host.hidden = !this.open;
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
if (this.toast && !this.open) {
this.removeFromStack();
}
}
}
appendToStack() {
if (!stack.parentElement) {
document.body.append(stack);
}
stack.clientWidth; // force a reflow
stack.append(this.host);
}
removeFromStack() {
this.host.remove();
// Remove the stack from the DOM when there are no more alerts
if (stack.querySelector('sl-alert') === null) {
stack.remove();
}
}
@@ -190,7 +187,6 @@ export class Alert {
alert: true,
'alert--open': this.open,
'alert--closable': this.closable,
'alert--toast': this.toast,
'alert--primary': this.type === 'primary',
'alert--success': this.type === 'success',
'alert--info': this.type === 'info',