diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 0dfe13ca..9726a2b2 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -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. diff --git a/src/components/FolderTree.test.tsx b/src/components/FolderTree.test.tsx index 2b94c07d..5fbf4cb8 100644 --- a/src/components/FolderTree.test.tsx +++ b/src/components/FolderTree.test.tsx @@ -287,6 +287,21 @@ describe('FolderTree', () => { expect(onDeleteFolder).toHaveBeenCalledWith('projects') }) + it('dismisses the folder context menu on Escape', () => { + render( + , + ) + 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() diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 75c5b4b6..a16b2afa 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -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 }) diff --git a/src/components/Sidebar.typeActions.test.tsx b/src/components/Sidebar.typeActions.test.tsx index 8522d667..c3e306e8 100644 --- a/src/components/Sidebar.typeActions.test.tsx +++ b/src/components/Sidebar.typeActions.test.tsx @@ -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 }) diff --git a/src/components/SidebarParts.tsx b/src/components/SidebarParts.tsx index 0d7c50ac..221a7a59 100644 --- a/src/components/SidebarParts.tsx +++ b/src/components/SidebarParts.tsx @@ -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(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 ( - 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" /> ) } diff --git a/src/components/folder-tree/FolderNameInput.tsx b/src/components/folder-tree/FolderNameInput.tsx index debeda29..a0da91bf 100644 --- a/src/components/folder-tree/FolderNameInput.tsx +++ b/src/components/folder-tree/FolderNameInput.tsx @@ -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(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 (
@@ -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} /> diff --git a/src/components/folder-tree/useFolderContextMenu.ts b/src/components/folder-tree/useFolderContextMenu.ts index 958667fe..41b0612d 100644 --- a/src/components/folder-tree/useFolderContextMenu.ts +++ b/src/components/folder-tree/useFolderContextMenu.ts @@ -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, - 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(null) - const menuRef = useRef(null) - - const closeContextMenu = useCallback(() => setContextMenu(null), []) - useContextMenuDismiss(contextMenu, menuRef, closeContextMenu) + const { + closeContextMenu, + contextMenu, + contextMenuRef, + openContextMenuFromPointer, + } = useSidebarContextMenu() const handleOpenMenu = useCallback((node: FolderNode, event: ReactMouseEvent) => { - 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, } } diff --git a/src/components/sidebar/SidebarViewActions.tsx b/src/components/sidebar/SidebarViewActions.tsx index c0654f22..7f3f5fef 100644 --- a/src/components/sidebar/SidebarViewActions.tsx +++ b/src/components/sidebar/SidebarViewActions.tsx @@ -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(null) - - useEffect(() => { inputRef.current?.focus(); inputRef.current?.select() }, []) - - const handleKeyDown = (event: KeyboardEvent) => { - 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 ( onSubmit(value)} + onBlur={() => { void submitValue() }} onChange={(event) => setValue(event.target.value)} onClick={(event) => event.stopPropagation()} onDoubleClick={(event) => event.stopPropagation()} diff --git a/src/components/sidebar/sidebarHooks.ts b/src/components/sidebar/sidebarHooks.ts index b55b6aca..b3a71e21 100644 --- a/src/components/sidebar/sidebarHooks.ts +++ b/src/components/sidebar/sidebarHooks.ts @@ -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, isOpen: boolean, onClose: () => void) { +export interface SidebarMenuPosition { + x: number + y: number +} + +export interface SidebarContextMenuState { + 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 | 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( + ref: RefObject, + isOpen: boolean, + onClose: () => void, +) { useEffect(() => { if (!isOpen) return const handler = (event: MouseEvent) => { @@ -19,6 +66,97 @@ export function useOutsideClick(ref: RefObject, isOpen: bool }, [ref, isOpen, onClose]) } +export function useDismissableSidebarLayer( + ref: RefObject, + 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() { + const [contextMenu, setContextMenu] = useState | null>(null) + const contextMenuRef = useRef(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(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) => { + 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(() => { diff --git a/src/components/sidebar/useSidebarTypeInteractions.ts b/src/components/sidebar/useSidebarTypeInteractions.ts index aefc1f7d..a2d12340 100644 --- a/src/components/sidebar/useSidebarTypeInteractions.ts +++ b/src/components/sidebar/useSidebarTypeInteractions.ts @@ -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(null) - const [contextMenuPos, setContextMenuPos] = useState<{ x: number; y: number } | null>(null) - const [contextMenuType, setContextMenuType] = useState(null) const [renamingType, setRenamingType] = useState(null) const [renameInitialValue, setRenameInitialValue] = useState('') const [showCustomize, setShowCustomize] = useState(false) - const contextMenuRef = useRef(null) const popoverRef = useRef(null) const customizeRef = useRef(null) - - const closeContextMenu = useCallback(() => { - setContextMenuPos(null) - setContextMenuType(null) - }, []) + const { + closeContextMenu, + contextMenu, + contextMenuRef, + openContextMenuFromPointer, + } = useSidebarContextMenu() 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) => { diff --git a/src/components/sidebar/useSidebarViewItemInteractions.ts b/src/components/sidebar/useSidebarViewItemInteractions.ts index f51a54bc..430b92e2 100644 --- a/src/components/sidebar/useSidebarViewItemInteractions.ts +++ b/src/components/sidebar/useSidebarViewItemInteractions.ts @@ -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): 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(null) const [customizePos, setCustomizePos] = useState(null) const [isRenaming, setIsRenaming] = useState(false) - const contextMenuRef = useRef(null) const customizeRef = useRef(null) const rowRef = useRef(null) - const closeContextMenu = useCallback(() => setContextMenuPos(null), []) + const { + closeContextMenu, + contextMenu, + contextMenuRef, + openContextMenuAt, + openContextMenuFromPointer, + } = useSidebarContextMenu() 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 - setContextMenuPos: Dispatch> setCustomizePos: Dispatch> }) { 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({