refactor: consolidate sidebar item interactions

This commit is contained in:
lucaronin
2026-05-01 06:34:25 +02:00
parent 09759d204c
commit 4acbe17e1e
11 changed files with 263 additions and 150 deletions

View File

@@ -309,6 +309,7 @@ type SidebarSelection =
`SidebarSelection.kind === 'folder'` is a first-class navigation target, not just a visual highlight.
- `FolderTree` keeps the folder interaction surface decomposed into `FolderTreeRow`, `FolderNameInput`, `FolderContextMenu`, and disclosure/context-menu hooks so nested row rendering, inline rename, and right-click actions stay isolated. The UI wraps backend folder nodes in a synthetic vault-root row with `path: ""` and `rootPath` set to the opened vault so root-level files can be listed without turning the vault root into a mutable folder. Non-mutating reveal/copy-path menu items stay callback-driven from `App` so filesystem convenience actions do not leak into folder mutation hooks.
- `src/components/sidebar/sidebarHooks.ts` owns the shared sidebar interaction primitives for menu positioning/dismissal and inline rename input behavior. Folder, Type, and saved View rows keep their domain-specific actions local, but use those primitives so right-click menus and rename fields have the same outside-click, Escape, focus, blur, and submit semantics.
- `useFolderActions()` composes `useFolderRename()` and `useFolderDelete()` to keep folder mutations selection-aware while the rest of `App.tsx` only wires the resulting callbacks into `Sidebar` and the command registry.
- `useNoteRetargeting()` is the shared retargeting abstraction for note drops and command-palette actions. It owns the "can drop here?" checks, updates `type:` via frontmatter when a note lands on a type section, and delegates folder moves through the same crash-safe rename pipeline used by the backend rename commands.
- A successful folder rename reloads the folder tree plus vault entries, rewrites any affected folder-scoped tabs, and updates `SidebarSelection` to the new relative path when the renamed folder stays selected.

View File

