Skip to content

fix(desktop): announce successful settings save to assistive tech#356

Merged
ceilf6 merged 2 commits into
developfrom
improve/settings-save-announce
Jun 14, 2026
Merged

fix(desktop): announce successful settings save to assistive tech#356
ceilf6 merged 2 commits into
developfrom
improve/settings-save-announce

Conversation

@ceilf6

@ceilf6 ceilf6 commented Jun 14, 2026

Copy link
Copy Markdown
Owner

What & why

SettingsPanel announces a save failure to assistive tech via role="alert", but a save success ("✓ 已保存") was a plain conditionally-mounted <span> with no live-region — so screen-reader users got no confirmation the save actually worked. This is a WCAG 2.1 SC 4.1.3 (Status Messages) parity gap.

Closes #355

Change (single component + minimal CSS)

  • SettingsPanel.tsx: render the success confirmation as an always-present polite status live-region (role="status" aria-live="polite") whose text toggles on the existing saved flag. A live region must exist in the DOM before content is inserted to be announced reliably, so the container is always rendered and its text is empty until saved. The failure role="alert" paths are untouched.
  • theme.css: guard .settings-saved's margin-left with :not(:empty) so the now-always-rendered (empty when unsaved) span introduces no layout gap. Visual affordance is otherwise identical.

