Skip to content

fix(ui): arrow key navigation for org page#2339

Open
tylersayshi wants to merge 8 commits intonpmx-dev:mainfrom
tylersayshi:tyler-fix-org-keys
Open

fix(ui): arrow key navigation for org page#2339
tylersayshi wants to merge 8 commits intonpmx-dev:mainfrom
tylersayshi:tyler-fix-org-keys

Conversation

@tylersayshi
Copy link
Copy Markdown

@tylersayshi tylersayshi commented Mar 31, 2026

🔗 Linked issue

resolves #2338

🧭 Context

This adds the key control for the org page like on https://npmx.dev/org/atcute

📚 Description

  • adding a composable to handle arrows on org page
  • shared composable back with search results page
  • added tests for checking interactions on the org page for this behavior

Kapture 2026-03-30 at 17 47 48

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 11, 2026 0:33am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 11, 2026 0:33am
npmx-lunaria Ignored Ignored Apr 11, 2026 0:33am

Request Review

@github-actions
Copy link
Copy Markdown

Hello! Thank you for opening your first PR to npmx, @tylersayshi! 🚀

Here’s what will happen next:

  1. Our GitHub bots will run to check your changes.
    If they spot any issues you will see some error messages on this PR.
    Don’t hesitate to ask any questions if you’re not sure what these mean!

  2. In a few minutes, you’ll be able to see a preview of your changes on Vercel

  3. One or more of our maintainers will take a look and may ask you to make changes.
    We try to be responsive, but don’t worry if this takes a few days.

@tylersayshi
Copy link
Copy Markdown
Author

also, great meeting y'all at https://atmosphereconf.org - thanks for npmx ❤️

- adding a composable to handle arrows on org page
- shared composable back with search results page

fixes npmx-dev#2338
@tylersayshi tylersayshi changed the title fix(org-page): arrow key navigation for org page fix(ui): arrow key navigation for org page Mar 31, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a composable, useResultsKeyboardNavigation, that captures document keydown events (ArrowUp/ArrowDown/Enter) to move focus among result elements annotated with data-result-index and optionally data-suggestion-index. BaseCard gained an optional index prop, makes its root focusable when index is present, exposes data-result-index, and switched keyboard-focus styles from focus-within to focus-visible. The composable is used on the org results page and replaces prior keyboard handling on the search page. E2E tests were added for org page navigation and for keyboard-shortcuts-disabled scenarios. List rendering changes include removing role="list" on a keywords ul and changing per-page to global indexing for package cards in paginated mode. A test fixture for an org was also added.

Possibly related PRs

Suggested reviewers

  • serhalp
  • danielroe
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset, describing keyboard navigation for org page and extracting a shared composable.
Linked Issues check ✅ Passed All coding requirements from #2338 are met: keyboard navigation (arrow up/down/enter) is implemented and working on org page.
Out of Scope Changes check ✅ Passed All changes directly address the linked issue requirements—keyboard navigation composable, org page integration, search page refactoring, and e2e tests.

✏️ 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.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 2.98507% with 65 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/composables/useResultsKeyboardNavigation.ts 0.00% 43 Missing and 20 partials ⚠️
app/pages/search.vue 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/pages/search.vue (1)

461-487: ⚠️ Potential issue | 🟡 Minor

Scope Enter handling to the actual search input.

The Enter handler currently matches any <input> element on the page. Please check the header search box explicitly before reading its value to prevent pressing Enter in other focused inputs from rewriting committedQuery with unintended data.

