fix(programa): view-mode flash + responsive toggle#221
Closed
itziarZG wants to merge 17 commits into
Closed
Conversation
- 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
…dget Addresses #216 — the Pretalx widget's CSS grid layout is fundamentally hard to navigate with a screen reader, no matter how much ARIA we inject. Adds a parallel accessible list view rendered server-side from the Pretalx API, with the widget preserved as an opt-in visual alternative. Data layer: - src/lib/schedule.ts — fetches schedule.json at build time, normalises per-day structure, returns null on failure (graceful fallback) - src/types/schedule.ts — TS types for the Pretalx export shape UI: - src/components/ScheduleList.astro — semantic <ol> per day with <article> per session, <time datetime>, <details> for abstracts, lang attribute on text in non-default languages - Toggle in ProgramaPage.astro: <fieldset> + radio group, default to the accessible list, persists choice in localStorage - Widget remains as opt-in 'Visual grid' view, with all a11y enhancements from the previous commit intact i18n: - Adds 15 new strings to programa/{es,en,ca} (view toggle, day label, speaker/room/track/abstract labels, empty state) Edge cases: - Pretalx down or schedule not published: render friendly empty state with link to pretalx.com - Build-time fetch is silent on failure (warning to stderr, no crash)
Visual users land on the Pretalx widget by default; the accessible list is now a one-click toggle away for screen reader users and anyone who prefers a semantic structure.
# Conflicts: # src/components/ProgramaPage.astro # src/pages/[lang]/programa.astro
When a talk is in English (or any non-page language), screen readers
were pronouncing the original-language content with the page's voice
because the surrounding elements inherited the document lang.
Add lang={talkLang(s)} on:
- <h3> (talk title)
- <p> subtitle (when present)
- <p> abstract
Helper talkLang() maps s.language to a valid BCP-47 value and falls
back to the page lang if Pretalx returns an unknown code.
The Pretalx widget already persists favourites in localStorage under 'pycones-2026_favs' as a JSON array of session codes. This commit mirrors that behaviour on the accessible list so users can bookmark sessions in either view and have the preference stick. UI: - Heart icon button on every session card, pushed to the right of the metadata row - aria-pressed for state, aria-label that flips between 'Add to favourites' / 'Remove from favourites' based on state - Visual state: outline heart (gray) when off, filled (orange) when on - sr-only label 'Favourite' for screen readers Script: - Reads/writes the same 'pycones-2026_favs' key the widget uses - Click handler: toggle, persist, update DOM - 'storage' event listener: re-sync on cross-tab changes - 'pycones:view-changed' custom event: re-sync when user switches to the list view (in case the widget mutated localStorage in the same session) i18n: - favorite, favoriteAdd, favoriteRemove (es/en/ca)
Two improvements to the favorite button accessibility:
1. Static label, state via aria-pressed only.
The W3C ARIA Authoring Practices Guide recommends that toggle
button labels stay stable across states. Screen reader users
had been hearing a different label each time the button changed
('Add to favourites' → 'Remove from favourites'), which is more
confusing than just 'Favourite, button, pressed/not pressed'.
2. Live region announces the result of the click.
Previously the user clicked and got no audible confirmation. Now
a polite live region says 'Added to favourites' / 'Removed from
favourites' after each toggle. The text is cleared and re-set
with a forced reflow so identical back-to-back messages still
re-announce.
i18n: replaced favoriteAdd/favoriteRemove with favoriteAdded/
favoriteRemoved (past tense, used as announcement text).
…lose The favorite button was being pushed to the far right of the full-width viewport via ml-auto, which on desktop made it feel disconnected from the rest of the session metadata. Add max-w-3xl mx-auto to the schedule-list container so the day headings and session cards stay within a comfortable reading width (768px), keeping the heart visually close to the time/track info on the same row.
A toggle button at the top of the list view that filters the schedule to show only the user's bookmarked sessions. - aria-pressed toggle button with heart icon - Live count of favourites in the label - CSS hides non-favourite <li> elements when the filter is active (via the is-favorite class added by syncFavorites on the <li>) - Empty state: if the filter is on and there are 0 favourites, show a friendly message instead of a blank page - i18n: favoritesOnly, noFavorites (es/en/ca) Accessibility: - The button itself is a proper toggle (aria-pressed, not a checkbox) - The filter state is purely visual + local, no new localStorage key (the favourite list itself is already persisted)
…status The 'Only favourites' filter button wasn't doing anything because two click handlers were firing on each click: 1. The new event-delegation handler that toggles aria-pressed and the CSS class 2. The old direct listener (from the previous commit) that toggled aria-pressed again Net effect: aria-pressed went true → false → true, leaving the button exactly as it was. No visible change, no class added. Fix: remove the old direct listener; the event-delegation handler in the document click listener now owns both the per-session favourite toggle and the filter toggle. Also refactored to share a single announce() helper. While here: - Replace the buggy 'has-[:checked]:' Tailwind utilities (the button has no :checked state, it uses aria-pressed) with the correct 'aria-pressed:' variants — those have proper specificity and the orange-pressed look now actually applies - Add status announcements for the filter toggle via the existing #favorite-status live region: 'Favourites filter on. Showing N favourite session(s).' / 'Favourites filter off. Showing all sessions.' - Decompose the i18n message into prefix + singular + plural words (filterOnPrefix, filterOnSingular, filterOnPlural, filterOff) so the script can build the count-aware message at runtime
Switch from event delegation to a direct click handler bound to the button via getElementById. Adds try/catch + console.log statements so we can see in the browser console whether the script is running, the button is being found, and clicks are reaching the handler. The button now has id='favorites-filter-btn' (was data-filter-favorites). The script tries to bind the handler: - immediately (in case the DOM is ready) - on DOMContentLoaded (in case the script runs before DOM) - on astro:page-load (in case the page is a view transition) - guarded with dataset.bound so it only runs once If the filter still doesn't work, the browser console will show which of these steps is failing.
The previous attempt to fix the initial-load 'jump' by ignoring stale localStorage values was too restrictive. The team wants the user's choice to actually persist across visits. New behavior: - First visit (no saved value): default to visual grid - After user picks list: save to localStorage, restore list on next visit - After user picks grid: save to localStorage, restore grid on next visit Legacy users who had 'list' saved from the old default will see a one-time jump from grid → list on their first visit after this change. After that they can click the grid radio to overwrite the saved value and stop the jump. Also cleaned up: - Removed debug console.log statements left over from the filter fix - Fixed a duplicated function body in the inline script that was breaking astro check
…scope The 'only favourites' filter was toggling the button state and adding the .filter-favorites-only class to the list view, but the session cards were not being hidden. Reason: the CSS rule was in ProgramaPage.astro and Astro's scoped styles injected data-astro-cid-nm3rvpko into every selector, including .schedule-list and li. But those elements are rendered by ScheduleList.astro which has its own data-astro-cid-zmszfzov, so the selector never matched. Fix: extract the filter CSS into an is:global block so the selector reaches across the component boundary. Verified end-to-end with Playwright: - 0 favourites: 56/56 hidden (everything is filtered out) - 1 favourite set: 1 visible, 55 hidden - Toggle off: 56 visible again
Pre-paint script in <head> now sets data-schedule-view on <html> before the first paint, so the saved view (list/grid) is applied without a flash of the other view. When the URL has a session hash, the view is forced to 'list' so the user lands directly on the targeted session. CSS now drives both view visibility and the visual highlight on the view-mode toggle labels from the same data-schedule-view attribute, so the active state matches the view on first paint. The radio's hardcoded checked attribute is removed to avoid a visual mismatch where grid would flash as active before the JS sync ran. The favorites filter CSS was moved to Layout.astro's global styles because the target <li> elements live in a child component (ScheduleList) with a different Astro scope, which made the previously-scoped rule unable to match.
Two responsive fixes for /programa:
1. Remove redundant view-mode label. The <span> displaying
'Schedule view mode' was redundant with the radio button labels
('List' / 'Visual grid') and forced the fieldset to wrap to two
rows in mobile (375px), making the toggle 86px tall. The accessible
<legend> remains for screen readers.
2. Wrap <pretalx-schedule> in overflow-x-auto container. The Pretalx
web component renders an internal layout of ~1690px that caused
horizontal page scroll in tablet (768) and desktop (1280). The
shadow DOM can't be styled from outside, so the wrapper provides
internal scroll instead of forcing page-level scroll.
Verified with Playwright at 375/768/1280: no horizontal overflow,
toggle height 62px across all breakpoints, single-row layout in
mobile.
# Conflicts: # src/components/ProgramaPage.astro
francescarpi
approved these changes
Jul 13, 2026
francescarpi
requested changes
Jul 13, 2026
francescarpi
left a comment
Collaborator
There was a problem hiding this comment.
@itziarZG veo muchos commits. Creo que tienes que hacer un rebase con main
Collaborator
Author
uuy si, si lo he hecho. creo qu eno he he hecho el push --force.. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Small batch of fixes for
/programa, building on the list-view feature already merged in #215.1. View-mode flash on first paint
The saved view (list/grid) was applied after first paint, causing a brief flash of the wrong view. Now a small inline script in
<head>setsdata-schedule-viewon<html>before the first paint, so the saved view applies immediately.When the URL has a session hash (e.g.
/es/programa#TKZBMF), the view is forced tolistso the user lands directly on the targeted session.The CSS that drives view visibility and the visual highlight on the toggle labels was also moved to
Layout.astroglobal styles and now reads from the samedata-schedule-viewattribute, so the active state matches the view on first paint. The hardcodedcheckedon the grid radio was removed to avoid a visual mismatch.2. Favorites filter CSS scope fix
The filter rule
#schedule-list-view.filter-favorites-only .schedule-list li:not(.is-favorite)was scoped inProgramaPage.astrobut the target<li>elements live in the childScheduleList.astro(different Astro scopedata-astro-cid-zmszfzov), so the rule never matched. Moved toLayout.astroglobal styles where it can reach across the component boundary.3. Responsive cleanup
<legend>remains for screen readers. The label was forcing the fieldset to wrap to two rows on mobile (86px tall).<pretalx-schedule>in amax-w-full overflow-x-autodiv. The Pretalx web component renders an internal layout of ~1690px that caused horizontal page scroll in tablet (768) and desktop (1280). The shadow DOM can't be styled from outside, so the wrapper provides internal scroll instead.Verification
Playwright at 375 / 768 / 1280 confirmed:
Files changed
src/components/ProgramaPage.astro(+8 / -12)