From 2fdb2414b437ae83c6a4d80a326a4fafbdce71e6 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Thu, 20 Jun 2024 14:18:14 -0400 Subject: [PATCH 01/24] add toast demo --- content/bottom/toast.php | 7 +++++++ content/head/toast.php | 1 + 2 files changed, 8 insertions(+) create mode 100644 content/bottom/toast.php create mode 100644 content/head/toast.php diff --git a/content/bottom/toast.php b/content/bottom/toast.php new file mode 100644 index 00000000..9e642298 --- /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..475a4820 --- /dev/null +++ b/content/head/toast.php @@ -0,0 +1 @@ + \ No newline at end of file From 8c444e9087b3ba1d9940c6d71dfd4c4ba478f55b Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Thu, 20 Jun 2024 14:26:47 -0400 Subject: [PATCH 02/24] body --- content/body/toast.php | 227 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 content/body/toast.php diff --git a/content/body/toast.php b/content/body/toast.php new file mode 100644 index 00000000..9f3a282e --- /dev/null +++ b/content/body/toast.php @@ -0,0 +1,227 @@ + +

Toast notifications are a great way to provide feedback to users in a non-intrusive way. They are commonly used to show success messages, error alerts, warnings, and other notifications.

+

Accessible Toast Notifications Example

+ true]); ?> + false, +]); ?> + true]); ?> +

In order to make toast notifications accessible, there are a few considerations:

+
    +
  1. Toasts should be announced by screen readers when they appear.
  2. +
  3. Keyboard users should be able to navigate to and dismiss toasts.
  4. +
  5. Toasts should support different levels of severity (normal, error, warning, success), each with a unique color for visual distinction.
  6. +
+

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.

+
+
+ + + + + + + + + + + + +
+
+
+
+ + + + +Here's the full documentation page for the toast component, modeled after the tabs component example you provided: + + +

Toast notifications are a great way to provide feedback to users in a non-intrusive way. They are commonly used to show success messages, error alerts, warnings, and other notifications.

+

Accessible Toast Notifications Example

+ true]); ?> + false]); ?> + true]); ?> +

In order to make toast notifications accessible, there are a few considerations:

+
    +
  1. Toasts should be announced by screen readers when they appear.
  2. +
  3. Keyboard users should be able to navigate to and dismiss toasts.
  4. +
  5. Toasts should support different levels of severity (normal, error, warning, success), each with a unique color for visual distinction.
  6. +
+

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.

+ +
+
+
+ +
+ + + + + + +
+
+
+ + +
+
+ +
+ + + + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+ + +
+ +php +Copy code + + + + + + + + + + +
+
+ + + + + From 114db6990420ef0249158d816737d8120ee2dc6b Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Thu, 20 Jun 2024 14:31:05 -0400 Subject: [PATCH 03/24] improve toast examples, add toast modules --- content/body/toast.php | 322 +++++++++++++-------------------------- content/bottom/toast.php | 6 +- content/head/toast.php | 3 +- js/demos/toast.js | 118 ++++++++++++++ js/modules/toast.js | 184 ++++++++++++++++++++++ 5 files changed, 416 insertions(+), 217 deletions(-) create mode 100644 js/demos/toast.js create mode 100644 js/modules/toast.js diff --git a/content/body/toast.php b/content/body/toast.php index 9f3a282e..8e380dce 100644 --- a/content/body/toast.php +++ b/content/body/toast.php @@ -1,227 +1,123 @@ - -

Toast notifications are a great way to provide feedback to users in a non-intrusive way. They are commonly used to show success messages, error alerts, warnings, and other notifications.

-

Accessible Toast Notifications Example

- true]); ?> - false, -]); ?> - true]); ?> -

In order to make toast notifications accessible, there are a few considerations:

-
    -
  1. Toasts should be announced by screen readers when they appear.
  2. -
  3. Keyboard users should be able to navigate to and dismiss toasts.
  4. -
  5. Toasts should support different levels of severity (normal, error, warning, success), each with a unique color for visual distinction.
  6. -
-

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.

-
-
- - - - - +

Accessible Toast Notifications Example

- - + true, + "comment" => "Recommended for new and existing work.", + ]); ?> + true]); ?> - - - -
-
-
-
- - - - -Here's the full documentation page for the toast component, modeled after the tabs component example you provided: - - -

Toast notifications are a great way to provide feedback to users in a non-intrusive way. They are commonly used to show success messages, error alerts, warnings, and other notifications.

-

Accessible Toast Notifications Example

- true]); ?> - false]); ?> - true]); ?> -

In order to make toast notifications accessible, there are a few considerations:

-
    -
  1. Toasts should be announced by screen readers when they appear.
  2. -
  3. Keyboard users should be able to navigate to and dismiss toasts.
  4. -
  5. Toasts should support different levels of severity (normal, error, warning, success), each with a unique color for visual distinction.
  6. -
-

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.

