Skip to content

Commit df69d2d

Browse files
committed
fix(programa): wire favourites filter through event delegation + add 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
1 parent b418250 commit df69d2d

4 files changed

Lines changed: 75 additions & 50 deletions

File tree

src/components/ProgramaPage.astro

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,14 @@ const schedule = await getSchedule()
5151
<div class="max-w-3xl mx-auto mb-6 flex justify-end">
5252
<button
5353
type="button"
54-
class="favorites-filter-btn inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 text-sm text-pycon-gray-25 hover:border-pycon-orange/50 hover:text-pycon-orange transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pycon-yellow has-[:checked]:bg-pycon-orange has-[:checked]:text-pycon-black has-[:checked]:font-semibold has-[:checked]:border-pycon-orange"
54+
class="favorites-filter-btn inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 text-sm text-pycon-gray-25 hover:border-pycon-orange/50 hover:text-pycon-orange transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pycon-yellow aria-pressed:bg-pycon-orange aria-pressed:text-pycon-black aria-pressed:font-semibold aria-pressed:border-pycon-orange"
5555
aria-pressed="false"
56+
aria-label={t.list.favoritesOnly}
5657
data-filter-favorites
58+
data-filter-on-prefix={t.list.filterOnPrefix}
59+
data-filter-on-singular={t.list.filterOnSingular}
60+
data-filter-on-plural={t.list.filterOnPlural}
61+
data-filter-off-message={t.list.filterOff}
5762
>
5863
<svg
5964
class="w-4 h-4"
@@ -141,16 +146,6 @@ const schedule = await getSchedule()
141146
#schedule-list-view.filter-favorites-only .schedule-list li:not(.is-favorite) {
142147
display: none;
143148
}
144-
.favorites-filter-btn[aria-pressed='true'] {
145-
background-color: var(--color-pycon-orange, #fd6837);
146-
color: #03004b;
147-
font-weight: 600;
148-
border-color: var(--color-pycon-orange, #fd6837);
149-
}
150-
.favorites-filter-btn[aria-pressed='true'] svg {
151-
fill: currentColor;
152-
stroke: currentColor;
153-
}
154149
</style>
155150

156151
<script is:inline src="https://pretalx.com/pycones-2026/widgets/schedule.js"></script>
@@ -252,38 +247,68 @@ const schedule = await getSchedule()
252247
empty.hidden = !(filterActive && count === 0)
253248
}
254249

255-
// Click handler: toggle, persist, update visual state and announce
250+
// Click handler: handle both the per-session favourite button and
251+
// the "only favourites" filter button. Event delegation on document
252+
// so we don't depend on either button being in the DOM at script
253+
// init time.
256254
document.addEventListener('click', (e) => {
257-
const btn = e.target.closest && e.target.closest('.favorite-btn')
258-
if (!btn) return
259-
e.preventDefault()
260-
const code = btn.getAttribute('data-session-code')
261-
if (!code) return
262-
const favs = readFavs()
263-
const idx = favs.indexOf(code)
264-
let added
265-
if (idx >= 0) {
266-
favs.splice(idx, 1)
267-
added = false
268-
} else {
269-
favs.push(code)
270-
added = true
255+
const target = e.target.closest && e.target.closest('button')
256+
if (!target) return
257+
258+
// Per-session favourite toggle
259+
const favBtn = target.closest('.favorite-btn')
260+
if (favBtn) {
261+
e.preventDefault()
262+
const code = favBtn.getAttribute('data-session-code')
263+
if (!code) return
264+
const favs = readFavs()
265+
const idx = favs.indexOf(code)
266+
let added
267+
if (idx >= 0) {
268+
favs.splice(idx, 1)
269+
added = false
270+
} else {
271+
favs.push(code)
272+
added = true
273+
}
274+
writeFavs(favs)
275+
syncFavorites()
276+
announce(added ? favBtn.dataset.addedMessage : favBtn.dataset.removedMessage)
277+
return
271278
}
272-
writeFavs(favs)
273-
syncFavorites()
274-
// Announce the state change via the live region. We clear it first
275-
// so identical messages (e.g. clicking the same button twice) are
276-
// re-announced by screen readers.
277-
const status = document.getElementById('favorite-status')
278-
if (status) {
279-
const message = added ? btn.dataset.addedMessage : btn.dataset.removedMessage
280-
status.textContent = ''
281-
// Force a reflow so the new text triggers the live region reliably
282-
void status.offsetWidth
283-
status.textContent = message || ''
279+
280+
// "Only favourites" filter toggle
281+
const filterBtn = target.closest('[data-filter-favorites]')
282+
if (filterBtn) {
283+
e.preventDefault()
284+
const wasPressed = filterBtn.getAttribute('aria-pressed') === 'true'
285+
const nowPressed = !wasPressed
286+
filterBtn.setAttribute('aria-pressed', nowPressed ? 'true' : 'false')
287+
const listView = document.getElementById('schedule-list-view')
288+
if (listView) listView.classList.toggle('filter-favorites-only', nowPressed)
289+
updateEmptyState()
290+
const count = readFavs().length
291+
const word = count === 1 ? filterBtn.dataset.filterOnSingular : filterBtn.dataset.filterOnPlural
292+
const message = nowPressed
293+
? count > 0
294+
? `${filterBtn.dataset.filterOnPrefix}. Mostrando ${count} ${word}.`
295+
: `${filterBtn.dataset.filterOnPrefix}.`
296+
: filterBtn.dataset.filterOffMessage
297+
announce(message)
284298
}
285299
})
286300

301+
// Write a message to the live region. Clears + forces a reflow so
302+
// identical back-to-back messages still re-announce.
303+
function announce(message) {
304+
if (!message) return
305+
const status = document.getElementById('favorite-status')
306+
if (!status) return
307+
status.textContent = ''
308+
void status.offsetWidth
309+
status.textContent = message
310+
}
311+
287312
// Re-sync when the user switches to the list view (widget may have
288313
// mutated localStorage in the same session).
289314
document.addEventListener('pycones:view-changed', syncFavorites)
@@ -293,18 +318,6 @@ const schedule = await getSchedule()
293318
if (e.key === FAV_KEY) syncFavorites()
294319
})
295320

296-
// Filter button: show only favourited sessions
297-
const filterBtn = document.querySelector('[data-filter-favorites]')
298-
if (filterBtn) {
299-
filterBtn.addEventListener('click', () => {
300-
const pressed = filterBtn.getAttribute('aria-pressed') === 'true'
301-
filterBtn.setAttribute('aria-pressed', pressed ? 'false' : 'true')
302-
const listView = document.getElementById('schedule-list-view')
303-
if (listView) listView.classList.toggle('filter-favorites-only', !pressed)
304-
updateEmptyState()
305-
})
306-
}
307-
308321
// Initial render
309322
syncFavorites()
310323
})()

