diff --git a/docs/components/alert.md b/docs/components/alert.md
index d89679bbf..662dc4a2f 100644
--- a/docs/components/alert.md
+++ b/docs/components/alert.md
@@ -4,7 +4,71 @@
Alerts are used to display important messages.
-Alerts are designed to be shown dynamically, so you need to include the `open` attribute to display them.
+Alerts are designed to be shown dynamically, so you must include the `open` attribute to display them.
+
+
+
+
+
+```html preview
+
+ Primary
+ Success
+ Info
+ Warning
+ Danger
+
+
+
+ This is super informative
+ You can tell by how pretty the alert is.
+
+
+
+
+ Your changes have been saved
+ You can safely exit the app now.
+
+
+
+
+ Your settings have been updated
+ Some settings will take affect the next time you log in.
+
+
+
+
+ Your session has ended
+ Please login again to continue.
+
+
+
+
+ Your account has been deleted
+ We're very sorry to see you go!
+
+
+
+
+```
+
+
+
+
+
+
+
+
+
```html preview
@@ -46,16 +110,16 @@ Set the `type` attribute to change the alert's type.
- This will end your session
- You will be logged out until you log in again.
+ Your session has ended
+ Please login again to continue.
- Delete this file?
- This is permanent, which means forever!
+ Your account has been deleted
+ We're very sorry to see you go!
```
diff --git a/docs/tokens/z-index.md b/docs/tokens/z-index.md
index 41d6f3467..c596a5d73 100644
--- a/docs/tokens/z-index.md
+++ b/docs/tokens/z-index.md
@@ -7,4 +7,5 @@ Z-indexes are used to stack components in a logical manner.
| `--sl-z-index-drawer` | 700 |
| `--sl-z-index-dialog` | 800 |
| `--sl-z-index-dropdown` | 900 |
+| `--sl-z-index-alert-group` | 950 |
| `--sl-z-index-tooltip` | 1000 |
diff --git a/src/components.d.ts b/src/components.d.ts
index c108421be..940bf8459 100644
--- a/src/components.d.ts
+++ b/src/components.d.ts
@@ -11,6 +11,10 @@ export namespace Components {
* Set to true to make the alert closable.
*/
"closable": boolean;
+ /**
+ * The length of time, in milliseconds, the alert will show before closing itself.
+ */
+ "duration": number;
/**
* Hides the alert
*/
@@ -19,6 +23,10 @@ export namespace Components {
* Indicates whether or not the alert is open. You can use this in lieu of the show/hide methods.
*/
"open": boolean;
+ /**
+ * Determines how the alert will be shown. If this is anything other than `inline`, the alert will be shown in a stack as a "toast" notification. When the alert is shown as a notification, it will be hoisted to a stack and removed from the DOM when hidden. (You can reuse alerts that have been removed by storing a reference to the element.)
+ */
+ "placement": 'inline' | 'top-start' | 'top' | 'top-end' | 'bottom-start' | 'bottom' | 'bottom-end';
/**
* Shows the alert.
*/
@@ -1418,6 +1426,10 @@ declare namespace LocalJSX {
* Set to true to make the alert closable.
*/
"closable"?: boolean;
+ /**
+ * The length of time, in milliseconds, the alert will show before closing itself.
+ */
+ "duration"?: number;
/**
* Emitted after the alert closes and all transitions are complete.
*/
@@ -1438,6 +1450,10 @@ declare namespace LocalJSX {
* Indicates whether or not the alert is open. You can use this in lieu of the show/hide methods.
*/
"open"?: boolean;
+ /**
+ * Determines how the alert will be shown. If this is anything other than `inline`, the alert will be shown in a stack as a "toast" notification. When the alert is shown as a notification, it will be hoisted to a stack and removed from the DOM when hidden. (You can reuse alerts that have been removed by storing a reference to the element.)
+ */
+ "placement"?: 'inline' | 'top-start' | 'top' | 'top-end' | 'bottom-start' | 'bottom' | 'bottom-end';
/**
* The type of alert.
*/
diff --git a/src/components/alert/alert.light-dom.scss b/src/components/alert/alert.light-dom.scss
new file mode 100644
index 000000000..ec1238c10
--- /dev/null
+++ b/src/components/alert/alert.light-dom.scss
@@ -0,0 +1,49 @@
+:root {
+ --width: 28rem;
+ --spacing: var(--sl-spacing-medium);
+}
+
+.sl-alert-stack {
+ position: fixed;
+ z-index: var(--sl-z-index-toast);
+ width: var(--width);
+ max-width: 100%;
+ max-height: 100%;
+ overflow: auto;
+ padding: 0 var(--spacing);
+
+ sl-alert {
+ --box-shadow: var(--sl-shadow-large);
+ margin: var(--spacing) 0;
+ }
+}
+
+.sl-alert-stack[data-placement='top-start'] {
+ top: 0;
+ left: 0;
+}
+
+.sl-alert-stack[data-placement='top'] {
+ top: 0;
+ left: calc(50% - var(--width) / 2);
+}
+
+.sl-alert-stack[data-placement='top-end'] {
+ top: 0;
+ right: 0;
+}
+
+.sl-alert-stack[data-placement='bottom-start'] {
+ bottom: 0;
+ left: 0;
+}
+
+.sl-alert-stack[data-placement='bottom'] {
+ bottom: 0;
+ left: calc(50% - var(--width) / 2);
+}
+
+.sl-alert-stack[data-placement='bottom-end'] {
+ bottom: 0;
+ right: 0;
+}
diff --git a/src/components/alert/alert.scss b/src/components/alert/alert.scss
index 4911db012..d060ef768 100644
--- a/src/components/alert/alert.scss
+++ b/src/components/alert/alert.scss
@@ -1,6 +1,11 @@
@import 'component';
+/**
+ * @prop --box-shadow: The alert's box shadow.
+ */
:host {
+ --box-shadow: none;
+
display: block;
&[hidden] {
@@ -16,17 +21,20 @@
border: solid 1px var(--sl-color-gray-90);
border-top-width: 3px;
border-radius: var(--sl-border-radius-medium);
+ box-shadow: var(--box-shadow);
font-family: var(--sl-font-sans);
font-size: var(--sl-font-size-small);
font-weight: var(--sl-font-weight-normal);
line-height: 1.6;
color: var(--sl-color-gray-30);
opacity: 0;
- transition: var(--sl-transition-medium) opacity ease;
+ transform: scale(0.9);
+ transition: var(--sl-transition-medium) opacity ease, var(--sl-transition-medium) transform ease;
}
.alert--open {
opacity: 1;
+ transform: scale(1);
}
.alert__icon {
@@ -91,5 +99,5 @@
display: flex;
align-items: center;
font-size: var(--sl-font-size-large);
- padding: 0 var(--sl-spacing-medium);
+ padding-right: var(--sl-spacing-medium);
}
diff --git a/src/components/alert/alert.tsx b/src/components/alert/alert.tsx
index 9fcf4b1a0..3e9c87c15 100644
--- a/src/components/alert/alert.tsx
+++ b/src/components/alert/alert.tsx
@@ -13,6 +13,8 @@ import { Component, Element, Event, EventEmitter, Host, Method, Prop, Watch, h }
* @part close-button - The close button.
*/
+const stack = Object.assign(document.createElement('div'), { className: 'sl-alert-stack' });
+
@Component({
tag: 'sl-alert',
styleUrl: 'alert.scss',
@@ -20,6 +22,7 @@ import { Component, Element, Event, EventEmitter, Host, Method, Prop, Watch, h }
})
export class Alert {
alert: HTMLElement;
+ autoHideTimeout: any;
isShowing = false;
@Element() host: HTMLSlAlertElement;
@@ -33,11 +36,31 @@ export class Alert {
/** The type of alert. */
@Prop() type: 'primary' | 'success' | 'info' | 'warning' | 'danger' = 'primary';
+ /**
+ * Determines how the alert will be shown. If this is anything other than `inline`, the alert will be shown in a stack
+ * as a "toast" notification. When the alert is shown as a notification, it will be hoisted to a stack and removed
+ * from the DOM when hidden. (You can reuse alerts that have been removed by storing a reference to the element.)
+ */
+ @Prop() placement: 'inline' | 'top-start' | 'top' | 'top-end' | 'bottom-start' | 'bottom' | 'bottom-end' = 'inline';
+
+ /** The length of time, in milliseconds, the alert will show before closing itself. */
+ @Prop() duration = Infinity;
+
@Watch('open')
handleOpenChange() {
this.open ? this.show() : this.hide();
}
+ @Watch('duration')
+ handleDurationChange() {
+ clearTimeout(this.autoHideTimeout);
+
+ // Restart the timeout if the duration changes and the alert is open
+ if (this.open && this.duration < Infinity) {
+ this.autoHideTimeout = setTimeout(() => this.hide(), this.duration);
+ }
+ }
+
/** Emitted when the alert opens. Calling `event.preventDefault()` will prevent it from being opened. */
@Event() slShow: EventEmitter;
@@ -80,6 +103,14 @@ export class Alert {
this.host.clientWidth; // force a reflow
this.isShowing = true;
this.open = true;
+
+ if (this.placement !== 'inline') {
+ this.appendToStack();
+ }
+
+ if (this.duration < Infinity) {
+ this.autoHideTimeout = setTimeout(() => this.hide(), this.duration);
+ }
}
/** Hides the alert */
@@ -96,6 +127,7 @@ export class Alert {
return;
}
+ clearTimeout(this.autoHideTimeout);
this.isShowing = false;
this.open = false;
}
@@ -110,10 +142,34 @@ export class Alert {
// Ensure we only emit one event when the target element is no longer visible
if (event.propertyName === 'opacity' && target.classList.contains('alert')) {
this.host.hidden = !this.open;
+
+ if (this.placement !== 'inline' && !this.open) {
+ this.removeFromStack();
+ }
+
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
}
}
+ appendToStack() {
+ if (!stack.parentElement) {
+ document.body.append(stack);
+ }
+
+ stack.dataset.placement = this.placement;
+ stack.append(this.host);
+ }
+
+ removeFromStack() {
+ this.host.remove();
+
+ // Remove the stack from the DOM when there are no more alerts
+ const openAlerts = [...stack.querySelectorAll('sl-alert')].filter((el: HTMLSlAlertElement) => el.open === true);
+ if (openAlerts.length === 0) {
+ stack.remove();
+ }
+ }
+
render() {
return (
@@ -145,7 +201,9 @@ export class Alert {
{this.closable && (
-
+
+
+
)}
diff --git a/src/styles/shoelace.scss b/src/styles/shoelace.scss
index 1e0fa5c48..b00b8e9a7 100644
--- a/src/styles/shoelace.scss
+++ b/src/styles/shoelace.scss
@@ -249,6 +249,7 @@
--sl-z-index-drawer: 700;
--sl-z-index-dialog: 800;
--sl-z-index-dropdown: 900;
+ --sl-z-index-toast: 950;
--sl-z-index-tooltip: 1000;
}
@@ -268,4 +269,5 @@
// Component light DOM styles - only follow this pattern when absolutely necessary!
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+@import '../components/alert/alert.light-dom';
@import '../components/button-group/button-group.light-dom';