No change to settingsController, the IPC contract, or any other file. Live-region text is kept equivalent to the existing "✓ 已保存" visual cue (per repo-guard's optional issue-review note).

Verification

  • pnpm --filter @frontagent/desktop typecheck — clean
  • pnpm --filter @frontagent/desktop test — 96/96 pass
  • root biome check . — clean (only the pre-existing useTemplate info in hallucination-guard, unrelated)
  • quality:precommit pre-commit hook — green (lint/typecheck/test/test:workflows 32/32)
  • Manual a11y check: with saved=false the status region is empty and adds no visible gap; on a successful save it receives "✓ 已保存" and a polite-live screen reader announces it without moving focus.

Follows the desktop no-DOM-test architecture (no @testing-library/jsdom); semantics verified via typecheck + biome a11y rules + controlled diff + this manual note, consistent with #348/#350.

GitNexus Impact Summary

  • Risk level: LOW
  • Critical skeleton changes: None — presentational a11y markup + scoped CSS only; no exported signature, IPC contract, or store change.
  • GitNexus impact: impact(SettingsPanel, upstream) → risk LOW, d=1 App (renders it), d=2 main.tsx; 1 process (App → Submit, step 1), module Components. detect_changes(all) → 1 changed symbol (SettingsPanel, touched), 0 affected processes, risk low. theme.css is non-symbol CSS.
  • Verification: desktop typecheck clean, 96/96 tests, biome clean, precommit green (see above).

The SettingsPanel failure path announces via role="alert", but a
successful save ("✓ 已保存") was a plain conditionally-mounted span
with no live-region, so screen-reader users got no confirmation the
save worked (WCAG 2.1 SC 4.1.3 status-message parity gap).

Render the success confirmation as an always-present polite status
live-region (role="status" aria-live="polite") whose text toggles on
the existing `saved` flag, so the inserted text is reliably announced.
Visual affordance is unchanged; .settings-saved margin is guarded with
:not(:empty) so the always-rendered empty span adds no layout gap.

Closes #355

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ ceilf6/repo-guard

代码评审报告: fix(desktop): announce successful settings save to assistive tech

风险等级:
处理建议: 请求修改
决策摘要: 暂不建议合并:当前把 role="status" 放在 native <button> 内部,按钮后代语义通常会被平台无障碍树扁平化/呈现化,成功消息 live-region 可能无法可靠暴露,直接影响 #355 的核心验收项。

级联分析

  • 变更符号: SettingsPanel 的保存按钮成功状态标记;.settings-saved CSS 间距规则
  • 受影响流程: 桌面端设置保存成功后的可访问状态反馈;App → SettingsPanel → save
  • 变更集外调用方: GitNexus 摘要显示 d=1 App,d=2 main.tsx;无 IPC、controller、store 或导出签名变更
  • 置信度: medium

问题发现

  1. [中] 成功 live-region 放在 <button> 内,可能无法作为 status 暴露给辅助技术
    • 证据: diff 将 <span className="settings-saved" role="status" aria-live="polite"> 作为保存按钮的子节点渲染。native button / button role 的后代在无障碍树中通常会被扁平化为按钮名称文本,子元素语义角色不可靠保留;因此 role="status" 可能不会成为独立 live region。
    • 受影响调用方/流程: 设置保存成功后,屏幕阅读器用户仍可能收不到 #355 要求的 polite status announcement;这正是本 PR 要修复的验收路径。
    • 最小可行修复: 将 always-present role="status" aria-live="polite" 节点移出 <button>,作为按钮旁边的 sibling 或单独的 visually-hidden status region;如需保留按钮内视觉 “✓ 已保存”,可将视觉副本标为 aria-hidden="true",由按钮外的 live-region 负责公告。

行级发现

  • [apps/desktop/src/renderer/components/SettingsPanel.tsx:59] role="status" 放在 native <button> 的子元素上不可靠,按钮后代语义可能被无障碍树扁平化,导致 live-region 不被公告;请把 status live-region 移到按钮外,必要时保留按钮内 aria-hidden 的视觉副本。

Karpathy 评审

  • 假设: 当前实现假设按钮内部的 span[role=status] 会作为 live-region 暴露;这是浏览器/AT 组合中不稳的隐式语义假设。
  • 简洁性: 变更很小,但把公告语义嵌进按钮标签内会把“按钮名称”和“状态公告”两个概念耦合;按钮外独立 status region 更直接。
  • 结构质量: 无导出契约或控制器层级退化;主要问题是 ARIA 语义边界放错层,影响可验证性和可靠性。
  • 变更范围: 范围符合 issue 约束,CSS 改动局部;但 issue 写的“single file”与 PR 增加 CSS 文件不完全一致,当前 CSS 是为避免空 span layout gap 的小范围配套改动,不是主要风险。
  • 验证: typecheck/test/biome 不能证明实际 AT 公告;修复后需要基于目标浏览器 + SR 的手动验证确认按钮外 live-region 在成功保存时公告。

缺失覆盖

  • 修复后补充/更新手动 a11y 验证说明:确认 role="status" 节点不在 <button> 内,保存成功时从空内容变为 “✓ 已保存” 并被 polite announce,失败 role="alert" 行为不变。

<button type="button" className="btn btn-primary" onClick={save} disabled={saving}>
{saving ? '保存中…' : status === 'save-error' ? '重试保存' : '保存设置'}
{saved ? <span className="settings-saved">✓ 已保存</span> : null}
<span className="settings-saved" role="status" aria-live="polite">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

role="status" 放在 native <button> 的子元素上不可靠,按钮后代语义可能被无障碍树扁平化,导致 live-region 不被公告;请把 status live-region 移到按钮外,必要时保留按钮内 aria-hidden 的视觉副本。

repo-guard (CHANGES_REQUESTED) correctly noted that a role="status"
live-region nested inside a native <button> is unreliable: button
descendants are flattened into the button's accessible name, so the
nested live region may never be exposed to assistive tech — defeating
#355's acceptance criterion.

Move the always-present role="status" aria-live="polite" region out of
the <button> to be its sibling, so it is a top-level live region that
reliably announces the "✓ 已保存" success text. The existing
.settings-saved:not(:empty) margin rule still spaces it from the button;
the role="alert" failure path is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🛡️ ceilf6/repo-guard

代码评审报告: fix(desktop): announce successful settings save to assistive tech

风险等级:
处理建议: 批准
决策摘要: ** 可以合并;变更满足 #355 的核心验收标准,未触碰 IPC/controller 契约,剩余风险主要是视觉位置从按钮内部变为按钮旁侧但不构成阻塞。

级联分析

  • 变更符号: 原始模型未提供结构化级联字段。
  • 受影响流程: 原始模型未提供结构化级联字段。
  • 变更集外调用方: unknown
  • 置信度: degraded

问题发现

  1. [低] 成功提示的视觉位置不再是按钮内容的一部分
    • 证据: diff 将 {saved ? <span className="settings-saved">✓ 已保存</span> : null}<button> 内移到按钮后的独立 <span role="status">;CSS 通过 .settings-saved:not(:empty) 保留左边距。
    • 受影响调用方/流程: 设置保存成功后的视觉反馈;原先按钮宽度/按钮文本包含 “✓ 已保存”,现在提示显示在按钮旁侧。
    • 最小可行修复: 如果产品要求“视觉 affordance 完全等价于原先按钮内文本”,可保留按钮内可见提示,并另加按钮外的 live-region;如果按钮旁提示是可接受设计,则无需修改。

行级发现

  • 无明确变更行归属。

Karpathy 评审

  • 假设: 模型输出需要归一化为固定 Markdown 契约。
  • 简洁性: 已提取 summary、finding、evidence 与 fix;原始 prose 不再附在评论中,避免占用下游解析与代理上下文。
  • 变更范围: 原始模型未提供结构化范围字段。
  • 验证: 需要查看 CI、测试或人工 CR 证据补强合并信心。

缺失覆盖

  • 输出未命中 Repo Guard Markdown 契约;建议补充真实模型质量评估覆盖。

@ceilf6 ceilf6 merged commit 6bda08e into develop Jun 14, 2026
11 checks passed
@ceilf6 ceilf6 deleted the improve/settings-save-announce branch June 14, 2026 09:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(desktop): announce successful settings save to assistive tech (status live-region)

1 participant