refactor: use PortalToFollowElem for dropdown with scroll handling

- Replace inline dropdown with PortalToFollowElem to prevent container overflow
- Use z-[100] for dropdown to ensure proper stacking
- Remove redundant outside click handler (handled by PortalToFollowElem)
- Add scroll event listener to auto-close dropdown when scrolling
- Dropdown now renders via portal outside message container
This commit is contained in:
lyzno1 2025-10-11 12:40:56 +08:00
parent d1f42d47fe
commit 376a084aca
No known key found for this signature in database
1 changed files with 53 additions and 40 deletions

View File

@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react'
import Avatar from '@/app/components/base/avatar'
import Divider from '@/app/components/base/divider'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/service/workflow-comment'
@ -197,21 +198,21 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
const previousReplyCountRef = useRef(replies.length)
const previousCommentIdRef = useRef(comment.id)
// Close dropdown when clicking outside
// Close dropdown when scrolling
useEffect(() => {
if (!activeReplyMenuId)
const container = messageListRef.current
if (!container || !activeReplyMenuId)
return
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('[data-reply-menu]'))
setActiveReplyMenuId(null)
const handleScroll = () => {
setActiveReplyMenuId(null)
}
document.addEventListener('click', handleClickOutside)
return () => document.removeEventListener('click', handleClickOutside)
container.addEventListener('scroll', handleScroll)
return () => container.removeEventListener('scroll', handleScroll)
}, [activeReplyMenuId])
// Auto-scroll to bottom on new messages
useEffect(() => {
const container = messageListRef.current
if (!container)
@ -333,44 +334,56 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
className='group relative rounded-lg py-2 transition-colors hover:bg-components-panel-on-panel-item-bg'
>
{isOwnReply && !isReplyEditing && (
<div
className={cn(
'absolute right-1 top-1 gap-1',
activeReplyMenuId === reply.id ? 'flex' : 'hidden group-hover:flex',
)}
data-reply-menu
<PortalToFollowElem
placement='bottom-end'
open={activeReplyMenuId === reply.id}
onOpenChange={(open) => {
if (!open)
setActiveReplyMenuId(null)
}}
>
<button
type='button'
className='flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
onClick={(e) => {
e.stopPropagation()
setActiveReplyMenuId(prev => prev === reply.id ? null : reply.id)
}}
aria-label={t('workflow.comments.aria.replyActions')}
<div
className={cn(
'absolute right-1 top-1 gap-1',
activeReplyMenuId === reply.id ? 'flex' : 'hidden group-hover:flex',
)}
data-reply-menu
>
<RiMoreFill className='h-4 w-4' />
</button>
{activeReplyMenuId === reply.id && (
<div className='absolute right-0 top-7 z-50 w-36 rounded-lg border border-components-panel-border bg-components-panel-bg shadow-lg'>
<PortalToFollowElemTrigger asChild>
<button
className='flex w-full items-center justify-start px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
onClick={() => handleStartEdit(reply)}
>
{t('workflow.comments.actions.editReply')}
</button>
<button
className='text-negative flex w-full items-center justify-start px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
onClick={() => {
setActiveReplyMenuId(null)
onReplyDelete?.(reply.id)
type='button'
className='flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
onClick={(e) => {
e.stopPropagation()
setActiveReplyMenuId(prev => prev === reply.id ? null : reply.id)
}}
aria-label={t('workflow.comments.aria.replyActions')}
>
{t('workflow.comments.actions.deleteReply')}
<RiMoreFill className='h-4 w-4' />
</button>
</div>
)}
</div>
</PortalToFollowElemTrigger>
</div>
<PortalToFollowElemContent
className='z-[100] w-36 rounded-lg border border-components-panel-border bg-components-panel-bg shadow-lg'
data-reply-menu
>
<button
className='flex w-full items-center justify-start px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
onClick={() => handleStartEdit(reply)}
>
{t('workflow.comments.actions.editReply')}
</button>
<button
className='text-negative flex w-full items-center justify-start px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
onClick={() => {
setActiveReplyMenuId(null)
onReplyDelete?.(reply.id)
}}
>
{t('workflow.comments.actions.deleteReply')}
</button>
</PortalToFollowElemContent>
</PortalToFollowElem>
)}
{isReplyEditing ? (
<div className='rounded-lg border border-components-chat-input-border bg-components-panel-bg-blur px-3 py-2 shadow-sm'>