Skip to content

TextAreaField 字数统计的 aria-live 区每次击键都整句重播:打一句话读屏被打断 52 次(实测) #3408

Closed

Description

@yinlianghui

实施 #3406(字数统计 aria-label 键化)时按 PM 裁定只做键化、不改行为,把 #3406 正文提出的「aria-live + 每击键重算是否本身该重设计」拆出来单独立单,并补上实测。

实测(在 #3406 的分支上跑,行为与 origin/main 一致)

在 zh 会话下渲染 TextAreaField(maxLength: 500),逐字输入一句 52 字符的普通备注(Follow up with the customer about the renewal quote.),记录每次 change 之后 live region 的可访问名:

{
 "keystrokes": 52,
 "ariaLiveOnRegion": "polite",
 "regionIsAriaHidden": null,
 "textareaDescribedBy": null,
 "announcementsFired": 52,
 "distinctAnnouncements": 52,
 "charsSpokenPerKeystroke": 18,
 "totalCharsSpoken": 979,
 "first": "已输入 1 个字符,最多 500 个",
 "last": "已输入 52 个字符,最多 500 个"
}

即:52 次击键 = 52 次互不相同的整句播报,累计 979 个字符,没有任何 debounce、没有阈值门控。用户想输入的那句话本身只有 52 个字符 —— 播报量是输入量的近 19 倍,且每一次都会打断读屏对用户自己输入内容的回读。

另外两点顺带测到的、与之相关的结构问题:

  1. regionIsAriaHidden: null —— 可见的 {n}/{max} 数字没有 aria-hidden,它既是视觉计数器又是 live region 本体;
  2. textareaDescribedBy: null —— textarea 与这个计数器之间没有 aria-describedby 关联,所以聚焦字段时读屏不会提到「有字数上限」,只有开始打字之后才被动地一遍遍听见。

可达性

只在字段元数据声明了 maxLength 时渲染,且只有读屏用户感知 —— 与 #3406 同一块 DOM、同一批用户。不是死代码,当前每一个带 maxLength 的长文本字段都命中。

候选模式(未裁定,列出来供 triage)

  • A. debounce 后再播报:可见数字标 aria-hidden="true",另起一个 visually-hidden 的 aria-live="polite" 状态节点,输入停顿约 1s 后才更新。这是 GOV.UK Design System 的 character-count 组件采用的形状,也是改动面最小、语义最稳的一种。
  • B. 阈值门控:剩余字数进入末段(如剩余 10% 或最后 20 字)之前 live region 保持静默 —— 计数在快到上限时才真正有信息量。可与 A 叠加。
  • C. 取消 live region,改成 aria-describedby:计数器挂到 textarea 的 aria-describedby 上,聚焦时读一次、之后按需查询,完全不打断输入。信息可得性最好,但失去「接近上限」的主动提示,故通常要配 B。
  • D. 维持现状:显式列出以便被否掉,而不是默认继承。

个人倾向 A + B(先静默、临近上限再以 debounce 后的整句提示),但这是可访问性契约形状问题,不自行裁定。

与 #3406 的关系

不是 #3406 的子问题:PM 已明确把 #3406 的完成范围裁定为「键化,行为不变」,本条落在那条边界之外,故独立立单。也不构成阻塞 —— 本条可以在 #3406 落地前后任一时点做,只是两者会碰同一段 JSX(#3406 的 PR 里已把该 aria-live 属性用测试钉住,做本条时需要一并更新那条 pin)。新键 fields.textarea.characterCount 由 #3406 引入;若采用 B,可能还需要一条「接近上限」的新文案键,同样要十包齐。

未打标签,交 PM triage 定级 —— 我判断它是「当前有人踩得到的具体缺陷」而非观察类,但立单时的严重度判断本就不可靠,不自行加权。


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 6, 2026
  2. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    认领:objectui 分片 PM 滚动补位第 22 单
    会话:session_01GTRjn8xBqp75dk7kFupVRt
    分支:claude/issue-3408-charcount-live-region
    Worktree:objectui-issue-3408
    域:objectui 分片(fields + i18n locales)
    文件面:packages/fields/src/widgets/TextAreaField.tsx、packages/i18n/src/locales/*.ts(如需「接近上限」新键则十包齐)+ useFieldTranslation.ts FIELD_DEFAULTS + 相应测试(含更新 #3406 钉住 aria-live 的既有 pin)+ .changeset/*.md(patch)。当前在飞 0。

    方向裁定(PM,否决窗口):A + B 组合,外加 C 的 describedby 半边(三者兼容,合并修掉实测的全部三个结构问题)——

    1. 可见 {n}/{max} 数字 aria-hidden="true"(不再兼任 live region 本体);
    2. 独立 visually-hidden aria-live="polite" 状态节点,输入停顿 ~1s(debounce)后才更新(GOV.UK character-count 形制);
    3. 阈值门控:剩余进入末段(取剩余 ≤10% 或 ≤20 字符,二者取先到)前保持静默 —— 计数临近上限才有信息量;
    4. aria-describedby 把 textarea 与计数说明关联 —— 聚焦时读一次「有上限」,替代被动重复轰炸;
    5. D(现状)否决。
      验收:复刻 issue 的 52 击键探针 —— 修后播报次数应为个位数(仅阈值内的 debounce 快照);聚焦时 describedby 生效;fields.textarea.characterCount 键继续消费,新键(如 nearLimit 文案)十包齐。

    Generated by Claude Code

  3. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    PM 验收:ACCEPT → PR #3416 已转 ready + auto-merge(CI 16/16 终态:13 绿 3 预期跳过,mergeable clean),跟到 MERGED。

    落地内容(裁定 A+B+describedby 全落):计数块拆三节点(可见数字 aria-hidden / describedby 说明追加拼接不覆盖宿主 FormControl 注入的错误 id / 独立隐藏 polite 区);阈值 剩余 ≤ max(ceil(10%), 20) + 1s debounce(假计时器测试,零真实 sleep);修前探针与 issue 逐字段一致,修后同探针播报归零至阈值内快照。两处超预期的工程判断,追认:① 静默逻辑从 setState-in-effect 改派生值,顺带堵掉「离开警告带再回来重播旧计数」的隐蔽坑(反向验证 C 肢单独钉住);② 新键 charactersRemaining 刻意用冒号后置形式 —— {{count}} 开头的句式会触发 i18next 复数键(_one)查找而十包只声明基础键,后置形式 count=1 天然正确。三肢反向验证(整体回退 16 红 / 摘门控 4 红 15 绿证两机制独立承重 / 换遮罩 1 红)方向全部预判命中。

    流程记账:dev 曾停机等 monitor(停机后不会被唤醒),SendMessage 原地续跑一次后完整收尾 —— 接手协议按预期工作。

    衍生:#3417(FullscreenFieldEditor footer 裸数字计数,读屏全屏对话框内拿不到字数信息;建议排 #3416 合并后 —— 同段 footer 回调)已入队;#3418(createSafeTranslation 回退只替换首个占位符出现 —— 今日休眠,finding)留分诊轮。

    objectui 分片 PM session_01GTRjn8xBqp75dk7kFupVRt


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions