feat(centralui): DialogHost ShowAsync<T> custom-content + focus trap/restore + backdrop hook (T33a)
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// Focus management helpers for the centralised DialogHost (T33a). Small and
|
||||
// dependency-free: capture/restore the trigger element's focus across a modal's
|
||||
// lifetime, and trap Tab focus inside an open modal. Blazor calls these via
|
||||
// JS interop (window.sbDialog.*); they are no-ops when there is nothing to act
|
||||
// on, so prerender / loose-mode test calls do no harm.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// CSS selector for elements that can receive keyboard focus within the modal.
|
||||
const FOCUSABLE =
|
||||
"a[href],button:not([disabled]),input:not([disabled])," +
|
||||
"select:not([disabled]),textarea:not([disabled])," +
|
||||
"[tabindex]:not([tabindex='-1'])";
|
||||
|
||||
// The element that had focus when the most recent dialog opened. Module-level
|
||||
// so it survives between the capture (on open) and restore (on close) calls.
|
||||
let savedActiveElement = null;
|
||||
|
||||
window.sbDialog = {
|
||||
// Stash the currently focused element so it can be restored on close.
|
||||
// Called BEFORE the modal steals focus.
|
||||
captureActiveElement: function () {
|
||||
savedActiveElement = document.activeElement;
|
||||
},
|
||||
|
||||
// Return focus to the stashed element if it is still in the document,
|
||||
// then clear the stash so a later dialog starts fresh.
|
||||
restoreActiveElement: function () {
|
||||
const el = savedActiveElement;
|
||||
savedActiveElement = null;
|
||||
if (el && typeof el.focus === 'function' && el.isConnected) {
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
// Cycle focus within the modal: at the first focusable element a
|
||||
// Shift+Tab wraps to the last; at the last a Tab wraps to the first. In
|
||||
// both cases the default browser move (which would leave the modal) is
|
||||
// prevented. Anywhere else, the native Tab order applies untouched.
|
||||
focusTrap: function (modalEl, shiftKey) {
|
||||
if (!modalEl) return;
|
||||
const focusable = modalEl.querySelectorAll(FOCUSABLE);
|
||||
if (focusable.length === 0) return;
|
||||
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
const active = document.activeElement;
|
||||
|
||||
// Blazor interop does not hand us the DOM event, but window.event is
|
||||
// populated during the synchronous keydown dispatch on the engines we
|
||||
// target — guard the preventDefault so the wrap still works if it is
|
||||
// ever absent (the explicit .focus() override is the real trap).
|
||||
const evt = window.event;
|
||||
if (shiftKey && active === first) {
|
||||
last.focus();
|
||||
if (evt) evt.preventDefault();
|
||||
} else if (!shiftKey && active === last) {
|
||||
first.focus();
|
||||
if (evt) evt.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user