Repository: vercel/next.js
Pull request: bundle-analyzer: add kbd shortcut labels and fix focus bugs

- Adds decorated kbd-style shortcut labels to cue the user into using them
- Listens only for the appropriate OS or Ctrl key on Mac/Others
- Esc now clears and focuses the search field and the treemap focus
- Does not trigger in the case where the user has another element focused

![image.png](https://app.graphite.com/user-attachments/assets/6a85d1e7-a711-432d-86b0-f65c0b89de74.png)



Changed files:
filename: apps/bundle-analyzer/app/page.tsx
patch: @@ -1,18 +1,15 @@
 'use client'
 
 import type React from 'react'
-import { useEffect, useMemo, useRef, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
 import useSWR from 'swr'
 import { ErrorState } from '@/components/error-state'
-import {
-  RouteTypeahead,
-  type RouteTypeaheadRef,
-} from '@/components/route-typeahead'
+import { FileSearch } from '@/components/file-search'
+import { RouteTypeahead } from '@/components/route-typeahead'
 import { Sidebar } from '@/components/sidebar'
 import { TreemapVisualizer } from '@/components/treemap-visualizer'
 
 import { Badge } from '@/components/ui/badge'
-import { Input } from '@/components/ui/input'
 import { TreemapSkeleton } from '@/components/ui/skeleton'
 import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
 import { AnalyzeData, ModulesData } from '@/lib/analyze-data'
@@ -72,31 +69,27 @@ export default function Home() {
     client?: boolean
   } | null>(null)
   const [searchQuery, setSearchQuery] = useState('')
-  const [searchFocused, setSearchFocused] = useState(false)
-
-  const routeTypeaheadRef = useRef<RouteTypeaheadRef>(null)
-  const searchInputRef = useRef<HTMLInputElement>(null)
 
   useEffect(() => {
     const handleKeyDown = (e: KeyboardEvent) => {
-      // Cmd+K or Ctrl+K to focus route filter
-      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
-        e.preventDefault()
-        routeTypeaheadRef.current?.focus()
-      }
-      // / to focus search (only if not already in an input)
-      else if (
-        e.key === '/' &&
-        !['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)
-      ) {
-        e.preventDefault()
-        searchInputRef.current?.focus()
+      // esc clears current treemap source selection
+      if (e.key === 'Escape') {
+        const activeElement = document.activeElement
+        const isInputFocused =
+          activeElement && ['INPUT', 'TEXTAREA'].includes(activeElement.tagName)
+
+        if (!isInputFocused) {
+          e.preventDefault()
+          const rootSourceIndex = getRootSourceIndex(analyzeData)
+          setSelectedSourceIndex(rootSourceIndex)
+          setFocusedSourceIndex(rootSourceIndex)
+        }
       }
     }
 
     window.addEventListener('keydown', handleKeyDown)
     return () => window.removeEventListener('keydown', handleKeyDown)
-  }, [])
+  }, [analyzeData])
 
   // Compute module depth map from active entries
   const moduleDepthMap = useMemo(() => {
@@ -155,7 +148,6 @@ export default function Home() {
       <div className="flex-none px-4 py-2 border-b border-border flex items-center gap-4">
         <div className="basis-1/3 flex">
           <RouteTypeahead
-            ref={routeTypeaheadRef}
             selectedRoute={selectedRoute}
             onRouteSelected={(route) => {
               setSelectedRoute(route)
@@ -196,27 +188,7 @@ export default function Home() {
                 <ToggleGroupItem value="asset">Asset</ToggleGroupItem>
               </ToggleGroup>
 
-              {!searchFocused && (
-                <div className="flex items-center gap-4 text-xs">
-                  <p className="text-muted-foreground">
-                    {
-                      analyzeData.source(focusedSourceIndex ?? rootSourceIndex)
-                        ?.path
-                    }
-                  </p>
-                </div>
-              )}
-
-              <Input
-                ref={searchInputRef}
-                type="search"
-                value={searchQuery}
-                onChange={(e) => setSearchQuery(e.target.value)}
-                onFocus={() => setSearchFocused(true)}
-                onBlur={() => setSearchFocused(false)}
-                placeholder="Search files..."
-                className="w-48 focus:w-80 transition-all duration-200"
-              />
+              <FileSearch value={searchQuery} onChange={setSearchQuery} />
             </>
           )}
         </div>

filename: apps/bundle-analyzer/components/file-search.tsx
patch: @@ -0,0 +1,63 @@
+'use client'
+
+import { useEffect, useRef, useState } from 'react'
+import { Input } from '@/components/ui/input'
+import { Kbd } from '@/components/ui/kbd'
+
+interface FileSearchProps {
+  value: string
+  onChange: (value: string) => void
+}
+
+export function FileSearch({ value, onChange }: FileSearchProps) {
+  const [focused, setFocused] = useState(false)
+  const inputRef = useRef<HTMLInputElement>(null)
+
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (
+        e.key === '/' &&
+        !['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)
+      ) {
+        e.preventDefault()
+        inputRef.current?.focus()
+      } else if (
+        e.key === 'Escape' &&
+        document.activeElement === inputRef.current
+      ) {
+        e.preventDefault()
+        onChange('')
+        inputRef.current?.blur()
+      }
+    }
+
+    window.addEventListener('keydown', handleKeyDown)
+    return () => window.removeEventListener('keydown', handleKeyDown)
+  }, [onChange])
+
+  const handleFocus = () => {
+    setFocused(true)
+  }
+
+  const handleBlur = () => {
+    setFocused(false)
+  }
+
+  return (
+    <div className="relative">
+      <Input
+        ref={inputRef}
+        type="search"
+        value={value}
+        onChange={(e) => onChange(e.target.value)}
+        onFocus={handleFocus}
+        onBlur={handleBlur}
+        placeholder="Search files..."
+        className="w-48 focus:w-80 transition-all duration-200 pr-8"
+      />
+      {!value && !focused && (
+        <Kbd className="absolute right-2 top-1/2 -translate-y-1/2">/</Kbd>
+      )}
+    </div>
+  )
+}

filename: apps/bundle-analyzer/components/route-typeahead.tsx
patch: @@ -2,7 +2,7 @@
 
 import useSWR from 'swr'
 import { Check, ChevronsUpDown, Loader, Route } from 'lucide-react'
-import { forwardRef, useImperativeHandle, useState } from 'react'
+import { useEffect, useState } from 'react'
 import { Button } from '@/components/ui/button'
 import {
   Command,
@@ -19,27 +19,44 @@ import {
 } from '@/components/ui/popover'
 import { cn, jsonFetcher } from '@/lib/utils'
 import { NetworkError } from '@/lib/errors'
+import { Kbd } from '@/components/ui/kbd'
 
 interface RouteTypeaheadProps {
   selectedRoute: string | null
   onRouteSelected: (routeName: string) => void
 }
 
-export interface RouteTypeaheadRef {
-  focus: () => void
-}
-
-export const RouteTypeahead = forwardRef<
-  RouteTypeaheadRef,
-  RouteTypeaheadProps
->(function RouteTypeahead({ selectedRoute, onRouteSelected }, ref) {
+export function RouteTypeahead({
+  selectedRoute,
+  onRouteSelected,
+}: RouteTypeaheadProps) {
   const [open, setOpen] = useState(false)
+  const [shortcutLabel, setShortcutLabel] = useState<string | null>(null)
 
-  useImperativeHandle(ref, () => ({
-    focus: () => {
-      setOpen(true)
-    },
-  }))
+  useEffect(() => {
+    const isAppleDevice = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
+    setShortcutLabel(isAppleDevice ? '⌘K' : 'Ctrl+K')
+
+    const handleKeyDown = (e: KeyboardEvent) => {
+      const activeElement = document.activeElement
+      const isInputFocused =
+        activeElement && ['INPUT', 'TEXTAREA'].includes(activeElement.tagName)
+
+      if (isInputFocused) return
+
+      const isShortcutPressed = isAppleDevice
+        ? e.metaKey && e.key === 'k'
+        : e.ctrlKey && e.key === 'k'
+
+      if (isShortcutPressed) {
+        e.preventDefault()
+        setOpen(true)
+      }
+    }
+
+    window.addEventListener('keydown', handleKeyDown)
+    return () => window.removeEventListener('keydown', handleKeyDown)
+  }, [])
 
   const {
     data: routes,
@@ -96,7 +113,10 @@ export const RouteTypeahead = forwardRef<
 
               {ctaText}
             </div>
-            <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+            <div className="flex items-center gap-2">
+              {shortcutLabel && <Kbd>{shortcutLabel}</Kbd>}
+              <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
+            </div>
           </Button>
         </PopoverTrigger>
         <PopoverContent className="w-96 p-0">
@@ -133,4 +153,4 @@ export const RouteTypeahead = forwardRef<
       </Popover>
     </div>
   )
-})
+}

filename: apps/bundle-analyzer/components/ui/kbd.tsx
patch: @@ -0,0 +1,28 @@
+import { cn } from '@/lib/utils'
+
+function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
+  return (
+    <kbd
+      data-slot="kbd"
+      className={cn(
+        'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
+        "[&_svg:not([class*='size-'])]:size-3",
+        '[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <kbd
+      data-slot="kbd-group"
+      className={cn('inline-flex items-center gap-1', className)}
+      {...props}
+    />
+  )
+}
+
+export { Kbd, KbdGroup }