Hide inactive flyouts from screen readers (+ visual-hide CSS)

Clean rebase of sheldon-im's #5404 (rebase-first).

Co-authored-by: sheldon-im <sheldon-im@users.noreply.github.com>
This commit is contained in:
nesquena-hermesandsheldon-im committed 2026-07-02 07:15:53 +00:00
1 parent d096b5f5d9
commit 012c66d12c
5 files changed
+108 -2

No files matched your search

+2 -2
View File
@@ -473,7 +473,7 @@
<div id="queueCard" class="queue-card" role="region" aria-label="Queued messages" aria-live="polite">
<div id="queueChips" class="queue-card-inner"></div>
</div>
<div class="approval-card" id="approvalCard" role="alertdialog" aria-labelledby="approvalHeading" aria-describedby="approvalDesc">
<div class="approval-card" id="approvalCard" role="alertdialog" aria-labelledby="approvalHeading" aria-describedby="approvalDesc" hidden aria-hidden="true" inert>
<div class="approval-inner">
<div class="approval-header">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
@@ -509,7 +509,7 @@
</div>
</div>
</div>
<div class="clarify-card" id="clarifyCard" role="dialog" aria-labelledby="clarifyHeading" aria-describedby="clarifyQuestion clarifyHint">
<div class="clarify-card" id="clarifyCard" role="dialog" aria-labelledby="clarifyHeading" aria-describedby="clarifyQuestion clarifyHint" hidden aria-hidden="true" inert>
<div class="clarify-inner">
<div class="clarify-header">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 17h.01"/><path d="M9.09 9a3 3 0 1 1 5.82 1c0 2-3 2-3 4"/><circle cx="12" cy="12" r="10"/></svg>
+25
View File
@@ -6066,6 +6066,26 @@ const APPROVAL_MIN_VISIBLE_MS = 30000;
// showApprovalCard moved above respondApproval
function _setPromptFlyoutHidden(card, hidden) {
if (!card) return;
if (hidden) {
card.setAttribute("aria-hidden", "true");
card.setAttribute("inert", "");
const markHidden = () => {
if (!card.classList || !card.classList.contains("visible")) card.hidden = true;
};
if (typeof setTimeout === "function") setTimeout(markHidden, 450);
else markHidden();
return;
}
card.hidden = false;
card.setAttribute("aria-hidden", "false");
card.removeAttribute("inert");
// Force the unhidden, pre-visible state to be observed so the existing
// transform/opacity transition can still animate when `.visible` is added.
void card.offsetHeight;
}
function _clearApprovalHideTimer() {
if (_approvalHideTimer) {
clearTimeout(_approvalHideTimer);
@@ -6099,6 +6119,7 @@ function hideApprovalCard(force=false) {
_resetApprovalCardState();
card.classList.remove("visible");
card.classList.remove("collapsed");
_setPromptFlyoutHidden(card, true);
_syncApprovalTranscriptSpace(null);
$("approvalCmd").textContent = "";
$("approvalDesc").textContent = "";
@@ -6261,6 +6282,7 @@ function showApprovalCard(pending, pendingCount) {
responding ? _approvalResponding.choice : null,
responding,
);
_setPromptFlyoutHidden(card, false);
card.classList.add("visible");
_syncApprovalCollapseButton(card);
_syncApprovalTranscriptSpace(card, {immediate: true});
@@ -7038,6 +7060,7 @@ function _ensureClarifyCardDom() {
card.setAttribute("role", "dialog");
card.setAttribute("aria-labelledby", "clarifyHeading");
card.setAttribute("aria-describedby", "clarifyQuestion clarifyHint");
_setPromptFlyoutHidden(card, true);
card.innerHTML = `
<div class="clarify-inner">
<div class="clarify-header">
@@ -7250,6 +7273,7 @@ function hideClarifyCard(force=false, reason="dismissed") {
_clarifySessionId = null;
_resetClarifyCardState();
card.classList.remove("visible");
_setPromptFlyoutHidden(card, true);
_syncClarifyTranscriptSpace(null);
if (typeof unlockComposerForClarify === "function") unlockComposerForClarify();
$("clarifyQuestion").textContent = "";
@@ -7368,6 +7392,7 @@ function showClarifyCard(pending) {
}
_clarifySetControlsDisabled(false, false);
_ensureClarifyResizeListener();
_setPromptFlyoutHidden(card, false);
card.classList.add("visible");
_syncClarifyCollapseButton(card);
_syncClarifyTranscriptSpace(card, {immediate: true});
+1
View File
@@ -1947,6 +1947,7 @@
.composer-flyout{position:relative;height:0;z-index:1;}
.composer-wrap.terminal-dock-visible .composer-flyout{z-index:4;}
/* ── Approval card ── */
.approval-card[hidden],.clarify-card[hidden]{display:none!important;}
.approval-card{position:absolute;left:0;right:0;bottom:-24px;max-width:var(--msg-max);margin:0 auto;padding:0 20px;box-sizing:border-box;width:100%;overflow:hidden;pointer-events:none;}
.approval-card.visible{pointer-events:auto;z-index:3;}
.approval-inner{background:var(--surface);backdrop-filter:blur(8px);border:1px solid var(--accent-bg-strong);border-radius:14px;padding:16px 18px 40px;transform:translateY(100%);opacity:0;transition:transform .4s cubic-bezier(.32,.72,.16,1),opacity .25s ease;}
@@ -0,0 +1,70 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
INDEX = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
STYLE = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
def _function_body(name: str, *, limit: int = 5000) -> str:
start = MESSAGES_JS.index(f"function {name}")
return MESSAGES_JS[start : start + limit]
def test_inactive_prompt_flyouts_are_hidden_from_accessibility_tree_initially():
"""Approval/clarify flyouts are not persistent dialogs when no prompt is pending."""
approval_start = INDEX.index('id="approvalCard"')
approval_tag = INDEX[INDEX.rfind("<div", 0, approval_start) : INDEX.index(">", approval_start) + 1]
clarify_start = INDEX.index('id="clarifyCard"')
clarify_tag = INDEX[INDEX.rfind("<div", 0, clarify_start) : INDEX.index(">", clarify_start) + 1]
for tag in (approval_tag, clarify_tag):
assert " hidden" in tag
assert 'aria-hidden="true"' in tag
assert " inert" in tag
assert 'role="alertdialog"' in approval_tag
assert 'role="dialog"' in clarify_tag
def test_prompt_flyout_hidden_css_guard_keeps_hidden_flyouts_display_none():
"""Future display rules must not override hidden prompt flyouts."""
compact_style = STYLE.replace(" ", "")
assert ".approval-card[hidden],.clarify-card[hidden]{display:none!important;}" in compact_style
def test_prompt_flyout_hidden_helper_toggles_hidden_aria_and_inert():
body = _function_body("_setPromptFlyoutHidden", limit=800)
assert 'card.setAttribute("aria-hidden", "true")' in body
assert 'card.setAttribute("aria-hidden", "false")' in body
assert 'card.setAttribute("inert", "")' in body
assert 'card.removeAttribute("inert")' in body
assert "card.hidden = true" in body
assert "card.hidden = false" in body
assert "void card.offsetHeight" in body
def test_approval_show_and_hide_toggle_accessibility_visibility():
hide_body = _function_body("hideApprovalCard")
show_body = _function_body("showApprovalCard")
assert 'card.classList.remove("visible")' in hide_body
assert "_setPromptFlyoutHidden(card, true)" in hide_body
assert "_setPromptFlyoutHidden(card, false)" in show_body
assert 'card.classList.add("visible")' in show_body
assert show_body.index("_setPromptFlyoutHidden(card, false)") < show_body.index('card.classList.add("visible")')
def test_clarify_show_and_hide_toggle_accessibility_visibility():
ensure_body = _function_body("_ensureClarifyCardDom")
hide_body = _function_body("hideClarifyCard")
show_body = _function_body("showClarifyCard")
assert "_setPromptFlyoutHidden(card, true)" in ensure_body
assert 'card.classList.remove("visible")' in hide_body
assert "_setPromptFlyoutHidden(card, true)" in hide_body
assert "_setPromptFlyoutHidden(card, false)" in show_body
assert 'card.classList.add("visible")' in show_body
assert show_body.index("_setPromptFlyoutHidden(card, false)") < show_body.index('card.classList.add("visible")')
@@ -62,6 +62,7 @@ def _run_failure_case(api_js: str) -> dict:
_extract_fn(MESSAGES_JS, "_renderPendingApprovalForActiveSession"),
_extract_fn(MESSAGES_JS, "_approvalResponseMatches"),
_extract_fn(MESSAGES_JS, "_setApprovalControlsDisabled"),
_extract_fn(MESSAGES_JS, "_setPromptFlyoutHidden"),
_extract_fn(MESSAGES_JS, "showApprovalCard"),
_extract_fn(MESSAGES_JS, "_restoreFailedApprovalResponse"),
_extract_fn(MESSAGES_JS, "respondApproval", prefix="async function "),
@@ -117,6 +118,10 @@ const card = {{
else this.values.delete(value);
}},
}},
hidden: false,
attributes: {{}},
setAttribute(name, value) {{ this.attributes[name] = String(value); }},
removeAttribute(name) {{ delete this.attributes[name]; }},
querySelector() {{ return null; }},
}};
const approvalDesc = {{ textContent: '' }};
@@ -239,6 +244,7 @@ def test_poll_rerender_keeps_inflight_buttons_disabled_and_blocks_duplicates():
_extract_fn(MESSAGES_JS, "_renderPendingApprovalForActiveSession"),
_extract_fn(MESSAGES_JS, "_approvalResponseMatches"),
_extract_fn(MESSAGES_JS, "_setApprovalControlsDisabled"),
_extract_fn(MESSAGES_JS, "_setPromptFlyoutHidden"),
_extract_fn(MESSAGES_JS, "showApprovalCard"),
_extract_fn(MESSAGES_JS, "_restoreFailedApprovalResponse"),
_extract_fn(MESSAGES_JS, "respondApproval", prefix="async function "),
@@ -291,6 +297,10 @@ const card = {{
else this.values.delete(value);
}},
}},
hidden: false,
attributes: {{}},
setAttribute(name, value) {{ this.attributes[name] = String(value); }},
removeAttribute(name) {{ delete this.attributes[name]; }},
querySelector() {{ return null; }},
}};
const approvalDesc = {{ textContent: '' }};