Skip to content

Commit 5d77517

Browse files
committed
Toast Messages SDC + Web Component
1 parent 27c5902 commit 5d77517

8 files changed

Lines changed: 451 additions & 8 deletions

File tree

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,15 @@ Fundamental building blocks that cannot be broken down further.
8585
| [Back To Top](./components/01-atoms/back-to-top) | experimental | **SDC + Web Component** |
8686

8787
### Molecules
88-
| Component | Status | Description |
89-
|:-------------------------------------------------------|:---------------|:----------------------------------------------------|
90-
| [Accordion](./components/02-molecules/accordion) | stable | **SDC** - collapsible content sections |
91-
| [Article card](./components/02-molecules/article-card) | experimental | **SDC** - publication card with image |
92-
| [Tabs](./components/02-molecules/tabs) | experimental | **SDC** - accessible tabs |
93-
| [Pagination](./components/02-molecules/pagination) | stable | **SDC** - pager component |
94-
| [Countdown](./components/02-molecules/countdown) | experimental | **SDC + Web Component** - countdown |
95-
| [Stat Card](./components/02-molecules/stat-card) | experimental | **SDC + Web Component** - card with animated number |
88+
| Component | Status | Description |
89+
|:-----------------------------------------------------------|:--------------|:----------------------------------------------------|
90+
| [Accordion](./components/02-molecules/accordion) | stable | **SDC** - collapsible content sections |
91+
| [Article card](./components/02-molecules/article-card) | experimental | **SDC** - publication card with image |
92+
| [Tabs](./components/02-molecules/tabs) | experimental | **SDC** - accessible tabs |
93+
| [Pagination](./components/02-molecules/pagination) | stable | **SDC** - pager component |
94+
| [Countdown](./components/02-molecules/countdown) | experimental | **SDC + Web Component** - countdown |
95+
| [Stat Card](./components/02-molecules/stat-card) | experimental | **SDC + Web Component** - card with animated number |
96+
| [Toast Messages](./components/02-molecules/toast-messages) | experimental | **SDC + Web Component** - notifications |
9697

9798
### Organisms
9899
| Component | Status | Description |
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Toast Messages (SDC)
2+
3+
A premium, accessible (a11y) notification system designed for the Drupal SDC (Single Directory Components) ecosystem. This component bridges server-side Drupal system messages with a dynamic client-side JavaScript API, featuring a synchronized progress bar and hover-control.
4+
5+
## Features
6+
7+
* **Native Drupal Integration:** Seamlessly replaces the standard `status-messages.html.twig`.
8+
* **Smart Dismissal Logic:**
9+
* `Status` & `Warning`: Auto-dismiss based on duration.
10+
* `Error`: Persistent until manually closed (WCAG compliance).
11+
* **Interactive Hover State:**
12+
* **Pause:** Hovering over a toast pauses the countdown.
13+
* **Reset:** Moving the mouse away restarts the timer and animation from the beginning.
14+
* **Visual Feedback:** A CSS-driven progress bar synchronized with the JavaScript timer.
15+
* **Global JS API:** Trigger notifications from any external script via Custom Events.
16+
17+
18+
## Drupal Implementation
19+
20+
To override the default Drupal system messages, place this in your theme's `templates/layout/status-messages.html.twig`:
21+
22+
```twig
23+
{% if message_list is not empty %}
24+
{{ include('wudo:toast-messages', {
25+
message_list: message_list,
26+
position: 'top-right',
27+
duration: 5000
28+
}, with_context = false) }}
29+
{% endif %}
30+
```
31+
32+
## JavaScript API
33+
34+
Trigger a notification from your custom JS (e.g., AJAX success, form validation) using the global `wudo-toast` event:
35+
36+
```javascript
37+
window.dispatchEvent(new CustomEvent('wudo-toast', {
38+
detail: {
39+
message: 'Profile updated successfully!',
40+
type: 'status' // Options: status, warning, error
41+
}
42+
}));
43+
```
44+
45+
## Configuration (Props)
46+
47+
| Prop | Type | Default | Description |
48+
| :--- | :--- | :--- | :--- |
49+
| `position` | `string` | `top-right` | UI corner: `top-right`, `top-left`, `bottom-right`, `bottom-left`. |
50+
| `duration` | `int` | `5000` | Auto-dismiss delay in milliseconds. |
51+
52+
---
53+
54+
## Accessibility (a11y)
55+
56+
* **Screen Readers:** Uses `role="alert"` for immediate announcement of new messages.
57+
* **Interaction:** The "Close" button features a translatable `aria-label`.
58+
* **Persistence:** Critical `error` messages disable the auto-dismiss timer to ensure users have sufficient time to perceive and understand the failure.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'$schema': 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json'
2+
3+
name: Toast Messages
4+
status: experimental
5+
group: Molecules
6+
7+
props:
8+
type: object
9+
properties:
10+
position:
11+
type: string
12+
title: Position
13+
enum: [top-right, top-left, bottom-right, bottom-left]
14+
default: top-right
15+
duration:
16+
type: integer
17+
title: Auto-hide duration (ms)
18+
default: 5000
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
.toast-wrapper {
2+
--toast-bg: var(--color-white, #ffffff);
3+
--toast-accent: #ccc;
4+
--toast-color: var(--color-text-primary, #222222);
5+
--progress-bg: var(--color-neutral-30, #bdbdbd);
6+
position: fixed;
7+
z-index: 9999;
8+
pointer-events: none;
9+
}
10+
.toast-wrapper[data-position=top-right] {
11+
top: 2rem;
12+
right: 2rem;
13+
}
14+
.toast-wrapper[data-position=top-left] {
15+
top: 2rem;
16+
left: 2rem;
17+
}
18+
.toast-wrapper[data-position=bottom-right] {
19+
bottom: 2rem;
20+
right: 2rem;
21+
}
22+
.toast-wrapper .toast-container {
23+
display: flex;
24+
flex-direction: column;
25+
gap: 1rem;
26+
}
27+
.toast-wrapper .toast-item {
28+
pointer-events: auto;
29+
min-width: 300px;
30+
max-width: 450px;
31+
padding: 1rem 0.5rem 1rem 1rem;
32+
color: var(--toast-color);
33+
background: var(--toast-bg);
34+
border-left: 5px solid var(--toast-accent);
35+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
36+
display: flex;
37+
justify-content: space-between;
38+
align-items: center;
39+
gap: 0.5rem;
40+
animation: toast-in 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards;
41+
position: relative;
42+
overflow: hidden;
43+
z-index: 0;
44+
}
45+
.toast-wrapper .toast-item.is-hiding {
46+
animation: toast-out 0.3s ease forwards !important;
47+
animation-play-state: running !important;
48+
pointer-events: none;
49+
}
50+
.toast-wrapper .toast-item__progress {
51+
position: absolute;
52+
bottom: 0;
53+
left: 0;
54+
height: 3px;
55+
width: 100%;
56+
background: var(--progress-bg);
57+
transform-origin: left;
58+
transform: scaleX(0);
59+
z-index: 2;
60+
}
61+
.toast-wrapper .toast-item.is-active .toast-item__progress {
62+
animation: toast-progress linear forwards;
63+
animation-duration: var(--toast-lifetime, 5000ms);
64+
}
65+
.toast-wrapper .toast-item--status {
66+
--toast-bg: var(--color-semantic-success-10, #e8fcf1);
67+
--toast-accent: var(--color-semantic-success-60, #00632b);
68+
--toast-color: var(--color-semantic-success-60, #00632b);
69+
--progress-bg: var(--color-semantic-success-40, #419e6a);
70+
}
71+
.toast-wrapper .toast-item--warning {
72+
--toast-bg: var(--color-semantic-warning-10, #f8ede6);
73+
--toast-accent: var(--color-semantic-warning-60, #bc4b00);
74+
--toast-color: var(--color-semantic-warning-60, #bc4b00);
75+
--progress-bg: var(--color-semantic-warning-40, #ef9117);
76+
}
77+
.toast-wrapper .toast-item--error {
78+
--toast-bg: var(--color-semantic-error-10, #ffebf0);
79+
--toast-accent: var(--color-semantic-error-60, #b01212);
80+
--toast-color: var(--color-semantic-error-60, #b01212);
81+
--progress-bg: var(--color-semantic-error-40, #d83232);
82+
}
83+
.toast-wrapper .toast-item:hover:not(.is-hiding) {
84+
animation-play-state: paused;
85+
}
86+
.toast-wrapper .toast-item:hover:not(.is-hiding) .toast-item__progress {
87+
animation-play-state: paused;
88+
}
89+
90+
@keyframes toast-in {
91+
from {
92+
transform: translateX(100%);
93+
opacity: 0;
94+
}
95+
to {
96+
transform: translateX(0);
97+
opacity: 1;
98+
}
99+
}
100+
@keyframes toast-out {
101+
to {
102+
transform: translateX(100%);
103+
opacity: 0;
104+
}
105+
}
106+
@keyframes toast-progress {
107+
from {
108+
transform: scaleX(1);
109+
}
110+
to {
111+
transform: scaleX(0);
112+
}
113+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
class ToastMessages extends HTMLElement {
2+
connectedCallback() {
3+
this.container = this.querySelector('[data-toast-container]');
4+
this.template = this.querySelector('#toast-template');
5+
this.duration = parseInt(this.dataset.duration) || 5000;
6+
7+
// Initialize existing toasts
8+
this.container.querySelectorAll('.toast-item').forEach(toast => this.initToast(toast));
9+
10+
// Listen for custom toast events
11+
window.addEventListener('wudo-toast', (e) => {
12+
this.addToast(e.detail.message, e.detail.type || 'status');
13+
});
14+
}
15+
16+
addToast(message, type = 'status') {
17+
if (!this.template) return;
18+
19+
// Create toast from template
20+
const clone = this.template.content.cloneNode(true);
21+
const toast = clone.querySelector('.toast-item');
22+
23+
toast.classList.add(`toast-item--${type}`);
24+
toast.querySelector('.toast-item__content').textContent = message;
25+
26+
this.container.appendChild(toast);
27+
this.initToast(toast);
28+
}
29+
30+
initToast(toast) {
31+
const isError = toast.classList.contains('toast-item--error');
32+
let timer;
33+
34+
const handleClose = (e) => {
35+
if (e) e.stopPropagation();
36+
clearTimeout(timer);
37+
this.closeToast(toast);
38+
};
39+
40+
const closeBtn = toast.querySelector('.toast-item__close');
41+
if (closeBtn) {
42+
closeBtn.addEventListener('click', handleClose);
43+
}
44+
45+
if (isError) {
46+
toast.classList.add('is-error-permanent');
47+
return;
48+
}
49+
50+
toast.style.setProperty('--toast-lifetime', `${this.duration}ms`);
51+
52+
const startTimeout = () => {
53+
timer = setTimeout(handleClose, this.duration);
54+
};
55+
56+
const resetToast = () => {
57+
clearTimeout(timer);
58+
toast.classList.remove('is-active');
59+
void toast.offsetWidth;
60+
toast.classList.add('is-active');
61+
startTimeout();
62+
};
63+
64+
requestAnimationFrame(() => {
65+
toast.classList.add('is-active');
66+
startTimeout();
67+
});
68+
69+
toast.addEventListener('mouseenter', () => clearTimeout(timer));
70+
toast.addEventListener('mouseleave', resetToast);
71+
}
72+
73+
closeToast(toast) {
74+
if (toast._timer) clearTimeout(toast._timer);
75+
clearTimeout(this._hoverTimer);
76+
toast.classList.add('is-hiding');
77+
toast.addEventListener('animationend', (e) => {
78+
if (e.animationName === 'toast-out') {
79+
toast.remove();
80+
}
81+
}, { once: true });
82+
}
83+
}
84+
85+
if (!customElements.get('toast-messages')) {
86+
customElements.define('toast-messages', ToastMessages);
87+
}

0 commit comments

Comments
 (0)