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:
1 parent
d096b5f5d9
commit
012c66d12c
5 files changed
+108
-2
No files matched your search
+2
-2
@@ -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>
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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: '' }};
|
||||
|
||||
Reference in new issue
Block a user