- -
-
-
- -
- - - - - - +
+
+
+ +
+ + + + + + +
-
-
- - -
-
- -
- - - - +
+ +
-
-
- -
- - +
+ +
+ + + + +
+
+
+ +
+ + +
+
+
+ + +
+
+ +
-
- - -
-
- - +
+ +
+
+
-
- -
-
-
-
- - -
- -php -Copy code - - - - + - - - -
-
-
-
- - - - + + + + + + diff --git a/content/bottom/toast.php b/content/bottom/toast.php index 9e642298..66b6cebb 100644 --- a/content/bottom/toast.php +++ b/content/bottom/toast.php @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/content/head/toast.php b/content/head/toast.php index 475a4820..0dfde4c8 100644 --- a/content/head/toast.php +++ b/content/head/toast.php @@ -1 +1,2 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/js/demos/toast.js b/js/demos/toast.js new file mode 100644 index 00000000..61a3e110 --- /dev/null +++ b/js/demos/toast.js @@ -0,0 +1,118 @@ +'use strict'; + +/******************************************************************************* + * toast.js - Example logic for Accessible Toast Notifications + * + * Written by [Your Name] + * Part of the Enable accessible component library. + * Version 1.0 released + * + * More information about this script available at: + * + * Released under the MIT License. + ******************************************************************************/ +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 = `toast-container ${e.target.value}`; + if (e.target.value.includes('top')) { + this.toast.container.style.flexDirection = 'column-reverse'; + } else { + this.toast.container.style.flexDirection = 'column'; + } + } 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 = + `Total Visible: ${totalVisible}, Total Notifications: ${totalNotifications}`; + document.getElementById('rackContentStatus').textContent = + `Toast rack contains ${totalNotifications} toasts.`; + }; +})(); + +// app.init(); +export default app; diff --git a/js/modules/toast.js b/js/modules/toast.js new file mode 100644 index 00000000..23bcaecd --- /dev/null +++ b/js/modules/toast.js @@ -0,0 +1,184 @@ +'use strict' + +/******************************************************************************* + * toast.js - UI for Accessible Toast Notifications + * + * Written by [Your Name] + * Part of the Enable accessible component library. + * Version 1.0 released [Release Date] + * + * More information about this script available at: + * [Your Website] + * + * Released under the MIT License. + ******************************************************************************/ + +const toastModule = new function() { + + this.init = function() { + this.rackContent = document.getElementById('rackContent'); + }; + + 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'); + } + + // Create the container for the toasts + createContainer(position) { + const container = document.createElement('div'); + container.className = `toast-container ${position}`; + if (position.includes('top')) { + container.style.flexDirection = 'column-reverse'; + } else { + container.style.flexDirection = 'column'; + } + 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); + + // Force reflow to ensure the element is rendered before adding the visible class + toastElement.offsetHeight; + toastElement.classList.add('toast-visible'); + + // Update the toast rack with the new toast + this.updateToastRack(); + + this.updateVisibleToasts(); + } + + // Create the individual toast element + createToastElement(toastData) { + const { message, level, id } = toastData; + const toast = document.createElement('div'); + toast.className = 'toast'; + toast.style.backgroundColor = this.levels[level]?.color || '#333'; + toast.setAttribute('tabindex', '-1'); + toast.setAttribute('aria-live', this.ariaLive); // Set aria-live attribute + toast.setAttribute('data-id', id); + + const messageSpan = document.createElement('span'); + messageSpan.textContent = message; + toast.appendChild(messageSpan); + + const closeButton = document.createElement('button'); + closeButton.setAttribute('aria-label', 'close alert'); + closeButton.textContent = '✖'; // Improved close button style + 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('toast-exit'); + setTimeout(() => { + toastElement.remove(); + this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); + this.updateVisibleToasts(); + this.updateToastRack(); + }, 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('toast-visible'); + } else { + toastElement.classList.remove('toast-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 = 'toast toast-visible'; // Ensure opacity is 1 + 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}`); // Debugging log + 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.`; + } + + // 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(); + } + } +}; + +export default toastModule; From db934dbe02fc10b1fc2b6460d815140fad020651 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Fri, 21 Jun 2024 10:54:22 -0400 Subject: [PATCH 04/24] css files --- css/toast-demo.css | 29 ++++++++++++++++++ css/toast.css | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 css/toast-demo.css create mode 100644 css/toast.css diff --git a/css/toast-demo.css b/css/toast-demo.css new file mode 100644 index 00000000..382b0b0e --- /dev/null +++ b/css/toast-demo.css @@ -0,0 +1,29 @@ +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; +} +.controls .control-group { + display: flex; + flex-direction: column; + margin-bottom: 10px; +} +.controls label { + font-weight: normal; +} +.controls .button-group { + display: flex; + gap: 10px; +} +.status { + margin-top: 20px; + font-size: 16px; +} diff --git a/css/toast.css b/css/toast.css new file mode 100644 index 00000000..c525f2d2 --- /dev/null +++ b/css/toast.css @@ -0,0 +1,76 @@ +.toast-container { + position: fixed; + z-index: 9999; + display: flex; + transition: all 0.5s ease-in-out; +} +.toast-container.bottom-right, +.toast-container.bottom-left, +.toast-container.bottom-center { + bottom: 20px; +} +.toast-container.top-right, +.toast-container.top-left, +.toast-container.top-center { + top: 20px; +} +.toast-container.bottom-right, +.toast-container.top-right { + right: 20px; +} +.toast-container.bottom-left, +.toast-container.top-left { + left: 20px; +} +.toast-container.top-center, +.toast-container.bottom-center, +.toast-container.middle-center { + left: 50%; + transform: translateX(-50%); +} +.toast-container.middle-center { + top: 50%; + transform: translate(-50%, -50%); +} +.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; +} +.toast.toast-visible { + opacity: 1; +} +.toast.toast-exit { + opacity: 0; +} +.toast button { + background: none; + border: none; + color: #fff; + font-size: 16px; + cursor: pointer; +} +.toast button:hover { + color: #ff0000; +} +.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; +} From 28cf0709d18dcb8c832233fb9046ca83f0b87040 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Fri, 21 Jun 2024 11:06:28 -0400 Subject: [PATCH 05/24] add less files --- css/toast-demo.css | 1 - less/toast-demo.less | 41 +++++++++++++++++ less/toast.less | 106 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 less/toast-demo.less create mode 100644 less/toast.less diff --git a/css/toast-demo.css b/css/toast-demo.css index 382b0b0e..9d5e033f 100644 --- a/css/toast-demo.css +++ b/css/toast-demo.css @@ -1,5 +1,4 @@ body { - font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; diff --git a/less/toast-demo.less b/less/toast-demo.less new file mode 100644 index 00000000..9f97b299 --- /dev/null +++ b/less/toast-demo.less @@ -0,0 +1,41 @@ +// Variables +@margin: 20px; +@gap: 10px; +@font-size: 16px; + +// Body styles +body { + display: flex; + flex-direction: column; + align-items: center; + margin: @margin; +} + +// Controls styles +.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; + } +} + +// Status styles +.status { + margin-top: @margin; + font-size: @font-size; +} diff --git a/less/toast.less b/less/toast.less new file mode 100644 index 00000000..4b737d23 --- /dev/null +++ b/less/toast.less @@ -0,0 +1,106 @@ +// 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 +.toast-container { + position: fixed; + z-index: 9999; + display: flex; + 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 element styles +.toast { + 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; + + &.toast-visible { + opacity: 1; + } + + &.toast-exit { + opacity: 0; + } + + button { + background: none; + border: none; + color: @toast-close-color; + font-size: 16px; + cursor: pointer; + + &:hover { + color: @toast-close-hover-color; + } + } +} + +// Toast rack styles +.toast-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; +} From 1a4750e1b77860246940504ce379acc9609cbe23 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Fri, 21 Jun 2024 11:17:49 -0400 Subject: [PATCH 06/24] comments --- js/demos/toast.js | 11 ----------- js/modules/toast.js | 6 +++--- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/js/demos/toast.js b/js/demos/toast.js index 61a3e110..e70f519b 100644 --- a/js/demos/toast.js +++ b/js/demos/toast.js @@ -1,16 +1,5 @@ 'use strict'; -/******************************************************************************* - * toast.js - Example logic for Accessible Toast Notifications - * - * Written by [Your Name] - * Part of the Enable accessible component library. - * Version 1.0 released - * - * More information about this script available at: - * - * Released under the MIT License. - ******************************************************************************/ import toastModule from '../modules/toast.js'; const app = new (function () { diff --git a/js/modules/toast.js b/js/modules/toast.js index 23bcaecd..be624c72 100644 --- a/js/modules/toast.js +++ b/js/modules/toast.js @@ -3,12 +3,12 @@ /******************************************************************************* * toast.js - UI for Accessible Toast Notifications * - * Written by [Your Name] + * Written by Otto Wachter * Part of the Enable accessible component library. - * Version 1.0 released [Release Date] + * Version 1.x released [Release Date] * * More information about this script available at: - * [Your Website] + * https://www.useragentman.com/enable/toast.php * * Released under the MIT License. ******************************************************************************/ From d12fe8631f4ad373ed5247ba0425e5951e74beed Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Fri, 21 Jun 2024 11:29:09 -0400 Subject: [PATCH 07/24] small improvements/fixes --- content/body/toast.php | 12 ++++++------ css/toast-demo.css | 10 ---------- js/demos/toast.js | 2 +- less/toast-demo.less | 14 -------------- 4 files changed, 7 insertions(+), 31 deletions(-) diff --git a/content/body/toast.php b/content/body/toast.php index 8e380dce..9c86d63d 100644 --- a/content/body/toast.php +++ b/content/body/toast.php @@ -34,6 +34,10 @@
+
+ + +
@@ -46,11 +50,7 @@
- - -
-
- +
@@ -71,7 +71,7 @@
- +
diff --git a/css/toast-demo.css b/css/toast-demo.css index 9d5e033f..c7b4c9df 100644 --- a/css/toast-demo.css +++ b/css/toast-demo.css @@ -1,9 +1,3 @@ -body { - display: flex; - flex-direction: column; - align-items: center; - margin: 20px; -} .controls { margin-bottom: 20px; display: flex; @@ -22,7 +16,3 @@ body { display: flex; gap: 10px; } -.status { - margin-top: 20px; - font-size: 16px; -} diff --git a/js/demos/toast.js b/js/demos/toast.js index e70f519b..4a325ff9 100644 --- a/js/demos/toast.js +++ b/js/demos/toast.js @@ -97,7 +97,7 @@ const app = new (function () { const totalVisible = this.toast.visibleQueue.length; const totalNotifications = this.toast.toastQueue.length; document.getElementById('status').textContent = - `Total Visible: ${totalVisible}, Total Notifications: ${totalNotifications}`; + `Visible: ${totalVisible}, Total: ${totalNotifications}`; document.getElementById('rackContentStatus').textContent = `Toast rack contains ${totalNotifications} toasts.`; }; diff --git a/less/toast-demo.less b/less/toast-demo.less index 9f97b299..97fb9021 100644 --- a/less/toast-demo.less +++ b/less/toast-demo.less @@ -3,14 +3,6 @@ @gap: 10px; @font-size: 16px; -// Body styles -body { - display: flex; - flex-direction: column; - align-items: center; - margin: @margin; -} - // Controls styles .controls { margin-bottom: @margin; @@ -33,9 +25,3 @@ body { gap: @gap; } } - -// Status styles -.status { - margin-top: @margin; - font-size: @font-size; -} From 6080e1366dea68e75efc56f2e29e30d067a32f57 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Wed, 26 Jun 2024 11:06:52 -0400 Subject: [PATCH 08/24] work --- js/modules/toast.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/js/modules/toast.js b/js/modules/toast.js index be624c72..d53749e5 100644 --- a/js/modules/toast.js +++ b/js/modules/toast.js @@ -17,6 +17,7 @@ const toastModule = new function() { this.init = function() { this.rackContent = document.getElementById('rackContent'); + this.updateToggleRackButton(); }; this.Toast = class { @@ -31,6 +32,7 @@ const toastModule = new function() { // Initialize the toast rack this.rackContent = document.getElementById('rackContent'); + this.toggleRackButton = document.getElementById('toggleRackButton'); } // Create the container for the toasts @@ -68,6 +70,7 @@ const toastModule = new function() { this.updateToastRack(); this.updateVisibleToasts(); + this.updateToggleRackButton(); } // Create the individual toast element @@ -106,6 +109,7 @@ const toastModule = new function() { this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); this.updateVisibleToasts(); this.updateToastRack(); + this.updateToggleRackButton(); }, 500); // Match the CSS animation duration } } @@ -164,6 +168,14 @@ const toastModule = new function() { 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 => { @@ -177,6 +189,7 @@ const toastModule = new function() { console.log("All toasts cleared"); this.updateStatus(); this.updateToastRack(); + this.updateToggleRackButton(); } } }; From d61254932f2468c6bd67dcd2e8d8e6a3dc99ebcf Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Wed, 26 Jun 2024 12:48:30 -0400 Subject: [PATCH 09/24] add standalone html file to test toast --- html_example/toast_example.html | 210 ++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 html_example/toast_example.html 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: +

+
    +
  1. + Toasts should be announced by screen readers when they appear. +
  2. +
  3. + Keyboard users should be able to navigate to and dismiss toasts. +
  4. +
  5. + Toasts should support different levels of severity (normal, + error, warning, success), each with a unique color for visual + distinction. +
  6. +
+

+ 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. +

+ +
+
+
+ + +
+
+ +
+ + + + + + +
+
+
+ +
+ + + + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+
+ + + + From 08b9fd838348325fa6ddb95f9b1138dc946b55a2 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Wed, 26 Jun 2024 13:04:37 -0400 Subject: [PATCH 10/24] update units: px to rem --- less/toast-demo.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/less/toast-demo.less b/less/toast-demo.less index 97fb9021..ea65360c 100644 --- a/less/toast-demo.less +++ b/less/toast-demo.less @@ -1,7 +1,7 @@ // Variables -@margin: 20px; -@gap: 10px; -@font-size: 16px; +@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 .controls { From 4d0723f02d3bd8adcae0d97074314082c8bb59d7 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Thu, 27 Jun 2024 10:47:11 -0400 Subject: [PATCH 11/24] toast.php --- content/body/toast.php | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/content/body/toast.php b/content/body/toast.php index 9c86d63d..5e53e3fc 100644 --- a/content/body/toast.php +++ b/content/body/toast.php @@ -1,13 +1,3 @@ - - - - - - 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.

@@ -90,8 +80,8 @@ "steps": [ { "label": "Create markup", - "highlight": "data-tooltip", - "notes": "Our script uses the data-tooltip attribute instead of the title attribute, since title is rendered by user agents by default and cannot be styled." + "highlight": "", + "notes": "" }, { "label": "Create JavaScript events for toast script", @@ -119,5 +109,3 @@ - - From f0308a5202d2f4777ef81ed9292d1be6f6acaf61 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Thu, 27 Jun 2024 10:47:38 -0400 Subject: [PATCH 12/24] px to rem units --- concat.py | 39 ++ concat.txt | 852 ++++++++++++++++++++++++++++++++++++++++ css/app.css | 59 +++ css/toast-demo.css | 8 +- js/modules/es4/toast.js | 196 +++++++++ 5 files changed, 1150 insertions(+), 4 deletions(-) create mode 100644 concat.py create mode 100644 concat.txt create mode 100644 css/app.css create mode 100644 js/modules/es4/toast.js diff --git a/concat.py b/concat.py new file mode 100644 index 00000000..67226bec --- /dev/null +++ b/concat.py @@ -0,0 +1,39 @@ +import os +import sys +import fnmatch + +def concatenate_files(pattern, output_file, dry_run=False): + if not pattern or not output_file: + print("Usage: python concat_files.py [--dry-run]") + sys.exit(1) + + # Find all files matching the pattern + matching_files = [] + for root, dirnames, filenames in os.walk('.'): + for filename in fnmatch.filter(filenames, pattern): + matching_files.append(os.path.join(root, filename)) + + if dry_run: + print("Dry run: The following files would be concatenated:") + for file in matching_files: + print(file) + else: + print(f"Concatenating files to {output_file}...") + with open(output_file, 'w') as outfile: + for file in matching_files: + print(f"Processing {file}") + outfile.write(f"\n# File: {file}\n") + with open(file, 'r') as infile: + outfile.write(infile.read()) + print("Concatenation complete.") + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: python concat_files.py [--dry-run]") + sys.exit(1) + + pattern = sys.argv[1] + output_file = sys.argv[2] + dry_run = '--dry-run' in sys.argv + + concatenate_files(pattern, output_file, dry_run) diff --git a/concat.txt b/concat.txt new file mode 100644 index 00000000..7c400276 --- /dev/null +++ b/concat.txt @@ -0,0 +1,852 @@ + +# File: ./css/toast.css +.toast-container { + position: fixed; + z-index: 9999; + display: flex; + transition: all 0.5s ease-in-out; +} +.toast-container.bottom-right, +.toast-container.bottom-left, +.toast-container.bottom-center { + bottom: 20px; +} +.toast-container.top-right, +.toast-container.top-left, +.toast-container.top-center { + top: 20px; +} +.toast-container.bottom-right, +.toast-container.top-right { + right: 20px; +} +.toast-container.bottom-left, +.toast-container.top-left { + left: 20px; +} +.toast-container.top-center, +.toast-container.bottom-center, +.toast-container.middle-center { + left: 50%; + transform: translateX(-50%); +} +.toast-container.middle-center { + top: 50%; + transform: translate(-50%, -50%); +} +.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; +} +.toast.toast-visible { + opacity: 1; +} +.toast.toast-exit { + opacity: 0; +} +.toast button { + background: none; + border: none; + color: #fff; + font-size: 16px; + cursor: pointer; +} +.toast button:hover { + color: #ff0000; +} +.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; +} + +# File: ./css/toast-demo.css +.controls { + margin-bottom: 20px; + display: flex; + flex-direction: column; + gap: 10px; +} +.controls .control-group { + display: flex; + flex-direction: column; + margin-bottom: 10px; +} +.controls label { + font-weight: normal; +} +.controls .button-group { + display: flex; + gap: 10px; +} + +# File: ./less/toast-demo.less +// Variables +@margin: 20px; +@gap: 10px; +@font-size: 16px; + +// Controls styles +.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; + } +} + +# File: ./less/toast.less +// 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 +.toast-container { + position: fixed; + z-index: 9999; + display: flex; + 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 element styles +.toast { + 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; + + &.toast-visible { + opacity: 1; + } + + &.toast-exit { + opacity: 0; + } + + button { + background: none; + border: none; + color: @toast-close-color; + font-size: 16px; + cursor: pointer; + + &:hover { + color: @toast-close-hover-color; + } + } +} + +// Toast rack styles +.toast-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; +} + +# File: ./js/modules/toast.js +'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.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'); + } + + // Create the container for the toasts + createContainer(position) { + const container = document.createElement('div'); + container.className = `toast-container ${position}`; + if (position.includes('top')) { + container.style.flexDirection = 'column-reverse'; + } else { + container.style.flexDirection = 'column'; + } + 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); + + // Force reflow to ensure the element is rendered before adding the visible class + toastElement.offsetHeight; + toastElement.classList.add('toast-visible'); + + // Update the toast rack with the new toast + this.updateToastRack(); + + this.updateVisibleToasts(); + } + + // Create the individual toast element + createToastElement(toastData) { + const { message, level, id } = toastData; + const toast = document.createElement('div'); + toast.className = 'toast'; + toast.style.backgroundColor = this.levels[level]?.color || '#333'; + toast.setAttribute('tabindex', '-1'); + toast.setAttribute('aria-live', this.ariaLive); // Set aria-live attribute + toast.setAttribute('data-id', id); + + const messageSpan = document.createElement('span'); + messageSpan.textContent = message; + toast.appendChild(messageSpan); + + const closeButton = document.createElement('button'); + closeButton.setAttribute('aria-label', 'close alert'); + closeButton.textContent = '✖'; // Improved close button style + 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('toast-exit'); + setTimeout(() => { + toastElement.remove(); + this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); + this.updateVisibleToasts(); + this.updateToastRack(); + }, 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('toast-visible'); + } else { + toastElement.classList.remove('toast-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 = 'toast toast-visible'; // Ensure opacity is 1 + 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}`); // Debugging log + 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.`; + } + + // 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(); + } + } +}; + +export default toastModule; + +# File: ./js/modules/es4/toast.js +'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.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'); + } + + // Create the container for the toasts + createContainer(position) { + const container = document.createElement('div'); + container.className = `toast-container ${position}`; + if (position.includes('top')) { + container.style.flexDirection = 'column-reverse'; + } else { + container.style.flexDirection = 'column'; + } + 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); + + // Force reflow to ensure the element is rendered before adding the visible class + toastElement.offsetHeight; + toastElement.classList.add('toast-visible'); + + // Update the toast rack with the new toast + this.updateToastRack(); + + this.updateVisibleToasts(); + } + + // Create the individual toast element + createToastElement(toastData) { + const { message, level, id } = toastData; + const toast = document.createElement('div'); + toast.className = 'toast'; + toast.style.backgroundColor = this.levels[level]?.color || '#333'; + toast.setAttribute('tabindex', '-1'); + toast.setAttribute('aria-live', this.ariaLive); // Set aria-live attribute + toast.setAttribute('data-id', id); + + const messageSpan = document.createElement('span'); + messageSpan.textContent = message; + toast.appendChild(messageSpan); + + const closeButton = document.createElement('button'); + closeButton.setAttribute('aria-label', 'close alert'); + closeButton.textContent = '✖'; // Improved close button style + 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('toast-exit'); + setTimeout(() => { + toastElement.remove(); + this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); + this.updateVisibleToasts(); + this.updateToastRack(); + }, 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('toast-visible'); + } else { + toastElement.classList.remove('toast-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 = 'toast toast-visible'; // Ensure opacity is 1 + 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}`); // Debugging log + 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.`; + } + + // 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(); + } + } +}; + + +# File: ./js/demos/toast.js +'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 = `toast-container ${e.target.value}`; + if (e.target.value.includes('top')) { + this.toast.container.style.flexDirection = 'column-reverse'; + } else { + this.toast.container.style.flexDirection = 'column'; + } + } 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.`; + }; +})(); + +// app.init(); +export default app; + +# File: ./content/body/toast.php + + + + + + 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

+ + true, + "comment" => "Recommended for new and existing work.", + ]); ?> + true]); ?> + +

