Files
supabase/apps/studio/components/ui/Shortcut.tsx
T
Ali Waseem 618c902e1a feat(studio): add keyboard shortcuts to Database listing pages (#45467)
## Summary

Adds the `O→S` / `Shift+F` / `Shift+N` / `F→C` shortcut set across every
Database listing page, on top of the schema-visualizer pattern from
#45386. Fixes [FE-3131](https://linear.app/supabase/issue/FE-3131).

A shared `list-page.*` registry replaces what would have been a per-page
registry file for each route, and `useShortcut`/`Shortcut` gain a
`label` override so a single ID renders contextually in Cmd+K and hover
tooltips.

## Pages and shortcuts

| Page | `O→S` | `Shift+F` | `Shift+N` | `F→C` |
| --- | --- | --- | --- | --- |
| Tables | Open schema selector | Search tables | Create new table |
Clears search + entity-type filter |
| Functions | Open schema selector | Search functions | Create new
function | Clears search + Return Type + Security |
| Triggers — data | Open schema selector | Search triggers | Create new
trigger | Clears search + Table filter |
| Triggers — event | — | Search event triggers | Create new event
trigger | Clears search + Owner filter |
| Enumerated Types | Open schema selector | Search enumerated types |
Create new enumerated type | Clears search |
| Indexes | Open schema selector | Search indexes | Create new index |
Clears search |
| Roles | — | Search roles | Add new role | Clears search + filter type
→ 'all' |
| Publications | — | Search publications | — | Clears search |
| Extensions | — | Search extensions | — | Clears search |

`Shift+N` only fires when the page-specific gate allows it (permission +
unlocked schema + any other prerequisite like `hasTables` for triggers).

## Test plan

- [ ] On each of the 9 pages, all four shortcuts behave as listed in the
table above
- [ ] Hover the wrapped controls — tooltip shows the page-specific label
and the right keybind badge
- [ ] Open the schema selector via `O→S` — the hover tooltip is
suppressed while the popover is open
- [ ] `Shift+N` is a no-op on locked schemas / when permission is
missing / when a prereq fails (e.g. no tables on Triggers data)
- [ ] `Shift+F` focuses the search input; while focused, Escape clears
(with text) → Escape blurs (when empty)
- [ ] `Cmd+K` shows the page-specific shortcut name while on each page;
the entry goes away on navigation
- [ ] `Mod+/` reference sheet shows one "List pages" group with 4
generic entries
- [ ] No regression to existing schema-visualizer / table-editor /
SQL-editor shortcut behavior

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a dedicated list-page shortcut group in the shortcuts reference.

* **Improvements**
* Keyboard shortcuts across database list pages: focus & select search,
reset filters, and create-new-item shortcuts.
* Escape now clears/searches or blurs inputs to avoid accidental popover
closes.
* Create/new buttons respond to shortcuts when allowed; disabled actions
keep permission tooltips for clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-04 07:08:35 -06:00

86 lines
2.6 KiB
TypeScript

import { TooltipContentProps } from '@ui/components/shadcn/ui/tooltip'
import type { ReactNode } from 'react'
import { ShortcutTooltip } from './ShortcutTooltip'
import type { ShortcutId } from '@/state/shortcuts/registry'
import type { ShortcutOptions } from '@/state/shortcuts/types'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface ShortcutProps {
/** Registered shortcut id — drives both the hotkey binding and the tooltip. */
id: ShortcutId
/** Fires on the hotkey. Usually the same handler wired to the child's `onClick`. */
onTrigger: () => void
/** Element to bind the shortcut to and wrap in the tooltip. */
children: ReactNode
/** Per-mount overrides for the shortcut — see `ShortcutOptions`. */
options?: ShortcutOptions
side?: TooltipContentProps['side']
align?: TooltipContentProps['align']
sideOffset?: number
delayDuration?: number
/**
* Override the label from the registry. Use when the wrapped element's
* action is a narrower/contextual variant of the registered shortcut.
*/
label?: string
/**
* Controlled open state for the tooltip. Pass `false` to force the tooltip
* closed (e.g. while a popover or dialog opened by the wrapped element is
* visible). Leave `undefined` for default uncontrolled behavior.
*/
tooltipOpen?: boolean
}
/**
* Bind a registered shortcut to an element AND show its keybind on hover,
* Linear-style. Single source of truth: one `id` drives both the hotkey
* listener and the tooltip, so they can't drift.
*
* The wrapped child stays fully interactive — Radix `asChild` passes clicks,
* focus, and refs through untouched.
*
* @example
* <Shortcut id={SHORTCUT_IDS.RESULTS_COPY_MARKDOWN} onTrigger={handleCopy}>
* <Button onClick={handleCopy}>Copy</Button>
* </Shortcut>
*
* @example
* // Gate the hotkey on local state; tooltip still renders:
* <Shortcut
* id={SHORTCUT_IDS.ACTION_BAR_SAVE}
* onTrigger={handleSave}
* options={{ enabled: hasUnsavedChanges }}
* >
* <Button onClick={handleSave} disabled={!hasUnsavedChanges}>Save</Button>
* </Shortcut>
*/
export const Shortcut = ({
id,
onTrigger,
children,
options,
side,
align,
sideOffset,
delayDuration,
label,
tooltipOpen,
}: ShortcutProps) => {
useShortcut(id, onTrigger, label !== undefined ? { ...options, label } : options)
return (
<ShortcutTooltip
shortcutId={id}
side={side}
align={align}
sideOffset={sideOffset}
delayDuration={delayDuration}
label={label}
open={tooltipOpen}
>
{children}
</ShortcutTooltip>
)
}