refactor(ui): move toast stack chrome to a server-rendered partial
Mirrors the detail-drawer move: the .app-toast-stack container is no longer lazy-created in JS on first toast, instead it lives in templates/partials/app-toast-stack.phtml and is mounted once in default.phtml alongside the confirm dialog. The aria-live attribute now sits in the SSR markup so screen readers register the live region from page load, not from the first toast. getToastStack() drops to a plain querySelector and warns via warnOnce when the partial is missing instead of constructing a fallback container. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -337,6 +337,7 @@ $componentPageConfig['moduleRuntimeComponents'] = $moduleRuntimeComponents;
|
|||||||
|
|
||||||
<script src="<?php e(assetVersion('vendor/fslightbox/fslightbox.js')); ?>"></script>
|
<script src="<?php e(assetVersion('vendor/fslightbox/fslightbox.js')); ?>"></script>
|
||||||
<?php require __DIR__ . '/partials/app-confirm-dialog.phtml'; ?>
|
<?php require __DIR__ . '/partials/app-confirm-dialog.phtml'; ?>
|
||||||
|
<?php require __DIR__ . '/partials/app-toast-stack.phtml'; ?>
|
||||||
<?php if ($isLoggedIn): ?>
|
<?php if ($isLoggedIn): ?>
|
||||||
<?php require __DIR__ . '/partials/app-search-dialog.phtml'; ?>
|
<?php require __DIR__ . '/partials/app-search-dialog.phtml'; ?>
|
||||||
<?php require __DIR__ . '/partials/app-session-warning-dialog.phtml'; ?>
|
<?php require __DIR__ . '/partials/app-session-warning-dialog.phtml'; ?>
|
||||||
|
|||||||
12
templates/partials/app-toast-stack.phtml
Normal file
12
templates/partials/app-toast-stack.phtml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async toast stack — global container for JS-triggered notifications.
|
||||||
|
*
|
||||||
|
* Mounted once at the bottom of the shell so any page can fire toasts via
|
||||||
|
* showAsyncFlash() without the JS having to lazy-create the container.
|
||||||
|
* Empty by default; toasts are appended/removed by web/js/components/app-async-flash.js.
|
||||||
|
*/
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="app-toast-stack" aria-live="polite"></div>
|
||||||
@@ -2,8 +2,12 @@ import { warnOnce } from '../core/app-dom.js';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Async Flash Messages & Loading State
|
* Async Flash Messages & Loading State
|
||||||
* Renders flash messages into the .app-toast-stack for JS-triggered notifications.
|
*
|
||||||
* Creates the stack container on demand if it doesn't exist yet.
|
* Renders flash messages into the .app-toast-stack for JS-triggered
|
||||||
|
* notifications. The stack container is rendered server-side by
|
||||||
|
* templates/partials/app-toast-stack.phtml (mounted once in default.phtml);
|
||||||
|
* this module only appends/removes toasts inside it.
|
||||||
|
*
|
||||||
* Provides loading cursor state management.
|
* Provides loading cursor state management.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -43,18 +47,13 @@ const defaultTimeouts = {
|
|||||||
const MAX_VISIBLE_TOASTS = 5;
|
const MAX_VISIBLE_TOASTS = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create the toast stack container.
|
* Resolve the server-rendered toast stack container.
|
||||||
* @returns {HTMLElement}
|
* Returns null if the partial is missing — callers must handle that case.
|
||||||
|
* @returns {HTMLElement|null}
|
||||||
*/
|
*/
|
||||||
function getToastStack() {
|
function getToastStack() {
|
||||||
let stack = document.querySelector('.app-toast-stack');
|
const stack = document.querySelector('.app-toast-stack');
|
||||||
if (!stack) {
|
return stack instanceof HTMLElement ? stack : null;
|
||||||
stack = document.createElement('div');
|
|
||||||
stack.className = 'app-toast-stack';
|
|
||||||
stack.setAttribute('aria-live', 'polite');
|
|
||||||
document.body.appendChild(stack);
|
|
||||||
}
|
|
||||||
return stack;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,6 +94,12 @@ function enforceMaxVisible(stack) {
|
|||||||
*/
|
*/
|
||||||
export function showAsyncFlash(type, message, timeout) {
|
export function showAsyncFlash(type, message, timeout) {
|
||||||
const stack = getToastStack();
|
const stack = getToastStack();
|
||||||
|
if (!stack) {
|
||||||
|
warnOnce('UI_EL_MISSING', 'Missing .app-toast-stack — partial not mounted?', {
|
||||||
|
module: 'async-flash',
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const effectiveTimeout = timeout ?? defaultTimeouts[type] ?? 5000;
|
const effectiveTimeout = timeout ?? defaultTimeouts[type] ?? 5000;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user