diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index fb1f8a8d..13b70010 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -104,7 +104,7 @@ function Sidebar({ className }: { className?: string }) { } // Select the 0th index on focus if none selected - focusActiveItem(); + setTimeout(focusActiveItem, 100); }); const handleDragEnd = useCallback(async function handleDragEnd({ @@ -360,6 +360,12 @@ const sidebarTreeAtom = atom | null>((get) => { }); const actions = { + 'sidebar.context_menu': { + enable: isSidebarFocused, + cb: async function (tree: TreeHandle) { + tree.showContextMenu(); + }, + }, 'sidebar.selected.delete': { enable: isSidebarFocused, cb: async function (_: TreeHandle, items: SidebarModel[]) { diff --git a/src-web/components/core/tree/Tree.tsx b/src-web/components/core/tree/Tree.tsx index 6a958bfd..926ec577 100644 --- a/src-web/components/core/tree/Tree.tsx +++ b/src-web/components/core/tree/Tree.tsx @@ -76,9 +76,11 @@ export interface TreeProps { } export interface TreeHandle { + treeId: string; focus: () => void; selectItem: (id: string) => void; renameItem: (id: string) => void; + showContextMenu: () => void; } function TreeInner( @@ -132,14 +134,24 @@ function TreeInner( const treeHandle = useMemo( () => ({ + treeId, focus: tryFocus, renameItem: (id) => treeItemRefs.current[id]?.rename(), selectItem: (id) => { setSelected([id], false); jotaiStore.set(focusIdsFamily(treeId), { anchorId: id, lastId: id }); }, + showContextMenu: async () => { + if (getContextMenu == null) return; + const items = getSelectedItems(treeId, selectableItems); + const menuItems = await getContextMenu(treeHandle, items); + const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId; + const rect = lastSelectedId ? treeItemRefs.current[lastSelectedId]?.rect() : null; + if (rect == null) return; + setShowContextMenu({ items: menuItems, x: rect.x, y: rect.y }); + }, }), - [setSelected, treeId, tryFocus], + [getContextMenu, selectableItems, setSelected, treeId, tryFocus], ); useImperativeHandle(ref, (): TreeHandle => treeHandle, [treeHandle]); @@ -268,7 +280,7 @@ function TreeInner( ); useKey( - 'ArrowUp', + (e) => e.key === 'ArrowUp' || e.key.toLowerCase() === 'k', (e) => { if (!isSidebarFocused()) return; e.preventDefault(); @@ -279,7 +291,7 @@ function TreeInner( ); useKey( - 'ArrowDown', + (e) => e.key === 'ArrowDown' || e.key.toLowerCase() === 'j', (e) => { if (!isSidebarFocused()) return; e.preventDefault(); @@ -291,7 +303,7 @@ function TreeInner( // If the selected item is a collapsed folder, expand it. Otherwise, select next item useKey( - 'ArrowRight', + (e) => e.key === 'ArrowRight' || e.key.toLowerCase() === 'l', (e) => { if (!isSidebarFocused()) return; e.preventDefault(); @@ -317,7 +329,7 @@ function TreeInner( // If the selected item is in a folder, select its parent. // If the selected item is an expanded folder, collapse it. useKey( - 'ArrowLeft', + (e) => e.key === 'ArrowLeft' || e.key.toLowerCase() === 'h', (e) => { if (!isSidebarFocused()) return; e.preventDefault(); diff --git a/src-web/components/core/tree/TreeItem.tsx b/src-web/components/core/tree/TreeItem.tsx index 56ce5698..d710e3d4 100644 --- a/src-web/components/core/tree/TreeItem.tsx +++ b/src-web/components/core/tree/TreeItem.tsx @@ -37,6 +37,7 @@ export type TreeItemProps = Pick< export interface TreeItemHandle { rename: () => void; isRenaming: boolean; + rect: () => DOMRect; } const HOVER_CLOSED_FOLDER_DELAY = 800; @@ -70,6 +71,12 @@ function TreeItem_({ } }, isRenaming: editing, + rect: () => { + if (listItemRef.current == null) { + return new DOMRect(0, 0, 0, 0); + } + return listItemRef.current.getBoundingClientRect(); + }, }); }, [addRef, editing, getEditOptions, node.item]); @@ -225,7 +232,7 @@ function TreeItem_({ e.preventDefault(); e.stopPropagation(); const items = await getContextMenu(node.item); - setShowContextMenu({ items, x: e.clientX, y: e.clientY }); + setShowContextMenu({ items, x: e.clientX ?? 100, y: e.clientY ?? 100 }); }, [getContextMenu, node.item], ); diff --git a/src-web/hooks/useHotKey.ts b/src-web/hooks/useHotKey.ts index 31cddaa4..cdfd8d2a 100644 --- a/src-web/hooks/useHotKey.ts +++ b/src-web/hooks/useHotKey.ts @@ -28,6 +28,7 @@ export type HotkeyAction = | 'sidebar.selected.duplicate' | 'sidebar.selected.rename' | 'sidebar.focus' + | 'sidebar.context_menu' | 'url_bar.focus' | 'workspace_settings.show'; @@ -51,6 +52,7 @@ const hotkeys: Record = { 'sidebar.selected.duplicate': ['CmdCtrl+d'], 'sidebar.selected.rename': ['Enter'], 'sidebar.focus': ['CmdCtrl+b'], + 'sidebar.context_menu': type() === 'macos' ? ['Control+Enter'] : ['Alt+Insert'], 'url_bar.focus': ['CmdCtrl+l'], 'workspace_settings.show': ['CmdCtrl+;'], }; @@ -75,6 +77,7 @@ const hotkeyLabels: Record = { 'sidebar.selected.duplicate': 'Duplicate Selected Sidebar Item', 'sidebar.selected.rename': 'Rename Selected Sidebar Item', 'sidebar.focus': 'Focus or Toggle Sidebar', + 'sidebar.context_menu': 'Show Context Menu', 'url_bar.focus': 'Focus URL', 'workspace_settings.show': 'Open Workspace Settings', };