polish(viewer): 广告轮播 a11y 补全——aria-current/focus-visible/touch-action/键名对齐

评审跟进(Task 10 a11y 两项 + 三个顺手项):
- 圆点当前项程序化状态 :aria-current(ThumbnailStrip:103 同形),active 圆点
  类名对齐 sibling 约定 ad-dot--active → .active(viewer 无 BEM -- 修饰符)
- .ad-slide/.ad-close/.ad-dot 补 :focus-visible 2px 主色描边
  (AlbumHeader/ThumbnailStrip 同规;主色对暗色衬底/蒙层对比 ≈4:1)
- .ad-carousel 补 touch-action: pan-y(ThumbnailStrip pan-x 对偶:
  水平翻页自持、纵向滚动交还页面)
- sessionStorage 键名 viewer:ads-dismissed → viewer.adsDismissed(sibling
  点号命名约定;仅 ActivityPage 两处引用,测试无引用)
- ad-carousel.spec:类名断言随改 + aria-current 前后迁移断言

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-29 01:08:13 +08:00
parent f1c13bff7c
commit 7f9789eb46
3 changed files with 21 additions and 5 deletions
@@ -56,9 +56,13 @@ describe('AdCarousel', () => {
const dots = wrapper.findAll('.ad-dots button');
expect(dots.length).toBe(3);
expect(dots[0].attributes('aria-current')).toBe('true'); // 当前项程序化状态
expect(dots[1].attributes('aria-current')).toBeUndefined();
await dots[1].trigger('click');
expect(currentSrc(wrapper)).toBe('https://ad.test/b.jpg');
expect(dots[1].classes()).toContain('ad-dot--active');
expect(dots[1].classes()).toContain('active');
expect(dots[1].attributes('aria-current')).toBe('true');
expect(dots[0].attributes('aria-current')).toBeUndefined();
});
it('emits close when the ✕ button is clicked', async () => {
+14 -2
View File
@@ -108,7 +108,8 @@ function openAd(ad: PublicAd): void {
<nav v-if="ads.length > 1" class="ad-dots" :aria-label="t('ads.dots')">
<button
v-for="(ad, i) in ads" :key="ad.adId" type="button" class="ad-dot"
:class="{ 'ad-dot--active': i === current }" :aria-label="t('ads.goto', { n: i + 1 })"
:class="{ active: i === current }" :aria-current="i === current ? 'true' : undefined"
:aria-label="t('ads.goto', { n: i + 1 })"
@click="goTo(i)"
/>
</nav>
@@ -127,6 +128,9 @@ function openAd(ad: PublicAd): void {
border-radius: var(--viewer-radius-img);
background: var(--viewer-dark-bg); /* 图像加载期的暗色衬底 */
font-family: var(--viewer-font-family);
/* 水平翻页手势自持(touchstart/end 判定),纵向滚动交还页面——
* ThumbnailStrip pan-x 的对偶(pan-y)。 */
touch-action: pan-y;
}
.ad-slide {
display: block;
@@ -191,7 +195,15 @@ function openAd(ad: PublicAd): void {
border-radius: 50%;
background: color-mix(in srgb, var(--viewer-surface) 55%, transparent);
}
.ad-dot--active::before {
.ad-dot.active::before {
background: var(--viewer-surface);
}
/* 键盘焦点指示(AlbumHeader/ThumbnailStrip 同规:2px 主色描边)。主色对
* 暗色衬底/color-mix 蒙层对比 ≈4:1(非文本 ≥3:1)。 */
.ad-slide:focus-visible,
.ad-close:focus-visible,
.ad-dot:focus-visible {
outline: 2px solid var(--viewer-primary);
outline-offset: 1px;
}
</style>
+2 -2
View File
@@ -77,7 +77,7 @@ async function load(): Promise<void> {
// 广告独立拉取(Task 10):失败静默(广告缺席不阻塞相册浏览);会话级
// 关闭记忆先行(命中则连请求都省)。沿用 sequence 防跨相册竞态落地。
ads.value = [];
if (sessionStorage.getItem('viewer:ads-dismissed') !== '1') {
if (sessionStorage.getItem('viewer.adsDismissed') !== '1') {
getPublicAds(token.value)
.then((result) => {
if (sequence !== loadSequence) return;
@@ -122,7 +122,7 @@ function selectCategory(categoryId: string | null): void {
/** 关闭广告(Task 10):会话级记忆(本次会话不再拉取/渲染),组件只 emit。 */
function dismissAds(): void {
sessionStorage.setItem('viewer:ads-dismissed', '1');
sessionStorage.setItem('viewer.adsDismissed', '1');
ads.value = [];
}