+ In order to make toast notifications accessible, there are a few considerations: +

+
    +
  1. Toasts should be announced by screen readers when they appear.
  2. +
  3. Keyboard users should be able to navigate to and dismiss toasts.
  4. +
  5. Toasts should support different levels of severity (normal, error, warning, success), each with a unique color for visual distinction.
  6. +
+

+ 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. +

+ +
+
+
+ + +
+
+ +
+ + + + + + +
+
+
+ +
+ + + + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+
+ + + + + + + + + + +# File: ./content/head/toast.php + + +# File: ./content/bottom/toast.php + + \ 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 index c7b4c9df..8fc2fa88 100644 --- a/css/toast-demo.css +++ b/css/toast-demo.css @@ -1,18 +1,18 @@ .controls { - margin-bottom: 20px; + margin-bottom: 1.25rem; display: flex; flex-direction: column; - gap: 10px; + gap: 0.625rem; } .controls .control-group { display: flex; flex-direction: column; - margin-bottom: 10px; + margin-bottom: 0.625rem; } .controls label { font-weight: normal; } .controls .button-group { display: flex; - gap: 10px; + gap: 0.625rem; } diff --git a/js/modules/es4/toast.js b/js/modules/es4/toast.js new file mode 100644 index 00000000..e091f7a0 --- /dev/null +++ b/js/modules/es4/toast.js @@ -0,0 +1,196 @@ +'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 = `toast-container ${position}`; + if (position.includes('top')) { + container.style.flexDirection = 'column-reverse'; + } else { + container.style.flexDirection = 'column'; + } + 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); + + // Force reflow to ensure the element is rendered before adding the visible class + toastElement.offsetHeight; + toastElement.classList.add('toast-visible'); + + // Update the toast rack with the new toast + this.updateToastRack(); + + this.updateVisibleToasts(); + this.updateToggleRackButton(); + } + + // Create the individual toast element + createToastElement(toastData) { + const { message, level, id } = toastData; + const toast = document.createElement('div'); + toast.className = 'toast'; + toast.style.backgroundColor = this.levels[level]?.color || '#333'; + toast.setAttribute('tabindex', '-1'); + toast.setAttribute('aria-live', this.ariaLive); // Set aria-live attribute + toast.setAttribute('data-id', id); + + const messageSpan = document.createElement('span'); + messageSpan.textContent = message; + toast.appendChild(messageSpan); + + const closeButton = document.createElement('button'); + closeButton.setAttribute('aria-label', 'close alert'); + closeButton.textContent = '✖'; // Improved close button style + 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('toast-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('toast-visible'); + } else { + toastElement.classList.remove('toast-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 = 'toast toast-visible'; // Ensure opacity is 1 + 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}`); // Debugging log + 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(); + } + } +}; + From ac75c8d82463d5c01c46dcd9503f31e7262ed235 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Fri, 28 Jun 2024 12:19:03 -0400 Subject: [PATCH 13/24] set role and add slight delay appending to DOM for screenreaders --- js/modules/es4/toast.js | 17 ++++++++--------- js/modules/toast.js | 17 ++++++++--------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/js/modules/es4/toast.js b/js/modules/es4/toast.js index e091f7a0..3635c1c5 100644 --- a/js/modules/es4/toast.js +++ b/js/modules/es4/toast.js @@ -57,18 +57,16 @@ const toastModule = new function() { 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 - // Force reflow to ensure the element is rendered before adding the visible class - toastElement.offsetHeight; - toastElement.classList.add('toast-visible'); + setTimeout(() => { + toastElement.classList.add('toast-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(); } @@ -81,6 +79,7 @@ const toastModule = new function() { toast.style.backgroundColor = this.levels[level]?.color || '#333'; toast.setAttribute('tabindex', '-1'); toast.setAttribute('aria-live', this.ariaLive); // Set aria-live attribute + toast.setAttribute('role', 'alert'); // Set aria-live attribute toast.setAttribute('data-id', id); const messageSpan = document.createElement('span'); @@ -89,7 +88,7 @@ const toastModule = new function() { const closeButton = document.createElement('button'); closeButton.setAttribute('aria-label', 'close alert'); - closeButton.textContent = '✖'; // Improved close button style + closeButton.textContent = '✖'; closeButton.addEventListener('click', () => { console.log("Close button clicked for toast:", message); this.dismissToast(toastData); @@ -151,7 +150,7 @@ const toastModule = new function() { this.toastQueue.forEach(toastData => { const { message, level, id } = toastData; const toastElement = document.createElement('div'); - toastElement.className = 'toast toast-visible'; // Ensure opacity is 1 + toastElement.className = 'toast toast-visible'; toastElement.style.backgroundColor = this.levels[level]?.color || '#333'; toastElement.setAttribute('data-id', id); @@ -159,7 +158,7 @@ const toastModule = new function() { messageSpan.textContent = message; toastElement.appendChild(messageSpan); - console.log(`Appending toast to rack: ${message}`); // Debugging log + console.log(`Appending toast to rack: ${message}`); this.rackContent.appendChild(toastElement); }); const totalNotifications = this.toastQueue.length; diff --git a/js/modules/toast.js b/js/modules/toast.js index d53749e5..4443a2a2 100644 --- a/js/modules/toast.js +++ b/js/modules/toast.js @@ -57,18 +57,16 @@ const toastModule = new function() { 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 - // Force reflow to ensure the element is rendered before adding the visible class - toastElement.offsetHeight; - toastElement.classList.add('toast-visible'); + setTimeout(() => { + toastElement.classList.add('toast-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(); } @@ -81,6 +79,7 @@ const toastModule = new function() { toast.style.backgroundColor = this.levels[level]?.color || '#333'; toast.setAttribute('tabindex', '-1'); toast.setAttribute('aria-live', this.ariaLive); // Set aria-live attribute + toast.setAttribute('role', 'alert'); // Set aria-live attribute toast.setAttribute('data-id', id); const messageSpan = document.createElement('span'); @@ -89,7 +88,7 @@ const toastModule = new function() { const closeButton = document.createElement('button'); closeButton.setAttribute('aria-label', 'close alert'); - closeButton.textContent = '✖'; // Improved close button style + closeButton.textContent = '✖'; closeButton.addEventListener('click', () => { console.log("Close button clicked for toast:", message); this.dismissToast(toastData); @@ -151,7 +150,7 @@ const toastModule = new function() { this.toastQueue.forEach(toastData => { const { message, level, id } = toastData; const toastElement = document.createElement('div'); - toastElement.className = 'toast toast-visible'; // Ensure opacity is 1 + toastElement.className = 'toast toast-visible'; toastElement.style.backgroundColor = this.levels[level]?.color || '#333'; toastElement.setAttribute('data-id', id); @@ -159,7 +158,7 @@ const toastModule = new function() { messageSpan.textContent = message; toastElement.appendChild(messageSpan); - console.log(`Appending toast to rack: ${message}`); // Debugging log + console.log(`Appending toast to rack: ${message}`); this.rackContent.appendChild(toastElement); }); const totalNotifications = this.toastQueue.length; From b8d061f5dd05221573b5da7ec390e4da68ce94b6 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Wed, 3 Jul 2024 15:48:27 -0400 Subject: [PATCH 14/24] new example --- content/body/toast.php | 247 +++++++++++++++++++++++++---------------- 1 file changed, 150 insertions(+), 97 deletions(-) diff --git a/content/body/toast.php b/content/body/toast.php index 5e53e3fc..f3fb3c6e 100644 --- a/content/body/toast.php +++ b/content/body/toast.php @@ -1,111 +1,164 @@ -

