eleven / messenger Public

web: make modal sheets accessible (role=dialog, focus trap + restore)

Sheets (new-chat, edit-group, edit-profile, members, add-people, add-device) were
plain divs: keyboard/screen-reader users could tab behind them and focus wasn't
restored on close. Add role=dialog/aria-modal, trap Tab within the sheet, move
focus in on open (unless the caller already focused a field), and restore focus to
the trigger on close.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Paul Campbell ff8ed3d18604ff102bdb4459c04af9aca598b492 parent da94774
1 file changed, +36 −1
  • web/static/chat.js +36 −1
diff --git a/web/static/chat.js b/web/static/chat.js
index b60edbd..d52e7a9 100644
--- a/web/static/chat.js
+++ b/web/static/chat.js
@@ -2574,22 +2574,57 @@ function renderText(el, text) {
// A centered modal sheet. Click the backdrop or Esc to dismiss.
function modal() {
+ const prevFocus = document.activeElement; // restored when the sheet closes
const backdrop = document.createElement("div");
backdrop.className = "overlay";
const card = document.createElement("div");
card.className = "card sheet";
+ card.setAttribute("role", "dialog");
+ card.setAttribute("aria-modal", "true");
+ card.tabIndex = -1; // focusable as a fallback when there's no field/button
backdrop.appendChild(card);
+ const focusables = () =>
+ [...card.querySelectorAll('a[href], button:not([disabled]), input, textarea, select, [tabindex]:not([tabindex="-1"])')].filter(
+ (el) => el.offsetParent !== null
+ );
const close = () => {
backdrop.remove();
document.removeEventListener("keydown", onKey);
+ if (prevFocus && prevFocus.focus) {
+ try {
+ prevFocus.focus();
+ } catch {}
+ }
};
const onKey = (e) => {
- if (e.key === "Escape") close();
+ if (e.key === "Escape") {
+ close();
+ return;
+ }
+ if (e.key === "Tab") {
+ // Trap Tab within the sheet so keyboard/screen-reader users can't tab behind it.
+ const f = focusables();
+ if (!f.length) return;
+ const first = f[0];
+ const last = f[f.length - 1];
+ if (e.shiftKey && document.activeElement === first) {
+ e.preventDefault();
+ last.focus();
+ } else if (!e.shiftKey && document.activeElement === last) {
+ e.preventDefault();
+ first.focus();
+ }
+ }
};
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) close();
});
document.addEventListener("keydown", onKey);
document.body.appendChild(backdrop);
+ // Move focus into the sheet once the caller has populated it — unless the caller
+ // already focused something inside (e.g. a screen-name field).
+ setTimeout(() => {
+ if (!card.contains(document.activeElement)) (focusables()[0] || card).focus();
+ }, 0);
return backdrop;
}