Suggested approach
 function handleSearchInputEnter(e: KeyboardEvent) {
   if (!keyboardShortcuts.value) {
     return
   }
+  const activeElement = document.activeElement
   // If the active element is an input, navigate to exact match or wait for results
-  if (e.key === 'Enter' && document.activeElement?.tagName === 'INPUT') {
+  if (
+    e.key === 'Enter' &&
+    activeElement instanceof HTMLInputElement &&
+    activeElement.matches('#header-search, input[name="q"]')
+  ) {
     // Get value directly from input (not from route query, which may be debounced)
-    const inputValue = (document.activeElement as HTMLInputElement).value.trim()
+    const inputValue = activeElement.value.trim()
🧹 Nitpick comments (1)
test/e2e/interactions.spec.ts (1)

313-362: Add one regression case for focused controls.

The new handlers are document-scoped, but these tests only drive the page from neutral focus. A small case that presses ArrowDown on the org sort control and Enter on a toolbar input on the search page would catch the two easiest regressions here.

Also applies to: 411-445


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f006bc64-b295-4607-b776-f0360ae50e9a

📥 Commits

Reviewing files that changed from the base of the PR and between 516808b and 2e616f6.

📒 Files selected for processing (6)
  • app/components/BaseCard.vue
  • app/components/Package/Card.vue
  • app/composables/useResultsKeyboardNavigation.ts
  • app/pages/org/[org].vue
  • app/pages/search.vue
  • test/e2e/interactions.spec.ts

Comment on lines +73 to +145
function handleKeydown(e: KeyboardEvent) {
// Only handle arrow keys and Enter
if (!['ArrowDown', 'ArrowUp', 'Enter'].includes(e.key)) {
return
}

if (!keyboardShortcuts.value) {
return
}

const elements = getFocusableElements()
const currentIndex = elements.findIndex(el => el === document.activeElement)

if (e.key === 'ArrowDown') {
// If there are results available, handle navigation
if (elements.length > 0) {
e.preventDefault()
e.stopPropagation()

// If no result is focused, focus the first one
if (currentIndex < 0) {
const firstEl = elements[0]
if (firstEl) focusElement(firstEl)
return
}

// If a result is already focused, move to the next one
const nextIndex = Math.min(currentIndex + 1, elements.length - 1)
const el = elements[nextIndex]
if (el) focusElement(el)
}
return
}

if (e.key === 'ArrowUp') {
// Only intercept if a result is already focused
if (currentIndex >= 0) {
e.preventDefault()
e.stopPropagation()

// At first result
if (currentIndex === 0) {
// Call custom callback if provided (e.g., return focus to search input)
if (options?.onArrowUpAtStart) {
options.onArrowUpAtStart()
}
return
}
const nextIndex = currentIndex - 1
const el = elements[nextIndex]
if (el) focusElement(el)
}
return
}

if (e.key === 'Enter') {
// Handle Enter on focused card - click the main link inside
if (document.activeElement && elements.includes(document.activeElement as HTMLElement)) {
const card = document.activeElement as HTMLElement
// Find the first link inside the card and click it
const link = card.querySelector('a')
if (link) {
e.preventDefault()
e.stopPropagation()
link.click()
}
}
}
}

// Register keyboard event listeners using useEventListener for better control
// Use capture phase to intercept before other handlers
useEventListener(document, 'keydown', handleKeydown, { capture: true })
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 31, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't steal ArrowUp/ArrowDown from native controls.

This handler runs at document scope and calls preventDefault() before the page's own controls can react. On both app/pages/org/[org].vue:267-288 and app/pages/search.vue:700-724, pressing ArrowDown inside the toolbar's selects or text inputs will jump to the first result instead of moving the caret or opening the control. Please ignore interactive targets unless focus is already on a result element or on an explicitly allowed hand-off source.

Possible direction
 export function useResultsKeyboardNavigation(options?: {
   includeSuggestions?: boolean
   onArrowUpAtStart?: () => void
+  canStartFrom?: (target: HTMLElement | null) => boolean
 }) {
@@
     const elements = getFocusableElements()
     const currentIndex = elements.findIndex(el => el === document.activeElement)
+    const target = e.target instanceof HTMLElement ? e.target : null
+    const isInteractiveTarget = !!target?.closest(
+      'input, textarea, select, button, [contenteditable="true"]',
+    )
+
+    if (currentIndex < 0 && isInteractiveTarget && !options?.canStartFrom?.(target)) {
+      return
+    }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I believe stealing the arrow keys is desired in this instance, but anyone please tell me if I'm wrong here 👍

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.

The Comment image shows an organization page at localhost:3000/org/atcute in a npm-like UI. Visible elements include:

  • Top bar with a search input labeled “/ search packages …”, and actions: compare, settings, connect.
  • Org header with avatar “A”, handle “@ATCUTE”, “58 public packages”, and an “npm” badge with “58,784 / week”.
  • A PACKAGES section: “58 packages”, a “Filters” button, a sort control showing “Downloads/wk” (native select), and list/grid view toggles.
  • A list of package cards (e.g., @atcute/time-ms, @atcute/bluesky) each with description, date, version, and weekly downloads.

@tylersayshi I love the consistency goal, but stealing ArrowUp/ArrowDown from native controls (inputs/selects/textarea/contenteditable) at the document level will block expected keyboard behavior (caret movement, opening selects, adjusting ranges) and hurts accessibility. Right now, ArrowDown is intercepted even when focus is inside the sort select or the search box, because we call preventDefault in capture before those controls can react.

A small compromise keeps the desired list navigation while preserving native control behavior:

  • Only start navigation when:
    • focus is already on a result element, or
    • the current target is explicitly whitelisted (e.g., the search input on /search).
  • Otherwise, let native controls handle Arrow keys.

Minimal patch to the composable

// app/composables/useResultsKeyboardNavigation.ts
export function useResultsKeyboardNavigation(options?: {
  includeSuggestions?: boolean
  onArrowUpAtStart?: () => void
  canStartFrom?: (target: HTMLElement | null) => boolean
}) {
  // ...

  function handleKeydown(e: KeyboardEvent) {
    if (!['ArrowDown', 'ArrowUp', 'Enter'].includes(e.key) || !keyboardShortcuts.value) return

    const elements = getFocusableElements()
    const currentIndex = elements.findIndex(el => el === document.activeElement)

    const target = e.target instanceof HTMLElement ? e.target : null
    const isInteractiveTarget = !!target?.closest('input, textarea, select, button, [contenteditable="true"]')

    // Don’t hijack Arrow keys from interactive widgets unless navigation is already active
    // or the caller explicitly allows starting from this target.
    if (currentIndex < 0 && isInteractiveTarget && !options?.canStartFrom?.(target)) {
      return
    }

    // ...existing ArrowDown/ArrowUp/Enter logic unchanged...
  }

  useEventListener(document, 'keydown', handleKeydown, { capture: true })
}

Call-site usage

  • Search page: allow starting from the search input only (so ArrowDown in the box moves to first result as today).
useResultsKeyboardNavigation({
  includeSuggestions: true,
  onArrowUpAtStart: focusSearchInput,
  canStartFrom: (el) => !!el?.closest('input[type="search"], [data-search-input="true"]'),
})
  • Org page: don’t pass canStartFrom so Arrow keys inside “Downloads/wk” select, Filters inputs, etc., keep their native behavior; navigation still works once focus is on a result card.

Optional guard

  • Also ignore modified keys to avoid surprises:
if (e.altKey || e.ctrlKey || e.metaKey) return

Tests to lock behavior

  • Add e2e asserting:
    • ArrowDown inside the sort does not move focus to the first result on /org/atcute. ArrowDown in the search input on /search starts navigation (whitelisted). ArrowDown on the document body starts at first result when nothing interactive is focused. If you’d prefer truly global “steal-from-anywhere,” I can roll with that, but it will block selects and text inputs across the page. The above tweak preserves your intended UX on /search while keeping native controls functional on the org page. Want me to prep a patch? 🧠 Learnings used Learnt from: btea Repo: npmx-dev/npmx.dev PR: 1739 File: app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue:259-262 Timestamp: 2026-02-28T07:17:28.172Z Learning: In app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue, the scroll-to-top button for the code preview container should be shown on both desktop and touch devices (gated only by scroll position using SCROLL_TO_TOP_THRESHOLD), not restricted to touch devices. This is different from the window-level ScrollToTop.client.vue component which is touch-device-only, because the code container scroll-to-top helps users navigate long code files where keyboard shortcuts are less practical for container-specific scrolling. Learnt from: graphieros Repo: npmx-dev/npmx.dev PR: 2273 File: app/components/Package/TrendsChart.vue:1648-1648 Timestamp: 2026-03-25T12:31:33.715Z Learning: In `app/components/Package/TrendsChart.vue`, the `tabindex` values on the view-toggle tab buttons (combined/split chart layout) are intentionally inverted from the standard WAI-ARIA pattern. The **selected** tab receives `tabindex="-1"` and the **unselected** tab receives `tabindex="0"`. Do not flag this as a bug or suggest reversing the values to match the WAI-ARIA tabs pattern. Learnt from: userquin Repo: npmx-dev/npmx.dev PR: 1596 File: app/pages/about.vue:318-341 Timestamp: 2026-02-23T05:31:58.922Z Learning: In the npmx.dev project, the contributor popover on the about page is intentionally a mouse-only progressive enhancement. Keyboard support (focus/blur with full popover interaction) was tested but rejected due to Firefox performance issues (3-4 second lag with per-contributor popovers in the Top Layer) and implementation complexity (aria-controls, aria-expanded, focus trapping, Escape handling). All contributor information remains accessible via the clickable link to the GitHub profile. Learnt from: ShroXd Repo: npmx-dev/npmx.dev PR: 2115 File: app/pages/search.vue:365-368 Timestamp: 2026-03-17T06:53:49.002Z Learning: In npmx-dev/npmx.dev (app/pages/search.vue), the `showClaimPrompt` computed intentionally returns `true` during a `status === 'pending'` fetch even when `avail.name` doesn't match `committedQuery.value.trim()`. This is by design to prevent the claim prompt from flickering/disappearing while the user is typing (avoiding layout shifts). The claim button is separately guarded with `:disabled="status === 'pending'"` to prevent accidental clicks on stale data. Do not suggest adding an `avail.name === committedQuery` guard to the pending branch. Learnt from: danielroe Repo: npmx-dev/npmx.dev PR: 1204 File: app/composables/npm/useUserPackages.ts:40-41 Timestamp: 2026-02-08T13:24:06.104Z Learning: In Nuxt 3, when using useAsyncData or useLazyAsyncData, allow and rely on reactive keys by typing them as MaybeRefOrGetter<string>. Keys can be a string, a Ref<string>, a ComputedRef<string>, or a getter () => string. Reactive keys should automatically trigger refetches when their value changes. In reviews, verify that data scripts pass a MaybeRefOrGetter<string> (not a plain string-only key if reactivity is intended), and that the logic handles dynamic key changes without stale data. Learnt from: MatteoGabriele Repo: npmx-dev/npmx.dev PR: 1922 File: app/composables/usePackageListPreferences.ts:52-53 Timestamp: 2026-03-05T10:14:50.799Z Learning: In Nuxt projects (here npmx-dev/npmx.dev), exports from shared/types/* (e.g., PAGE_SIZE_OPTIONS, DEFAULT_COLUMNS, DEFAULT_PREFERENCES, PageSize) are auto-imported by Nuxt for composables and components. Do not add explicit import statements for these constants/types when using files under shared/types/, and rely on the auto-imported bindings in files under app/composables (and similarly in components). This pattern applies to all TS files within app/composables that reference these shared/types exports.

@ghostdevv ghostdevv requested a review from knowler April 4, 2026 20:44
@serhalp serhalp added the needs review This PR is waiting for a review from a maintainer label Apr 6, 2026
@serhalp serhalp added this to the Up Next milestone Apr 10, 2026
@github-project-automation github-project-automation bot moved this to Backlog in Releases Apr 10, 2026
Copy link
Copy Markdown
Member

@knowler knowler left a comment

Choose a reason for hiding this comment

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

This is a partial review.

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

♻️ Duplicate comments (1)
app/composables/useResultsKeyboardNavigation.ts (1)

75-93: ⚠️ Potential issue | 🟠 Major

ArrowDown still hijacks native control navigation

Line 75 intercepts ArrowDown globally when results exist, so focused inputs/selects can lose native arrow-key behaviour. This concern was already raised and appears unresolved in this revision.

Suggested adjustment
 export function useResultsKeyboardNavigation(options?: {
   includeSuggestions?: boolean
   onArrowUpAtStart?: () => void
+  canStartFrom?: (target: HTMLElement | null) => boolean
 }) {
@@
     if (e.key === 'ArrowDown') {
+      const target = e.target instanceof HTMLElement ? e.target : null
+      const isInteractiveTarget = !!target?.closest(
+        'input, textarea, select, button, [contenteditable="true"]',
+      )
+
+      if (currentIndex < 0 && isInteractiveTarget && !options?.canStartFrom?.(target)) {
+        return
+      }
+
       // If there are results available, handle navigation
       if (elements.length > 0) {
🧹 Nitpick comments (1)
app/composables/useResultsKeyboardNavigation.ts (1)

62-130: Split handleKeydown into per-key handlers for maintainability

handleKeydown is doing three flows and is now quite long; extracting handleArrowDown, handleArrowUp, and handleEnter would keep this composable easier to test and evolve.

As per coding guidelines, "Keep functions focused and manageable (generally under 50 lines)".


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fc18951c-d3d0-44f6-9c25-ea714f5f3c46

📥 Commits

Reviewing files that changed from the base of the PR and between 1374d9f and ab5cc25.

📒 Files selected for processing (1)
  • app/composables/useResultsKeyboardNavigation.ts


// Register keyboard event listeners using useEventListener for better control
// Use capture phase to intercept before other handlers
useEventListener(document, 'keydown', handleKeydown, { capture: true })
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Locate all call sites:"
rg -n -C2 '\buseResultsKeyboardNavigation\s*\(' --type=vue --type=ts

echo
echo "2) Check whether call sites are in client-only files or guarded blocks:"
rg -n -C3 'useResultsKeyboardNavigation|import\.meta\.client' app/pages app/components app/composables

echo
echo "3) Confirm direct document usage in the composable:"
rg -n -C2 '\bdocument\b' app/composables/useResultsKeyboardNavigation.ts

Repository: npmx-dev/npmx.dev

Length of output: 131


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Locate all call sites of useResultsKeyboardNavigation:"
rg -n 'useResultsKeyboardNavigation' app/pages app/components --type ts || echo "No matches found in pages/components"

echo
echo "2) Check composable file for .client suffix and document usage:"
ls -la app/composables/useResultsKeyboardNavigation* 2>/dev/null || echo "Checking exact filename..."
rg -n '\bdocument\b' app/composables/useResultsKeyboardNavigation.ts || echo "No document usage found"

echo
echo "3) Check for import.meta.client or import.meta.server guards in related composables:"
rg -n 'import\.meta\.(client|server)' app/composables/ --type ts -A2 -B2 | head -50

echo
echo "4) Verify the composable signature and return type:"
rg -n -A20 'export function useResultsKeyboardNavigation' app/composables/useResultsKeyboardNavigation.ts

Repository: npmx-dev/npmx.dev

Length of output: 4573


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Search entire repository for useResultsKeyboardNavigation:"
rg -n 'useResultsKeyboardNavigation' --type ts --type js || echo "No matches in codebase"

echo
echo "2) Check if exported from any barrel files (index.ts/index.js):"
find app -name 'index.ts' -o -name 'index.js' | xargs grep -l 'useResultsKeyboardNavigation' 2>/dev/null || echo "Not in barrel exports"

echo
echo "3) Check entire composable for return statement (to see if it returns early on SSR):"
cat -n app/composables/useResultsKeyboardNavigation.ts | tail -20

echo
echo "4) Look for any import.meta checks at start of composable:"
head -30 app/composables/useResultsKeyboardNavigation.ts | cat -n

Repository: npmx-dev/npmx.dev

Length of output: 2570


🏁 Script executed:

#!/bin/bash
set -euo pipefill

echo "1) Check page filenames (are they .client.vue?):"
ls -la app/pages/search.vue app/pages/org/\[org\].vue

echo
echo "2) Check context around useResultsKeyboardNavigation calls:"
echo "--- search.vue around line 448 ---"
sed -n '440,460p' app/pages/search.vue | cat -n

echo
echo "--- org/[org].vue around line 127 ---"
sed -n '120,135p' app/pages/org/\[org\].vue | cat -n

echo
echo "3) Check if pages have any import.meta.client guards:"
rg -n 'import\.meta\.(client|server)' app/pages/search.vue app/pages/org/\[org\].vue

Repository: npmx-dev/npmx.dev

Length of output: 114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Check page filenames (are they .client.vue?):"
ls -la app/pages/search.vue app/pages/org/\[org\].vue

echo
echo "2) Check context around useResultsKeyboardNavigation calls:"
echo "--- search.vue around line 448 ---"
sed -n '440,460p' app/pages/search.vue | cat -n

echo
echo "--- org/[org].vue around line 127 ---"
sed -n '120,135p' app/pages/org/\[org\].vue | cat -n

echo
echo "3) Check if pages have any import.meta.client guards:"
rg -n 'import\.meta\.(client|server)' app/pages/search.vue app/pages/org/\[org\].vue || echo "No guards found"

Repository: npmx-dev/npmx.dev

Length of output: 1768


Guard document access for SSR-safe composable execution

This composable is called during page setup in both app/pages/search.vue and app/pages/org/[org].vue, which are SSR-enabled. Direct document access on lines 30, 42, 73, 119, 120, and 134 will throw document is not defined during server-side rendering.

SSR-safe patch
 export function useResultsKeyboardNavigation(options?: {
   includeSuggestions?: boolean
   onArrowUpAtStart?: () => void
 }) {
+  if (import.meta.server) {
+    return {
+      getFocusableElements: (): HTMLElement[] => [],
+      focusElement: (_el: HTMLElement) => {},
+    }
+  }
+
   const keyboardShortcuts = useKeyboardShortcuts()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review This PR is waiting for a review from a maintainer

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

org results page keyboard controls not working

3 participants