src/i18n/programa/ca.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export const ca = {
3131
favoriteRemoved: 'Tret de preferits',
3232
favoritesOnly: 'Només preferits',
3333
noFavorites: "Encara no tens preferits. Marca les sessions que t'interessin amb el cor.",
34+
filterOnPrefix: 'Filtre de preferits activat',
35+
filterOnSingular: 'sessió preferida',
36+
filterOnPlural: 'sessions preferides',
37+
filterOff: 'Filtre de preferits desactivat. Mostrant totes les sessions.',
3438
showAbstract: 'Mostra la descripció',
3539
hideAbstract: 'Amaga la descripció',
3640
talkLink: 'Veure detalls de la xerrada a Pretalx',

src/i18n/programa/en.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export const en = {
3131
favoriteRemoved: 'Removed from favourites',
3232
favoritesOnly: 'Only favourites',
3333
noFavorites: "You don't have any favourites yet. Tap the heart on the sessions you're interested in.",
34+
filterOnPrefix: 'Favourites filter on',
35+
filterOnSingular: 'favourite session',
36+
filterOnPlural: 'favourite sessions',
37+
filterOff: 'Favourites filter off. Showing all sessions.',
3438
showAbstract: 'Show description',
3539
hideAbstract: 'Hide description',
3640
talkLink: 'View talk details on Pretalx',

src/i18n/programa/es.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export const es = {
3131
favoriteRemoved: 'Quitado de favoritos',
3232
favoritesOnly: 'Solo favoritos',
3333
noFavorites: 'No tienes favoritos todavía. Marca las sesiones que te interesen con el corazón.',
34+
filterOnPrefix: 'Filtro de favoritos activado',
35+
filterOnSingular: 'sesión favorita',
36+
filterOnPlural: 'sesiones favoritas',
37+
filterOff: 'Filtro de favoritos desactivado. Mostrando todas las sesiones.',
3438
showAbstract: 'Ver descripción',
3539
hideAbstract: 'Ocultar descripción',
3640
talkLink: 'Ver detalles de la charla en Pretalx',

0 commit comments

Comments
 (0)