- 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. -

+

+ 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

+

Shopping Site Toast Notifications Example

- true, - "comment" => "Recommended for new and existing work.", - ]); ?> - true]); ?> +
+
+
+

Products:

+ + + +
+
+ + +
+
+ + +
+
+
-

- In order to make toast notifications accessible, there are a few considerations: -

-
    -
  1. Toasts should be announced by screen readers when they appear.
  2. -
  3. Keyboard users should be able to navigate to and dismiss toasts.
  4. -
  5. Toasts should support different levels of severity (normal, error, warning, success), each with a unique color for visual distinction.
  6. -
-

- 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. -

+ -
-
-
- - -
-
- -
- - - - - - -
-
-
- -
- - - - -
-
-
- -
- - -
+ + +

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: +

+
    +
  1. Toasts should be announced by screen readers when they appear.
  2. +
  3. Keyboard users should be able to navigate to and dismiss toasts.
  4. +
  5. Toasts should support different levels of severity (normal, error, warning, success), each with a unique color for visual distinction.
  6. +
+

+ 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. +

+ +
+
+
+ + +
+
+ +
+ + + + + +
-
- - +
+
+ +
+ + + +
-
- - +
+
+ +
+ +
-
- -
-
+
+ +
-
+
+ + +
+
+
+ +
+
+
+
+ + + + + + }); - - + checkoutButton.addEventListener('click', function () { + console.log('Checkout initiated'); + app.toast.showToast('Your order has been placed successfully. Order number: 123456.', 'success'); + }); + }); + From e1eb85f6be5ff456de8229aa27bb16dfb1ce05c2 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Wed, 3 Jul 2024 15:49:57 -0400 Subject: [PATCH 15/24] delete --- concat.py | 39 --- concat.txt | 852 ----------------------------------------------------- 2 files changed, 891 deletions(-) delete mode 100644 concat.py delete mode 100644 concat.txt diff --git a/concat.py b/concat.py deleted file mode 100644 index 67226bec..00000000 --- a/concat.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -import sys -import fnmatch - -def concatenate_files(pattern, output_file, dry_run=False): - if not pattern or not output_file: - print("Usage: python concat_files.py [--dry-run]") - sys.exit(1) - - # Find all files matching the pattern - matching_files = [] - for root, dirnames, filenames in os.walk('.'): - for filename in fnmatch.filter(filenames, pattern): - matching_files.append(os.path.join(root, filename)) - - if dry_run: - print("Dry run: The following files would be concatenated:") - for file in matching_files: - print(file) - else: - print(f"Concatenating files to {output_file}...") - with open(output_file, 'w') as outfile: - for file in matching_files: - print(f"Processing {file}") - outfile.write(f"\n# File: {file}\n") - with open(file, 'r') as infile: - outfile.write(infile.read()) - print("Concatenation complete.") - -if __name__ == "__main__": - if len(sys.argv) < 3: - print("Usage: python concat_files.py [--dry-run]") - sys.exit(1) - - pattern = sys.argv[1] - output_file = sys.argv[2] - dry_run = '--dry-run' in sys.argv - - concatenate_files(pattern, output_file, dry_run) diff --git a/concat.txt b/concat.txt deleted file mode 100644 index 7c400276..00000000 --- a/concat.txt +++ /dev/null @@ -1,852 +0,0 @@ - -# File: ./css/toast.css -.toast-container { - position: fixed; - z-index: 9999; - display: flex; - transition: all 0.5s ease-in-out; -} -.toast-container.bottom-right, -.toast-container.bottom-left, -.toast-container.bottom-center { - bottom: 20px; -} -.toast-container.top-right, -.toast-container.top-left, -.toast-container.top-center { - top: 20px; -} -.toast-container.bottom-right, -.toast-container.top-right { - right: 20px; -} -.toast-container.bottom-left, -.toast-container.top-left { - left: 20px; -} -.toast-container.top-center, -.toast-container.bottom-center, -.toast-container.middle-center { - left: 50%; - transform: translateX(-50%); -} -.toast-container.middle-center { - top: 50%; - transform: translate(-50%, -50%); -} -.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; -} -.toast.toast-visible { - opacity: 1; -} -.toast.toast-exit { - opacity: 0; -} -.toast button { - background: none; - border: none; - color: #fff; - font-size: 16px; - cursor: pointer; -} -.toast button:hover { - color: #ff0000; -} -.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; -} - -# File: ./css/toast-demo.css -.controls { - margin-bottom: 20px; - display: flex; - flex-direction: column; - gap: 10px; -} -.controls .control-group { - display: flex; - flex-direction: column; - margin-bottom: 10px; -} -.controls label { - font-weight: normal; -} -.controls .button-group { - display: flex; - gap: 10px; -} - -# File: ./less/toast-demo.less -// Variables -@margin: 20px; -@gap: 10px; -@font-size: 16px; - -// Controls styles -.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; - } -} - -# File: ./less/toast.less -// 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 -.toast-container { - position: fixed; - z-index: 9999; - display: flex; - 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 element styles -.toast { - 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; - - &.toast-visible { - opacity: 1; - } - - &.toast-exit { - opacity: 0; - } - - button { - background: none; - border: none; - color: @toast-close-color; - font-size: 16px; - cursor: pointer; - - &:hover { - color: @toast-close-hover-color; - } - } -} - -// Toast rack styles -.toast-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; -} - -# File: ./js/modules/toast.js -'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.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'); - } - - // Create the container for the toasts - createContainer(position) { - const container = document.createElement('div'); - container.className = `toast-container ${position}`; - if (position.includes('top')) { - container.style.flexDirection = 'column-reverse'; - } else { - container.style.flexDirection = 'column'; - } - 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); - - // Force reflow to ensure the element is rendered before adding the visible class - toastElement.offsetHeight; - toastElement.classList.add('toast-visible'); - - // Update the toast rack with the new toast - this.updateToastRack(); - - this.updateVisibleToasts(); - } - - // Create the individual toast element - createToastElement(toastData) { - const { message, level, id } = toastData; - const toast = document.createElement('div'); - toast.className = 'toast'; - toast.style.backgroundColor = this.levels[level]?.color || '#333'; - toast.setAttribute('tabindex', '-1'); - toast.setAttribute('aria-live', this.ariaLive); // Set aria-live attribute - toast.setAttribute('data-id', id); - - const messageSpan = document.createElement('span'); - messageSpan.textContent = message; - toast.appendChild(messageSpan); - - const closeButton = document.createElement('button'); - closeButton.setAttribute('aria-label', 'close alert'); - closeButton.textContent = '✖'; // Improved close button style - 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('toast-exit'); - setTimeout(() => { - toastElement.remove(); - this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); - this.updateVisibleToasts(); - this.updateToastRack(); - }, 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('toast-visible'); - } else { - toastElement.classList.remove('toast-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 = 'toast toast-visible'; // Ensure opacity is 1 - 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}`); // Debugging log - 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.`; - } - - // 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(); - } - } -}; - -export default toastModule; - -# File: ./js/modules/es4/toast.js -'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.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'); - } - - // Create the container for the toasts - createContainer(position) { - const container = document.createElement('div'); - container.className = `toast-container ${position}`; - if (position.includes('top')) { - container.style.flexDirection = 'column-reverse'; - } else { - container.style.flexDirection = 'column'; - } - 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); - - // Force reflow to ensure the element is rendered before adding the visible class - toastElement.offsetHeight; - toastElement.classList.add('toast-visible'); - - // Update the toast rack with the new toast - this.updateToastRack(); - - this.updateVisibleToasts(); - } - - // Create the individual toast element - createToastElement(toastData) { - const { message, level, id } = toastData; - const toast = document.createElement('div'); - toast.className = 'toast'; - toast.style.backgroundColor = this.levels[level]?.color || '#333'; - toast.setAttribute('tabindex', '-1'); - toast.setAttribute('aria-live', this.ariaLive); // Set aria-live attribute - toast.setAttribute('data-id', id); - - const messageSpan = document.createElement('span'); - messageSpan.textContent = message; - toast.appendChild(messageSpan); - - const closeButton = document.createElement('button'); - closeButton.setAttribute('aria-label', 'close alert'); - closeButton.textContent = '✖'; // Improved close button style - 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('toast-exit'); - setTimeout(() => { - toastElement.remove(); - this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); - this.updateVisibleToasts(); - this.updateToastRack(); - }, 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('toast-visible'); - } else { - toastElement.classList.remove('toast-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 = 'toast toast-visible'; // Ensure opacity is 1 - 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}`); // Debugging log - 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.`; - } - - // 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(); - } - } -}; - - -# File: ./js/demos/toast.js -'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 = `toast-container ${e.target.value}`; - if (e.target.value.includes('top')) { - this.toast.container.style.flexDirection = 'column-reverse'; - } else { - this.toast.container.style.flexDirection = 'column'; - } - } 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.`; - }; -})(); - -// app.init(); -export default app; - -# File: ./content/body/toast.php - - - - - - 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

