Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried role="dialog" — and several close buttons had no accessible name. - The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook, Settings) now carry role="dialog" + an accessible name. They are dockable, tiling windows, so they are non-modal dialogs (intentionally no aria-modal). - The four unlabelled close buttons (theme, prompt, cookbook, settings) get an aria-label so they no longer read as just "heavy multiplication x". - styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" + aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus — restore focus to the triggering element on close and trap Tab within the dialog (they already moved focus in on open). tests/test_dialog_aria.py pins the roles, labels, and focus management.
This commit is contained in:
@@ -242,7 +242,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<!-- Memory Management Modal -->
|
<!-- Memory Management Modal -->
|
||||||
<div id="memory-modal" class="modal hidden">
|
<div id="memory-modal" class="modal hidden">
|
||||||
<div class="modal-content memory-modal-content" style="background:var(--bg)">
|
<div class="modal-content memory-modal-content" role="dialog" aria-label="Brain" style="background:var(--bg)">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/></svg>Brain</h4>
|
<h4><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/></svg>Brain</h4>
|
||||||
<button class="close-btn" id="close-memory-modal" aria-label="Close memory modal">✖</button>
|
<button class="close-btn" id="close-memory-modal" aria-label="Close memory modal">✖</button>
|
||||||
@@ -432,14 +432,14 @@
|
|||||||
|
|
||||||
<!-- Theme Popup (floating panel) -->
|
<!-- Theme Popup (floating panel) -->
|
||||||
<div id="theme-modal" class="modal hidden">
|
<div id="theme-modal" class="modal hidden">
|
||||||
<div id="theme-popup" class="modal-content admin-modal-content" style="background:var(--bg)">
|
<div id="theme-popup" class="modal-content admin-modal-content" role="dialog" aria-label="Theme" style="background:var(--bg)">
|
||||||
<div class="modal-header theme-popup-header" id="theme-popup-header">
|
<div class="modal-header theme-popup-header" id="theme-popup-header">
|
||||||
<h4><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><circle cx="12" cy="12" r="10"/><path d="M12 2a7 7 0 0 0 0 20 4 4 0 0 1 0-8 4 4 0 0 0 0-8"/><circle cx="8" cy="9" r="1.5" fill="currentColor"/><circle cx="15" cy="14" r="1.5" fill="currentColor"/><circle cx="9" cy="15" r="1.5" fill="currentColor"/></svg>Theme</h4>
|
<h4><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><circle cx="12" cy="12" r="10"/><path d="M12 2a7 7 0 0 0 0 20 4 4 0 0 1 0-8 4 4 0 0 0 0-8"/><circle cx="8" cy="9" r="1.5" fill="currentColor"/><circle cx="15" cy="14" r="1.5" fill="currentColor"/><circle cx="9" cy="15" r="1.5" fill="currentColor"/></svg>Theme</h4>
|
||||||
<button type="button" class="theme-opacity-wrap theme-opacity-toggle hidden" id="theme-opacity-wrap" title="Fade this window to preview the page behind it" aria-pressed="false">
|
<button type="button" class="theme-opacity-wrap theme-opacity-toggle hidden" id="theme-opacity-wrap" title="Fade this window to preview the page behind it" aria-pressed="false">
|
||||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
<span class="theme-opacity-label">Peek</span>
|
<span class="theme-opacity-label">Peek</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="close-btn" id="close-theme-popup">✖</button>
|
<button class="close-btn" id="close-theme-popup" aria-label="Close theme">✖</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Theme tabs -->
|
<!-- Theme tabs -->
|
||||||
<div class="admin-tabs" id="theme-tabs">
|
<div class="admin-tabs" id="theme-tabs">
|
||||||
@@ -1115,10 +1115,10 @@
|
|||||||
|
|
||||||
<!-- Character (custom preset) modal -->
|
<!-- Character (custom preset) modal -->
|
||||||
<div id="custom-preset-modal" class="modal hidden">
|
<div id="custom-preset-modal" class="modal hidden">
|
||||||
<div class="modal-content preset-modal-content" style="background:var(--bg)">
|
<div class="modal-content preset-modal-content" role="dialog" aria-label="Prompt" style="background:var(--bg)">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="m18 2 4 4"/><path d="m17 7 3-3"/><path d="M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5"/><path d="m9 11 4 4"/><path d="m5 19-3 3"/><path d="m14 4 6 6"/></svg>Prompt</h4>
|
<h4><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="m18 2 4 4"/><path d="m17 7 3-3"/><path d="M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5"/><path d="m9 11 4 4"/><path d="m5 19-3 3"/><path d="m14 4 6 6"/></svg>Prompt</h4>
|
||||||
<button class="close-btn" id="close-custom-preset">✖</button>
|
<button class="close-btn" id="close-custom-preset" aria-label="Close prompt">✖</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body preset-modal-body">
|
<div class="modal-body preset-modal-body">
|
||||||
<div id="char-fields-wrap">
|
<div id="char-fields-wrap">
|
||||||
@@ -1262,7 +1262,7 @@
|
|||||||
|
|
||||||
<!-- Rename Session Modal -->
|
<!-- Rename Session Modal -->
|
||||||
<div id="rename-session-modal" class="modal hidden">
|
<div id="rename-session-modal" class="modal hidden">
|
||||||
<div class="modal-content" style="width: 400px;">
|
<div class="modal-content" role="dialog" aria-label="Rename session" style="width: 400px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4>Rename Session</h4>
|
<h4>Rename Session</h4>
|
||||||
<button class="close-btn" id="close-rename-session" aria-label="Close rename session modal">✖</button>
|
<button class="close-btn" id="close-rename-session" aria-label="Close rename session modal">✖</button>
|
||||||
@@ -1288,10 +1288,10 @@
|
|||||||
|
|
||||||
<!-- Cookbook Modal -->
|
<!-- Cookbook Modal -->
|
||||||
<div id="cookbook-modal" class="modal hidden">
|
<div id="cookbook-modal" class="modal hidden">
|
||||||
<div class="modal-content" style="width: min(780px, 92vw); height: 94vh; max-height: 94vh; background: var(--bg);">
|
<div class="modal-content" role="dialog" aria-label="Cookbook" style="width: min(780px, 92vw); height: 94vh; max-height: 94vh; background: var(--bg);">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 style="margin:0;margin-right:auto"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>Cookbook</h4>
|
<h4 style="margin:0;margin-right:auto"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>Cookbook</h4>
|
||||||
<button class="close-btn" id="close-cookbook-modal">✖</button>
|
<button class="close-btn" id="close-cookbook-modal" aria-label="Close cookbook">✖</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body cookbook-body"></div>
|
<div class="modal-body cookbook-body"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1299,14 +1299,14 @@
|
|||||||
|
|
||||||
<!-- Settings Modal (all users) -->
|
<!-- Settings Modal (all users) -->
|
||||||
<div id="settings-modal" class="modal hidden">
|
<div id="settings-modal" class="modal hidden">
|
||||||
<div class="modal-content settings-modal-content">
|
<div class="modal-content settings-modal-content" role="dialog" aria-label="Settings">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4><span style="vertical-align:-1px;margin-right:6px;font-size:15px">⚙</span>Settings</h4>
|
<h4><span style="vertical-align:-1px;margin-right:6px;font-size:15px">⚙</span>Settings</h4>
|
||||||
<button type="button" class="theme-opacity-wrap theme-opacity-toggle hidden" id="settings-opacity-wrap" title="Fade this window to preview the page behind it" aria-pressed="false">
|
<button type="button" class="theme-opacity-wrap theme-opacity-toggle hidden" id="settings-opacity-wrap" title="Fade this window to preview the page behind it" aria-pressed="false">
|
||||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||||
<span class="theme-opacity-label">Peek</span>
|
<span class="theme-opacity-label">Peek</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="close-btn">✖</button>
|
<button class="close-btn" aria-label="Close settings">✖</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-toggle-sub" style="padding:0 12px 8px;opacity:0.6;font-size:11px;">Toggle on/off visibility of tools and modules across the interface.</div>
|
<div class="admin-toggle-sub" style="padding:0 12px 8px;opacity:0.6;font-size:11px;">Toggle on/off visibility of tools and modules across the interface.</div>
|
||||||
<div class="settings-layout">
|
<div class="settings-layout">
|
||||||
|
|||||||
@@ -579,8 +579,8 @@ export function styledConfirm(message, { confirmText = 'Confirm', cancelText = '
|
|||||||
overlay.id = 'styled-confirm-overlay';
|
overlay.id = 'styled-confirm-overlay';
|
||||||
overlay.className = 'modal';
|
overlay.className = 'modal';
|
||||||
overlay.innerHTML =
|
overlay.innerHTML =
|
||||||
'<div class="modal-content styled-confirm-box">' +
|
'<div class="modal-content styled-confirm-box" role="dialog" aria-modal="true" aria-labelledby="styled-confirm-title" aria-describedby="styled-confirm-msg">' +
|
||||||
'<div class="modal-header"><h4>Confirm</h4></div>' +
|
'<div class="modal-header"><h4 id="styled-confirm-title">Confirm</h4></div>' +
|
||||||
'<div class="modal-body"><p id="styled-confirm-msg"></p></div>' +
|
'<div class="modal-body"><p id="styled-confirm-msg"></p></div>' +
|
||||||
'<div class="modal-footer">' +
|
'<div class="modal-footer">' +
|
||||||
'<button id="styled-confirm-cancel"></button>' +
|
'<button id="styled-confirm-cancel"></button>' +
|
||||||
@@ -600,6 +600,8 @@ export function styledConfirm(message, { confirmText = 'Confirm', cancelText = '
|
|||||||
okBtn.className = danger ? 'confirm-btn confirm-btn-danger' : 'confirm-btn confirm-btn-primary';
|
okBtn.className = danger ? 'confirm-btn confirm-btn-danger' : 'confirm-btn confirm-btn-primary';
|
||||||
cancelBtn.className = 'confirm-btn confirm-btn-secondary';
|
cancelBtn.className = 'confirm-btn confirm-btn-secondary';
|
||||||
|
|
||||||
|
// Remember what had focus so we can restore it when the dialog closes.
|
||||||
|
const _prevFocus = document.activeElement;
|
||||||
overlay.classList.remove('hidden');
|
overlay.classList.remove('hidden');
|
||||||
overlay.style.display = '';
|
overlay.style.display = '';
|
||||||
|
|
||||||
@@ -610,6 +612,7 @@ export function styledConfirm(message, { confirmText = 'Confirm', cancelText = '
|
|||||||
cancelBtn.removeEventListener('click', onCancel);
|
cancelBtn.removeEventListener('click', onCancel);
|
||||||
overlay.removeEventListener('click', onBackdrop);
|
overlay.removeEventListener('click', onBackdrop);
|
||||||
document.removeEventListener('keydown', onKey);
|
document.removeEventListener('keydown', onKey);
|
||||||
|
try { _prevFocus && _prevFocus.focus && _prevFocus.focus(); } catch {}
|
||||||
resolve(result);
|
resolve(result);
|
||||||
}
|
}
|
||||||
function onOk() { cleanup(true); }
|
function onOk() { cleanup(true); }
|
||||||
@@ -626,6 +629,13 @@ export function styledConfirm(message, { confirmText = 'Confirm', cancelText = '
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
cleanup(false);
|
cleanup(false);
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
// Trap focus inside the dialog so Tab can't wander to the page behind.
|
||||||
|
e.preventDefault();
|
||||||
|
const f = [cancelBtn, okBtn];
|
||||||
|
const i = f.indexOf(document.activeElement);
|
||||||
|
const n = e.shiftKey ? (i <= 0 ? f.length - 1 : i - 1) : (i >= f.length - 1 ? 0 : i + 1);
|
||||||
|
f[n].focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,7 +666,7 @@ export function styledPrompt(message, {
|
|||||||
overlay.id = 'styled-prompt-overlay';
|
overlay.id = 'styled-prompt-overlay';
|
||||||
overlay.className = 'modal';
|
overlay.className = 'modal';
|
||||||
overlay.innerHTML =
|
overlay.innerHTML =
|
||||||
'<div class="modal-content styled-confirm-box styled-prompt-box">' +
|
'<div class="modal-content styled-confirm-box styled-prompt-box" role="dialog" aria-modal="true" aria-labelledby="styled-prompt-title" aria-describedby="styled-prompt-msg">' +
|
||||||
'<div class="modal-header"><h4 id="styled-prompt-title"></h4></div>' +
|
'<div class="modal-header"><h4 id="styled-prompt-title"></h4></div>' +
|
||||||
'<div class="modal-body">' +
|
'<div class="modal-body">' +
|
||||||
'<p id="styled-prompt-msg"></p>' +
|
'<p id="styled-prompt-msg"></p>' +
|
||||||
@@ -685,6 +695,8 @@ export function styledPrompt(message, {
|
|||||||
okBtn.textContent = confirmText;
|
okBtn.textContent = confirmText;
|
||||||
cancelBtn.textContent = cancelText;
|
cancelBtn.textContent = cancelText;
|
||||||
|
|
||||||
|
// Remember what had focus so we can restore it when the dialog closes.
|
||||||
|
const _prevFocus = document.activeElement;
|
||||||
overlay.classList.remove('hidden');
|
overlay.classList.remove('hidden');
|
||||||
overlay.style.display = '';
|
overlay.style.display = '';
|
||||||
|
|
||||||
@@ -696,6 +708,7 @@ export function styledPrompt(message, {
|
|||||||
overlay.removeEventListener('click', onBackdrop);
|
overlay.removeEventListener('click', onBackdrop);
|
||||||
document.removeEventListener('keydown', onKey);
|
document.removeEventListener('keydown', onKey);
|
||||||
input.removeEventListener('keydown', onInputKey);
|
input.removeEventListener('keydown', onInputKey);
|
||||||
|
try { _prevFocus && _prevFocus.focus && _prevFocus.focus(); } catch {}
|
||||||
resolve(result);
|
resolve(result);
|
||||||
}
|
}
|
||||||
function onOk() { cleanup((input.value || '').trim()); }
|
function onOk() { cleanup((input.value || '').trim()); }
|
||||||
@@ -707,6 +720,13 @@ export function styledPrompt(message, {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
cleanup(null);
|
cleanup(null);
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
// Trap focus inside the dialog (input → Cancel → OK → input …).
|
||||||
|
e.preventDefault();
|
||||||
|
const f = [input, cancelBtn, okBtn];
|
||||||
|
const i = f.indexOf(document.activeElement);
|
||||||
|
const n = e.shiftKey ? (i <= 0 ? f.length - 1 : i - 1) : (i >= f.length - 1 ? 0 : i + 1);
|
||||||
|
f[n].focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onInputKey(e) {
|
function onInputKey(e) {
|
||||||
|
|||||||
56
tests/test_dialog_aria.py
Normal file
56
tests/test_dialog_aria.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"""Pin the dialog accessibility semantics added for the roadmap a11y pass.
|
||||||
|
|
||||||
|
Screen readers only announce "dialog" (and its name) when the container
|
||||||
|
carries role="dialog" plus an accessible name. These checks lock that in for
|
||||||
|
the static modals in index.html and the JS-built confirm/prompt dialogs, and
|
||||||
|
guard against a close button shipping without an accessible label again.
|
||||||
|
|
||||||
|
Plain text/regex assertions (no bs4 dependency), matching the lightweight style
|
||||||
|
of the other tests in this suite.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_REPO = Path(__file__).resolve().parent.parent
|
||||||
|
_INDEX = (_REPO / "static" / "index.html").read_text(encoding="utf-8")
|
||||||
|
_UI = (_REPO / "static" / "js" / "ui.js").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_modals_expose_dialog_role_and_name():
|
||||||
|
# Each static tool window must announce itself as a named dialog. These are
|
||||||
|
# dockable/tiling windows, so they are role="dialog" WITHOUT aria-modal.
|
||||||
|
for name in ("Brain", "Theme", "Prompt", "Rename session", "Cookbook", "Settings"):
|
||||||
|
assert f'role="dialog" aria-label="{name}"' in _INDEX, f"missing dialog role/name for {name!r}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_modal_close_button_is_unlabeled():
|
||||||
|
# Every .close-btn must carry an accessible name (text glyph alone reads as
|
||||||
|
# "heavy multiplication x"). Catch any new close button that forgets one.
|
||||||
|
buttons = re.findall(r'<button[^>]*class="close-btn"[^>]*>', _INDEX)
|
||||||
|
assert buttons, "expected to find close-btn buttons in index.html"
|
||||||
|
unlabeled = [b for b in buttons if "aria-label=" not in b]
|
||||||
|
assert not unlabeled, f"close buttons missing aria-label: {unlabeled}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_styled_confirm_and_prompt_are_modal_dialogs():
|
||||||
|
# The JS-built confirm/prompt overlays ARE blocking modals, so they get
|
||||||
|
# role="dialog" + aria-modal="true" and are labelled by their title.
|
||||||
|
assert 'class="modal-content styled-confirm-box" role="dialog" aria-modal="true"' in _UI
|
||||||
|
assert 'aria-labelledby="styled-confirm-title"' in _UI
|
||||||
|
assert '<h4 id="styled-confirm-title">Confirm</h4>' in _UI
|
||||||
|
|
||||||
|
assert 'styled-prompt-box" role="dialog" aria-modal="true"' in _UI
|
||||||
|
assert 'aria-labelledby="styled-prompt-title"' in _UI
|
||||||
|
# The label/description targets the styled-prompt dialog points at must exist.
|
||||||
|
assert 'id="styled-prompt-title"' in _UI
|
||||||
|
assert 'id="styled-prompt-msg"' in _UI
|
||||||
|
|
||||||
|
|
||||||
|
def test_styled_dialogs_manage_focus():
|
||||||
|
# A dialog is only really accessible if it restores focus to the trigger on
|
||||||
|
# close and traps Tab while open. Both styledConfirm and styledPrompt should
|
||||||
|
# capture the previously-focused element, restore it, and trap Tab.
|
||||||
|
assert _UI.count("const _prevFocus = document.activeElement;") == 2
|
||||||
|
assert _UI.count("_prevFocus && _prevFocus.focus && _prevFocus.focus()") == 2
|
||||||
|
assert _UI.count("e.key === 'Tab'") == 2
|
||||||
|
|
||||||
Reference in New Issue
Block a user