iter(g87): 轮播补焦点暂停与轮播语义

This commit is contained in:
HIsheR
2026-10-26 13:06:12 +08:00
parent 9ff2c7b651
commit cf4ec96946
3 changed files with 18 additions and 7 deletions
+8
View File
@@ -1052,6 +1052,14 @@
"acceptance": "overlay 新增 bindOutside(root, isOpen, close):仅展开期间挂全局 click、收起即摘,内部点击直接返回;8 个弹层全部改用它并删掉各自的重复实现;popover 删掉空 keydown,点弹层内部不再误关;npm run check 全绿", "acceptance": "overlay 新增 bindOutside(root, isOpen, close):仅展开期间挂全局 click、收起即摘,内部点击直接返回;8 个弹层全部改用它并删掉各自的重复实现;popover 删掉空 keydown,点弹层内部不再误关;npm run check 全绿",
"notes": "自补:稳健性 + 去重;展开动作本身也是 click,靠 root.contains 卫语句避免刚开就被关", "notes": "自补:稳健性 + 去重;展开动作本身也是 click,靠 root.contains 卫语句避免刚开就被关",
"status": "done" "status": "done"
},
{
"id": "g87",
"title": "轮播补焦点暂停与轮播语义",
"motivation": "自动播放只在 pointerenter 暂停——键盘用户没有 hover,Tab 进轮播后幻灯片照样自己换页,正在读的内容被抽走(WCAG 2.2.2);区域与幻灯片也只有笼统的 aria-label,读屏不知道这是轮播的第几张",
"acceptance": "焦点进入(focusin)与移出(focusout)同样暂停自动播放,两者都不在才前进;区域补 aria-roledescription=轮播,每张补 aria-roledescription=幻灯片 且标签为「第 x 张,共 n 张」;演示描述同步;npm run check 全绿",
"notes": "自补:无障碍;hover 与 focus 分开计数,避免一个 pointerleave 就误解除暂停",
"status": "open"
} }
] ]
} }
+1 -1
View File
@@ -667,7 +667,7 @@ const content = h(
section( section(
'sec-carousel', 'sec-carousel',
'轮播', '轮播',
'carousel:items 横排位移,指示点 / 箭头 / ←→ 键切换,autoplay 毫秒自动前进且悬停暂停;index 传信号即可外部受控。', 'carousel:items 横排位移,指示点 / 箭头 / ←→ 键切换,autoplay 毫秒自动前进,悬停或焦点进入即暂停;index 传信号即可外部受控。',
h( h(
'div', 'div',
{ class: 'row' }, { class: 'row' },
+9 -6
View File
@@ -19,7 +19,7 @@ export function carousel(props: CarouselProps): HTMLElement {
}; };
props.items.forEach((item, i) => { props.items.forEach((item, i) => {
track.append(h('div', { class: 'ua-carousel-slide', role: 'group', 'aria-label': `第 ${i + 1} 张` }, item)); track.append(h('div', { class: 'ua-carousel-slide', role: 'group', 'aria-roledescription': '幻灯片', 'aria-label': `第 ${i + 1} 张,共 ${total} 张` }, item));
const dot = h('button', { type: 'button', class: 'ua-carousel-dot', 'aria-label': `跳到第 ${i + 1} 张`, onClick: () => go(i) }) as HTMLButtonElement; const dot = h('button', { type: 'button', class: 'ua-carousel-dot', 'aria-label': `跳到第 ${i + 1} 张`, onClick: () => go(i) }) as HTMLButtonElement;
dots.push(dot); dots.push(dot);
dotsBar.append(dot); dotsBar.append(dot);
@@ -31,7 +31,7 @@ export function carousel(props: CarouselProps): HTMLElement {
const root = h( const root = h(
'div', 'div',
{ class: 'ua-carousel', tabindex: '0', role: 'region', 'aria-label': props.label ?? '轮播' }, { class: 'ua-carousel', tabindex: '0', role: 'region', 'aria-roledescription': '轮播', 'aria-label': props.label ?? '轮播' },
track, track,
arrow(-1, '上一张'), arrow(-1, '上一张'),
arrow(1, '下一张'), arrow(1, '下一张'),
@@ -56,10 +56,13 @@ export function carousel(props: CarouselProps): HTMLElement {
}); });
if (props.autoplay && total > 1) { if (props.autoplay && total > 1) {
let paused = false; // 悬停期间暂停自动播放 let hover = false; // 悬停中
root.addEventListener('pointerenter', () => { paused = true; }); let focused = false; // 焦点在轮播内(键盘用户没有 hover,必须单独计)
root.addEventListener('pointerleave', () => { paused = false; }); root.addEventListener('pointerenter', () => { hover = true; });
window.setInterval(() => { if (!paused) go(index() + 1); }, props.autoplay); root.addEventListener('pointerleave', () => { hover = false; });
root.addEventListener('focusin', () => { focused = true; });
root.addEventListener('focusout', () => { focused = false; });
window.setInterval(() => { if (!hover && !focused) go(index() + 1); }, props.autoplay); // 两者都不在才前进
} }
return root; return root;
} }