Skip to content

fix(programa): view-mode flash + responsive toggle#221

Closed
itziarZG wants to merge 17 commits into
mainfrom
feat/programa-a11y
Closed

fix(programa): view-mode flash + responsive toggle#221
itziarZG wants to merge 17 commits into
mainfrom
feat/programa-a11y

Conversation

@itziarZG

@itziarZG itziarZG commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

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> sets data-schedule-view on <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 to list so 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.astro global styles and now reads from the same data-schedule-view attribute, so the active state matches the view on first paint. The hardcoded checked on 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 in ProgramaPage.astro but the target <li> elements live in the child ScheduleList.astro (different Astro scope data-astro-cid-zmszfzov), so the rule never matched. Moved to Layout.astro global styles where it can reach across the component boundary.

3. Responsive cleanup

  • Removed the redundant "Schedule view mode" label from the view-mode toggle. The radio labels ("List" / "Visual grid") already convey the purpose, and the accessible <legend> remains for screen readers. The label was forcing the fieldset to wrap to two rows on mobile (86px tall).
  • Wrapped <pretalx-schedule> in a max-w-full overflow-x-auto div. 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:

  • No horizontal overflow on list or grid view
  • Toggle height 62px across all breakpoints, single-row layout on mobile
  • No flash of incorrect view on first paint
  • View persists across page loads via localStorage
  • Hash navigation forces list view
  • Favorites filter correctly hides non-favorite sessions

Files changed

  • src/components/ProgramaPage.astro (+8 / -12)

itziarZG and others added 17 commits July 10, 2026 08:29
- 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
@itziarZG itziarZG changed the title feat(programa): favourites in list view + view-mode flash fix + responsive feat(programa): responsive Jul 13, 2026
@itziarZG itziarZG changed the title feat(programa): responsive fix(programa): view-mode flash + responsive toggle Jul 13, 2026

@francescarpi francescarpi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@itziarZG veo muchos commits. Creo que tienes que hacer un rebase con main

@itziarZG

Copy link
Copy Markdown
Collaborator Author

@itziarZG veo muchos commits. Creo que tienes que hacer un rebase con main

uuy si, si lo he hecho. creo qu eno he he hecho el push --force..

@itziarZG itziarZG closed this Jul 13, 2026
@itziarZG itziarZG deleted the feat/programa-a11y branch July 13, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants