feat(editor): keyboard shortcuts j/k/Enter/n per handoff Screen 2
Handoff line 438: 'Pressing j/k moves selected row; Enter opens drawer;
Esc closes; n opens new-record drawer; / focuses search.'
Implementation:
- rowCursor state — index в visibleRecords (-1 = no focus)
- j или ArrowDown → cursor++ (clamped)
- k или ArrowUp → cursor-- (clamped)
- Enter → setEdit({kind:'edit', record}) когда cursor valid
- n → setEdit({kind:'create'}) когда canMutate
- Esc — handled Radix Dialog natively через RecordDrawer
- / — handled TopBar (⌘K equivalent, focuses global search input)
Guards:
- Skip когда target inside input/textarea/contentEditable (no conflict с typing)
- Skip когда drawer/modal open (edit.kind !== 'closed' — focus в portal)
- Cursor reset на page/filter change (stale index безопасности)
Visual: active row получает bg-accent-bg/40 outline accent для clarity.
This commit is contained in:
@@ -331,6 +331,48 @@ function DictionaryDetail() {
|
|||||||
? 'indeterminate'
|
? 'indeterminate'
|
||||||
: allVisibleSelected
|
: allVisibleSelected
|
||||||
|
|
||||||
|
// === Keyboard shortcuts per handoff Screen 2 line 438 ===
|
||||||
|
// j/k — move row cursor up/down
|
||||||
|
// Enter — open drawer для cursor row
|
||||||
|
// Esc — close drawer (handled Radix natively)
|
||||||
|
// n — open new-record drawer
|
||||||
|
// / — focus search (handled TopBar ⌘K shortcut; tab-style / focuses search)
|
||||||
|
//
|
||||||
|
// Cursor — index в visibleRecords. -1 когда нет focus (initial state).
|
||||||
|
const [rowCursor, setRowCursor] = useState(-1)
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
// Skip когда юзер в textarea/input — не мешаем typing.
|
||||||
|
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) return
|
||||||
|
if (target && target.isContentEditable) return
|
||||||
|
// Skip когда drawer / modal открыт (focus inside Radix portal).
|
||||||
|
if (edit.kind !== 'closed') return
|
||||||
|
// Skip если другие routes — ловим только на /dictionaries/$name тут.
|
||||||
|
|
||||||
|
if (e.key === 'j' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
setRowCursor((c) => Math.min(visibleRecords.length - 1, c + 1))
|
||||||
|
} else if (e.key === 'k' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
setRowCursor((c) => Math.max(0, c - 1))
|
||||||
|
} else if (e.key === 'Enter' && rowCursor >= 0 && rowCursor < visibleRecords.length) {
|
||||||
|
e.preventDefault()
|
||||||
|
setEdit({ kind: 'edit', record: visibleRecords[rowCursor] })
|
||||||
|
} else if (e.key === 'n' && canMutate) {
|
||||||
|
e.preventDefault()
|
||||||
|
setEdit({ kind: 'create' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', handler)
|
||||||
|
return () => window.removeEventListener('keydown', handler)
|
||||||
|
}, [visibleRecords, rowCursor, edit.kind, canMutate])
|
||||||
|
|
||||||
|
// Reset cursor когда visibleRecords меняется (filter, page).
|
||||||
|
useEffect(() => {
|
||||||
|
setRowCursor(-1)
|
||||||
|
}, [page, search, scopeFilter, aoi])
|
||||||
|
|
||||||
const toggleSelect = (key: string) => {
|
const toggleSelect = (key: string) => {
|
||||||
setSelection((prev) => {
|
setSelection((prev) => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
@@ -790,8 +832,12 @@ function DictionaryDetail() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{visibleRecords.map((r) => (
|
{visibleRecords.map((r, idx) => (
|
||||||
<TableRow key={r.id} data-selected={selection.has(r.businessKey)}>
|
<TableRow
|
||||||
|
key={r.id}
|
||||||
|
data-selected={selection.has(r.businessKey)}
|
||||||
|
className={rowCursor === idx ? 'bg-accent-bg/40 outline outline-1 outline-accent/30' : undefined}
|
||||||
|
>
|
||||||
{canMutate && (
|
{canMutate && (
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
Reference in New Issue
Block a user