# Global Search Keyboard Shortcut Design

## Business Problem

When exploring large family trees in Topola Genealogy, users frequently need to
locate specific individuals quickly using the search feature. Currently,
accessing the search box requires moving the hand to the mouse or trackpad,
navigating the cursor to the top bar, and clicking the input field, which breaks
the flow of keyboard-driven navigation. This document proposes introducing a
global keyboard shortcut (the `/` key) to instantly focus the search input,
allowing users to search without manual mouse interaction. By streamlining this
transition, the application provides a faster, more accessible, and premium
keyboard-centric workflow for power users navigating complex genealogical data.

---

## Technical Plan

The shortcut mechanism is designed to be lightweight, modular, and resilient
against responsive layout duplication. Rather than introducing complex
third-party shortcut libraries, the feature leverages standard web browser event
handling, custom React hooks, standard accessibility attributes, and a
centralized input registry.

### Major Components and Workflow

1.  **The Encapsulated Hook (`useSearchShortcut`):** The keyboard shortcut logic
    is encapsulated within a custom hook `useSearchShortcut`
    (`src/menu/use_search_shortcut.ts`). Rather than being called by individual
    `SearchBar` components, the hook is invoked once at the layout level in
    `TopBar` (`src/menu/top_bar.tsx`). Individual `SearchBar` instances register
    their underlying `<input>` DOM elements in a centralized module-level
    registry (`registeredSearchInputs` `Set`). This ensures a single `window`
    `keydown` event listener manages focus across all responsive search bar
    instances.
2.  **The Modifier Guard:** To prevent conflicts with system-level and
    browser-level shortcuts (such as `Cmd + /` for help or extensions, or
    `Ctrl + /` for toggling comments), the listener ignores keydown events with
    `Cmd/Meta`. It also ignores `Ctrl` or `Alt` unless both are pressed
    simultaneously (`Ctrl + Alt`), allowing `AltGr` combinations on
    international keyboards to function properly.
3.  **The IME Composition & Default Prevented Guard:** To avoid hijacking
    keystrokes when international users are typing using an Input Method Editor
    (IME) for CJK (Chinese, Japanese, Korean) languages or when another event
    handler has already intercepted the keypress, the listener ignores keydown
    events when `event.isComposing === true` or
    `event.defaultPrevented === true`.
4.  **The Smart Filter (Collision, Repeat & Modal Guards):** Before taking
    action, the listener verifies `event.key === '/'` and checks three
    conditions:
    - **Collision Guard (`isTextEditable`):** It extracts the event path
      (supporting Shadow DOM encapsulation via `event.composedPath()`) and
      traverses all nodes in the path to check for `<input>`, `<textarea>`,
      `<select>`, `<button>`, elements with `contenteditable` active, or
      elements with `role="textbox"`, `role="searchbox"`, `role="spinbutton"`,
      or `role="combobox"`. This guarantees that typing `/` or pressing `/`
      while focused inside form controls, rich text editors, or ARIA fields does
      not trigger the shortcut.
    - **Repeat Guard:** It checks if the key event is repeated
      (`event.repeat === true`). If so, it ignores it to prevent key-repeat
      cycles.
    - **Modal Guard:** It inspects both the main `document` and the event
      target's `getRootNode()` for active modal dialogs
      (`dialog[open], .ui.modal.visible.active`) and ignores any modal with
      `aria-hidden="true"`. It verifies visibility
      (`checkVisibility() || getBoundingClientRect().width > 0`) without forcing
      synchronous layout recalculations (simple dropdown menus are not blocked).
      If an active visible modal is open, the shortcut is ignored to prevent
      stealing focus. If all guards pass and a visible search input is found,
      the listener intercepts the keypress (using `event.preventDefault()`) and
      initiates the focus transition.
5.  **The Centralized Search Input Registry:** Because `@artsy/fresnel` mounts
    both the desktop and mobile top bar menus in the DOM simultaneously, two
    separate instances of the `SearchBar` component exist concurrently. To avoid
    duplicate IDs in the DOM, `id="search"` is removed from the component. When
    mounted, each `SearchBar` registers its underlying HTML `<input>` element in
    a module-level `Set` (`registeredSearchInputs`) via `registerSearchInput` /
    `unregisterSearchInput`. When the shortcut is triggered, `useSearchShortcut`
    iterates through the registered inputs and identifies the active instance by
    checking if it is visible in the viewport
    (`input.checkVisibility() || input.getBoundingClientRect().width > 0`). Once
    found, it focuses and selects it:
    - Focus is requested with `{ preventScroll: true }` to avoid jarring page
      shifts.
    - `select()` is called on the HTML `<input>` element to highlight all
      existing text, allowing the user to immediately overwrite it with a new
      query.
6.  **The Translation Engine (Full Placeholder Localization):** To keep
    translation keys clean and avoid runtime string concatenation issues across
    different languages, two complete placeholder strings are defined:
    `"menu.search.placeholder"` (`"Search for people"`) and
    `"menu.search.placeholder_with_shortcut"`
    (`"Search for people (press '/')"`).
    - On desktop (where `hideShortcutHint` is false or omitted), `SearchBar`
      uses `placeholder_with_shortcut`.
    - On mobile (where `hideShortcutHint` is true), `SearchBar` uses
      `placeholder`, saving screen space on touch devices.
