实施 #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 的可访问名: ```json { "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](https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt)_
Activity
yinlianghui commentedon Aug 6, 2026
认领:objectui 分片 PM 滚动补位第 22 单
会话:
session_01GTRjn8xBqp75dk7kFupVRt分支:
claude/issue-3408-charcount-live-regionWorktree:
objectui-issue-3408域:objectui 分片(fields + i18n locales)
文件面:
packages/fields/src/widgets/TextAreaField.tsx、packages/i18n/src/locales/*.ts(如需「接近上限」新键则十包齐)+useFieldTranslation.tsFIELD_DEFAULTS + 相应测试(含更新 #3406 钉住 aria-live 的既有 pin)+.changeset/*.md(patch)。当前在飞 0。方向裁定(PM,否决窗口):A + B 组合,外加 C 的 describedby 半边(三者兼容,合并修掉实测的全部三个结构问题)——
{n}/{max}数字aria-hidden="true"(不再兼任 live region 本体);aria-live="polite"状态节点,输入停顿 ~1s(debounce)后才更新(GOV.UK character-count 形制);aria-describedby把 textarea 与计数说明关联 —— 聚焦时读一次「有上限」,替代被动重复轰炸;验收:复刻 issue 的 52 击键探针 —— 修后播报次数应为个位数(仅阈值内的 debounce 快照);聚焦时 describedby 生效;
fields.textarea.characterCount键继续消费,新键(如 nearLimit 文案)十包齐。Generated by Claude Code
refactor(fields): derive the counter status region's silence instead …
style(fields): blank line between the threshold helper and the widget (…
yinlianghui commentedon Aug 6, 2026
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_01GTRjn8xBqp75dk7kFupVRtGenerated by Claude Code
fix(fields): 让 TextAreaField 字数统计的可访问名跟随会话语言 (objectstack-ai#3406) (o…
fix(fields): 字数统计不再每次击键整句重播,改用 describedby + 阈值门控的 debounce 状态区 (obje…
fix(fields): share the character counter across TextAreaField's two e…