diff --git a/.gitignore b/.gitignore
index 250a7c02..83c9fad4 100755
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,4 @@ report/*
.idea
# auto-gen PHP files/logs
-php_errors.log
+php_errors.log
\ No newline at end of file
diff --git a/content/body/toast.php b/content/body/toast.php
new file mode 100644
index 00000000..0896e987
--- /dev/null
+++ b/content/body/toast.php
@@ -0,0 +1,166 @@
+
+ Toast notifications are non-blocking alerts that provide feedback or information to users. They are hidden by default and become visible when triggered by a user action or system event. Toast notifications can be styled, positioned, and customized to suit various needs. This demo showcases an accessible implementation of toast notifications.
+
+
+Shopping Site Toast Notifications Example
+
+
+
+
+
+
+
+Accessible Toast Notifications Example
+
+ true,
+ "comment" => "Recommended for new and existing work.",
+]); ?>
+ true]); ?>
+
+
+ In order to make toast notifications accessible, there are a few considerations:
+
+
+ Toasts should be announced by screen readers when they appear.
+ Keyboard users should be able to navigate to and dismiss toasts.
+ Toasts should support different levels of severity (normal, error, warning, success), each with a unique color for visual distinction.
+
+
+ Our implementation ensures that toasts are fully accessible and follow best practices for ARIA and keyboard interaction.
+ When a toast appears, it is announced to screen readers. Toasts will remain visible until manually dismissed by the user.
+ Additionally, all toasts are stored in a toast rack for future reference.
+
+
+
+
+
+ Toast Message:
+
+
+
+
+
+
+ Max Visible:
+
+
+
+ Show Toast
+ Toggle Toast Rack
+
+
+
+
+
+
+
+
+
+
diff --git a/content/bottom/toast.php b/content/bottom/toast.php
new file mode 100644
index 00000000..66b6cebb
--- /dev/null
+++ b/content/bottom/toast.php
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/content/head/toast.php b/content/head/toast.php
new file mode 100644
index 00000000..0dfde4c8
--- /dev/null
+++ b/content/head/toast.php
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/css/app.css b/css/app.css
new file mode 100644
index 00000000..135c728e
--- /dev/null
+++ b/css/app.css
@@ -0,0 +1,59 @@
+body {
+ font-family: Arial, sans-serif;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 20px;
+}
+
+.controls {
+ margin-bottom: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.control-group {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin-bottom: 10px;
+}
+
+.control-group label {
+ font-weight: normal;
+}
+
+.control-group div {
+ display: flex;
+ gap: 10px;
+}
+
+.button-group {
+ display: flex;
+ gap: 10px;
+}
+
+.toast-rack {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: rgba(241, 241, 241, 0.9);
+ border: 1px solid #ddd;
+ padding: 10px;
+ width: 300px;
+ max-height: 400px;
+ overflow-y: auto;
+ display: none;
+ z-index: 9998;
+}
+
+.toast-rack button {
+ display: block;
+ margin-bottom: 10px;
+}
+
+.status {
+ margin-top: 20px;
+ font-size: 16px;
+}
diff --git a/css/toast-demo.css b/css/toast-demo.css
new file mode 100644
index 00000000..e0a0c11c
--- /dev/null
+++ b/css/toast-demo.css
@@ -0,0 +1,18 @@
+.enable-toast__controls {
+ margin-bottom: 1.25rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.625rem;
+}
+.enable-toast__controls__control-group {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 0.625rem;
+}
+.enable-toast__controls__label {
+ font-weight: normal;
+}
+.enable-toast__controls__button-group {
+ display: flex;
+ gap: 0.625rem;
+}
diff --git a/css/toast.css b/css/toast.css
new file mode 100644
index 00000000..57ca2d95
--- /dev/null
+++ b/css/toast.css
@@ -0,0 +1,76 @@
+.enable-toast__container {
+ position: fixed;
+ z-index: 9999;
+ display: flex;
+ transition: all 0.5s ease-in-out;
+}
+.enable-toast__container--bottom-right,
+.enable-toast__container--bottom-left,
+.enable-toast__container--bottom-center {
+ bottom: 20px;
+}
+.enable-toast__container--top-right,
+.enable-toast__container--top-left,
+.enable-toast__container--top-center {
+ top: 20px;
+}
+.enable-toast__container--bottom-right,
+.enable-toast__container--top-right {
+ right: 20px;
+}
+.enable-toast__container--bottom-left,
+.enable-toast__container--top-left {
+ left: 20px;
+}
+.enable-toast__container--top-center,
+.enable-toast__container--bottom-center,
+.enable-toast__container--middle-center {
+ left: 50%;
+ transform: translateX(-50%);
+}
+.enable-toast__container--middle-center {
+ top: 50%;
+ transform: translate(-50%, -50%);
+}
+.enable-toast__toast {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 10px 0;
+ background: #333;
+ color: #fff;
+ padding: 10px;
+ border-radius: 5px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ opacity: 0;
+ transition: opacity 0.5s ease-in-out;
+}
+.enable-toast__toast--visible {
+ opacity: 1;
+}
+.enable-toast__toast--exit {
+ opacity: 0;
+}
+.enable-toast__close-button {
+ background: none;
+ border: none;
+ color: #fff;
+ font-size: 16px;
+ cursor: pointer;
+}
+.enable-toast__close-button:hover {
+ color: #ff0000;
+}
+.enable-toast__rack {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: rgba(241, 241, 241, 0.9);
+ border: 1px solid #ddd;
+ padding: 10px;
+ width: 300px;
+ max-height: 400px;
+ overflow-y: auto;
+ display: none;
+ z-index: 9998;
+}
diff --git a/html_example/toast_example.html b/html_example/toast_example.html
new file mode 100644
index 00000000..2b0fecf5
--- /dev/null
+++ b/html_example/toast_example.html
@@ -0,0 +1,210 @@
+
+
+
+
+
+ Accessible Toast Notifications
+
+
+
+
+
+
+ Toast notifications are non-blocking alerts that provide
+ feedback or information to users. They are hidden by default and
+ become visible when triggered by a user action or system
+ event.
+ Toast notifications can be styled, positioned, and customized to
+ suit various needs. This demo showcases an accessible implementation
+ of toast notifications.
+
+
+ Accessible Toast Notifications Example
+
+
+ In order to make toast notifications accessible, there are a few
+ considerations:
+
+
+
+ Toasts should be announced by screen readers when they appear.
+
+
+ Keyboard users should be able to navigate to and dismiss toasts.
+
+
+ Toasts should support different levels of severity (normal,
+ error, warning, success), each with a unique color for visual
+ distinction.
+
+
+
+ Our implementation ensures that toasts are fully accessible and
+ follow best practices for ARIA and keyboard interaction. When a
+ toast appears, it is announced to screen readers. Toasts can be
+ configured to stay visible for a set amount of time or until
+ manually dismissed by the user. Additionally, all toasts are stored
+ in a toast rack for future reference.
+
+
+
+
+
+ Toast Message:
+
+
+
+
+
+
+ Max Visible:
+
+
+
+ Show Toast
+ Toggle Toast Rack
+
+
+
+
+
+
+
+
+
diff --git a/js/demos/toast.js b/js/demos/toast.js
new file mode 100644
index 00000000..b984269c
--- /dev/null
+++ b/js/demos/toast.js
@@ -0,0 +1,101 @@
+'use strict';
+
+import toastModule from '../modules/toast.js';
+
+const app = new (function () {
+ console.log('toast demo!!!');
+ let toastIndex = 0;
+
+ this.init = function () {
+ this.toast = new toastModule.Toast({
+ position: 'bottom-right',
+ style: 'padding: 10px; border-radius: 5px;',
+ alertArea: true,
+ maxVisible: 2,
+ levels: {
+ normal: { color: '#007bff' },
+ error: { color: '#dc3545' },
+ warning: { color: '#ffc107' },
+ success: { color: '#28a745' },
+ },
+ ariaLive: 'polite', // Default aria-live value
+ });
+
+ this.attachEventListeners();
+ };
+
+ this.attachEventListeners = function () {
+ document
+ .getElementById('showToastButton')
+ .addEventListener('click', this.showToastButtonClickEvent);
+ document
+ .getElementById('clearAllButton')
+ .addEventListener('click', this.clearAllButtonClickEvent);
+ document
+ .getElementById('toggleRackButton')
+ .addEventListener('click', this.toggleRackButtonClickEvent);
+
+ document.querySelectorAll('input[name="position"]').forEach((radio) => {
+ radio.addEventListener('change', this.positionChangeEvent);
+ });
+ };
+
+ this.showToastButtonClickEvent = (e) => {
+ const message = document.getElementById('messageInput').value;
+ const level = document.querySelector(
+ 'input[name="level"]:checked',
+ ).value;
+ const ariaLive = document.querySelector(
+ 'input[name="ariaLive"]:checked',
+ ).value;
+ this.toast.ariaLive = ariaLive; // Set aria-live value
+ toastIndex++;
+ const fullMessage = `Toast ${toastIndex}: ${message}`;
+ try {
+ this.toast.showToast(fullMessage, level);
+ } catch (error) {
+ console.error('Error showing toast:', error);
+ }
+ this.updateStatus();
+ };
+
+ this.clearAllButtonClickEvent = (e) => {
+ try {
+ this.toast.clearAllToasts();
+ } catch (error) {
+ console.error('Error clearing toasts:', error);
+ }
+ this.updateStatus();
+ };
+
+ this.toggleRackButtonClickEvent = (e) => {
+ const rack = document.getElementById('toastRack');
+ if (rack.style.display === 'block') {
+ rack.style.display = 'none';
+ } else {
+ rack.style.display = 'block';
+ }
+ console.log(
+ `Toast rack ${rack.style.display === 'block' ? 'shown' : 'hidden'}`,
+ );
+ };
+
+ this.positionChangeEvent = (e) => {
+ try {
+ this.toast.container.className = `enable-toast enable-toast--${e.target.value}`;
+ } catch (error) {
+ console.error('Error changing toast position:', error);
+ }
+ };
+
+ this.updateStatus = function () {
+ const totalVisible = this.toast.visibleQueue.length;
+ const totalNotifications = this.toast.toastQueue.length;
+ document.getElementById('status').textContent =
+ `Visible: ${totalVisible}, Total: ${totalNotifications}`;
+ document.getElementById('rackContentStatus').textContent =
+ `Toast rack contains ${totalNotifications} toasts.`;
+ };
+})();
+
+export default app;
diff --git a/js/modules/es4/toast.js b/js/modules/es4/toast.js
new file mode 100644
index 00000000..1da066f7
--- /dev/null
+++ b/js/modules/es4/toast.js
@@ -0,0 +1,191 @@
+'use strict'
+
+/*******************************************************************************
+ * toast.js - UI for Accessible Toast Notifications
+ *
+ * Written by Otto Wachter
+ * Part of the Enable accessible component library.
+ * Version 1.x released [Release Date]
+ *
+ * More information about this script available at:
+ * https://www.useragentman.com/enable/toast.php
+ *
+ * Released under the MIT License.
+ ******************************************************************************/
+
+const toastModule = new function() {
+
+ this.init = function() {
+ this.rackContent = document.getElementById('rackContent');
+ this.updateToggleRackButton();
+ };
+
+ this.Toast = class {
+ constructor(options = {}) {
+ this.container = this.createContainer(options.position || 'bottom-right');
+ this.maxVisible = options.maxVisible || 1;
+ this.toastQueue = [];
+ this.visibleQueue = [];
+ this.levels = options.levels || {};
+ this.ariaLive = options.ariaLive || 'polite'; // Default to polite
+ document.body.appendChild(this.container);
+
+ // Initialize the toast rack
+ this.rackContent = document.getElementById('rackContent');
+ this.toggleRackButton = document.getElementById('toggleRackButton');
+ }
+
+ // Create the container for the toasts
+ createContainer(position) {
+ const container = document.createElement('div');
+ container.className = `enable-toast__container enable-toast__container--${position}`;
+ return container;
+ }
+
+ // Show a new toast notification
+ showToast(message, level = 'normal') {
+ const toastData = {
+ message,
+ level,
+ id: Date.now() // unique id for each toast
+ };
+ this.toastQueue.push(toastData);
+ this.visibleQueue.push(toastData);
+ console.log("Toast added to queue and visibleQueue:", toastData);
+ const toastElement = this.createToastElement(toastData);
+ this.container.appendChild(toastElement);
+ toastElement.offsetHeight; // Force reflow to ensure the element is rendered before adding the visible class
+
+ setTimeout(() => {
+ toastElement.classList.add('enable-toast__item--visible');
+ }, 100); // Slight delay to ensure screen readers catch the change
+
+ // Update the toast rack with the new toast
+ this.updateToastRack();
+ this.updateVisibleToasts();
+ this.updateToggleRackButton();
+ }
+
+ // Create the individual toast element (Using enable-toast__item)
+ createToastElement(toastData) {
+ const { message, level, id } = toastData;
+ const toast = document.createElement('div');
+ toast.className = 'enable-toast__item';
+ toast.style.backgroundColor = this.levels[level]?.color || '#333';
+ toast.setAttribute('tabindex', '-1');
+ toast.setAttribute('aria-live', this.ariaLive);
+ toast.setAttribute('role', 'alert');
+ toast.setAttribute('data-id', id);
+
+ const messageSpan = document.createElement('span');
+ messageSpan.textContent = message;
+ toast.appendChild(messageSpan);
+
+ const closeButton = document.createElement('button');
+ closeButton.className = 'enable-toast__close';
+ closeButton.setAttribute('aria-label', 'close alert');
+ closeButton.textContent = '✖';
+ closeButton.addEventListener('click', () => {
+ console.log("Close button clicked for toast:", message);
+ this.dismissToast(toastData);
+ });
+ toast.appendChild(closeButton);
+
+ return toast;
+ }
+
+ // Dismiss a toast notification
+ dismissToast(toastData) {
+ const toastElement = this.container.querySelector(`[data-id="${toastData.id}"]`);
+ if (toastElement) {
+ toastElement.classList.add('enable-toast__item--exit');
+ setTimeout(() => {
+ toastElement.remove();
+ this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id);
+ this.updateVisibleToasts();
+ this.updateToastRack();
+ this.updateToggleRackButton();
+ }, 500); // Match the CSS animation duration
+ }
+ }
+
+ // Update the visible toasts according to the max visible limit
+ updateVisibleToasts() {
+ console.log("Updating visible toasts");
+
+ // Remove excess toasts from the visibleQueue
+ while (this.visibleQueue.length > this.maxVisible) {
+ const toastToRemove = this.visibleQueue.shift();
+ this.dismissToast(toastToRemove);
+ }
+
+ // Ensure only maxVisible toasts are displayed
+ this.visibleQueue.forEach((toast, index) => {
+ const toastElement = this.container.querySelector(`[data-id="${toast.id}"]`);
+ if (index < this.maxVisible) {
+ toastElement.classList.add('enable-toast__item--visible');
+ } else {
+ toastElement.classList.remove('enable-toast__item--visible');
+ }
+ });
+
+ this.updateStatus();
+ }
+
+ // Update the status of the toasts
+ updateStatus() {
+ const totalVisible = this.visibleQueue.length;
+ const totalNotifications = this.toastQueue.length;
+ document.getElementById('status').textContent = `Total Visible: ${totalVisible}, Total Notifications: ${totalNotifications}`;
+ }
+
+ // Update the toast rack
+ updateToastRack() {
+ console.log("Updating toast rack");
+ this.rackContent.innerHTML = '';
+ this.toastQueue.forEach(toastData => {
+ const { message, level, id } = toastData;
+ const toastElement = document.createElement('div');
+ toastElement.className = 'enable-toast__item enable-toast__item--visible';
+ toastElement.style.backgroundColor = this.levels[level]?.color || '#333';
+ toastElement.setAttribute('data-id', id);
+
+ const messageSpan = document.createElement('span');
+ messageSpan.textContent = message;
+ toastElement.appendChild(messageSpan);
+
+ console.log(`Appending toast to rack: ${message}`);
+ this.rackContent.appendChild(toastElement);
+ });
+ const totalNotifications = this.toastQueue.length;
+ console.log(`Toast rack updated with ${totalNotifications} toasts.`);
+ this.rackContent.setAttribute('aria-live', 'assertive');
+ document.getElementById('rackContentStatus').textContent = `Toast rack contains ${totalNotifications} toasts.`;
+ }
+
+ // Update the toggle rack button with the number of toasts
+ updateToggleRackButton() {
+ const totalNotifications = this.toastQueue.length;
+ if (this.toggleRackButton) {
+ this.toggleRackButton.textContent = `Toggle Toast Rack (${totalNotifications})`;
+ }
+ }
+
+ // Clear all toasts
+ clearAllToasts() {
+ this.visibleQueue.forEach(toast => {
+ const toastElement = this.container.querySelector(`[data-id="${toast.id}"]`);
+ if (toastElement) {
+ toastElement.remove();
+ }
+ });
+ this.toastQueue = [];
+ this.visibleQueue = [];
+ console.log("All toasts cleared");
+ this.updateStatus();
+ this.updateToastRack();
+ this.updateToggleRackButton();
+ }
+ }
+};
+
diff --git a/js/modules/toast.js b/js/modules/toast.js
new file mode 100644
index 00000000..fbf32cf8
--- /dev/null
+++ b/js/modules/toast.js
@@ -0,0 +1,192 @@
+'use strict'
+
+/*******************************************************************************
+ * toast.js - UI for Accessible Toast Notifications
+ *
+ * Written by Otto Wachter
+ * Part of the Enable accessible component library.
+ * Version 1.x released [Release Date]
+ *
+ * More information about this script available at:
+ * https://www.useragentman.com/enable/toast.php
+ *
+ * Released under the MIT License.
+ ******************************************************************************/
+
+const toastModule = new function() {
+
+ this.init = function() {
+ this.rackContent = document.getElementById('rackContent');
+ this.updateToggleRackButton();
+ };
+
+ this.Toast = class {
+ constructor(options = {}) {
+ this.container = this.createContainer(options.position || 'bottom-right');
+ this.maxVisible = options.maxVisible || 1;
+ this.toastQueue = [];
+ this.visibleQueue = [];
+ this.levels = options.levels || {};
+ this.ariaLive = options.ariaLive || 'polite'; // Default to polite
+ document.body.appendChild(this.container);
+
+ // Initialize the toast rack
+ this.rackContent = document.getElementById('rackContent');
+ this.toggleRackButton = document.getElementById('toggleRackButton');
+ }
+
+ // Create the container for the toasts
+ createContainer(position) {
+ const container = document.createElement('div');
+ container.className = `enable-toast__container enable-toast__container--${position}`;
+ return container;
+ }
+
+ // Show a new toast notification
+ showToast(message, level = 'normal') {
+ const toastData = {
+ message,
+ level,
+ id: Date.now() // unique id for each toast
+ };
+ this.toastQueue.push(toastData);
+ this.visibleQueue.push(toastData);
+ console.log("Toast added to queue and visibleQueue:", toastData);
+ const toastElement = this.createToastElement(toastData);
+ this.container.appendChild(toastElement);
+ toastElement.offsetHeight; // Force reflow to ensure the element is rendered before adding the visible class
+
+ setTimeout(() => {
+ toastElement.classList.add('enable-toast__item--visible');
+ }, 100); // Slight delay to ensure screen readers catch the change
+
+ // Update the toast rack with the new toast
+ this.updateToastRack();
+ this.updateVisibleToasts();
+ this.updateToggleRackButton();
+ }
+
+ // Create the individual toast element (Using enable-toast__item)
+ createToastElement(toastData) {
+ const { message, level, id } = toastData;
+ const toast = document.createElement('div');
+ toast.className = 'enable-toast__item';
+ toast.style.backgroundColor = this.levels[level]?.color || '#333';
+ toast.setAttribute('tabindex', '-1');
+ toast.setAttribute('aria-live', this.ariaLive);
+ toast.setAttribute('role', 'alert');
+ toast.setAttribute('data-id', id);
+
+ const messageSpan = document.createElement('span');
+ messageSpan.textContent = message;
+ toast.appendChild(messageSpan);
+
+ const closeButton = document.createElement('button');
+ closeButton.className = 'enable-toast__close';
+ closeButton.setAttribute('aria-label', 'close alert');
+ closeButton.textContent = '✖';
+ closeButton.addEventListener('click', () => {
+ console.log("Close button clicked for toast:", message);
+ this.dismissToast(toastData);
+ });
+ toast.appendChild(closeButton);
+
+ return toast;
+ }
+
+ // Dismiss a toast notification
+ dismissToast(toastData) {
+ const toastElement = this.container.querySelector(`[data-id="${toastData.id}"]`);
+ if (toastElement) {
+ toastElement.classList.add('enable-toast__item--exit');
+ setTimeout(() => {
+ toastElement.remove();
+ this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id);
+ this.updateVisibleToasts();
+ this.updateToastRack();
+ this.updateToggleRackButton();
+ }, 500); // Match the CSS animation duration
+ }
+ }
+
+ // Update the visible toasts according to the max visible limit
+ updateVisibleToasts() {
+ console.log("Updating visible toasts");
+
+ // Remove excess toasts from the visibleQueue
+ while (this.visibleQueue.length > this.maxVisible) {
+ const toastToRemove = this.visibleQueue.shift();
+ this.dismissToast(toastToRemove);
+ }
+
+ // Ensure only maxVisible toasts are displayed
+ this.visibleQueue.forEach((toast, index) => {
+ const toastElement = this.container.querySelector(`[data-id="${toast.id}"]`);
+ if (index < this.maxVisible) {
+ toastElement.classList.add('enable-toast__item--visible');
+ } else {
+ toastElement.classList.remove('enable-toast__item--visible');
+ }
+ });
+
+ this.updateStatus();
+ }
+
+ // Update the status of the toasts
+ updateStatus() {
+ const totalVisible = this.visibleQueue.length;
+ const totalNotifications = this.toastQueue.length;
+ document.getElementById('status').textContent = `Total Visible: ${totalVisible}, Total Notifications: ${totalNotifications}`;
+ }
+
+ // Update the toast rack
+ updateToastRack() {
+ console.log("Updating toast rack");
+ this.rackContent.innerHTML = '';
+ this.toastQueue.forEach(toastData => {
+ const { message, level, id } = toastData;
+ const toastElement = document.createElement('div');
+ toastElement.className = 'enable-toast__item enable-toast__item--visible';
+ toastElement.style.backgroundColor = this.levels[level]?.color || '#333';
+ toastElement.setAttribute('data-id', id);
+
+ const messageSpan = document.createElement('span');
+ messageSpan.textContent = message;
+ toastElement.appendChild(messageSpan);
+
+ console.log(`Appending toast to rack: ${message}`);
+ this.rackContent.appendChild(toastElement);
+ });
+ const totalNotifications = this.toastQueue.length;
+ console.log(`Toast rack updated with ${totalNotifications} toasts.`);
+ this.rackContent.setAttribute('aria-live', 'assertive');
+ document.getElementById('rackContentStatus').textContent = `Toast rack contains ${totalNotifications} toasts.`;
+ }
+
+ // Update the toggle rack button with the number of toasts
+ updateToggleRackButton() {
+ const totalNotifications = this.toastQueue.length;
+ if (this.toggleRackButton) {
+ this.toggleRackButton.textContent = `Toggle Toast Rack (${totalNotifications})`;
+ }
+ }
+
+ // Clear all toasts
+ clearAllToasts() {
+ this.visibleQueue.forEach(toast => {
+ const toastElement = this.container.querySelector(`[data-id="${toast.id}"]`);
+ if (toastElement) {
+ toastElement.remove();
+ }
+ });
+ this.toastQueue = [];
+ this.visibleQueue = [];
+ console.log("All toasts cleared");
+ this.updateStatus();
+ this.updateToastRack();
+ this.updateToggleRackButton();
+ }
+ }
+};
+
+export default toastModule;
diff --git a/js/test/toast-1.js b/js/test/toast-1.js
new file mode 100644
index 00000000..353dec88
--- /dev/null
+++ b/js/test/toast-1.js
@@ -0,0 +1,67 @@
+// __tests__/toast.test.js
+import { Toast } from '../js/modules/toast';
+
+describe('Toast', () => {
+ let toast;
+ let mockContainer;
+
+ beforeEach(() => {
+ document.body.innerHTML =
+ '
';
+ mockContainer = document.createElement('div');
+ document.body.appendChild(mockContainer);
+ toast = new Toast({ position: 'bottom-right', maxVisible: 2 });
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('should create a toast container with the correct position', () => {
+ const container = toast.createContainer('top-left');
+ expect(container.className).toBe(
+ 'enable-toast__container enable-toast__container--top-left',
+ );
+ });
+
+ it('should show a toast with the correct message and level', () => {
+ toast.showToast('Test message', 'success');
+ const toastElement = toast.container.querySelector(
+ '.enable-toast__toast',
+ );
+ expect(toastElement).not.toBeNull();
+ expect(toastElement.querySelector('span').textContent).toBe(
+ 'Test message',
+ );
+ expect(toastElement.style.backgroundColor).toBe('#28a745'); // success level color
+ });
+
+ it('should dismiss a toast when the close button is clicked', () => {
+ toast.showToast('Test message', 'normal');
+ const toastElement = toast.container.querySelector(
+ '.enable-toast__toast',
+ );
+ toastElement.querySelector('.enable-toast__close-button').click();
+ expect(
+ toastElement.classList.contains('enable-toast__toast--exit'),
+ ).toBe(true);
+ });
+
+ it('should update visible toasts correctly', () => {
+ toast.showToast('Message 1', 'normal');
+ toast.showToast('Message 2', 'normal');
+ toast.showToast('Message 3', 'normal');
+ expect(toast.visibleQueue.length).toBe(2); // maxVisible is 2
+ expect(toast.toastQueue.length).toBe(3);
+ });
+
+ it('should toggle toast rack visibility', () => {
+ const toggleRackButton = document.getElementById('toggleRackButton');
+ const rack = document.getElementById('rackContent');
+ rack.style.display = 'none';
+ toggleRackButton.click();
+ expect(rack.style.display).toBe('block');
+ toggleRackButton.click();
+ expect(rack.style.display).toBe('none');
+ });
+});
diff --git a/js/test/toast.js b/js/test/toast.js
new file mode 100644
index 00000000..1112e239
--- /dev/null
+++ b/js/test/toast.js
@@ -0,0 +1,131 @@
+// __tests__/toast.test.js
+import { Toast } from '../js/modules/toast';
+
+describe('Toast Module', () => {
+ let toast;
+ let container;
+
+ beforeEach(() => {
+ document.body.innerHTML =
+ '
';
+ container = document.createElement('div');
+ container.id = 'toastContainer';
+ document.body.appendChild(container);
+
+ toast = new Toast({
+ position: 'bottom-right',
+ maxVisible: 3,
+ levels: {
+ normal: { color: '#007bff' },
+ error: { color: '#dc3545' },
+ warning: { color: '#ffc107' },
+ success: { color: '#28a745' },
+ },
+ });
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('should create a container with correct position', () => {
+ expect(
+ toast.container.classList.contains(
+ 'enable-toast__container--bottom-right',
+ ),
+ ).toBe(true);
+ });
+
+ it('should display a toast message with correct level', () => {
+ toast.showToast('This is a normal toast', 'normal');
+ const toastElement = toast.container.querySelector(
+ '.enable-toast__toast',
+ );
+ expect(toastElement).not.toBeNull();
+ expect(toastElement.style.backgroundColor).toBe('#007bff');
+ expect(
+ toastElement.textContent.includes('This is a normal toast'),
+ ).toBe(true);
+ });
+
+ it('should display multiple toasts up to maxVisible limit', () => {
+ toast.showToast('Toast 1', 'normal');
+ toast.showToast('Toast 2', 'success');
+ toast.showToast('Toast 3', 'warning');
+ toast.showToast('Toast 4', 'error');
+
+ const visibleToasts = toast.container.querySelectorAll(
+ '.enable-toast__toast--visible',
+ );
+ expect(visibleToasts.length).toBe(3);
+ expect(visibleToasts[0].textContent.includes('Toast 2')).toBe(true);
+ expect(visibleToasts[1].textContent.includes('Toast 3')).toBe(true);
+ expect(visibleToasts[2].textContent.includes('Toast 4')).toBe(true);
+ });
+
+ it('should remove toast from DOM when dismissed', () => {
+ toast.showToast('Dismiss me', 'normal');
+ const toastElement = toast.container.querySelector(
+ '.enable-toast__toast',
+ );
+ const closeButton = toastElement.querySelector(
+ '.enable-toast__close-button',
+ );
+
+ closeButton.click();
+
+ setTimeout(() => {
+ expect(toastElement.parentNode).toBeNull();
+ }, 500);
+ });
+
+ it('should update aria-live attribute when specified', () => {
+ toast.ariaLive = 'assertive';
+ toast.showToast('Assertive toast', 'normal');
+ const toastElement = toast.container.querySelector(
+ '.enable-toast__toast',
+ );
+ expect(toastElement.getAttribute('aria-live')).toBe('assertive');
+ });
+
+ it('should add and remove toasts from rack correctly', () => {
+ toast.showToast('Toast in rack', 'normal');
+ const rackContent = document.getElementById('rackContent');
+ const rackToasts = rackContent.querySelectorAll('.enable-toast__toast');
+ expect(rackToasts.length).toBe(1);
+
+ toast.dismissToast(toast.toastQueue[0]);
+ setTimeout(() => {
+ expect(
+ rackContent.querySelectorAll('.enable-toast__toast').length,
+ ).toBe(0);
+ }, 500);
+ });
+
+ it('should toggle the toast rack visibility', () => {
+ const toggleRackButton = document.getElementById('toggleRackButton');
+ const rack = document.getElementById('toastRack');
+
+ toggleRackButton.click();
+ expect(rack.style.display).toBe('block');
+
+ toggleRackButton.click();
+ expect(rack.style.display).toBe('none');
+ });
+
+ it('should handle keyboard navigation for dismissing toasts', () => {
+ toast.showToast('Keyboard dismiss', 'normal');
+ const toastElement = toast.container.querySelector(
+ '.enable-toast__toast',
+ );
+ toastElement.focus();
+ const event = new KeyboardEvent('keydown', { key: 'Escape' });
+ toastElement.dispatchEvent(event);
+
+ setTimeout(() => {
+ expect(
+ toastElement.classList.contains('enable-toast__toast--exit'),
+ ).toBe(true);
+ }, 500);
+ });
+});
diff --git a/less/toast-demo.less b/less/toast-demo.less
new file mode 100644
index 00000000..f10cf6fd
--- /dev/null
+++ b/less/toast-demo.less
@@ -0,0 +1,27 @@
+// Variables
+@margin: 1.25rem; // 20px converted to rem (assuming base font-size is 16px)
+@gap: 0.625rem; // 10px converted to rem
+@font-size: 1rem; // 16px converted to rem
+
+// Controls styles
+.enable-toast__controls {
+ margin-bottom: @margin;
+ display: flex;
+ flex-direction: column;
+ gap: @gap;
+
+ &__control-group {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: @gap;
+ }
+
+ &__label {
+ font-weight: normal;
+ }
+
+ &__button-group {
+ display: flex;
+ gap: @gap;
+ }
+}
diff --git a/less/toast.less b/less/toast.less
new file mode 100644
index 00000000..b2da24b7
--- /dev/null
+++ b/less/toast.less
@@ -0,0 +1,109 @@
+// Variables
+@toast-background: #333;
+@toast-color: #fff;
+@toast-padding: 10px;
+@toast-border-radius: 5px;
+@toast-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+@toast-transition: all 0.5s ease-in-out;
+@toast-close-color: @toast-color;
+@toast-close-hover-color: #ff0000;
+@rack-background: rgba(241, 241, 241, 0.9);
+@rack-border: 1px solid #ddd;
+@rack-padding: 10px;
+@rack-width: 300px;
+@rack-max-height: 400px;
+
+// Toast container positions
+.enable-toast {
+ &__container {
+ position: fixed;
+ z-index: 9999;
+ display: flex;
+ flex-direction: column;
+ transition: @toast-transition;
+
+ &--bottom-right,
+ &--bottom-left,
+ &--bottom-center {
+ bottom: 20px;
+ }
+
+ &--top-right,
+ &--top-left,
+ &--top-center {
+ top: 20px;
+ }
+
+ &--bottom-right,
+ &--top-right {
+ right: 20px;
+ }
+
+ &--bottom-left,
+ &--top-left {
+ left: 20px;
+ }
+
+ &--top-center,
+ &--bottom-center,
+ &--middle-center {
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ &--middle-center {
+ top: 50%;
+ transform: translate(-50%, -50%);
+ }
+ }
+
+ // Toast item styles
+ &__item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 10px 0;
+ background: @toast-background;
+ color: @toast-color;
+ padding: @toast-padding;
+ border-radius: @toast-border-radius;
+ box-shadow: @toast-box-shadow;
+ opacity: 0;
+ transition: opacity 0.5s ease-in-out;
+
+ &--visible {
+ opacity: 1;
+ }
+
+ &--exit {
+ opacity: 0;
+ }
+ }
+
+ &__close {
+ background: none;
+ border: none;
+ color: @toast-close-color;
+ font-size: 16px;
+ cursor: pointer;
+
+ &:hover {
+ color: @toast-close-hover-color;
+ }
+ }
+
+ // Toast rack
+ &__rack {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: @rack-background;
+ border: @rack-border;
+ padding: @rack-padding;
+ width: @rack-width;
+ max-height: @rack-max-height;
+ overflow-y: auto;
+ display: none;
+ z-index: 9998;
+ }
+}