7.  **Accessibility (a11y):** The search input is decorated with the standard
    `aria-keyshortcuts="/"` attribute to declare the global shortcut to screen
    readers and assistive technologies.

### Component Interaction Diagram

```mermaid
graph TD
    A["Web Browser (Global Window)"] -->|1. Keyboard Event ('/')| B["Shared Listener (handleKeyDown)"]
    B -->|2. Check Repeat, Composing, DefaultPrevented & Modifiers| C{"Valid Keypress?"}
    C -->|No| D["Ignore Event"]
    C -->|Yes| E{"3. Check Active Modals & isTextEditable(event)?"}
    E -->|Yes (Modal Active or Typing)| D
    E -->|No| F{"4. Iterate registeredSearchInputs for visible instance?"}
    F -->|No visible input| D
    F -->|Yes| G["5. Intercept event (preventDefault)"]
    G --> H["6. Focus (preventScroll) & Select"]
    H --> I["HTML Input Element"]
```

---

## Alternatives Considered

### 1. Per-Component Event Listeners in `SearchBar`

- **Alternative:** Invoke `useSearchShortcut` inside each `SearchBar` instance
  so that each component attaches its own `window` `keydown` event listener.
- **Why Rejected:** Attaching multiple window event listeners concurrently (one
  for desktop, one for mobile) can lead to duplicate event handling or race
  conditions. Instead, `useSearchShortcut` is called once at the layout level in
  `TopBar`, while individual `SearchBar` components register their DOM `<input>`
  nodes in a shared module-level `Set` (`registeredSearchInputs`).

### 2. Using React Refs to Focus the Input

- **Alternative:** Pass a React `ref` to `<Search>` and call `focus()` or
  `getBoundingClientRect()` directly on it.
- **Why Rejected:** Semantic UI React's `Search` / `Input` components are class
  components or wrappers where refs do not consistently return the raw HTML
  `<input>` DOM node required for calling `select()`. Querying the wrapper
  element (`wrapperRef.current.querySelector('input')`) reliably targets the
  underlying HTML input element for registration.

---

## Detailed Implementation

### Affected Files and Rationales

#### 1. [use_search_shortcut.ts](file:///home/pwiech/personal/github/topola-viewer/src/menu/use_search_shortcut.ts)

- **Rationale:** New custom hook and registry file. Implements
  `registerSearchInput` / `unregisterSearchInput` to manage mounted search input
  instances in a module-level `Set`. Implements `isTextEditable(event)` to guard
  against input hijacking across standard form controls (`<input>`,
  `<textarea>`, `<select>`, `<button>`) and ARIA text fields (`textbox`,
  `searchbox`, `spinbutton`, `combobox`) by traversing all nodes in the composed
  path. Implements `isModalActive(event)` to verify active visible modals
  (`dialog[open], .ui.modal.visible.active`) across main document and Shadow DOM
  roots (ignoring `aria-hidden="true"`). Implements `useSearchShortcut()` to
  attach a single window `keydown` listener (matching `event.key === '/'` with
  `AltGr` support), iterate registered inputs to identify the visible instance,
  intercept events with `preventDefault()`, and focus/select the search input.

#### 2. [search.tsx](file:///home/pwiech/personal/github/topola-viewer/src/menu/search.tsx)

- **Rationale:** Remove `id="search"` to comply with HTML specs. Register and
  unregister the underlying `<input>` DOM node in `registeredSearchInputs` upon
  mount/unmount. Select between `"menu.search.placeholder"` and
  `"menu.search.placeholder_with_shortcut"` based on the `hideShortcutHint`
  prop. Add `aria-keyshortcuts="/"` to the input shorthand.

#### 3. [top_bar.tsx](file:///home/pwiech/personal/github/topola-viewer/src/menu/top_bar.tsx)

- **Rationale:** Invoke `useSearchShortcut()` at the layout level. Pass
  `hideShortcutHint={true}` to the mobile `SearchBar` instance rendered inside
  `mobileMenus()`.

#### 4. Localized Translation Files:

- [de.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/de.json)
- [pl.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/pl.json)
- [fr.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/fr.json)
- [it.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/it.json)
- [ru.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/ru.json)
- [bg.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/bg.json)
- [cs.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/cs.json)
- **Rationale:** Define `"menu.search.placeholder_with_shortcut"` with localized
  translations across all supported languages.

#### 5. [search.spec.ts](file:///home/pwiech/personal/github/topola-viewer/tests/search.spec.ts)

- **Rationale:** Playwright E2E test suite validating:
  - Shortcut focus and text selection upon pressing `/`.
  - Viewport switching where search query is preserved when resizing between
    desktop and mobile.
  - Collision safety when typing `/` into active modal inputs (e.g., "Load from
    URL").
  - Modal safety preventing focus theft when a modal is open and blurred.
  - Form control protection preventing focus theft when focused on buttons.
  - Modifier key exclusion (`Ctrl+/`, `Alt+/`, `Meta+/`).
  - Landing page safety where no search bar is rendered.
  - Question mark safety ensuring pressing `?` (`Shift+/`) does not trigger the
    shortcut.
  - Dropdown menu safety allowing the shortcut to focus the search bar while a
    dropdown menu is open.
  - Combobox wrapper interaction where clicking inside the search wrapper and
    pressing `/` focuses the input.
  - AltGr compatibility ensuring international keyboard combinations
    (`Ctrl+Alt+/`) trigger the shortcut.