- - true, - "comment" => "Recommended for new and existing work.", - ]); ?> - true]); ?> - -

- In order to make toast notifications accessible, there are a few considerations: -

-
    -
  1. Toasts should be announced by screen readers when they appear.
  2. -
  3. Keyboard users should be able to navigate to and dismiss toasts.
  4. -
  5. Toasts should support different levels of severity (normal, error, warning, success), each with a unique color for visual distinction.
  6. -
-

- 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. -

- -
-
-
- - -
-
- -
- - - - - - -
-
-
- -
- - - - -
-
-
- -
- - -
-
-
- - -
-
- - -
-
-
- -
-
-
-
-
- - - - - - - - - - -# File: ./content/head/toast.php - - -# File: ./content/bottom/toast.php - - \ No newline at end of file From 733f19074bffaa072025d2ce64bc58efcb9e61a4 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Mon, 8 Jul 2024 13:30:53 -0400 Subject: [PATCH 16/24] update description for accessible toasts --- content/body/toast.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/content/body/toast.php b/content/body/toast.php index f3fb3c6e..0896e987 100644 --- a/content/body/toast.php +++ b/content/body/toast.php @@ -70,7 +70,9 @@
  • 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. + 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.

    From 2922425a099b6f2c3c2dd9dd057b97d638f41fb5 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Mon, 8 Jul 2024 13:55:14 -0400 Subject: [PATCH 17/24] namespace styles --- css/toast-demo.css | 8 ++++---- css/toast.css | 42 ++++++++++++++++++++--------------------- js/demos/toast.js | 8 +------- js/modules/es4/toast.js | 24 ++++++++++------------- js/modules/toast.js | 26 +++++++++++-------------- less/toast-demo.less | 6 +++--- less/toast.less | 40 +++++++++++++++++++-------------------- 7 files changed, 70 insertions(+), 84 deletions(-) diff --git a/css/toast-demo.css b/css/toast-demo.css index 8fc2fa88..564699b5 100644 --- a/css/toast-demo.css +++ b/css/toast-demo.css @@ -1,18 +1,18 @@ -.controls { +.enable-toast__controls { margin-bottom: 1.25rem; display: flex; flex-direction: column; gap: 0.625rem; } -.controls .control-group { +.enable-toast__controls .enable-toast__control-group { display: flex; flex-direction: column; margin-bottom: 0.625rem; } -.controls label { +.enable-toast__controls label { font-weight: normal; } -.controls .button-group { +.enable-toast__controls .enable-toast__button-group { display: flex; gap: 0.625rem; } diff --git a/css/toast.css b/css/toast.css index c525f2d2..d7c14add 100644 --- a/css/toast.css +++ b/css/toast.css @@ -1,38 +1,38 @@ -.toast-container { +.enable-toast__container { position: fixed; z-index: 9999; display: flex; transition: all 0.5s ease-in-out; } -.toast-container.bottom-right, -.toast-container.bottom-left, -.toast-container.bottom-center { +.enable-toast__container.enable-toast__container--bottom-right, +.enable-toast__container.enable-toast__container--bottom-left, +.enable-toast__container.enable-toast__container--bottom-center { bottom: 20px; } -.toast-container.top-right, -.toast-container.top-left, -.toast-container.top-center { +.enable-toast__container.enable-toast__container--top-right, +.enable-toast__container.enable-toast__container--top-left, +.enable-toast__container.enable-toast__container--top-center { top: 20px; } -.toast-container.bottom-right, -.toast-container.top-right { +.enable-toast__container.enable-toast__container--bottom-right, +.enable-toast__container.enable-toast__container--top-right { right: 20px; } -.toast-container.bottom-left, -.toast-container.top-left { +.enable-toast__container.enable-toast__container--bottom-left, +.enable-toast__container.enable-toast__container--top-left { left: 20px; } -.toast-container.top-center, -.toast-container.bottom-center, -.toast-container.middle-center { +.enable-toast__container.enable-toast__container--top-center, +.enable-toast__container.enable-toast__container--bottom-center, +.enable-toast__container.enable-toast__container--middle-center { left: 50%; transform: translateX(-50%); } -.toast-container.middle-center { +.enable-toast__container.enable-toast__container--middle-center { top: 50%; transform: translate(-50%, -50%); } -.toast { +.enable-toast__toast { display: flex; align-items: center; justify-content: space-between; @@ -45,23 +45,23 @@ opacity: 0; transition: opacity 0.5s ease-in-out; } -.toast.toast-visible { +.enable-toast__toast.enable-toast__toast--visible { opacity: 1; } -.toast.toast-exit { +.enable-toast__toast.enable-toast__toast--exit { opacity: 0; } -.toast button { +.enable-toast__toast .enable-toast__close-button { background: none; border: none; color: #fff; font-size: 16px; cursor: pointer; } -.toast button:hover { +.enable-toast__toast .enable-toast__close-button:hover { color: #ff0000; } -.toast-rack { +.enable-toast__rack { position: fixed; top: 20px; right: 20px; diff --git a/js/demos/toast.js b/js/demos/toast.js index 4a325ff9..1190fc68 100644 --- a/js/demos/toast.js +++ b/js/demos/toast.js @@ -82,12 +82,7 @@ const app = new (function () { this.positionChangeEvent = (e) => { try { - this.toast.container.className = `toast-container ${e.target.value}`; - if (e.target.value.includes('top')) { - this.toast.container.style.flexDirection = 'column-reverse'; - } else { - this.toast.container.style.flexDirection = 'column'; - } + this.toast.container.className = `enable-toast__container enable-toast__container--${e.target.value}`; } catch (error) { console.error('Error changing toast position:', error); } @@ -103,5 +98,4 @@ const app = new (function () { }; })(); -// app.init(); export default app; diff --git a/js/modules/es4/toast.js b/js/modules/es4/toast.js index 3635c1c5..a561a68f 100644 --- a/js/modules/es4/toast.js +++ b/js/modules/es4/toast.js @@ -38,12 +38,7 @@ const toastModule = new function() { // Create the container for the toasts createContainer(position) { const container = document.createElement('div'); - container.className = `toast-container ${position}`; - if (position.includes('top')) { - container.style.flexDirection = 'column-reverse'; - } else { - container.style.flexDirection = 'column'; - } + container.className = `enable-toast__container enable-toast__container--${position}`; return container; } @@ -62,7 +57,7 @@ const toastModule = new function() { toastElement.offsetHeight; // Force reflow to ensure the element is rendered before adding the visible class setTimeout(() => { - toastElement.classList.add('toast-visible'); + toastElement.classList.add('enable-toast__toast--visible'); }, 100); // Slight delay to ensure screen readers catch the change // Update the toast rack with the new toast @@ -75,11 +70,11 @@ const toastModule = new function() { createToastElement(toastData) { const { message, level, id } = toastData; const toast = document.createElement('div'); - toast.className = 'toast'; + toast.className = 'enable-toast__toast'; toast.style.backgroundColor = this.levels[level]?.color || '#333'; toast.setAttribute('tabindex', '-1'); - toast.setAttribute('aria-live', this.ariaLive); // Set aria-live attribute - toast.setAttribute('role', 'alert'); // Set aria-live attribute + toast.setAttribute('aria-live', this.ariaLive); + toast.setAttribute('role', 'alert'); toast.setAttribute('data-id', id); const messageSpan = document.createElement('span'); @@ -87,6 +82,7 @@ const toastModule = new function() { toast.appendChild(messageSpan); const closeButton = document.createElement('button'); + closeButton.className = 'enable-toast__close-button'; closeButton.setAttribute('aria-label', 'close alert'); closeButton.textContent = '✖'; closeButton.addEventListener('click', () => { @@ -102,7 +98,7 @@ const toastModule = new function() { dismissToast(toastData) { const toastElement = this.container.querySelector(`[data-id="${toastData.id}"]`); if (toastElement) { - toastElement.classList.add('toast-exit'); + toastElement.classList.add('enable-toast__toast--exit'); setTimeout(() => { toastElement.remove(); this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); @@ -127,9 +123,9 @@ const toastModule = new function() { this.visibleQueue.forEach((toast, index) => { const toastElement = this.container.querySelector(`[data-id="${toast.id}"]`); if (index < this.maxVisible) { - toastElement.classList.add('toast-visible'); + toastElement.classList.add('enable-toast__toast--visible'); } else { - toastElement.classList.remove('toast-visible'); + toastElement.classList.remove('enable-toast__toast--visible'); } }); @@ -150,7 +146,7 @@ const toastModule = new function() { this.toastQueue.forEach(toastData => { const { message, level, id } = toastData; const toastElement = document.createElement('div'); - toastElement.className = 'toast toast-visible'; + toastElement.className = 'enable-toast__toast enable-toast__toast--visible'; toastElement.style.backgroundColor = this.levels[level]?.color || '#333'; toastElement.setAttribute('data-id', id); diff --git a/js/modules/toast.js b/js/modules/toast.js index 4443a2a2..2fe4ba72 100644 --- a/js/modules/toast.js +++ b/js/modules/toast.js @@ -38,12 +38,7 @@ const toastModule = new function() { // Create the container for the toasts createContainer(position) { const container = document.createElement('div'); - container.className = `toast-container ${position}`; - if (position.includes('top')) { - container.style.flexDirection = 'column-reverse'; - } else { - container.style.flexDirection = 'column'; - } + container.className = `enable-toast__container enable-toast__container--${position}`; return container; } @@ -62,7 +57,7 @@ const toastModule = new function() { toastElement.offsetHeight; // Force reflow to ensure the element is rendered before adding the visible class setTimeout(() => { - toastElement.classList.add('toast-visible'); + toastElement.classList.add('enable-toast__toast--visible'); }, 100); // Slight delay to ensure screen readers catch the change // Update the toast rack with the new toast @@ -75,11 +70,11 @@ const toastModule = new function() { createToastElement(toastData) { const { message, level, id } = toastData; const toast = document.createElement('div'); - toast.className = 'toast'; + toast.className = 'enable-toast__toast'; toast.style.backgroundColor = this.levels[level]?.color || '#333'; toast.setAttribute('tabindex', '-1'); - toast.setAttribute('aria-live', this.ariaLive); // Set aria-live attribute - toast.setAttribute('role', 'alert'); // Set aria-live attribute + toast.setAttribute('aria-live', this.ariaLive); + toast.setAttribute('role', 'alert'); toast.setAttribute('data-id', id); const messageSpan = document.createElement('span'); @@ -87,6 +82,7 @@ const toastModule = new function() { toast.appendChild(messageSpan); const closeButton = document.createElement('button'); + closeButton.className = 'enable-toast__close-button'; closeButton.setAttribute('aria-label', 'close alert'); closeButton.textContent = '✖'; closeButton.addEventListener('click', () => { @@ -102,7 +98,7 @@ const toastModule = new function() { dismissToast(toastData) { const toastElement = this.container.querySelector(`[data-id="${toastData.id}"]`); if (toastElement) { - toastElement.classList.add('toast-exit'); + toastElement.classList.add('enable-toast__toast--exit'); setTimeout(() => { toastElement.remove(); this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); @@ -127,9 +123,9 @@ const toastModule = new function() { this.visibleQueue.forEach((toast, index) => { const toastElement = this.container.querySelector(`[data-id="${toast.id}"]`); if (index < this.maxVisible) { - toastElement.classList.add('toast-visible'); + toastElement.classList.add('enable-toast__toast--visible'); } else { - toastElement.classList.remove('toast-visible'); + toastElement.classList.remove('enable-toast__toast--visible'); } }); @@ -150,7 +146,7 @@ const toastModule = new function() { this.toastQueue.forEach(toastData => { const { message, level, id } = toastData; const toastElement = document.createElement('div'); - toastElement.className = 'toast toast-visible'; + toastElement.className = 'enable-toast__toast enable-toast__toast--visible'; toastElement.style.backgroundColor = this.levels[level]?.color || '#333'; toastElement.setAttribute('data-id', id); @@ -193,4 +189,4 @@ const toastModule = new function() { } }; -export default toastModule; +export default toastModule; \ No newline at end of file diff --git a/less/toast-demo.less b/less/toast-demo.less index ea65360c..827cf297 100644 --- a/less/toast-demo.less +++ b/less/toast-demo.less @@ -4,13 +4,13 @@ @font-size: 1rem; // 16px converted to rem // Controls styles -.controls { +.enable-toast__controls { margin-bottom: @margin; display: flex; flex-direction: column; gap: @gap; - .control-group { + .enable-toast__control-group { display: flex; flex-direction: column; margin-bottom: @gap; @@ -20,7 +20,7 @@ font-weight: normal; } - .button-group { + .enable-toast__button-group { display: flex; gap: @gap; } diff --git a/less/toast.less b/less/toast.less index 4b737d23..b32836bb 100644 --- a/less/toast.less +++ b/less/toast.less @@ -14,49 +14,49 @@ @rack-max-height: 400px; // Toast container positions -.toast-container { +.enable-toast__container { position: fixed; z-index: 9999; display: flex; transition: @toast-transition; - &.bottom-right, - &.bottom-left, - &.bottom-center { + &.enable-toast__container--bottom-right, + &.enable-toast__container--bottom-left, + &.enable-toast__container--bottom-center { bottom: 20px; } - &.top-right, - &.top-left, - &.top-center { + &.enable-toast__container--top-right, + &.enable-toast__container--top-left, + &.enable-toast__container--top-center { top: 20px; } - &.bottom-right, - &.top-right { + &.enable-toast__container--bottom-right, + &.enable-toast__container--top-right { right: 20px; } - &.bottom-left, - &.top-left { + &.enable-toast__container--bottom-left, + &.enable-toast__container--top-left { left: 20px; } - &.top-center, - &.bottom-center, - &.middle-center { + &.enable-toast__container--top-center, + &.enable-toast__container--bottom-center, + &.enable-toast__container--middle-center { left: 50%; transform: translateX(-50%); } - &.middle-center { + &.enable-toast__container--middle-center { top: 50%; transform: translate(-50%, -50%); } } // Toast element styles -.toast { +.enable-toast__toast { display: flex; align-items: center; justify-content: space-between; @@ -69,15 +69,15 @@ opacity: 0; transition: opacity 0.5s ease-in-out; - &.toast-visible { + &.enable-toast__toast--visible { opacity: 1; } - &.toast-exit { + &.enable-toast__toast--exit { opacity: 0; } - button { + .enable-toast__close-button { background: none; border: none; color: @toast-close-color; @@ -91,7 +91,7 @@ } // Toast rack styles -.toast-rack { +.enable-toast__rack { position: fixed; top: 20px; right: 20px; From 14b7e1c6355bd7bae67e967c7156b26b8209bcbe Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Mon, 8 Jul 2024 14:12:12 -0400 Subject: [PATCH 18/24] update __container and __close-button BEM style --- js/demos/toast.js | 2 +- js/modules/es4/toast.js | 16 ++++++++-------- js/modules/toast.js | 16 ++++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/js/demos/toast.js b/js/demos/toast.js index 1190fc68..b984269c 100644 --- a/js/demos/toast.js +++ b/js/demos/toast.js @@ -82,7 +82,7 @@ const app = new (function () { this.positionChangeEvent = (e) => { try { - this.toast.container.className = `enable-toast__container enable-toast__container--${e.target.value}`; + this.toast.container.className = `enable-toast enable-toast--${e.target.value}`; } catch (error) { console.error('Error changing toast position:', error); } diff --git a/js/modules/es4/toast.js b/js/modules/es4/toast.js index a561a68f..f4655ee3 100644 --- a/js/modules/es4/toast.js +++ b/js/modules/es4/toast.js @@ -38,7 +38,7 @@ const toastModule = new function() { // Create the container for the toasts createContainer(position) { const container = document.createElement('div'); - container.className = `enable-toast__container enable-toast__container--${position}`; + container.className = `enable-toast enable-toast--${position}`; return container; } @@ -57,7 +57,7 @@ const toastModule = new function() { toastElement.offsetHeight; // Force reflow to ensure the element is rendered before adding the visible class setTimeout(() => { - toastElement.classList.add('enable-toast__toast--visible'); + toastElement.classList.add('enable-toast--visible'); }, 100); // Slight delay to ensure screen readers catch the change // Update the toast rack with the new toast @@ -70,7 +70,7 @@ const toastModule = new function() { createToastElement(toastData) { const { message, level, id } = toastData; const toast = document.createElement('div'); - toast.className = 'enable-toast__toast'; + toast.className = 'enable-toast__item'; toast.style.backgroundColor = this.levels[level]?.color || '#333'; toast.setAttribute('tabindex', '-1'); toast.setAttribute('aria-live', this.ariaLive); @@ -82,7 +82,7 @@ const toastModule = new function() { toast.appendChild(messageSpan); const closeButton = document.createElement('button'); - closeButton.className = 'enable-toast__close-button'; + closeButton.className = 'enable-toast__close'; closeButton.setAttribute('aria-label', 'close alert'); closeButton.textContent = '✖'; closeButton.addEventListener('click', () => { @@ -98,7 +98,7 @@ const toastModule = new function() { dismissToast(toastData) { const toastElement = this.container.querySelector(`[data-id="${toastData.id}"]`); if (toastElement) { - toastElement.classList.add('enable-toast__toast--exit'); + toastElement.classList.add('enable-toast--exit'); setTimeout(() => { toastElement.remove(); this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); @@ -123,9 +123,9 @@ const toastModule = new function() { this.visibleQueue.forEach((toast, index) => { const toastElement = this.container.querySelector(`[data-id="${toast.id}"]`); if (index < this.maxVisible) { - toastElement.classList.add('enable-toast__toast--visible'); + toastElement.classList.add('enable-toast--visible'); } else { - toastElement.classList.remove('enable-toast__toast--visible'); + toastElement.classList.remove('enable-toast--visible'); } }); @@ -146,7 +146,7 @@ const toastModule = new function() { this.toastQueue.forEach(toastData => { const { message, level, id } = toastData; const toastElement = document.createElement('div'); - toastElement.className = 'enable-toast__toast enable-toast__toast--visible'; + toastElement.className = 'enable-toast__item enable-toast--visible'; toastElement.style.backgroundColor = this.levels[level]?.color || '#333'; toastElement.setAttribute('data-id', id); diff --git a/js/modules/toast.js b/js/modules/toast.js index 2fe4ba72..a9eaa495 100644 --- a/js/modules/toast.js +++ b/js/modules/toast.js @@ -38,7 +38,7 @@ const toastModule = new function() { // Create the container for the toasts createContainer(position) { const container = document.createElement('div'); - container.className = `enable-toast__container enable-toast__container--${position}`; + container.className = `enable-toast enable-toast--${position}`; return container; } @@ -57,7 +57,7 @@ const toastModule = new function() { toastElement.offsetHeight; // Force reflow to ensure the element is rendered before adding the visible class setTimeout(() => { - toastElement.classList.add('enable-toast__toast--visible'); + toastElement.classList.add('enable-toast--visible'); }, 100); // Slight delay to ensure screen readers catch the change // Update the toast rack with the new toast @@ -70,7 +70,7 @@ const toastModule = new function() { createToastElement(toastData) { const { message, level, id } = toastData; const toast = document.createElement('div'); - toast.className = 'enable-toast__toast'; + toast.className = 'enable-toast__item'; toast.style.backgroundColor = this.levels[level]?.color || '#333'; toast.setAttribute('tabindex', '-1'); toast.setAttribute('aria-live', this.ariaLive); @@ -82,7 +82,7 @@ const toastModule = new function() { toast.appendChild(messageSpan); const closeButton = document.createElement('button'); - closeButton.className = 'enable-toast__close-button'; + closeButton.className = 'enable-toast__close'; closeButton.setAttribute('aria-label', 'close alert'); closeButton.textContent = '✖'; closeButton.addEventListener('click', () => { @@ -98,7 +98,7 @@ const toastModule = new function() { dismissToast(toastData) { const toastElement = this.container.querySelector(`[data-id="${toastData.id}"]`); if (toastElement) { - toastElement.classList.add('enable-toast__toast--exit'); + toastElement.classList.add('enable-toast--exit'); setTimeout(() => { toastElement.remove(); this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); @@ -123,9 +123,9 @@ const toastModule = new function() { this.visibleQueue.forEach((toast, index) => { const toastElement = this.container.querySelector(`[data-id="${toast.id}"]`); if (index < this.maxVisible) { - toastElement.classList.add('enable-toast__toast--visible'); + toastElement.classList.add('enable-toast--visible'); } else { - toastElement.classList.remove('enable-toast__toast--visible'); + toastElement.classList.remove('enable-toast--visible'); } }); @@ -146,7 +146,7 @@ const toastModule = new function() { this.toastQueue.forEach(toastData => { const { message, level, id } = toastData; const toastElement = document.createElement('div'); - toastElement.className = 'enable-toast__toast enable-toast__toast--visible'; + toastElement.className = 'enable-toast__item enable-toast--visible'; toastElement.style.backgroundColor = this.levels[level]?.color || '#333'; toastElement.setAttribute('data-id', id); From 8bffff1c85943dc913a7189fba7fc47b6ba6cc81 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Thu, 11 Jul 2024 13:52:03 -0400 Subject: [PATCH 19/24] fix/improve BEM usage --- css/toast-demo.css | 6 +- css/toast.css | 36 ++++++------ less/toast-demo.less | 6 +- less/toast.less | 132 +++++++++++++++++++++---------------------- 4 files changed, 90 insertions(+), 90 deletions(-) diff --git a/css/toast-demo.css b/css/toast-demo.css index 564699b5..e0a0c11c 100644 --- a/css/toast-demo.css +++ b/css/toast-demo.css @@ -4,15 +4,15 @@ flex-direction: column; gap: 0.625rem; } -.enable-toast__controls .enable-toast__control-group { +.enable-toast__controls__control-group { display: flex; flex-direction: column; margin-bottom: 0.625rem; } -.enable-toast__controls label { +.enable-toast__controls__label { font-weight: normal; } -.enable-toast__controls .enable-toast__button-group { +.enable-toast__controls__button-group { display: flex; gap: 0.625rem; } diff --git a/css/toast.css b/css/toast.css index d7c14add..57ca2d95 100644 --- a/css/toast.css +++ b/css/toast.css @@ -4,31 +4,31 @@ display: flex; transition: all 0.5s ease-in-out; } -.enable-toast__container.enable-toast__container--bottom-right, -.enable-toast__container.enable-toast__container--bottom-left, -.enable-toast__container.enable-toast__container--bottom-center { +.enable-toast__container--bottom-right, +.enable-toast__container--bottom-left, +.enable-toast__container--bottom-center { bottom: 20px; } -.enable-toast__container.enable-toast__container--top-right, -.enable-toast__container.enable-toast__container--top-left, -.enable-toast__container.enable-toast__container--top-center { +.enable-toast__container--top-right, +.enable-toast__container--top-left, +.enable-toast__container--top-center { top: 20px; } -.enable-toast__container.enable-toast__container--bottom-right, -.enable-toast__container.enable-toast__container--top-right { +.enable-toast__container--bottom-right, +.enable-toast__container--top-right { right: 20px; } -.enable-toast__container.enable-toast__container--bottom-left, -.enable-toast__container.enable-toast__container--top-left { +.enable-toast__container--bottom-left, +.enable-toast__container--top-left { left: 20px; } -.enable-toast__container.enable-toast__container--top-center, -.enable-toast__container.enable-toast__container--bottom-center, -.enable-toast__container.enable-toast__container--middle-center { +.enable-toast__container--top-center, +.enable-toast__container--bottom-center, +.enable-toast__container--middle-center { left: 50%; transform: translateX(-50%); } -.enable-toast__container.enable-toast__container--middle-center { +.enable-toast__container--middle-center { top: 50%; transform: translate(-50%, -50%); } @@ -45,20 +45,20 @@ opacity: 0; transition: opacity 0.5s ease-in-out; } -.enable-toast__toast.enable-toast__toast--visible { +.enable-toast__toast--visible { opacity: 1; } -.enable-toast__toast.enable-toast__toast--exit { +.enable-toast__toast--exit { opacity: 0; } -.enable-toast__toast .enable-toast__close-button { +.enable-toast__close-button { background: none; border: none; color: #fff; font-size: 16px; cursor: pointer; } -.enable-toast__toast .enable-toast__close-button:hover { +.enable-toast__close-button:hover { color: #ff0000; } .enable-toast__rack { diff --git a/less/toast-demo.less b/less/toast-demo.less index 827cf297..f10cf6fd 100644 --- a/less/toast-demo.less +++ b/less/toast-demo.less @@ -10,17 +10,17 @@ flex-direction: column; gap: @gap; - .enable-toast__control-group { + &__control-group { display: flex; flex-direction: column; margin-bottom: @gap; } - label { + &__label { font-weight: normal; } - .enable-toast__button-group { + &__button-group { display: flex; gap: @gap; } diff --git a/less/toast.less b/less/toast.less index b32836bb..8702bdfb 100644 --- a/less/toast.less +++ b/less/toast.less @@ -14,70 +14,71 @@ @rack-max-height: 400px; // Toast container positions -.enable-toast__container { - position: fixed; - z-index: 9999; - display: flex; - transition: @toast-transition; +.enable-toast { + &__container { + position: fixed; + z-index: 9999; + display: flex; + transition: @toast-transition; - &.enable-toast__container--bottom-right, - &.enable-toast__container--bottom-left, - &.enable-toast__container--bottom-center { - bottom: 20px; - } + &--bottom-right, + &--bottom-left, + &--bottom-center { + bottom: 20px; + } - &.enable-toast__container--top-right, - &.enable-toast__container--top-left, - &.enable-toast__container--top-center { - top: 20px; - } + &--top-right, + &--top-left, + &--top-center { + top: 20px; + } - &.enable-toast__container--bottom-right, - &.enable-toast__container--top-right { - right: 20px; - } + &--bottom-right, + &--top-right { + right: 20px; + } - &.enable-toast__container--bottom-left, - &.enable-toast__container--top-left { - left: 20px; - } + &--bottom-left, + &--top-left { + left: 20px; + } - &.enable-toast__container--top-center, - &.enable-toast__container--bottom-center, - &.enable-toast__container--middle-center { - left: 50%; - transform: translateX(-50%); - } + &--top-center, + &--bottom-center, + &--middle-center { + left: 50%; + transform: translateX(-50%); + } - &.enable-toast__container--middle-center { - top: 50%; - transform: translate(-50%, -50%); + &--middle-center { + top: 50%; + transform: translate(-50%, -50%); + } } -} -// Toast element styles -.enable-toast__toast { - 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; + &__toast { + 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; - &.enable-toast__toast--visible { - opacity: 1; - } + &--visible { + opacity: 1; + } - &.enable-toast__toast--exit { - opacity: 0; + &--exit { + opacity: 0; + } } - .enable-toast__close-button { + &__close-button { background: none; border: none; color: @toast-close-color; @@ -88,19 +89,18 @@ color: @toast-close-hover-color; } } -} -// Toast rack styles -.enable-toast__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; + &__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; + } } From 1d4f140bdd1e42b1e5a61428f482129f74106268 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Tue, 16 Jul 2024 14:22:02 -0400 Subject: [PATCH 20/24] comment --- .gitignore | 1 + js/test/toast-1.js | 67 +++++++++++++++++++++++ js/test/toast.js | 131 +++++++++++++++++++++++++++++++++++++++++++++ less/toast.less | 1 + 4 files changed, 200 insertions(+) create mode 100644 js/test/toast-1.js create mode 100644 js/test/toast.js diff --git a/.gitignore b/.gitignore index 250a7c02..e691470c 100755 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ report/* # auto-gen PHP files/logs php_errors.log +.aider* 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.less b/less/toast.less index 8702bdfb..164fb73d 100644 --- a/less/toast.less +++ b/less/toast.less @@ -90,6 +90,7 @@ } } + // toast rack &__rack { position: fixed; top: 20px; From a8590c768f1725348f607fc6fe6cd74cd1a376d2 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Tue, 16 Jul 2024 14:24:04 -0400 Subject: [PATCH 21/24] revert --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e691470c..83c9fad4 100755 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,4 @@ report/* .idea # auto-gen PHP files/logs -php_errors.log -.aider* +php_errors.log \ No newline at end of file From 79c82d8ad64a8194dd7cfcb2f8b0ad44245c0bfe Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Wed, 9 Oct 2024 10:44:18 -0400 Subject: [PATCH 22/24] fix styles, debugging styles --- js/modules/toast.js | 16 ++++++++-------- less/toast.less | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/js/modules/toast.js b/js/modules/toast.js index a9eaa495..fbf32cf8 100644 --- a/js/modules/toast.js +++ b/js/modules/toast.js @@ -38,7 +38,7 @@ const toastModule = new function() { // Create the container for the toasts createContainer(position) { const container = document.createElement('div'); - container.className = `enable-toast enable-toast--${position}`; + container.className = `enable-toast__container enable-toast__container--${position}`; return container; } @@ -57,7 +57,7 @@ const toastModule = new function() { toastElement.offsetHeight; // Force reflow to ensure the element is rendered before adding the visible class setTimeout(() => { - toastElement.classList.add('enable-toast--visible'); + 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 @@ -66,7 +66,7 @@ const toastModule = new function() { this.updateToggleRackButton(); } - // Create the individual toast element + // Create the individual toast element (Using enable-toast__item) createToastElement(toastData) { const { message, level, id } = toastData; const toast = document.createElement('div'); @@ -98,7 +98,7 @@ const toastModule = new function() { dismissToast(toastData) { const toastElement = this.container.querySelector(`[data-id="${toastData.id}"]`); if (toastElement) { - toastElement.classList.add('enable-toast--exit'); + toastElement.classList.add('enable-toast__item--exit'); setTimeout(() => { toastElement.remove(); this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); @@ -123,9 +123,9 @@ const toastModule = new function() { this.visibleQueue.forEach((toast, index) => { const toastElement = this.container.querySelector(`[data-id="${toast.id}"]`); if (index < this.maxVisible) { - toastElement.classList.add('enable-toast--visible'); + toastElement.classList.add('enable-toast__item--visible'); } else { - toastElement.classList.remove('enable-toast--visible'); + toastElement.classList.remove('enable-toast__item--visible'); } }); @@ -146,7 +146,7 @@ const toastModule = new function() { this.toastQueue.forEach(toastData => { const { message, level, id } = toastData; const toastElement = document.createElement('div'); - toastElement.className = 'enable-toast__item enable-toast--visible'; + toastElement.className = 'enable-toast__item enable-toast__item--visible'; toastElement.style.backgroundColor = this.levels[level]?.color || '#333'; toastElement.setAttribute('data-id', id); @@ -189,4 +189,4 @@ const toastModule = new function() { } }; -export default toastModule; \ No newline at end of file +export default toastModule; diff --git a/less/toast.less b/less/toast.less index 164fb73d..61337b5e 100644 --- a/less/toast.less +++ b/less/toast.less @@ -13,13 +13,22 @@ @rack-width: 300px; @rack-max-height: 400px; -// Toast container positions +// Debugging colors for visibility +@debug-border-color: yellow; +@debug-background: rgba(255, 255, 0, 0.2); +@debug-z-index: 9999; + +// Toast container positions with added debugging styles .enable-toast { &__container { position: fixed; - z-index: 9999; + z-index: @debug-z-index; // Increased z-index for debugging visibility display: flex; transition: @toast-transition; + border: 2px dashed @debug-border-color; // Debugging border to visualize container + + // Debugging background to visualize container + background-color: @debug-background; &--bottom-right, &--bottom-left, @@ -56,7 +65,8 @@ } } - &__toast { + // Toast item styles with debugging styles for visibility + &__item { display: flex; align-items: center; justify-content: space-between; @@ -69,6 +79,10 @@ opacity: 0; transition: opacity 0.5s ease-in-out; + // Debugging border and background to visualize the toast + border: 2px solid red; + background-color: @debug-background; + &--visible { opacity: 1; } @@ -78,19 +92,21 @@ } } - &__close-button { + &__close { background: none; border: none; color: @toast-close-color; font-size: 16px; cursor: pointer; + // Debugging hover color &:hover { color: @toast-close-hover-color; + border: 1px solid @debug-border-color; // Debugging border } } - // toast rack + // Toast rack with debugging styles &__rack { position: fixed; top: 20px; @@ -103,5 +119,14 @@ overflow-y: auto; display: none; z-index: 9998; + + // Debugging styles + background-color: rgba( + 0, + 255, + 0, + 0.2 + ); // Light green background for rack visibility + border: 2px dashed orange; } } From e46ecddf44217f58bbe7777678bd33649434d5e3 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Wed, 9 Oct 2024 10:48:30 -0400 Subject: [PATCH 23/24] remove debugging, fix flex direction --- less/toast.less | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/less/toast.less b/less/toast.less index 61337b5e..b2da24b7 100644 --- a/less/toast.less +++ b/less/toast.less @@ -13,22 +13,14 @@ @rack-width: 300px; @rack-max-height: 400px; -// Debugging colors for visibility -@debug-border-color: yellow; -@debug-background: rgba(255, 255, 0, 0.2); -@debug-z-index: 9999; - -// Toast container positions with added debugging styles +// Toast container positions .enable-toast { &__container { position: fixed; - z-index: @debug-z-index; // Increased z-index for debugging visibility + z-index: 9999; display: flex; + flex-direction: column; transition: @toast-transition; - border: 2px dashed @debug-border-color; // Debugging border to visualize container - - // Debugging background to visualize container - background-color: @debug-background; &--bottom-right, &--bottom-left, @@ -65,7 +57,7 @@ } } - // Toast item styles with debugging styles for visibility + // Toast item styles &__item { display: flex; align-items: center; @@ -79,10 +71,6 @@ opacity: 0; transition: opacity 0.5s ease-in-out; - // Debugging border and background to visualize the toast - border: 2px solid red; - background-color: @debug-background; - &--visible { opacity: 1; } @@ -99,14 +87,12 @@ font-size: 16px; cursor: pointer; - // Debugging hover color &:hover { color: @toast-close-hover-color; - border: 1px solid @debug-border-color; // Debugging border } } - // Toast rack with debugging styles + // Toast rack &__rack { position: fixed; top: 20px; @@ -119,14 +105,5 @@ overflow-y: auto; display: none; z-index: 9998; - - // Debugging styles - background-color: rgba( - 0, - 255, - 0, - 0.2 - ); // Light green background for rack visibility - border: 2px dashed orange; } } From 1486b9494f894443ac39b1b6a1103f900b845243 Mon Sep 17 00:00:00 2001 From: Otto Wachter Date: Wed, 9 Oct 2024 10:53:47 -0400 Subject: [PATCH 24/24] es4 module --- js/modules/es4/toast.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/js/modules/es4/toast.js b/js/modules/es4/toast.js index f4655ee3..1da066f7 100644 --- a/js/modules/es4/toast.js +++ b/js/modules/es4/toast.js @@ -38,7 +38,7 @@ const toastModule = new function() { // Create the container for the toasts createContainer(position) { const container = document.createElement('div'); - container.className = `enable-toast enable-toast--${position}`; + container.className = `enable-toast__container enable-toast__container--${position}`; return container; } @@ -57,7 +57,7 @@ const toastModule = new function() { toastElement.offsetHeight; // Force reflow to ensure the element is rendered before adding the visible class setTimeout(() => { - toastElement.classList.add('enable-toast--visible'); + 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 @@ -66,7 +66,7 @@ const toastModule = new function() { this.updateToggleRackButton(); } - // Create the individual toast element + // Create the individual toast element (Using enable-toast__item) createToastElement(toastData) { const { message, level, id } = toastData; const toast = document.createElement('div'); @@ -98,7 +98,7 @@ const toastModule = new function() { dismissToast(toastData) { const toastElement = this.container.querySelector(`[data-id="${toastData.id}"]`); if (toastElement) { - toastElement.classList.add('enable-toast--exit'); + toastElement.classList.add('enable-toast__item--exit'); setTimeout(() => { toastElement.remove(); this.visibleQueue = this.visibleQueue.filter(t => t.id !== toastData.id); @@ -123,9 +123,9 @@ const toastModule = new function() { this.visibleQueue.forEach((toast, index) => { const toastElement = this.container.querySelector(`[data-id="${toast.id}"]`); if (index < this.maxVisible) { - toastElement.classList.add('enable-toast--visible'); + toastElement.classList.add('enable-toast__item--visible'); } else { - toastElement.classList.remove('enable-toast--visible'); + toastElement.classList.remove('enable-toast__item--visible'); } }); @@ -146,7 +146,7 @@ const toastModule = new function() { this.toastQueue.forEach(toastData => { const { message, level, id } = toastData; const toastElement = document.createElement('div'); - toastElement.className = 'enable-toast__item enable-toast--visible'; + toastElement.className = 'enable-toast__item enable-toast__item--visible'; toastElement.style.backgroundColor = this.levels[level]?.color || '#333'; toastElement.setAttribute('data-id', id);