feat(portal): add useContextMenuFloating hook for coordinate-based context menus

Replace useClickAway + fixed positioning in file tree context menu with
a floating-ui based hook that provides collision detection (flip/shift),
ARIA role="menu", Escape/outside-click dismiss, and scroll dismiss via
passive capture listener with ref-stabilized callback.
This commit is contained in:
yyh
2026-01-29 14:01:36 +08:00
parent efb3657cfe
commit bacc5c32f5
3 changed files with 309 additions and 21 deletions

View File

@ -2,9 +2,10 @@
import type { TreeApi } from 'react-arborist'
import type { TreeNodeData } from '../type'
import { useClickAway } from 'ahooks'
import { FloatingPortal } from '@floating-ui/react'
import * as React from 'react'
import { useCallback, useRef } from 'react'
import { useCallback, useMemo } from 'react'
import { useContextMenuFloating } from '@/app/components/base/portal-to-follow-elem/use-context-menu-floating'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { getMenuNodeId, getNodeMenuType } from '../utils/tree-utils'
import NodeMenu from './node-menu'
@ -14,7 +15,6 @@ type TreeContextMenuProps = {
}
const TreeContextMenu = ({ treeRef }: TreeContextMenuProps) => {
const ref = useRef<HTMLDivElement>(null)
const contextMenu = useStore(s => s.contextMenu)
const storeApi = useWorkflowStore()
@ -22,29 +22,42 @@ const TreeContextMenu = ({ treeRef }: TreeContextMenuProps) => {
storeApi.getState().setContextMenu(null)
}, [storeApi])
useClickAway(() => {
handleClose()
}, ref)
const position = useMemo(() => ({
x: contextMenu?.left ?? 0,
y: contextMenu?.top ?? 0,
}), [contextMenu?.left, contextMenu?.top])
const { refs, floatingStyles, getFloatingProps, isPositioned } = useContextMenuFloating({
open: !!contextMenu,
onOpenChange: (open) => {
if (!open)
handleClose()
},
position,
})
if (!contextMenu)
return null
return (
<div
ref={ref}
className="fixed z-[100]"
style={{
top: contextMenu.top,
left: contextMenu.left,
}}
>
<NodeMenu
type={getNodeMenuType(contextMenu.type, contextMenu.isFolder)}
nodeId={getMenuNodeId(contextMenu.type, contextMenu.nodeId)}
onClose={handleClose}
treeRef={treeRef}
/>
</div>
<FloatingPortal>
<div
ref={refs.setFloating}
className="z-[100]"
style={{
...floatingStyles,
visibility: isPositioned ? 'visible' : 'hidden',
}}
{...getFloatingProps()}
>
<NodeMenu
type={getNodeMenuType(contextMenu.type, contextMenu.isFolder)}
nodeId={getMenuNodeId(contextMenu.type, contextMenu.nodeId)}
onClose={handleClose}
treeRef={treeRef}
/>
</div>
</FloatingPortal>
)
}