Gift Info Modal: Wrap context menu in portal (#6249)

This commit is contained in:
Alexander Zinchuk 2025-09-19 14:35:27 +02:00
parent c2badd4a4b
commit 472003b6ab
2 changed files with 58 additions and 30 deletions

View File

@ -38,7 +38,7 @@
.modalContent { .modalContent {
position: relative; position: relative;
max-height: min(92vh, 46rem) !important; max-height: min(92vh, 48rem) !important;
} }
.headerSplitButton { .headerSplitButton {

View File

@ -1,5 +1,5 @@
import type { FC, TeactNode } from '../../../../lib/teact/teact'; import type { TeactNode } from '../../../../lib/teact/teact';
import { memo, useMemo, useState } from '../../../../lib/teact/teact'; import { memo, useMemo, useRef, useState } from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global'; import { getActions, getGlobal, withGlobal } from '../../../../global';
import type { import type {
@ -27,6 +27,7 @@ import { formatPercent } from '../../../../util/textFormat';
import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts'; import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts';
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useFlag from '../../../../hooks/useFlag'; import useFlag from '../../../../hooks/useFlag';
import useLang from '../../../../hooks/useLang'; import useLang from '../../../../hooks/useLang';
@ -43,8 +44,8 @@ import SafeLink from '../../../common/SafeLink';
import Button from '../../../ui/Button'; import Button from '../../../ui/Button';
import Checkbox from '../../../ui/Checkbox'; import Checkbox from '../../../ui/Checkbox';
import ConfirmDialog from '../../../ui/ConfirmDialog'; import ConfirmDialog from '../../../ui/ConfirmDialog';
import DropdownMenu from '../../../ui/DropdownMenu';
import Link from '../../../ui/Link'; import Link from '../../../ui/Link';
import Menu from '../../../ui/Menu';
import TableInfoModal, { type TableData } from '../../common/TableInfoModal'; import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
import UniqueGiftHeader from '../UniqueGiftHeader'; import UniqueGiftHeader from '../UniqueGiftHeader';
@ -106,6 +107,17 @@ const GiftInfoModal = ({
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState<boolean>(false); const [isConfirmModalOpen, setIsConfirmModalOpen] = useState<boolean>(false);
const [shouldPayInTon, setShouldPayInTon] = useState<boolean>(false); const [shouldPayInTon, setShouldPayInTon] = useState<boolean>(false);
const splitButtonRef = useRef<HTMLDivElement>();
const menuRef = useRef<HTMLDivElement>();
const uniqueGiftHeaderRef = useRef<HTMLDivElement>();
const {
isContextMenuOpen,
contextMenuAnchor,
handleContextMenu,
handleContextMenuClose,
handleContextMenuHide,
} = useContextMenuHandlers(splitButtonRef);
const handleSymbolClick = useLastCallback(() => { const handleSymbolClick = useLastCallback(() => {
if (!gift || !giftAttributes?.pattern) return; if (!gift || !giftAttributes?.pattern) return;
@ -303,8 +315,8 @@ const GiftInfoModal = ({
return gift && getGiftAttributes(gift); return gift && getGiftAttributes(gift);
}, [gift]); }, [gift]);
const SettingsMenuButton: FC<{ onTrigger: () => void; isMenuOpen?: boolean }> = useMemo(() => { const SettingsMenuButton = useMemo(() => {
return ({ onTrigger }) => ( return (
<div <div
className={buildClassName( className={buildClassName(
styles.headerButton, styles.headerButton,
@ -314,7 +326,8 @@ const GiftInfoModal = ({
role="button" role="button"
aria-haspopup="menu" aria-haspopup="menu"
aria-label={lang('AriaMoreButton')} aria-label={lang('AriaMoreButton')}
onClick={onTrigger} onContextMenu={handleContextMenu}
onClick={handleContextMenu}
> >
<Icon <Icon
name="more" name="more"
@ -322,7 +335,7 @@ const GiftInfoModal = ({
/> />
</div> </div>
); );
}, [lang]); }, [lang, handleContextMenu]);
const renderFooterButton = useLastCallback(() => { const renderFooterButton = useLastCallback(() => {
if (canBuyGift) { if (canBuyGift) {
@ -480,22 +493,6 @@ const GiftInfoModal = ({
return canManage ? lang('GiftInfoReceived') : lang('GiftInfoTitle'); return canManage ? lang('GiftInfoReceived') : lang('GiftInfoTitle');
} }
const uniqueGiftContextMenu = (
<DropdownMenu
className="with-menu-transitions"
trigger={SettingsMenuButton}
positionX="right"
>
<GiftMenuItems
peerId={renderingModal!.peerId!}
gift={typeGift}
canManage={canManage}
collectibleEmojiStatuses={collectibleEmojiStatuses}
currentUserEmojiStatus={currentUserEmojiStatus}
/>
</DropdownMenu>
);
const uniqueGiftModalHeader = ( const uniqueGiftModalHeader = (
<div <div
className={styles.modalHeader} className={styles.modalHeader}
@ -513,8 +510,8 @@ const GiftInfoModal = ({
})} })}
</div> </div>
)} )}
<div className={styles.headerSplitButton}> <div className={styles.headerSplitButton} ref={splitButtonRef}>
{isOpen && uniqueGiftContextMenu} {SettingsMenuButton}
<div <div
className={buildClassName( className={buildClassName(
styles.headerButton, styles.headerButton,
@ -539,7 +536,7 @@ const GiftInfoModal = ({
); );
const uniqueGiftHeader = isGiftUnique && ( const uniqueGiftHeader = isGiftUnique && (
<div className={buildClassName(styles.header, styles.uniqueGift)}> <div ref={uniqueGiftHeaderRef} className={buildClassName(styles.header, styles.uniqueGift)}>
<UniqueGiftHeader <UniqueGiftHeader
backdropAttribute={giftAttributes!.backdrop!} backdropAttribute={giftAttributes!.backdrop!}
patternAttribute={giftAttributes!.pattern!} patternAttribute={giftAttributes!.pattern!}
@ -875,12 +872,42 @@ const GiftInfoModal = ({
typeGift, savedGift, renderingTargetPeer, giftSticker, lang, typeGift, savedGift, renderingTargetPeer, giftSticker, lang,
canManage, hasConvertOption, isSender, oldLang, tonExplorerUrl, canManage, hasConvertOption, isSender, oldLang, tonExplorerUrl,
gift, giftAttributes, renderFooterButton, isTargetChat, gift, giftAttributes, renderFooterButton, isTargetChat,
SettingsMenuButton, isGiftUnique, renderingModal, SettingsMenuButton, isGiftUnique, saleDateInfo,
collectibleEmojiStatuses, currentUserEmojiStatus, saleDateInfo, canBuyGift, giftOwnerTitle, resellPrice, giftSubtitle,
canBuyGift, giftOwnerTitle, isOpen, resellPrice, giftSubtitle,
releasedByPeer, handleSymbolClick, handleBackdropClick, handleModelClick, releasedByPeer, handleSymbolClick, handleBackdropClick, handleModelClick,
]); ]);
const getRootElement = useLastCallback(() => uniqueGiftHeaderRef.current);
const getTriggerElement = useLastCallback(() => splitButtonRef.current);
const getMenuElement = useLastCallback(() => menuRef.current);
const getLayout = useLastCallback(() => ({ withPortal: true }));
const uniqueGiftContextMenu = contextMenuAnchor && typeGift && (
<Menu
ref={menuRef}
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
className="gift-context-menu with-menu-transitions"
autoClose
withPortal
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
positionX="right"
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getMenuElement={getMenuElement}
getLayout={getLayout}
>
<GiftMenuItems
peerId={renderingModal!.peerId!}
gift={typeGift}
canManage={canManage}
collectibleEmojiStatuses={collectibleEmojiStatuses}
currentUserEmojiStatus={currentUserEmojiStatus}
/>
</Menu>
);
return ( return (
<> <>
<TableInfoModal <TableInfoModal
@ -897,6 +924,7 @@ const GiftInfoModal = ({
currencyInBalanceBar={confirmPrice?.currency} currencyInBalanceBar={confirmPrice?.currency}
isLowStackPriority isLowStackPriority
/> />
{uniqueGiftContextMenu}
{uniqueGift && currentUser && Boolean(confirmPrice) && ( {uniqueGift && currentUser && Boolean(confirmPrice) && (
<ConfirmDialog <ConfirmDialog
isOpen={isConfirmModalOpen} isOpen={isConfirmModalOpen}