chore: portoal to follow to popover

This commit is contained in:
Joel
2026-03-27 14:58:12 +08:00
parent 1907ea6ef9
commit 43b1042285
3 changed files with 90 additions and 77 deletions

View File

@@ -1,17 +1,19 @@
import type { LexicalNode } from 'lexical'
import type { Dispatch, SetStateAction } from 'react'
import {
flip,
offset,
shift,
useFloating,
} from '@floating-ui/react'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { LexicalTypeaheadMenuPlugin, MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import {
$insertNodes,
} from 'lexical'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { useCallback, useLayoutEffect, useMemo } from 'react'
import ReactDOM from 'react-dom'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useBasicTypeaheadTriggerMatch } from '@/app/components/base/prompt-editor/hooks'
import { $splitNodeContainingQuery } from '@/app/components/base/prompt-editor/utils'
import { FilePickerPanel } from './file-picker-panel'
@@ -23,8 +25,29 @@ class FilePickerMenuOption extends MenuOption {
}
}
type ReferenceSyncProps = {
anchor: HTMLElement | null
setReference: Dispatch<SetStateAction<HTMLElement | null>> | ((node: HTMLElement | null) => void)
}
const ReferenceSync = ({ anchor, setReference }: ReferenceSyncProps) => {
useLayoutEffect(() => {
setReference(anchor)
}, [anchor, setReference])
return null
}
const FilePickerBlock = () => {
const [editor] = useLexicalComposerContext()
const { refs, floatingStyles, isPositioned } = useFloating({
placement: 'bottom-start',
middleware: [
offset(0),
shift({ padding: 8 }),
flip(),
],
})
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
minLength: 0,
maxLength: 0,
@@ -54,30 +77,27 @@ const FilePickerBlock = () => {
const closeMenu = () => selectOptionAndCleanUp(options[0])
return ReactDOM.createPortal(
<PortalToFollowElem
open
placement="bottom-start"
offset={4}
onOpenChange={(open) => {
if (!open)
closeMenu()
}}
>
<PortalToFollowElemTrigger asChild>
<span className="inline-block h-0 w-0" />
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[1000]">
<>
<ReferenceSync anchor={anchorElementRef.current} setReference={refs.setReference} />
<div
ref={refs.setFloating}
style={{
...floatingStyles,
visibility: isPositioned ? 'visible' : 'hidden',
}}
className="z-[1002] outline-none"
>
<FilePickerPanel
onSelectNode={(node) => {
insertFileReference(node.id)
closeMenu()
}}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>,
</div>
</>,
anchorElementRef.current,
)
}, [insertFileReference, options])
}, [floatingStyles, insertFileReference, isPositioned, options, refs])
return (
<LexicalTypeaheadMenuPlugin

View File

@@ -9,12 +9,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useSelectOrDelete } from '@/app/components/base/prompt-editor/hooks'
import {
Popover,
PopoverContent,
} from '@/app/components/base/ui/popover'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
import { START_TAB_ID } from '@/app/components/workflow/skill/constants'
import { useSkillAssetNodeMap } from '@/app/components/workflow/skill/hooks/file-tree/data/use-skill-asset-tree'
@@ -162,13 +161,11 @@ const FileReferenceBlock = ({ nodeKey, resourceId }: FileReferenceBlockProps) =>
}, [previewOpen, ref, shouldPreview, updatePreviewPosition])
const fileBlock = (
<PortalToFollowElem
<Popover
open={open}
onOpenChange={setOpen}
placement="bottom-start"
offset={4}
>
<PortalToFollowElemTrigger ref={ref} className="inline-flex">
<div ref={ref} className="inline-flex">
<Tooltip>
<TooltipTrigger
disabled={!tooltipContent}
@@ -183,6 +180,7 @@ const FileReferenceBlock = ({ nodeKey, resourceId }: FileReferenceBlockProps) =>
onMouseDown={() => {
if (!isInteractive)
return
setOpen(prev => !prev)
}}
>
@@ -212,14 +210,19 @@ const FileReferenceBlock = ({ nodeKey, resourceId }: FileReferenceBlockProps) =>
<TooltipContent>{tooltipContent}</TooltipContent>
)}
</Tooltip>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[1000]">
</div>
<PopoverContent
placement="bottom-start"
sideOffset={4}
popupClassName="rounded-none border-none bg-transparent shadow-none"
positionerProps={{ anchor: ref }}
>
<FilePickerPanel
onSelectNode={handleSelect}
focusNodeId={resourceId}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
</PopoverContent>
</Popover>
)
if (!shouldPreview)

View File

@@ -5971,21 +5971,11 @@
"count": 2
}
},
"app/components/workflow/skill/editor/skill-editor/plugins/file-picker-block.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/workflow/skill/editor/skill-editor/plugins/file-picker-upload-modal.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/component.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/workflow/skill/viewer/sqlite-file-preview/table-selector.tsx": {
"no-restricted-imports": {
"count": 1
@@ -6568,14 +6558,42 @@
"count": 1
}
},
"utils/clipboard.ts": {
"utils/__tests__/completion-params.spec.ts": {
"ts/no-explicit-any": {
"count": 3
}
},
"utils/__tests__/get-icon.spec.ts": {
"ts/no-explicit-any": {
"count": 2
}
},
"utils/__tests__/index.spec.ts": {
"test/no-identical-title": {
"count": 2
},
"ts/no-explicit-any": {
"count": 8
}
},
"utils/__tests__/model-config.spec.ts": {
"ts/no-explicit-any": {
"count": 13
}
},
"utils/__tests__/navigation.spec.ts": {
"ts/no-explicit-any": {
"count": 4
}
},
"utils/__tests__/tool-call.spec.ts": {
"ts/no-explicit-any": {
"count": 1
}
},
"utils/__tests__/completion-params.spec.ts": {
"utils/clipboard.ts": {
"ts/no-explicit-any": {
"count": 3
"count": 1
}
},
"utils/completion-params.ts": {
@@ -6596,24 +6614,11 @@
"count": 1
}
},
"utils/__tests__/get-icon.spec.ts": {
"ts/no-explicit-any": {
"count": 2
}
},
"utils/gtag.ts": {
"ts/no-explicit-any": {
"count": 2
}
},
"utils/__tests__/index.spec.ts": {
"test/no-identical-title": {
"count": 2
},
"ts/no-explicit-any": {
"count": 8
}
},
"utils/index.ts": {
"ts/no-explicit-any": {
"count": 3
@@ -6624,29 +6629,14 @@
"count": 1
}
},
"utils/__tests__/model-config.spec.ts": {
"ts/no-explicit-any": {
"count": 13
}
},
"utils/model-config.ts": {
"ts/no-explicit-any": {
"count": 6
}
},
"utils/__tests__/navigation.spec.ts": {
"ts/no-explicit-any": {
"count": 4
}
},
"utils/__tests__/tool-call.spec.ts": {
"ts/no-explicit-any": {
"count": 1
}
},
"utils/validators.ts": {
"ts/no-explicit-any": {
"count": 2
}
}
}
}