Skip to content

Commit bb27cbc

Browse files
committed
feat(programa): improve accessibility of Pretalx widget
- Add global skip link to main content (3 i18n locales) - Wrap schedule in region with aria-labelledby, add live region announcing session count changes after filtering - Reinforce dialog ARIA (aria-modal, role, aria-labelledby) via Shadow DOM CSS/attribute injection - Add labels to search input and timezone select inside the widget - Restore focus to filter trigger when dialog closes - Set body[modality] attribute so widget's :focus-visible rings appear for keyboard users - Override --pretalx-clr-success to a distinguishable green Fixes 3 issues from initial a11y pass: - Widget uses Shadow DOM (Vue 3 attachShadow), not light DOM — all queries now use shadow root - modality must be a direct attribute, not data-modality - success color override must live on host's style attribute to cross the shadow boundary
1 parent edca606 commit bb27cbc

9 files changed

Lines changed: 307 additions & 18 deletions

File tree

src/components/ProgramaPage.astro

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ const t = programaTexts[lang as keyof typeof programaTexts]
1010
const menuT = menuTexts[lang as keyof typeof menuTexts]
1111
---
1212

13-
<section class="py-8">
14-
<h1 class="text-3xl md:text-5xl font-bold text-center mb-6">{t.title}</h1>
13+
<section class="py-8" aria-labelledby="schedule-title">
14+
<h1 id="schedule-title" class="text-3xl md:text-5xl font-bold text-center mb-6">{t.title}</h1>
1515
<p class="text-center text-base md:text-lg mb-10 max-w-2xl mx-auto opacity-80">{t.intro}</p>
1616

1717
<pretalx-schedule
1818
event-url="https://pretalx.com/pycones-2026/"
1919
locale={t.locale}
2020
format="grid"
21-
style="--pretalx-clr-primary: #03004b"></pretalx-schedule>
21+
style="--pretalx-clr-primary: #03004b; --pretalx-clr-success: #2e7d32"></pretalx-schedule>
22+
23+
<div id="schedule-status" role="status" aria-live="polite" aria-atomic="true" class="sr-only"></div>
24+
25+
<script is:inline type="application/json" id="schedule-i18n" set:html={JSON.stringify(t.a11y)} />
2226

2327
<noscript>
2428
<div class="pretalx-widget">

src/i18n/menu/ca.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,6 @@ export const ca = {
7272
href: '/past-editions',
7373
},
7474
],
75+
skipToContent: 'Salta al contingut principal',
7576
new_tab: "(s'obre en una pestanya nova)",
7677
} as const

src/i18n/menu/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,6 @@ export const en = {
7272
href: '/past-editions',
7373
},
7474
],
75+
skipToContent: 'Skip to main content',
7576
new_tab: '(opens in new tab)',
7677
} as const

src/i18n/menu/es.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,6 @@ export const es = {
7272
href: '/past-editions',
7373
},
7474
],
75+
skipToContent: 'Saltar al contenido principal',
7576
new_tab: '(se abre en nueva pestaña)',
7677
} as const

src/i18n/programa/ca.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,11 @@ export const ca = {
55
locale: 'ca',
66
noscript: 'JavaScript està desactivat al teu navegador. Per veure el programa sense JavaScript,',
77
noscriptLink: 'fes clic aquí',
8+
a11y: {
9+
statusTemplate: (count: number) => `Mostrant ${count} ${count === 1 ? 'sessió' : 'sessions'}`,
10+
statusEmpty: 'No hi ha sessions que coincideixin amb els filtres',
11+
searchLabel: 'Cerca sessions',
12+
timezoneLabel: 'Zona horària',
13+
closeFilters: 'Tanca els filtres',
14+
},
815
} as const

