Skip to content

fix: migrate live region to useAnnouncer #2268

Draft
RYGRIT wants to merge 1 commit intonpmx-dev:mainfrom
RYGRIT:fix/follow-up-1812
Draft

fix: migrate live region to useAnnouncer #2268
RYGRIT wants to merge 1 commit intonpmx-dev:mainfrom
RYGRIT:fix/follow-up-1812

Conversation

@RYGRIT
Copy link
Copy Markdown
Contributor

@RYGRIT RYGRIT commented Mar 25, 2026

🔗 Linked issue

follow up #1812

🧭 Context

nuxt/nuxt#34318

📚 Description

  • Add <NuxtAnnouncer /> global component

Known issue: typed route type-checking after Nuxt 4.4 upgrade

After upgrading to Nuxt 4.4.2, vue-tsc reports many route param type errors (string | string[] | undefined) and typed-router related issues.

@storybook-vue/nuxt@9.0.1 currently declares:

  • peer nuxt: ^3.13.0
  • dependency vue-router: ^4.3.0

while this repo is on:

  • nuxt: 4.4.2
  • vue-router: 5.x (typed pages enabled)

So the workspace ends up with mixed router majors (v4 + v5), which likely affects typed-router / volar behavior and amplifies route param typing issues.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 26, 2026 8:20am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 26, 2026 8:20am
npmx-lunaria Ignored Ignored Mar 26, 2026 8:20am

Request Review

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

The pull request introduces accessibility announcements using the Nuxt announcer system. The root component now includes a <NuxtAnnouncer /> component, whilst the search page transitions from a computed property-based live region pipeline to a useAnnouncer()-based approach. This includes desktop (250ms) and mobile (700ms) debounced announcers, logic to cancel pending announcements on unmount, and updated watcher logic to emit polite announcements for rate-limited states, pending operations, pagination views, and search results.

Possibly related PRs

Suggested reviewers

  • knowler
  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description relates to the changeset by explaining the follow-up work to issue #1812, referencing upstream Nuxt changes, and describing the migration to useAnnouncer and addition of the NuxtAnnouncer component.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/pages/search.vue (1)

654-661: Consider using snapshot values consistently.

Lines 654 and 658 access live refs (isRelevanceSort.value, visibleResults.value?.total) inside the watcher callback, while other values come from the snapshot. Since effectiveTotal already equals visibleResults.value?.total when relevance-sorted, you could derive the sort mode from the snapshot or add isRelevanceSort to the snapshot for consistency.

This isn't a bug since the watcher runs synchronously, but it makes the data flow clearer.

♻️ Optional: Add isRelevanceSort to snapshot
   watch(
     () => ({
       rateLimited: isRateLimited.value,
       searchStatus: status.value,
       count: displayResults.value.length,
       searchQuery: query.value,
       mode: viewMode.value,
       pagMode: paginationMode.value,
       total: effectiveTotal.value,
+      relevanceSort: isRelevanceSort.value,
     }),
-    ({ rateLimited, searchStatus, count, searchQuery, mode, pagMode, total }) => {
+    ({ rateLimited, searchStatus, count, searchQuery, mode, pagMode, total, relevanceSort }) => {
       // ... use relevanceSort instead of isRelevanceSort.value

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9042886f-160d-4214-a7db-2daa3c7e39cb

📥 Commits

Reviewing files that changed from the base of the PR and between eced357 and 4adf7fa.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • app/app.vue
  • app/pages/search.vue
  • package.json

pagMode: paginationMode.value,
total: effectiveTotal.value,
}),
({ rateLimited, searchStatus, count, searchQuery, mode, pagMode, total }) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rename searchQuery to avoid shadowing the outer scope variable.

The static analysis tool correctly flags that searchQuery in the watcher's destructuring shadows the outer searchQuery from useGlobalSearch() on line 49. Rename this to something like currentQuery or snapshotQuery for clarity.

🛠️ Proposed fix
-  ({ rateLimited, searchStatus, count, searchQuery, mode, pagMode, total }) => {
+  ({ rateLimited, searchStatus, count, searchQuery: currentQuery, mode, pagMode, total }) => {
     if (rateLimited) {
       announcePolite($t('search.rate_limited'))
       return
     }
     // ... rest of callback
     } else if (searchStatus === 'success' || searchStatus === 'error') {
-      if (searchQuery) {
-        announcePolite($t('search.no_results', { query: searchQuery }))
+      if (currentQuery) {
+        announcePolite($t('search.no_results', { query: currentQuery }))
       } else {
         cancelPendingAnnouncements()
       }
     }
🧰 Tools
🪛 GitHub Check: 🤖 Autofix code

[warning] 628-628: eslint(no-shadow)
'searchQuery' is already declared in the upper scope.

@RYGRIT RYGRIT force-pushed the fix/follow-up-1812 branch from 4d04b34 to a581219 Compare March 26, 2026 08:18
@RYGRIT
Copy link
Copy Markdown
Contributor Author

RYGRIT commented Mar 26, 2026

I'll convert this to a draft PR for now, and reopen it once #2047 has been merged.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8661a4c3-7a93-40f8-a5fc-59689ec6c8bd

📥 Commits

Reviewing files that changed from the base of the PR and between 4d04b34 and a581219.

📒 Files selected for processing (2)
  • app/app.vue
  • app/pages/search.vue
✅ Files skipped from review due to trivial changes (1)
  • app/app.vue

Comment on lines +612 to +615
function cancelPendingAnnouncements() {
announcePoliteDesktop.cancel()
announcePoliteMobile.cancel()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let me locate and examine the search.vue file
fd -t f "search.vue" --exec wc -l {} \;

Repository: npmx-dev/npmx.dev

Length of output: 125


🏁 Script executed:

# Read the specific lines mentioned in the review
cat -n app/pages/search.vue | sed -n '600,630p'

Repository: npmx-dev/npmx.dev

Length of output: 1069


🏁 Script executed:

# Search for where announcePoliteDesktop and announcePoliteMobile are defined/used
rg "announcePoliteDesktop|announcePoliteMobile" app/pages/search.vue -B 5 -A 5

Repository: npmx-dev/npmx.dev

Length of output: 770


🏁 Script executed:

# Check the entire file structure to understand the component
head -100 app/pages/search.vue

Repository: npmx-dev/npmx.dev

Length of output: 3001


🏁 Script executed:

# Find where cancelPendingAnnouncements is called
rg "cancelPendingAnnouncements" app/pages/search.vue -B 3 -A 3

Repository: npmx-dev/npmx.dev

Length of output: 645


🏁 Script executed:

# Look at the entire watch block that announces results
cat -n app/pages/search.vue | sed -n '618,750p'

Repository: npmx-dev/npmx.dev

Length of output: 5801


Ensure screen reader announcements are triggered for repeated identical messages.

When cancelPendingAnnouncements() cancels the debounces, subsequent calls to announcePolite() with identical message text will not produce a fresh announcer update. Since multiple searches can generate the same announcement text (e.g., two searches each finding 100 results), the announcer state needs to be reset to trigger the screen reader update. Either reset the announcer or vary the message before reusing it.

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