修复:历史加载错误行的重试按钮移出断言式 live region
- 内联错误行由 <p role=alert> 改为普通容器,role=alert 只包住错误文案本身 - 重试按钮留在 live region 之外,避免被卷进 assertive + atomic 的整体播报 - 组件用例断言 alert 不包含重试按钮,重试仍是可点击的唯一手动出路 - ADR 同步错误行的无障碍结构 Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
+19
@@ -136,6 +136,25 @@ describe('更早历史的入口', () => {
|
||||
fireEvent.click(view.getByText('重试'));
|
||||
expect(retry).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('断言式 live region 里不放可交互控件:重试按钮是 alert 之外的兄弟', () => {
|
||||
const retry = vi.fn();
|
||||
const view = renderConversation({
|
||||
historyHasMore: true,
|
||||
historyError: '读取更早的对话历史失败',
|
||||
onRetryEarlierHistory: retry,
|
||||
});
|
||||
|
||||
// `role="alert"` 隐含 aria-live="assertive" + aria-atomic="true":整段会被当成一条断言性
|
||||
// 播报,交互控件嵌在里面既可能不被读屏当成可聚焦按钮,点击也落在 live region 内部。
|
||||
const alert = view.getByRole('alert');
|
||||
expect(alert.textContent).toContain('加载更早对话失败');
|
||||
expect(alert.contains(view.getByText('重试'))).toBe(false);
|
||||
|
||||
// 拆开之后仍在同一行里,重试仍是唯一的手动出路。
|
||||
fireEvent.click(view.getByText('重试'));
|
||||
expect(retry).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('前插锚点的标记', () => {
|
||||
|
||||
+5
-6
@@ -37,11 +37,10 @@ export function DirectProjectHistoryErrorRow({
|
||||
onRetry: () => void;
|
||||
}) {
|
||||
return (
|
||||
<p
|
||||
className="flex items-center justify-center gap-2 py-2 text-xs text-[color:var(--platform-button-danger-text)]"
|
||||
role="alert"
|
||||
>
|
||||
{HISTORY_ERROR_TEXT}
|
||||
<div className="flex items-center justify-center gap-2 py-2 text-xs text-[color:var(--platform-button-danger-text)]">
|
||||
{/* `role="alert"` 隐含 assertive + atomic:只有文案本身进 live region,重试按钮留在外面,
|
||||
否则整段会被当成一条断言性播报,可聚焦控件也被卷进 live region 内部。 */}
|
||||
<span role="alert">{HISTORY_ERROR_TEXT}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-[color:var(--platform-subpanel-border)] px-2 py-0.5 text-xs text-[color:var(--platform-text-strong)] transition hover:bg-[var(--platform-neutral-bg)]"
|
||||
@@ -49,6 +48,6 @@ export function DirectProjectHistoryErrorRow({
|
||||
>
|
||||
{HISTORY_RETRY_TEXT}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ DirectProject 聊天区(`apps/ai-game-creator-shell/src/view/project-developme
|
||||
- 触发二(填充视口):首帧之后内容填不满视口(`scrollHeight <= clientHeight`)时继续加载,直到填满或 `hasMore=false`;不允许出现「历史比视口短、又没有按钮」的死局。
|
||||
- 两个触发都不越过既有的首屏订阅锚点 `lastCompletedItemId`;一次加载仍最多连拉 5 页(口径见 [`【ADR】DirectProject对话历史单一事实源-2026-09-16`](./【ADR】DirectProject对话历史单一事实源-2026-09-16.md))。
|
||||
- 加载中在列表最上方(比最旧一条回合更靠上)挂载一行 `role="status"`、`aria-live="polite"` 的「正在加载更早的对话」,带旋转圈;延迟 150ms 才显示,加载结束即卸载。它按需挂载,靠位置补偿(见第 2 条)保证下面的消息不跳。
|
||||
- 失败:挂起自动加载,列表顶部保留一行 `role="alert"` 的「加载更早对话失败 · 重试」,重试是可聚焦按钮;**不自动重试**,只有点重试(或切换项目)才重新开始;重试成功后错误行消失。
|
||||
- 失败:挂起自动加载,列表顶部保留一行内联错误行——`role="alert"` 只包住「加载更早对话失败」文案本身,重试是可聚焦按钮、留在 live region 之外(assertive + atomic 的 live region 里不放交互控件);**不自动重试**,只有点重试(或切换项目)才重新开始;重试成功后错误行消失。
|
||||
- 一次加载与它所属的**世代**绑定:切换项目或新起一次读取都推进世代号(`historyLoadTokenRef`),旧世代落地时整段失效——不合并条目、不写游标、不关加载态。只比项目路径不够:A→B→A 之后在飞的旧读取又落回同一个路径,原守卫放行,会把新一代的加载行与 `historyLoadingRef` 这道并发闸门一起改掉。
|
||||
|
||||
### 2. 前插锚定:回合 key + 块身份 + 偏移,关掉浏览器原生锚定
|
||||
|
||||
Reference in New Issue
Block a user