Compare commits

...

1 Commits

Author SHA1 Message Date
bymyself
f7119e4d9b a11y: add aria-expanded and aria-controls to ProductShowcaseSection accordion
Add aria-expanded, aria-controls, panel id, and role=region attributes
to the accordion buttons in ProductShowcaseSection for screen reader
support. Add vue plugin to vitest config and SSR-based accessibility
tests.

Fixes #11312
2026-05-02 03:55:50 +00:00
3 changed files with 67 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import { renderToString } from 'vue/server-renderer'
import { createSSRApp } from 'vue'
import { describe, expect, it } from 'vitest'
import ProductShowcaseSection from './ProductShowcaseSection.vue'
function renderComponent(props = {}) {
const app = createSSRApp(ProductShowcaseSection, props)
return renderToString(app)
}
describe('ProductShowcaseSection', () => {
describe('aria-expanded', () => {
it('sets aria-expanded="true" on the active accordion button', async () => {
const html = await renderComponent()
const buttons = html.match(/<button[^>]*>/g) ?? []
expect(buttons.length).toBe(3)
expect(buttons[0]).toContain('aria-expanded="true"')
})
it('sets aria-expanded="false" on inactive accordion buttons', async () => {
const html = await renderComponent()
const buttons = html.match(/<button[^>]*>/g) ?? []
expect(buttons[1]).toContain('aria-expanded="false"')
expect(buttons[2]).toContain('aria-expanded="false"')
})
})
describe('aria-controls', () => {
it('sets aria-controls linking each button to its panel', async () => {
const html = await renderComponent()
const buttons = html.match(/<button[^>]*>/g) ?? []
expect(buttons[0]).toContain('aria-controls="feature-panel-0"')
expect(buttons[1]).toContain('aria-controls="feature-panel-1"')
expect(buttons[2]).toContain('aria-controls="feature-panel-2"')
})
it('renders matching panel ids for aria-controls references', async () => {
const html = await renderComponent()
expect(html).toContain('id="feature-panel-0"')
expect(html).toContain('id="feature-panel-1"')
expect(html).toContain('id="feature-panel-2"')
})
})
describe('panel role', () => {
it('marks each panel with role="region"', async () => {
const html = await renderComponent()
const panels = html.match(/id="feature-panel-\d+"[^>]*/g) ?? []
expect(panels.length).toBe(3)
for (const panel of panels) {
expect(panel).toContain('role="region"')
}
})
})
})

View File

@@ -157,6 +157,8 @@ watch(activeIndex, (current, previous) => {
/>
<button
type="button"
:aria-expanded="activeIndex === i"
:aria-controls="`feature-panel-${i}`"
:class="
cn(
'rounded-5xl w-full cursor-pointer p-8 text-left transition-colors duration-300',
@@ -186,6 +188,8 @@ watch(activeIndex, (current, previous) => {
<!-- Animated description (stacked for constant height) -->
<div
:id="`feature-panel-${i}`"
role="region"
:class="
cn(
'grid transition-[grid-template-rows] duration-300',

View File

@@ -1,6 +1,8 @@
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'node',
include: ['src/**/*.{test,spec}.ts'],