@@ -287,6 +287,21 @@ describe('FolderTree', () => {
expect(onDeleteFolder).toHaveBeenCalledWith('projects')
})
it('dismisses the folder context menu on Escape', () => {
render(
<FolderTree
folders={mockFolders}
selection={defaultSelection}
onSelect={vi.fn()}
onDeleteFolder={vi.fn()}
onStartRenameFolder={vi.fn()}
/>,
)
fireEvent.contextMenu(screen.getByText('projects'))
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.queryByTestId('folder-context-menu')).not.toBeInTheDocument()
})
it('opens folder file actions from the context menu', () => {
const onRevealFolder = vi.fn()
const onCopyFolderPath = vi.fn()

View File

@@ -1290,6 +1290,16 @@ describe('Sidebar', () => {
expect(screen.getByText('Delete view')).toBeInTheDocument()
})
it('opens and dismisses the View context menu from the keyboard', () => {
renderViewActions()
const row = screen.getByText('Active Projects').closest('[class*="cursor-pointer"]')!
fireEvent.keyDown(row, { key: 'F10', shiftKey: true })
expect(screen.getByText('Edit view')).toBeInTheDocument()
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.queryByText('Edit view')).not.toBeInTheDocument()
})
it('calls onEditView from the context menu', () => {
const onEditView = vi.fn()
renderViewActions({ onEditView })

View File

@@ -80,6 +80,12 @@ describe('Sidebar Type row actions', () => {
expect(screen.getByText('Delete type')).toBeInTheDocument()
})
it('dismisses the type context menu on Escape', () => {
openProjectsContextMenu()
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.queryByText('Rename type…')).not.toBeInTheDocument()
})
it('calls onDeleteType from the context menu', () => {
const onDeleteType = vi.fn()
renderSidebar({ onDeleteType })

View File

@@ -1,10 +1,12 @@
import { type ComponentType, useState, useEffect, useRef } from 'react'
import { type ComponentType } from 'react'
import type { SidebarSelection } from '../types'
import { cn } from '@/lib/utils'
import { getTypeColor, getTypeLightColor } from '../utils/typeColors'
import { type IconProps } from '@phosphor-icons/react'
import { SIDEBAR_ITEM_PADDING } from './sidebar/sidebarStyles'
import { useSidebarInlineRenameInput } from './sidebar/sidebarHooks'
import { Button } from './ui/button'
import { Input } from './ui/input'
import { translate, type AppLocale } from '../lib/i18n'
const SIDEBAR_COUNT_PILL_STYLE = {
@@ -336,27 +338,28 @@ function InlineRenameInput({ initialValue, onSubmit, onCancel, locale }: {
onCancel: () => void
locale?: AppLocale
}) {
const [value, setValue] = useState(initialValue)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => { inputRef.current?.focus(); inputRef.current?.select() }, [])
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); onSubmit(value.trim()) }
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); onCancel() }
}
const {
handleKeyDown,
inputRef,
setValue,
submitValue,
value,
} = useSidebarInlineRenameInput({
initialValue,
onCancel,
onSubmit: (nextValue) => onSubmit(nextValue.trim()),
})
return (
<input
<Input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={() => onSubmit(value.trim())}
onBlur={() => { void submitValue() }}
onClick={(e) => e.stopPropagation()}
aria-label={translate(locale ?? 'en', 'sidebar.section.name')}
className="flex-1 rounded border border-primary bg-background text-[13px] font-medium text-foreground outline-none"
style={{ padding: '1px 4px' }}
className="h-auto min-h-0 flex-1 rounded border-primary bg-background px-1 py-px text-[13px] font-medium text-foreground"
/>
)
}

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Folder } from '@phosphor-icons/react'
import { Input } from '@/components/ui/input'
import { useSidebarInlineRenameInput } from '../sidebar/sidebarHooks'
interface FolderNameInputProps {
ariaLabel: string
@@ -25,26 +25,18 @@ export function FolderNameInput({
onCancel,
onSubmit,
}: FolderNameInputProps) {
const [value, setValue] = useState(initialValue)
const inputRef = useRef<HTMLInputElement>(null)
const submittingRef = useRef(false)
useEffect(() => {
const input = inputRef.current
if (!input) return
input.focus()
if (selectTextOnFocus) input.select()
}, [selectTextOnFocus])
const handleSubmit = useCallback(async () => {
if (submittingRef.current) return false
submittingRef.current = true
try {
return await onSubmit(value)
} finally {
submittingRef.current = false
}
}, [onSubmit, value])
const {
handleKeyDown,
inputRef,
setValue,
submitValue,
value,
} = useSidebarInlineRenameInput({
initialValue,
onCancel,
onSubmit,
selectTextOnFocus,
})
return (
<div className="flex items-center gap-2 rounded" style={{ paddingTop: 6, paddingBottom: 6, paddingRight: 16, paddingLeft: leftInset, borderRadius: 4 }}>
@@ -55,17 +47,8 @@ export function FolderNameInput({
className="h-auto min-h-0 flex-1 rounded-sm px-2 py-[3px] text-[13px] font-medium"
value={value}
onChange={(event) => setValue(event.target.value)}
onBlur={submitOnBlur ? () => { void handleSubmit() } : undefined}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault()
void handleSubmit()
}
if (event.key === 'Escape') {
event.preventDefault()
onCancel()
}
}}
onBlur={submitOnBlur ? () => { void submitValue() } : undefined}
onKeyDown={handleKeyDown}
placeholder={placeholder}
data-testid={testId}
/>

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState, type MouseEvent as ReactMouseEvent, type RefObject } from 'react'
import { useCallback, type MouseEvent as ReactMouseEvent } from 'react'
import type { FolderNode } from '../../types'
import type { FolderFileActions } from '../../hooks/useFileActions'
import type { FolderContextMenuState } from './FolderContextMenu'
import { useSidebarContextMenu } from '../sidebar/sidebarHooks'
interface UseFolderContextMenuInput {
onDeleteFolder?: (folderPath: string) => void
@@ -9,50 +9,21 @@ interface UseFolderContextMenuInput {
onStartRenameFolder?: (folderPath: string) => void
}
function useContextMenuDismiss(
contextMenu: FolderContextMenuState | null,
menuRef: RefObject<HTMLDivElement | null>,
closeContextMenu: () => void,
) {
useEffect(() => {
if (!contextMenu) return
const handleOutsideClick = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) closeContextMenu()
}
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') closeContextMenu()
}
document.addEventListener('mousedown', handleOutsideClick)
document.addEventListener('keydown', handleEscape)
return () => {
document.removeEventListener('mousedown', handleOutsideClick)
document.removeEventListener('keydown', handleEscape)
}
}, [closeContextMenu, contextMenu, menuRef])
}
export function useFolderContextMenu({
onDeleteFolder,
folderFileActions,
onStartRenameFolder,
}: UseFolderContextMenuInput) {
const [contextMenu, setContextMenu] = useState<FolderContextMenuState | null>(null)
const menuRef = useRef<HTMLDivElement>(null)
const closeContextMenu = useCallback(() => setContextMenu(null), [])
useContextMenuDismiss(contextMenu, menuRef, closeContextMenu)
const {
closeContextMenu,
contextMenu,
contextMenuRef,
openContextMenuFromPointer,
} = useSidebarContextMenu<string>()
const handleOpenMenu = useCallback((node: FolderNode, event: ReactMouseEvent<HTMLDivElement>) => {
event.preventDefault()
event.stopPropagation()
setContextMenu({
path: node.path,
x: event.clientX,
y: event.clientY,
})
}, [])
openContextMenuFromPointer(node.path, event)
}, [openContextMenuFromPointer])
const handleRenameFromMenu = useCallback((folderPath: string) => {
closeContextMenu()
@@ -73,15 +44,20 @@ export function useFolderContextMenu({
closeContextMenu()
folderFileActions?.copyFolderPath(folderPath)
}, [closeContextMenu, folderFileActions])
const menu = contextMenu ? {
path: contextMenu.target,
x: contextMenu.pos.x,
y: contextMenu.pos.y,
} : null
return {
closeContextMenu,
contextMenu,
contextMenu: menu,
handleCopyPathFromMenu,
handleDeleteFromMenu,
handleOpenMenu,
handleRevealFromMenu,
handleRenameFromMenu,
menuRef,
menuRef: contextMenuRef,
}
}

View File

@@ -1,5 +1,5 @@
import {
useEffect, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject,
type ReactNode, type RefObject,
} from 'react'
import {
Palette, PencilSimple, Trash,
@@ -9,6 +9,7 @@ import { Input } from '@/components/ui/input'
import type { ViewDefinition, ViewFile } from '../../types'
import { translate, type AppLocale } from '../../lib/i18n'
import { TypeCustomizePopover } from '../TypeCustomizePopover'
import { useSidebarInlineRenameInput } from './sidebarHooks'
export interface MenuPosition {
x: number
@@ -28,23 +29,13 @@ export function ViewRenameInput({
onCancel: () => void
onSubmit: (value: string) => void
}) {
const [value, setValue] = useState(initialValue)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => { inputRef.current?.focus(); inputRef.current?.select() }, [])
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault()
event.stopPropagation()
onSubmit(value)
}
if (event.key === 'Escape') {
event.preventDefault()
event.stopPropagation()
onCancel()
}
}
const {
handleKeyDown,
inputRef,
setValue,
submitValue,
value,
} = useSidebarInlineRenameInput({ initialValue, onCancel, onSubmit })
return (
<Input
@@ -52,7 +43,7 @@ export function ViewRenameInput({
aria-label={translate(locale, 'sidebar.view.name')}
className="h-6 min-w-0 flex-1 rounded border-primary bg-background px-1.5 py-0 text-[13px] font-medium"
value={value}
onBlur={() => onSubmit(value)}
onBlur={() => { void submitValue() }}
onChange={(event) => setValue(event.target.value)}
onClick={(event) => event.stopPropagation()}
onDoubleClick={(event) => event.stopPropagation()}

View File

@@ -1,4 +1,8 @@
import { useState, useMemo, useEffect, useCallback, type RefObject } from 'react'
import {
useState, useMemo, useEffect, useCallback, useRef,
type KeyboardEvent as ReactKeyboardEvent,
type RefObject,
} from 'react'
import type { VaultEntry } from '../../types'
import { APP_STORAGE_KEYS, LEGACY_APP_STORAGE_KEYS, getAppStorageItem } from '../../constants/appStorage'
import { buildTypeEntryMap } from '../../utils/typeColors'
@@ -8,7 +12,50 @@ import type { AllNotesFileVisibility } from '../../utils/allNotesFileVisibility'
export type SidebarGroupKey = 'favorites' | 'views' | 'sections' | 'folders'
export function useOutsideClick(ref: RefObject<HTMLElement | null>, isOpen: boolean, onClose: () => void) {
export interface SidebarMenuPosition {
x: number
y: number
}
export interface SidebarContextMenuState<T> {
target: T
pos: SidebarMenuPosition
}
interface PointerMenuEvent {
clientX: number
clientY: number
preventDefault?: () => void
stopPropagation?: () => void
}
interface SidebarInlineRenameInputOptions {
initialValue: string
onCancel: () => void
onSubmit: (value: string) => Promise<boolean> | boolean | void
selectTextOnFocus?: boolean
}
const KEYBOARD_MENU_FALLBACK: SidebarMenuPosition = { x: 20, y: 100 }
export function getPointerMenuPosition(event: PointerMenuEvent): SidebarMenuPosition {
return { x: event.clientX, y: event.clientY }
}
export function getElementMenuPosition(
element: HTMLElement | null,
fallback: SidebarMenuPosition = KEYBOARD_MENU_FALLBACK,
): SidebarMenuPosition {
const bounds = element?.getBoundingClientRect()
if (!bounds) return fallback
return { x: bounds.left + 16, y: bounds.top + bounds.height }
}
export function useOutsideClick<T extends HTMLElement>(
ref: RefObject<T | null>,
isOpen: boolean,
onClose: () => void,
) {
useEffect(() => {
if (!isOpen) return
const handler = (event: MouseEvent) => {
@@ -19,6 +66,97 @@ export function useOutsideClick(ref: RefObject<HTMLElement | null>, isOpen: bool
}, [ref, isOpen, onClose])
}
export function useDismissableSidebarLayer<T extends HTMLElement>(
ref: RefObject<T | null>,
isOpen: boolean,
onClose: () => void,
) {
useOutsideClick(ref, isOpen, onClose)
useEffect(() => {
if (!isOpen) return
const handler = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose()
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [isOpen, onClose])
}
export function useSidebarContextMenu<T>() {
const [contextMenu, setContextMenu] = useState<SidebarContextMenuState<T> | null>(null)
const contextMenuRef = useRef<HTMLDivElement>(null)
const closeContextMenu = useCallback(() => setContextMenu(null), [])
useDismissableSidebarLayer(contextMenuRef, !!contextMenu, closeContextMenu)
const openContextMenuAt = useCallback((target: T, pos: SidebarMenuPosition) => {
setContextMenu({ target, pos })
}, [])
const openContextMenuFromPointer = useCallback((target: T, event: PointerMenuEvent) => {
event.preventDefault?.()
event.stopPropagation?.()
openContextMenuAt(target, getPointerMenuPosition(event))
}, [openContextMenuAt])
return {
closeContextMenu,
contextMenu,
contextMenuRef,
openContextMenuAt,
openContextMenuFromPointer,
}
}
export function useSidebarInlineRenameInput({
initialValue,
onCancel,
onSubmit,
selectTextOnFocus = true,
}: SidebarInlineRenameInputOptions) {
const [value, setValue] = useState(initialValue)
const inputRef = useRef<HTMLInputElement>(null)
const submittingRef = useRef(false)
useEffect(() => {
const input = inputRef.current
if (!input) return
input.focus()
if (selectTextOnFocus) input.select()
}, [selectTextOnFocus])
const submitValue = useCallback(async () => {
if (submittingRef.current) return false
submittingRef.current = true
try {
return await onSubmit(value)
} finally {
submittingRef.current = false
}
}, [onSubmit, value])
const handleKeyDown = useCallback((event: ReactKeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault()
event.stopPropagation()
void submitValue()
}
if (event.key === 'Escape') {
event.preventDefault()
event.stopPropagation()
onCancel()
}
}, [onCancel, submitValue])
return {
handleKeyDown,
inputRef,
setValue,
submitValue,
value,
}
}
export function useSidebarSections(entries: VaultEntry[]) {
const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries])
const allSectionGroups = useMemo(() => {

View File

@@ -1,6 +1,6 @@
import { useCallback, useRef, useState } from 'react'
import type { VaultEntry } from '../../types'
import { applyCustomization, useOutsideClick } from './sidebarHooks'
import { applyCustomization, useOutsideClick, useSidebarContextMenu } from './sidebarHooks'
interface SidebarTypeGroup {
type: string
@@ -18,43 +18,39 @@ interface SidebarTypeInteractionsInput {
function useSidebarTypeState() {
const [customizeTarget, setCustomizeTarget] = useState<string | null>(null)
const [contextMenuPos, setContextMenuPos] = useState<{ x: number; y: number } | null>(null)
const [contextMenuType, setContextMenuType] = useState<string | null>(null)
const [renamingType, setRenamingType] = useState<string | null>(null)
const [renameInitialValue, setRenameInitialValue] = useState('')
const [showCustomize, setShowCustomize] = useState(false)
const contextMenuRef = useRef<HTMLDivElement>(null)
const popoverRef = useRef<HTMLDivElement>(null)
const customizeRef = useRef<HTMLDivElement>(null)
const closeContextMenu = useCallback(() => {
setContextMenuPos(null)
setContextMenuType(null)
}, [])
const {
closeContextMenu,
contextMenu,
contextMenuRef,
openContextMenuFromPointer,
} = useSidebarContextMenu<string>()
const closeCustomizeTarget = useCallback(() => setCustomizeTarget(null), [])
const closeCustomize = useCallback(() => setShowCustomize(false), [])
const cancelRename = useCallback(() => setRenamingType(null), [])
useOutsideClick(customizeRef, showCustomize, closeCustomize)
useOutsideClick(contextMenuRef, !!contextMenuPos, closeContextMenu)
useOutsideClick(popoverRef, !!customizeTarget, closeCustomizeTarget)
return {
cancelRename,
closeContextMenu,
closeCustomizeTarget,
contextMenuPos,
contextMenuPos: contextMenu?.pos ?? null,
contextMenuRef,
contextMenuType,
contextMenuType: contextMenu?.target ?? null,
customizeRef,
customizeTarget,
openContextMenuFromPointer,
popoverRef,
renameInitialValue,
renamingType,
setContextMenuPos,
setContextMenuType,
setCustomizeTarget,
setRenameInitialValue,
setRenamingType,
@@ -114,10 +110,7 @@ export function useSidebarTypeInteractions({
})
const handleContextMenu = useCallback((event: React.MouseEvent, type: string) => {
event.preventDefault()
event.stopPropagation()
state.setContextMenuPos({ x: event.clientX, y: event.clientY })
state.setContextMenuType(type)
state.openContextMenuFromPointer(type, event)
}, [state])
const handleCustomize = useCallback((prop: 'icon' | 'color', value: string) => {

View File

@@ -3,7 +3,7 @@ import {
type SetStateAction,
} from 'react'
import type { ViewDefinition, ViewFile } from '../../types'
import { useOutsideClick } from './sidebarHooks'
import { getElementMenuPosition, useOutsideClick, useSidebarContextMenu } from './sidebarHooks'
import type { MenuPosition, ViewDefinitionPatchHandler } from './SidebarViewActions'
interface SidebarViewItemInteractionInput {
@@ -16,14 +16,6 @@ interface SidebarViewItemInteractionInput {
type RowKeyboardAction = 'select' | 'rename' | 'menu'
function getKeyboardMenuPosition(row: HTMLDivElement | null): MenuPosition {
const bounds = row?.getBoundingClientRect()
return {
x: bounds ? bounds.left + 16 : 20,
y: bounds ? bounds.top + bounds.height : 100,
}
}
function getRowKeyboardAction(event: KeyboardEvent<HTMLDivElement>): RowKeyboardAction | null {
if (event.key === 'Enter' || event.key === ' ') return 'select'
if (event.key === 'F2') return 'rename'
@@ -43,30 +35,34 @@ function commitViewRename(
}
function useViewInteractionState() {
const [contextMenuPos, setContextMenuPos] = useState<MenuPosition | null>(null)
const [customizePos, setCustomizePos] = useState<MenuPosition | null>(null)
const [isRenaming, setIsRenaming] = useState(false)
const contextMenuRef = useRef<HTMLDivElement>(null)
const customizeRef = useRef<HTMLDivElement>(null)
const rowRef = useRef<HTMLDivElement>(null)
const closeContextMenu = useCallback(() => setContextMenuPos(null), [])
const {
closeContextMenu,
contextMenu,
contextMenuRef,
openContextMenuAt,
openContextMenuFromPointer,
} = useSidebarContextMenu<string>()
const closeCustomize = useCallback(() => setCustomizePos(null), [])
useOutsideClick(contextMenuRef, !!contextMenuPos, closeContextMenu)
useOutsideClick(customizeRef, !!customizePos, closeCustomize)
return {
closeContextMenu,
closeCustomize,
contextMenuPos,
contextMenuPos: contextMenu?.pos ?? null,
contextMenuRef,
customizePos,
customizeRef,
isRenaming,
rowRef,
setContextMenuPos,
setCustomizePos,
setIsRenaming,
openContextMenuAt,
openContextMenuFromPointer,
}
}
@@ -104,30 +100,30 @@ function useViewMenuActions({
onUpdateViewDefinition,
closeContextMenu,
contextMenuPos,
openContextMenuAt,
openContextMenuFromPointer,
rowRef,
setContextMenuPos,
setCustomizePos,
}: SidebarViewItemInteractionInput & {
closeContextMenu: () => void
contextMenuPos: MenuPosition | null
openContextMenuAt: (target: string, pos: MenuPosition) => void
openContextMenuFromPointer: (target: string, event: MouseEvent) => void
rowRef: RefObject<HTMLDivElement | null>
setContextMenuPos: Dispatch<SetStateAction<MenuPosition | null>>
setCustomizePos: Dispatch<SetStateAction<MenuPosition | null>>
}) {
const hasMenuActions = !!(onEditView || onDeleteView || onUpdateViewDefinition)
const handleContextMenu = useCallback((event: MouseEvent) => {
if (!hasMenuActions) return
event.preventDefault()
event.stopPropagation()
setCustomizePos(null)
setContextMenuPos({ x: event.clientX, y: event.clientY })
}, [hasMenuActions, setContextMenuPos, setCustomizePos])
openContextMenuFromPointer(view.filename, event)
}, [hasMenuActions, openContextMenuFromPointer, setCustomizePos, view.filename])
const openKeyboardContextMenu = useCallback(() => {
setCustomizePos(null)
setContextMenuPos(getKeyboardMenuPosition(rowRef.current))
}, [rowRef, setContextMenuPos, setCustomizePos])
openContextMenuAt(view.filename, getElementMenuPosition(rowRef.current))
}, [openContextMenuAt, rowRef, setCustomizePos, view.filename])
const handleEdit = useCallback(() => {
closeContextMenu()
@@ -205,8 +201,9 @@ export function useSidebarViewItemInteractions({
onUpdateViewDefinition,
closeContextMenu: state.closeContextMenu,
contextMenuPos: state.contextMenuPos,
openContextMenuAt: state.openContextMenuAt,
openContextMenuFromPointer: state.openContextMenuFromPointer,
rowRef: state.rowRef,
setContextMenuPos: state.setContextMenuPos,
setCustomizePos: state.setCustomizePos,
})
const handleRowKeyDown = useViewRowKeyboardActions({