src/i18n/programa/en.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,11 @@ export const en = {
55
locale: 'en',
66
noscript: 'JavaScript is disabled in your browser. To view the schedule without JavaScript,',
77
noscriptLink: 'click here',
8+
a11y: {
9+
statusTemplate: (count: number) => `Showing ${count} ${count === 1 ? 'session' : 'sessions'}`,
10+
statusEmpty: 'No sessions match the filters',
11+
searchLabel: 'Search sessions',
12+
timezoneLabel: 'Timezone',
13+
closeFilters: 'Close filters',
14+
},
815
} as const

src/i18n/programa/es.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,11 @@ export const es = {
55
locale: 'es',
66
noscript: 'JavaScript está deshabilitado en tu navegador. Para ver el programa sin JavaScript,',
77
noscriptLink: 'haz click aquí',
8+
a11y: {
9+
statusTemplate: (count: number) => `Mostrando ${count} ${count === 1 ? 'sesión' : 'sesiones'}`,
10+
statusEmpty: 'No hay sesiones que coincidan con los filtros',
11+
searchLabel: 'Buscar sesiones',
12+
timezoneLabel: 'Zona horaria',
13+
closeFilters: 'Cerrar filtros',
14+
},
815
} as const

src/layouts/Layout.astro

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import '@/style/global.css'
55
import '@fontsource-variable/jetbrains-mono'
66
import '@fontsource-variable/outfit'
77
import { ClientRouter } from 'astro:transitions'
8+
import { menuTexts } from '@/i18n/menu'
89
910
interface Props {
1011
title: string
@@ -13,6 +14,7 @@ interface Props {
1314
}
1415
const { lang = 'es' } = Astro.params
1516
const { title, description = 'PyconES 2026', fullWidth = false } = Astro.props
17+
const skipT = menuTexts[lang as keyof typeof menuTexts].skipToContent
1618
1719
const siteUrl = Astro.site ? Astro.site.origin : 'https://2026.es.pycon.org'
1820
const canonicalURL = new URL(Astro.url.pathname, siteUrl)
@@ -115,6 +117,7 @@ const alternates = [
115117
<body
116118
class="bg-pycon-black font-outfit text-pycon-white antialiased min-h-screen relative flex flex-col transition-colors duration-300"
117119
>
120+
<a href="#main-content" class="skip-link">{skipT}</a>
118121
<div
119122
class="fixed inset-0 bg-no-repeat bg-size-[80vh] bg-position-[140%_120%] opacity-5 pointer-events-none blur transition-all duration-300"
120123
style="background-image: var(--bg-symbol);"
@@ -123,6 +126,7 @@ const alternates = [
123126
<Header lang={lang as string} />
124127

125128
<main
129+
id="main-content"
126130
class:list={[
127131
'relative grow z-10 flex flex-col justify-center',
128132
fullWidth ? 'px-4 md:px-8' : 'container mx-auto p-4 md:p-8',
@@ -162,4 +166,39 @@ const alternates = [
162166
::-webkit-scrollbar-thumb:hover {
163167
background: #8e8e8d;
164168
}
169+
170+
/* Skip link (visible on focus) */
171+
.skip-link {
172+
position: absolute;
173+
top: -100px;
174+
left: 1rem;
175+
z-index: 1000;
176+
padding: 0.75rem 1rem;
177+
background: #fff;
178+
color: #03004b;
179+
font-weight: 600;
180+
text-decoration: none;
181+
border-radius: 0.5rem;
182+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
183+
transition: top 0.2s ease;
184+
}
185+
.skip-link:focus,
186+
.skip-link:focus-visible {
187+
top: 1rem;
188+
outline: 2px solid #03004b;
189+
outline-offset: 2px;
190+
}
191+
192+
/* Screen reader only utility */
193+
.sr-only {
194+
position: absolute;
195+
width: 1px;
196+
height: 1px;
197+
padding: 0;
198+
margin: -1px;
199+
overflow: hidden;
200+
clip: rect(0, 0, 0, 0);
201+
white-space: nowrap;
202+
border: 0;
203+
}
165204
</style>

0 commit comments

Comments
 (0)