fix(edit-menu): render mod edit modal in FloatingPortal to escape reply card containment

This commit is contained in:
plebeius
2026-02-27 15:00:39 +08:00
parent 326b40300f
commit f19ebe5f33
+119 -117
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { autoUpdate, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react'; import { autoUpdate, FloatingFocusManager, FloatingPortal, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
import { import {
Comment, Comment,
PublishCommentEditOptions, PublishCommentEditOptions,
@@ -271,135 +271,137 @@ const EditMenu = ({ post }: { post: Comment }) => {
/> />
</span> </span>
{isEditMenuOpen && (isAccountCommentAuthor || isAccountMod) && ( {isEditMenuOpen && (isAccountCommentAuthor || isAccountMod) && (
<FloatingFocusManager context={context} modal={false}> <FloatingPortal>
<div className={styles.modal} ref={refs.setFloating} style={floatingStyles} aria-labelledby={headingId} {...getFloatingProps()}> <FloatingFocusManager context={context} modal={false}>
<div className={styles.editMenu}> <div className={styles.modal} ref={refs.setFloating} style={floatingStyles} aria-labelledby={headingId} {...getFloatingProps()}>
{isAccountCommentAuthor && ( <div className={styles.editMenu}>
<> {isAccountCommentAuthor && (
<div className={styles.menuItem}> <>
<label> <div className={styles.menuItem}>
[
<input onChange={onCheckbox} checked={publishCommentEditOptions.deleted ?? false} type='checkbox' id='deleted' />
{capitalize(t('delete'))}?]
</label>
</div>
<div className={styles.menuItem}>
<label>
[
<input type='checkbox' onChange={() => setIsContentEditorOpen(!isContentEditorOpen)} checked={isContentEditorOpen} />
{capitalize(t('edit'))}?]
</label>
</div>
{isContentEditorOpen && (
<div>
<textarea
className={styles.editTextarea}
value={publishCommentEditOptions.content || ''}
onChange={(e) => {
const newContent = e.target.value;
setPublishCommentEditOptions((state) => ({ ...state, content: newContent }));
}}
/>
</div>
)}
</>
)}
{isAccountMod && (
<>
<div className={styles.menuItem}>
<label>
[
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.removed ?? false} type='checkbox' id='removed' />
{capitalize(t('remove'))}?]
</label>{' '}
<span className={styles.purgeItem}>
<label> <label>
[ [
<input onChange={onPurgeCheckbox} checked={publishCommentEditOptions.commentModeration?.purged ?? false} type='checkbox' id='purged' /> <input onChange={onCheckbox} checked={publishCommentEditOptions.deleted ?? false} type='checkbox' id='deleted' />
{capitalize(t('purge'))}?] {capitalize(t('delete'))}?]
</label> </label>
</span>
</div>
{!parentCid && (
<div className={styles.menuItem}>
[
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.locked ?? false} type='checkbox' id='locked' />
{capitalize(t('close_thread'))}?
</label>
]
</div> </div>
)}
<div className={styles.menuItem}>
[
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.spoiler ?? false} type='checkbox' id='spoiler' />
{capitalize(t('spoiler'))}?
</label>
]
</div>
<div className={styles.menuItem}>
[
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.pinned ?? false} type='checkbox' id='pinned' />
{capitalize(t('sticky'))}?
</label>
]
</div>
{!isCommentAuthorMod && isAccountMod && !isAccountCommentAuthor && (
<div className={styles.menuItem}> <div className={styles.menuItem}>
[
<label> <label>
<input [
onChange={onCheckbox} <input type='checkbox' onChange={() => setIsContentEditorOpen(!isContentEditorOpen)} checked={isContentEditorOpen} />
checked={publishCommentEditOptions.commentModeration?.author?.banExpiresAt !== undefined} {capitalize(t('edit'))}?]
type='checkbox' </label>
id='banUser' </div>
/> {isContentEditorOpen && (
<Trans <div>
i18nKey='ban_user_for' <textarea
shouldUnescape={true} className={styles.editTextarea}
components={{ value={publishCommentEditOptions.content || ''}
1: ( onChange={(e) => {
<input const newContent = e.target.value;
key='ban-duration-input' setPublishCommentEditOptions((state) => ({ ...state, content: newContent }));
className={styles.banInput}
onChange={onBanDurationChange}
type='number'
min={1}
max={100}
value={banDuration || ''}
/>
),
}} }}
/> />
? </div>
)}
</>
)}
{isAccountMod && (
<>
<div className={styles.menuItem}>
<label>
[
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.removed ?? false} type='checkbox' id='removed' />
{capitalize(t('remove'))}?]
</label>{' '}
<span className={styles.purgeItem}>
<label>
[
<input onChange={onPurgeCheckbox} checked={publishCommentEditOptions.commentModeration?.purged ?? false} type='checkbox' id='purged' />
{capitalize(t('purge'))}?]
</label>
</span>
</div>
{!parentCid && (
<div className={styles.menuItem}>
[
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.locked ?? false} type='checkbox' id='locked' />
{capitalize(t('close_thread'))}?
</label>
]
</div>
)}
<div className={styles.menuItem}>
[
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.spoiler ?? false} type='checkbox' id='spoiler' />
{capitalize(t('spoiler'))}?
</label> </label>
] ]
</div> </div>
)} <div className={styles.menuItem}>
</> [
)} <label>
<div className={`${styles.menuItem} ${styles.menuReason}`}> <input onChange={onCheckbox} checked={publishCommentEditOptions.commentModeration?.pinned ?? false} type='checkbox' id='pinned' />
{capitalize(t('reason'))}? ({t('optional')}) {capitalize(t('sticky'))}?
<input </label>
type='text' ]
value={publishCommentEditOptions.reason || ''} </div>
onChange={(e) => { {!isCommentAuthorMod && isAccountMod && !isAccountCommentAuthor && (
const newReason = e.target.value; <div className={styles.menuItem}>
setPublishCommentEditOptions((state) => ({ ...state, reason: newReason })); [
}} <label>
size={14} <input
/> onChange={onCheckbox}
</div> checked={publishCommentEditOptions.commentModeration?.author?.banExpiresAt !== undefined}
<div className={styles.bottom}> type='checkbox'
<button className={isMobile ? 'button' : ''} onClick={_publishCommentEdit}> id='banUser'
{t('save')} />
</button> <Trans
i18nKey='ban_user_for'
shouldUnescape={true}
components={{
1: (
<input
key='ban-duration-input'
className={styles.banInput}
onChange={onBanDurationChange}
type='number'
min={1}
max={100}
value={banDuration || ''}
/>
),
}}
/>
?
</label>
]
</div>
)}
</>
)}
<div className={`${styles.menuItem} ${styles.menuReason}`}>
{capitalize(t('reason'))}? ({t('optional')})
<input
type='text'
value={publishCommentEditOptions.reason || ''}
onChange={(e) => {
const newReason = e.target.value;
setPublishCommentEditOptions((state) => ({ ...state, reason: newReason }));
}}
size={14}
/>
</div>
<div className={styles.bottom}>
<button className={isMobile ? 'button' : ''} onClick={_publishCommentEdit}>
{t('save')}
</button>
</div>
</div> </div>
</div> </div>
</div> </FloatingFocusManager>
</FloatingFocusManager> </FloatingPortal>
)} )}
</> </>
); );