From a581219901dbbb3a1b2f4fd6d137548382ea9ec5 Mon Sep 17 00:00:00 2001 From: RYGRIT Date: Thu, 26 Mar 2026 16:17:19 +0800 Subject: [PATCH] fix: switch to useAnnouncer + --- app/app.vue | 2 + app/pages/search.vue | 147 +++++++++++++++++++++---------------------- 2 files changed, 73 insertions(+), 76 deletions(-) diff --git a/app/app.vue b/app/app.vue index 5916e3e328..aa599f5a7a 100644 --- a/app/app.vue +++ b/app/app.vue @@ -136,6 +136,8 @@ if (import.meta.client) { + + {{ route.name === 'search' ? `${$t('search.title_packages')} - npmx` : message }} diff --git a/app/pages/search.vue b/app/pages/search.vue index b635065aa0..663479222b 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -283,6 +283,7 @@ async function loadMore() { } onBeforeUnmount(() => { updateUrlPage.cancel() + cancelPendingAnnouncements() }) // Update URL when page changes from scrolling @@ -584,94 +585,92 @@ defineOgImageComponent('Default', { }) // ----------------------------------- -// Live region debouncing logic +// Live region announcements // ----------------------------------- const isMobile = useIsMobile() +const { polite } = useAnnouncer() -// Evaluate the text that should be announced to screen readers -const rawLiveRegionMessage = computed(() => { - if (isRateLimited.value) { - return $t('search.rate_limited') - } - - // If status is pending, no update phrase needed yet - if (status.value === 'pending') { - return '' - } - - if (visibleResults.value && displayResults.value.length > 0) { - if (viewMode.value === 'table' || paginationMode.value === 'paginated') { - const pSize = Math.min(preferredPageSize.value, effectiveTotal.value) - - return $t( - 'filters.count.showing_paginated', - { - pageSize: pSize.toString(), - count: $n(effectiveTotal.value), - }, - effectiveTotal.value, - ) - } - - if (isRelevanceSort.value) { - return $t( - 'search.found_packages', - { count: $n(visibleResults.value.total) }, - visibleResults.value.total, - ) - } +const announcePoliteDesktop = debounce((message: string) => { + polite(message) +}, 250) - return $t( - 'search.found_packages_sorted', - { count: $n(effectiveTotal.value) }, - effectiveTotal.value, - ) - } +const announcePoliteMobile = debounce((message: string) => { + polite(message) +}, 700) - if (status.value === 'success' || status.value === 'error') { - if (displayResults.value.length === 0 && query.value) { - return $t('search.no_results', { query: query.value }) - } +function announcePolite(message: string) { + if (isMobile.value) { + announcePoliteDesktop.cancel() + announcePoliteMobile(message) + return } - return '' -}) - -const debouncedLiveRegionMessage = ref('') - -const updateLiveRegionMobile = debounce((val: string) => { - debouncedLiveRegionMessage.value = val -}, 700) + announcePoliteMobile.cancel() + announcePoliteDesktop(message) +} -const updateLiveRegionDesktop = debounce((val: string) => { - debouncedLiveRegionMessage.value = val -}, 250) +function cancelPendingAnnouncements() { + announcePoliteDesktop.cancel() + announcePoliteMobile.cancel() +} +// Announce search results changes to screen readers watch( - rawLiveRegionMessage, - newVal => { - if (!newVal) { - updateLiveRegionMobile.cancel() - updateLiveRegionDesktop.cancel() - debouncedLiveRegionMessage.value = '' + () => ({ + rateLimited: isRateLimited.value, + searchStatus: status.value, + count: displayResults.value.length, + searchQuery: query.value, + mode: viewMode.value, + pagMode: paginationMode.value, + total: effectiveTotal.value, + }), + ({ rateLimited, searchStatus, count, searchQuery, mode, pagMode, total }) => { + if (rateLimited) { + announcePolite($t('search.rate_limited')) return } - if (isMobile.value) { - updateLiveRegionDesktop.cancel() - updateLiveRegionMobile(newVal) - } else { - updateLiveRegionMobile.cancel() - updateLiveRegionDesktop(newVal) + // Don't announce while searching + if (searchStatus === 'pending') { + cancelPendingAnnouncements() + return + } + + if (count > 0) { + if (mode === 'table' || pagMode === 'paginated') { + const pSize = Math.min(preferredPageSize.value, total) + + announcePolite( + $t( + 'filters.count.showing_paginated', + { + pageSize: pSize.toString(), + count: $n(total), + }, + total, + ), + ) + } else if (isRelevanceSort.value) { + announcePolite( + $t( + 'search.found_packages', + { count: $n(visibleResults.value?.total ?? 0) }, + visibleResults.value?.total ?? 0, + ), + ) + } else { + announcePolite($t('search.found_packages_sorted', { count: $n(total) }, total)) + } + } else if (searchStatus === 'success' || searchStatus === 'error') { + if (searchQuery) { + announcePolite($t('search.no_results', { query: searchQuery })) + } else { + cancelPendingAnnouncements() + } } }, - { immediate: true }, ) - -onBeforeUnmount(() => { - updateLiveRegionMobile.cancel() - updateLiveRegionDesktop.cancel() -})