[Perf] [Refactoring] Move useMenuPosition into Menu component

This commit is contained in:
Alexander Zinchuk 2024-09-06 15:43:21 +02:00
parent 8aa7eb2fcb
commit 03de021b82
26 changed files with 404 additions and 503 deletions

View File

@ -15,7 +15,6 @@ import renderText from '../../common/helpers/renderText';
import formatGroupCallVolume from './helpers/formatGroupCallVolume'; import formatGroupCallVolume from './helpers/formatGroupCallVolume';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useMenuPosition from '../../../hooks/useMenuPosition';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
@ -52,7 +51,7 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
const { const {
isContextMenuOpen, isContextMenuOpen,
contextMenuPosition, contextMenuAnchor,
handleContextMenu, handleContextMenu,
handleBeforeContextMenu, handleBeforeContextMenu,
handleContextMenuClose, handleContextMenuClose,
@ -76,16 +75,6 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
[], [],
); );
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
getLayout,
);
const hasCustomVolume = Boolean( const hasCustomVolume = Boolean(
!isMuted && isSpeaking && participant.volume && participant.volume !== GROUP_CALL_DEFAULT_VOLUME, !isMuted && isSpeaking && participant.volume && participant.volume !== GROUP_CALL_DEFAULT_VOLUME,
); );
@ -147,11 +136,11 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
<GroupCallParticipantMenu <GroupCallParticipantMenu
participant={participant} participant={participant}
isDropdownOpen={isContextMenuOpen} isDropdownOpen={isContextMenuOpen}
positionX={positionX} anchor={contextMenuAnchor}
positionY={positionY} getTriggerElement={getTriggerElement}
transformOriginX={transformOriginX} getRootElement={getRootElement}
transformOriginY={transformOriginY} getMenuElement={getMenuElement}
style={menuStyle} getLayout={getLayout}
onClose={handleContextMenuClose} onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide} onCloseAnimationEnd={handleContextMenuHide}
menuRef={menuRef} menuRef={menuRef}

View File

@ -1,10 +1,9 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { import React, { memo, useEffect, useState } from '../../../lib/teact/teact';
memo, useEffect, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { GroupCallParticipant } from '../../../lib/secret-sauce'; import type { GroupCallParticipant } from '../../../lib/secret-sauce';
import type { MenuPositionOptions } from '../../ui/Menu';
import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config'; import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
import { selectIsAdminInActiveGroupCall } from '../../../global/selectors/calls'; import { selectIsAdminInActiveGroupCall } from '../../../global/selectors/calls';
@ -26,18 +25,15 @@ import './GroupCallParticipantMenu.scss';
const SPEAKER_ICON_DISABLED_SEGMENT: [number, number] = [0, 17]; const SPEAKER_ICON_DISABLED_SEGMENT: [number, number] = [0, 17];
const SPEAKER_ICON_ENABLED_SEGMENT: [number, number] = [17, 34]; const SPEAKER_ICON_ENABLED_SEGMENT: [number, number] = [17, 34];
type OwnProps = { type OwnProps =
participant?: GroupCallParticipant; {
onCloseAnimationEnd: VoidFunction; participant?: GroupCallParticipant;
onClose: VoidFunction; onCloseAnimationEnd: VoidFunction;
isDropdownOpen: boolean; onClose: VoidFunction;
positionX?: 'left' | 'right'; isDropdownOpen: boolean;
positionY?: 'top' | 'bottom'; menuRef?: React.RefObject<HTMLDivElement>;
transformOriginX?: number; }
transformOriginY?: number; & MenuPositionOptions;
style?: string;
menuRef?: React.RefObject<HTMLDivElement>;
};
type StateProps = { type StateProps = {
isAdmin: boolean; isAdmin: boolean;
@ -58,12 +54,8 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
onClose, onClose,
isDropdownOpen, isDropdownOpen,
isAdmin, isAdmin,
positionY,
menuRef, menuRef,
positionX, ...menuPositionOptions
style,
transformOriginY,
transformOriginX,
}) => { }) => {
const { const {
toggleGroupCallMute, toggleGroupCallMute,
@ -175,16 +167,13 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
<div> <div>
<Menu <Menu
isOpen={isDropdownOpen} isOpen={isDropdownOpen}
positionX={positionX}
positionY={positionY}
transformOriginX={transformOriginX}
transformOriginY={transformOriginY}
style={style}
ref={menuRef} ref={menuRef}
withPortal withPortal
onClose={onClose} onClose={onClose}
onCloseAnimationEnd={onCloseAnimationEnd} onCloseAnimationEnd={onCloseAnimationEnd}
className="participant-menu with-menu-transitions" className="participant-menu with-menu-transitions"
// eslint-disable-next-line react/jsx-props-no-spreading
{...menuPositionOptions}
> >
{!isSelf && !shouldRaiseHand && ( {!isSelf && !shouldRaiseHand && (
<div className="group"> <div className="group">

View File

@ -22,7 +22,6 @@ import formatGroupCallVolume from './helpers/formatGroupCallVolume';
import useInterval from '../../../hooks/schedulers/useInterval'; import useInterval from '../../../hooks/schedulers/useInterval';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMenuPosition from '../../../hooks/useMenuPosition';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import FullNameTitle from '../../common/FullNameTitle'; import FullNameTitle from '../../common/FullNameTitle';
@ -209,7 +208,7 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
const { const {
isContextMenuOpen, isContextMenuOpen,
contextMenuPosition, contextMenuAnchor,
handleContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuClose,
handleContextMenuHide, handleContextMenuHide,
@ -232,16 +231,6 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
[], [],
); );
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
getLayout,
);
const handleClickPin = useCallback(() => { const handleClickPin = useCallback(() => {
setPinned(!isPinned ? { setPinned(!isPinned ? {
id: user?.id || chat!.id, id: user?.id || chat!.id,
@ -318,11 +307,11 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
<GroupCallParticipantMenu <GroupCallParticipantMenu
participant={participant} participant={participant}
isDropdownOpen={isContextMenuOpen} isDropdownOpen={isContextMenuOpen}
positionX={positionX} anchor={contextMenuAnchor}
positionY={positionY} getTriggerElement={getTriggerElement}
transformOriginX={transformOriginX} getRootElement={getRootElement}
transformOriginY={transformOriginY} getMenuElement={getMenuElement}
style={menuStyle} getLayout={getLayout}
onClose={handleContextMenuClose} onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide} onCloseAnimationEnd={handleContextMenuHide}
menuRef={menuRef} menuRef={menuRef}

View File

@ -29,8 +29,7 @@ import type {
ApiWebPage, ApiWebPage,
} from '../../api/types'; } from '../../api/types';
import type { import type {
ApiDraft, GlobalState, MessageList, ApiDraft, GlobalState, MessageList, MessageListType, TabState,
MessageListType, TabState,
} from '../../global/types'; } from '../../global/types';
import type { import type {
IAnchorPosition, InlineBotSettings, ISettings, ThreadId, IAnchorPosition, InlineBotSettings, ISettings, ThreadId,
@ -830,7 +829,7 @@ const Composer: FC<OwnProps & StateProps> = ({
} = useContextMenuHandlers(mainButtonRef, !(mainButtonState === MainButtonState.Send && canShowCustomSendMenu)); } = useContextMenuHandlers(mainButtonRef, !(mainButtonState === MainButtonState.Send && canShowCustomSendMenu));
const { const {
contextMenuPosition: storyReactionPickerPosition, contextMenuAnchor: storyReactionPickerAnchor,
handleContextMenu: handleStoryPickerContextMenu, handleContextMenu: handleStoryPickerContextMenu,
handleBeforeContextMenu: handleBeforeStoryPickerContextMenu, handleBeforeContextMenu: handleBeforeStoryPickerContextMenu,
handleContextMenuHide: handleStoryPickerContextMenuHide, handleContextMenuHide: handleStoryPickerContextMenuHide,
@ -839,15 +838,15 @@ const Composer: FC<OwnProps & StateProps> = ({
useEffect(() => { useEffect(() => {
if (isReactionPickerOpen) return; if (isReactionPickerOpen) return;
if (storyReactionPickerPosition) { if (storyReactionPickerAnchor) {
openStoryReactionPicker({ openStoryReactionPicker({
peerId: chatId, peerId: chatId,
storyId: storyId!, storyId: storyId!,
position: storyReactionPickerPosition, position: storyReactionPickerAnchor,
}); });
handleStoryPickerContextMenuHide(); handleStoryPickerContextMenuHide();
} }
}, [chatId, handleStoryPickerContextMenuHide, isReactionPickerOpen, storyId, storyReactionPickerPosition]); }, [chatId, handleStoryPickerContextMenuHide, isReactionPickerOpen, storyId, storyReactionPickerAnchor]);
useClipboardPaste( useClipboardPaste(
isForCurrentMessageList || isInStoryViewer, isForCurrentMessageList || isInStoryViewer,

View File

@ -17,7 +17,6 @@ import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
import useMenuPosition from '../../hooks/useMenuPosition';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import Button from '../ui/Button'; import Button from '../ui/Button';
@ -69,7 +68,7 @@ const GifButton: FC<OwnProps> = ({
const isVideoReady = loadAndPlay && isBuffered; const isVideoReady = loadAndPlay && isBuffered;
const { const {
isContextMenuOpen, contextMenuPosition, isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu, handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref); } = useContextMenuHandlers(ref);
@ -78,15 +77,6 @@ const GifButton: FC<OwnProps> = ({
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll, .no-scrollbar')); const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll, .no-scrollbar'));
const getMenuElement = useLastCallback(() => ref.current!.querySelector('.gif-context-menu .bubble')); const getMenuElement = useLastCallback(() => ref.current!.querySelector('.gif-context-menu .bubble'));
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
);
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
if (isContextMenuOpen || !onClick) return; if (isContextMenuOpen || !onClick) return;
onClick({ onClick({
@ -185,14 +175,13 @@ const GifButton: FC<OwnProps> = ({
{shouldRenderSpinner && ( {shouldRenderSpinner && (
<Spinner color={previewBlobUrl || withThumb ? 'white' : 'black'} /> <Spinner color={previewBlobUrl || withThumb ? 'white' : 'black'} />
)} )}
{onClick && contextMenuPosition !== undefined && ( {onClick && contextMenuAnchor !== undefined && (
<Menu <Menu
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
transformOriginX={transformOriginX} anchor={contextMenuAnchor}
transformOriginY={transformOriginY} getTriggerElement={getTriggerElement}
positionX={positionX} getRootElement={getRootElement}
positionY={positionY} getMenuElement={getMenuElement}
style={menuStyle}
className="gif-context-menu" className="gif-context-menu"
autoClose autoClose
onClose={handleContextMenuClose} onClose={handleContextMenuClose}

View File

@ -16,7 +16,6 @@ import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListene
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useMenuPosition from '../../hooks/useMenuPosition';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import Button from '../ui/Button'; import Button from '../ui/Button';
@ -118,29 +117,18 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
const isIntesectingForShowing = useIsIntersecting(ref, observeIntersectionForShowing); const isIntesectingForShowing = useIsIntersecting(ref, observeIntersectionForShowing);
const { const {
isContextMenuOpen, contextMenuPosition, isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu, handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref); } = useContextMenuHandlers(ref);
const shouldRenderContextMenu = Boolean(!noContextMenu && contextMenuPosition); const shouldRenderContextMenu = Boolean(!noContextMenu && contextMenuAnchor);
const getTriggerElement = useLastCallback(() => ref.current); const getTriggerElement = useLastCallback(() => ref.current);
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll, .no-scrollbar')); const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll, .no-scrollbar'));
const getMenuElement = useLastCallback(() => { const getMenuElement = useLastCallback(() => {
return isStatusPicker ? menuRef.current : ref.current!.querySelector('.sticker-context-menu .bubble'); return isStatusPicker ? menuRef.current : ref.current!.querySelector('.sticker-context-menu .bubble');
}); });
const getLayout = useLastCallback(() => ({ withPortal: isStatusPicker, shouldAvoidNegativePosition: true }));
const getLayout = () => ({ withPortal: isStatusPicker, shouldAvoidNegativePosition: true });
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
getLayout,
);
useEffect(() => { useEffect(() => {
if (isContextMenuOpen) { if (isContextMenuOpen) {
@ -345,11 +333,11 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
<Menu <Menu
ref={menuRef} ref={menuRef}
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
transformOriginX={transformOriginX} anchor={contextMenuAnchor}
transformOriginY={transformOriginY} getTriggerElement={getTriggerElement}
positionX={positionX} getRootElement={getRootElement}
positionY={positionY} getMenuElement={getMenuElement}
style={menuStyle} getLayout={getLayout}
className="sticker-context-menu" className="sticker-context-menu"
autoClose autoClose
withPortal={isStatusPicker} withPortal={isStatusPicker}

View File

@ -12,9 +12,7 @@ import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { FocusDirection, ThreadId } from '../../types'; import type { FocusDirection, ThreadId } from '../../types';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
import { import { getChatTitle, getMessageHtmlId, isJoinedChannelMessage } from '../../global/helpers';
getChatTitle, getMessageHtmlId, isJoinedChannelMessage,
} from '../../global/helpers';
import { getMessageReplyInfo } from '../../global/helpers/replies'; import { getMessageReplyInfo } from '../../global/helpers/replies';
import { import {
selectCanPlayAnimatedEmojis, selectCanPlayAnimatedEmojis,
@ -191,11 +189,11 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
]); ]);
const { const {
isContextMenuOpen, contextMenuPosition, isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu, handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref); } = useContextMenuHandlers(ref);
const isContextMenuShown = contextMenuPosition !== undefined; const isContextMenuShown = contextMenuAnchor !== undefined;
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => { const handleMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
preventMessageInputBlur(e); preventMessageInputBlur(e);
@ -372,10 +370,10 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
<ActionMessageSuggestedAvatar message={message} renderContent={renderContent} /> <ActionMessageSuggestedAvatar message={message} renderContent={renderContent} />
)} )}
{isJoinedMessage && <SimilarChannels chatId={targetChatId!} />} {isJoinedMessage && <SimilarChannels chatId={targetChatId!} />}
{contextMenuPosition && ( {contextMenuAnchor && (
<ContextMenuContainer <ContextMenuContainer
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
anchor={contextMenuPosition} anchor={contextMenuAnchor}
message={message} message={message}
messageListType="thread" messageListType="thread"
onClose={handleContextMenuClose} onClose={handleContextMenuClose}

View File

@ -142,12 +142,12 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
const menuButtonRef = useRef<HTMLButtonElement>(null); const menuButtonRef = useRef<HTMLButtonElement>(null);
const lang = useOldLang(); const lang = useOldLang();
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [menuPosition, setMenuPosition] = useState<IAnchorPosition | undefined>(undefined); const [menuAnchor, setMenuAnchor] = useState<IAnchorPosition | undefined>(undefined);
const handleHeaderMenuOpen = useLastCallback(() => { const handleHeaderMenuOpen = useLastCallback(() => {
setIsMenuOpen(true); setIsMenuOpen(true);
const rect = menuButtonRef.current!.getBoundingClientRect(); const rect = menuButtonRef.current!.getBoundingClientRect();
setMenuPosition({ x: rect.right, y: rect.bottom }); setMenuAnchor({ x: rect.right, y: rect.bottom });
}); });
const handleHeaderMenuClose = useLastCallback(() => { const handleHeaderMenuClose = useLastCallback(() => {
@ -155,7 +155,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
}); });
const handleHeaderMenuHide = useLastCallback(() => { const handleHeaderMenuHide = useLastCallback(() => {
setMenuPosition(undefined); setMenuAnchor(undefined);
}); });
const handleSubscribeClick = useLastCallback(() => { const handleSubscribeClick = useLastCallback(() => {
@ -420,12 +420,12 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
> >
<i className="icon icon-more" aria-hidden /> <i className="icon icon-more" aria-hidden />
</Button> </Button>
{menuPosition && ( {menuAnchor && (
<HeaderMenuContainer <HeaderMenuContainer
chatId={chatId} chatId={chatId}
threadId={threadId} threadId={threadId}
isOpen={isMenuOpen} isOpen={isMenuOpen}
anchor={menuPosition} anchor={menuAnchor}
withExtraActions={isMobile || !canExpandActions} withExtraActions={isMobile || !canExpandActions}
isChannel={isChannel} isChannel={isChannel}
canStartBot={canStartBot} canStartBot={canStartBot}

View File

@ -34,7 +34,6 @@ import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMenuPosition from '../../../hooks/useMenuPosition';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated'; import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated';
@ -160,7 +159,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
useEffect(() => (isShown ? captureEscKeyListener(clearEmbedded) : undefined), [isShown, clearEmbedded]); useEffect(() => (isShown ? captureEscKeyListener(clearEmbedded) : undefined), [isShown, clearEmbedded]);
const { const {
isContextMenuOpen, contextMenuPosition, handleContextMenu, isContextMenuOpen, contextMenuAnchor, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref); } = useContextMenuHandlers(ref);
@ -199,15 +198,6 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
const getRootElement = useLastCallback(() => ref.current!); const getRootElement = useLastCallback(() => ref.current!);
const getMenuElement = useLastCallback(() => ref.current!.querySelector('.forward-context-menu .bubble')); const getMenuElement = useLastCallback(() => ref.current!.querySelector('.forward-context-menu .bubble'));
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
);
useEffect(() => { useEffect(() => {
if (!shouldRender) { if (!shouldRender) {
handleContextMenuClose(); handleContextMenuClose();
@ -296,11 +286,10 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
{(isShowingReply || isForwarding) && !isContextMenuDisabled && ( {(isShowingReply || isForwarding) && !isContextMenuDisabled && (
<Menu <Menu
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
transformOriginX={transformOriginX} anchor={contextMenuAnchor}
transformOriginY={transformOriginY} getTriggerElement={getTriggerElement}
positionX={positionX} getRootElement={getRootElement}
positionY={positionY} getMenuElement={getMenuElement}
style={menuStyle}
className="forward-context-menu" className="forward-context-menu"
onClose={handleContextMenuClose} onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide} onCloseAnimationEnd={handleContextMenuHide}

View File

@ -7,6 +7,7 @@ import { withGlobal } from '../../../global';
import type { ApiSticker, ApiVideo } from '../../../api/types'; import type { ApiSticker, ApiVideo } from '../../../api/types';
import type { GlobalActions } from '../../../global'; import type { GlobalActions } from '../../../global';
import type { ThreadId } from '../../../types'; import type { ThreadId } from '../../../types';
import type { MenuPositionOptions } from '../../ui/Menu';
import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { selectIsContextMenuTranslucent, selectTabState } from '../../../global/selectors'; import { selectIsContextMenuTranslucent, selectTabState } from '../../../global/selectors';
@ -61,12 +62,8 @@ export type OwnProps = {
className?: string; className?: string;
isAttachmentModal?: boolean; isAttachmentModal?: boolean;
canSendPlainText?: boolean; canSendPlainText?: boolean;
positionX?: 'left' | 'right'; }
positionY?: 'top' | 'bottom'; & MenuPositionOptions;
transformOriginX?: number;
transformOriginY?: number;
style?: string;
};
type StateProps = { type StateProps = {
isLeftColumnShown: boolean; isLeftColumnShown: boolean;
@ -87,11 +84,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
isAttachmentModal, isAttachmentModal,
canSendPlainText, canSendPlainText,
className, className,
positionX,
positionY,
transformOriginX,
transformOriginY,
style,
isBackgroundTranslucent, isBackgroundTranslucent,
onLoad, onLoad,
onClose, onClose,
@ -103,6 +95,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
onSearchOpen, onSearchOpen,
addRecentEmoji, addRecentEmoji,
addRecentCustomEmoji, addRecentCustomEmoji,
...menuPositionOptions
}) => { }) => {
const [activeTab, setActiveTab] = useState<number>(0); const [activeTab, setActiveTab] = useState<number>(0);
const [recentEmojis, setRecentEmojis] = useState<string[]>([]); const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
@ -325,8 +318,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
return ( return (
<UnfreezableMenu <UnfreezableMenu
isOpen={isOpen} isOpen={isOpen}
positionX={isAttachmentModal ? positionX : 'left'}
positionY={isAttachmentModal ? positionY : 'bottom'}
onClose={onClose} onClose={onClose}
withPortal={isAttachmentModal} withPortal={isAttachmentModal}
className={buildClassName('SymbolMenu', className)} className={buildClassName('SymbolMenu', className)}
@ -335,9 +326,11 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined} onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
noCloseOnBackdrop={!IS_TOUCH_ENV} noCloseOnBackdrop={!IS_TOUCH_ENV}
noCompact noCompact
transformOriginX={transformOriginX} // eslint-disable-next-line react/jsx-props-no-spreading
transformOriginY={transformOriginY} {...(isAttachmentModal ? menuPositionOptions : {
style={style} positionX: 'left',
positionY: 'bottom',
})}
> >
{content} {content}
</UnfreezableMenu> </UnfreezableMenu>

View File

@ -10,7 +10,6 @@ import buildClassName from '../../../util/buildClassName';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMenuPosition from '../../../hooks/useMenuPosition';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton'; import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
@ -89,7 +88,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
const triggerRef = useRef<HTMLDivElement>(null); const triggerRef = useRef<HTMLDivElement>(null);
const [isSymbolMenuLoaded, onSymbolMenuLoadingComplete] = useFlag(); const [isSymbolMenuLoaded, onSymbolMenuLoadingComplete] = useFlag();
const [contextMenuPosition, setContextMenuPosition] = useState<IAnchorPosition | undefined>(undefined); const [contextMenuAnchor, setContextMenuAnchor] = useState<IAnchorPosition | undefined>(undefined);
const symbolMenuButtonClassName = buildClassName( const symbolMenuButtonClassName = buildClassName(
'mobile-symbol-menu-button', 'mobile-symbol-menu-button',
@ -106,7 +105,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
const triggerEl = triggerRef.current; const triggerEl = triggerRef.current;
if (!triggerEl) return; if (!triggerEl) return;
const { x, y } = triggerEl.getBoundingClientRect(); const { x, y } = triggerEl.getBoundingClientRect();
setContextMenuPosition({ x, y }); setContextMenuAnchor({ x, y });
}); });
const handleSearchOpen = useLastCallback((type: 'stickers' | 'gifs') => { const handleSearchOpen = useLastCallback((type: 'stickers' | 'gifs') => {
@ -141,16 +140,6 @@ const SymbolMenuButton: FC<OwnProps> = ({
const getMenuElement = useLastCallback(() => document.querySelector('#portals .SymbolMenu .bubble')); const getMenuElement = useLastCallback(() => document.querySelector('#portals .SymbolMenu .bubble'));
const getLayout = useLastCallback(() => ({ withPortal: true })); const getLayout = useLastCallback(() => ({ withPortal: true }));
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
getLayout,
);
return ( return (
<> <>
{isMobile ? ( {isMobile ? (
@ -199,11 +188,11 @@ const SymbolMenuButton: FC<OwnProps> = ({
isAttachmentModal={isAttachmentModal} isAttachmentModal={isAttachmentModal}
canSendPlainText={canSendPlainText} canSendPlainText={canSendPlainText}
className={buildClassName(className, forceDarkTheme && 'component-theme-dark')} className={buildClassName(className, forceDarkTheme && 'component-theme-dark')}
positionX={isAttachmentModal ? positionX : undefined} anchor={isAttachmentModal ? contextMenuAnchor : undefined}
positionY={isAttachmentModal ? positionY : undefined} getTriggerElement={isAttachmentModal ? getTriggerElement : undefined}
transformOriginX={isAttachmentModal ? transformOriginX : undefined} getRootElement={isAttachmentModal ? getRootElement : undefined}
transformOriginY={isAttachmentModal ? transformOriginY : undefined} getMenuElement={isAttachmentModal ? getMenuElement : undefined}
style={isAttachmentModal ? menuStyle : undefined} getLayout={isAttachmentModal ? getLayout : undefined}
/> />
</> </>
); );

View File

@ -1,7 +1,5 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
memo, useEffect, useRef,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
@ -23,7 +21,6 @@ import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useDerivedSignal from '../../../hooks/useDerivedSignal'; import useDerivedSignal from '../../../hooks/useDerivedSignal';
import useDerivedState from '../../../hooks/useDerivedState'; import useDerivedState from '../../../hooks/useDerivedState';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMenuPosition from '../../../hooks/useMenuPosition';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated'; import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated';
import useSyncEffect from '../../../hooks/useSyncEffect'; import useSyncEffect from '../../../hooks/useSyncEffect';
@ -122,7 +119,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
}); });
const { const {
isContextMenuOpen, contextMenuPosition, handleContextMenu, isContextMenuOpen, contextMenuAnchor, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref, isEditing, true); } = useContextMenuHandlers(ref, isEditing, true);
@ -132,15 +129,6 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
() => ref.current!.querySelector('.web-page-preview-context-menu .bubble'), () => ref.current!.querySelector('.web-page-preview-context-menu .bubble'),
); );
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
);
const handlePreviewClick = useLastCallback((e: React.MouseEvent): void => { const handlePreviewClick = useLastCallback((e: React.MouseEvent): void => {
handleContextMenu(e); handleContextMenu(e);
}); });
@ -172,11 +160,10 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
return ( return (
<Menu <Menu
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
transformOriginX={transformOriginX} anchor={contextMenuAnchor}
transformOriginY={transformOriginY} getTriggerElement={getTriggerElement}
positionX={positionX} getRootElement={getRootElement}
positionY={positionY} getMenuElement={getMenuElement}
style={menuStyle}
className="web-page-preview-context-menu" className="web-page-preview-context-menu"
onClose={handleContextMenuClose} onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide} onCloseAnimationEnd={handleContextMenuHide}
@ -185,7 +172,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
<> <>
{ {
isInvertedMedia ? ( isInvertedMedia ? (
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
<MenuItem icon="move-caption-up" onClick={() => updateIsInvertedMedia(undefined)}> <MenuItem icon="move-caption-up" onClick={() => updateIsInvertedMedia(undefined)}>
{lang('PreviewSender.MoveTextUp')} {lang('PreviewSender.MoveTextUp')}
</MenuItem> </MenuItem>

View File

@ -1,6 +1,11 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { import React, {
memo, useCallback, useEffect, useMemo, useRef, useState, memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
@ -20,15 +25,10 @@ import type {
ApiTypeStory, ApiTypeStory,
ApiUser, ApiUser,
} from '../../../api/types'; } from '../../../api/types';
import type { import type { ActiveEmojiInteraction, ChatTranslatedMessages, MessageListType } from '../../../global/types';
ActiveEmojiInteraction,
ChatTranslatedMessages,
MessageListType,
} from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { import type {
FocusDirection, IAlbum, ISettings, ScrollTargetPosition, FocusDirection, IAlbum, ISettings, ScrollTargetPosition, ThreadId,
ThreadId,
} from '../../../types'; } from '../../../types';
import type { Signal } from '../../../util/signals'; import type { Signal } from '../../../util/signals';
import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage'; import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage';
@ -66,7 +66,8 @@ import {
selectActiveDownloads, selectActiveDownloads,
selectAnimatedEmoji, selectAnimatedEmoji,
selectCanAutoLoadMedia, selectCanAutoLoadMedia,
selectCanAutoPlayMedia, selectCanReplyToMessage, selectCanAutoPlayMedia,
selectCanReplyToMessage,
selectChat, selectChat,
selectChatFullInfo, selectChatFullInfo,
selectChatMessage, selectChatMessage,
@ -443,7 +444,7 @@ const Message: FC<OwnProps & StateProps> = ({
const { const {
isContextMenuOpen, isContextMenuOpen,
contextMenuPosition, contextMenuAnchor,
contextMenuTarget, contextMenuTarget,
handleBeforeContextMenu, handleBeforeContextMenu,
handleContextMenu: onContextMenu, handleContextMenu: onContextMenu,
@ -535,7 +536,7 @@ const Message: FC<OwnProps & StateProps> = ({
) && !album.messages.some((msg) => Object.keys(msg.content).length === 0); ) && !album.messages.some((msg) => Object.keys(msg.content).length === 0);
const isInDocumentGroupNotFirst = isInDocumentGroup && !isFirstInDocumentGroup; const isInDocumentGroupNotFirst = isInDocumentGroup && !isFirstInDocumentGroup;
const isInDocumentGroupNotLast = isInDocumentGroup && !isLastInDocumentGroup; const isInDocumentGroupNotLast = isInDocumentGroup && !isLastInDocumentGroup;
const isContextMenuShown = contextMenuPosition !== undefined; const isContextMenuShown = contextMenuAnchor !== undefined;
const canShowActionButton = ( const canShowActionButton = (
!(isContextMenuShown || isInSelectMode || isForwarding) !(isContextMenuShown || isInSelectMode || isForwarding)
&& !isInDocumentGroupNotLast && !isInDocumentGroupNotLast
@ -1650,10 +1651,10 @@ const Message: FC<OwnProps & StateProps> = ({
/> />
)} )}
</div> </div>
{contextMenuPosition && ( {contextMenuAnchor && (
<ContextMenuContainer <ContextMenuContainer
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
anchor={contextMenuPosition} anchor={contextMenuAnchor}
targetHref={contextMenuTarget?.matches('a[href]') ? (contextMenuTarget as HTMLAnchorElement).href : undefined} targetHref={contextMenuTarget?.matches('a[href]') ? (contextMenuTarget as HTMLAnchorElement).href : undefined}
message={message} message={message}
album={album} album={album}

View File

@ -28,7 +28,6 @@ import { getMessageCopyOptions } from './helpers/copyOptions';
import useAppLayout from '../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMenuPosition from '../../../hooks/useMenuPosition';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import AvatarList from '../../common/AvatarList'; import AvatarList from '../../common/AvatarList';
@ -323,15 +322,11 @@ const MessageContextMenu: FC<OwnProps> = ({
}, ANIMATION_DURATION); }, ANIMATION_DURATION);
}, [isOpen, markIsReady, unmarkIsReady]); }, [isOpen, markIsReady, unmarkIsReady]);
const {
positionX, positionY, transformOriginX, transformOriginY, style, menuStyle, withScroll,
} = useMenuPosition(anchor, getTriggerElement, getRootElement, getMenuElement, getLayout);
useEffect(() => { useEffect(() => {
disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionPicker'); disableScrolling(scrollableRef.current, '.ReactionPicker');
return enableScrolling; return enableScrolling;
}, [withScroll]); }, [isOpen]);
const handleOpenMessageReactionPicker = useLastCallback((position: IAnchorPosition) => { const handleOpenMessageReactionPicker = useLastCallback((position: IAnchorPosition) => {
onReactionPickerOpen!(position); onReactionPickerOpen!(position);
@ -342,12 +337,11 @@ const MessageContextMenu: FC<OwnProps> = ({
<Menu <Menu
ref={menuRef} ref={menuRef}
isOpen={isOpen} isOpen={isOpen}
transformOriginX={transformOriginX} anchor={anchor}
transformOriginY={transformOriginY} getTriggerElement={getTriggerElement}
positionX={positionX} getRootElement={getRootElement}
positionY={positionY} getMenuElement={getMenuElement}
style={style} getLayout={getLayout}
bubbleStyle={menuStyle}
className={buildClassName( className={buildClassName(
'MessageContextMenu', 'fluid', withReactions && 'with-reactions', 'MessageContextMenu', 'fluid', withReactions && 'with-reactions',
)} )}
@ -376,13 +370,12 @@ const MessageContextMenu: FC<OwnProps> = ({
)} )}
<div <div
ref={scrollableRef}
className={buildClassName( className={buildClassName(
'MessageContextMenu_items scrollable-content custom-scroll', 'MessageContextMenu_items scrollable-content custom-scroll',
areItemsHidden && 'MessageContextMenu_items-hidden', areItemsHidden && 'MessageContextMenu_items-hidden',
)} )}
style={menuStyle}
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
ref={scrollableRef}
> >
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>} {canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>}
{canReschedule && ( {canReschedule && (

View File

@ -65,7 +65,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
threshold: 1, threshold: 1,
}); });
const { const {
isContextMenuOpen, contextMenuPosition, isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu, handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref, undefined, true, IS_ANDROID); } = useContextMenuHandlers(ref, undefined, true, IS_ANDROID);
@ -183,10 +183,10 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
)} )}
</div> </div>
</div> </div>
{contextMenuPosition && ( {contextMenuAnchor && (
<SponsoredMessageContextMenuContainer <SponsoredMessageContextMenuContainer
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
anchor={contextMenuPosition} anchor={contextMenuAnchor}
message={message!} message={message!}
onAboutAdsClick={openAboutAdsModal} onAboutAdsClick={openAboutAdsModal}
onReportAd={handleReportSponsoredMessage} onReportAd={handleReportSponsoredMessage}

View File

@ -5,15 +5,25 @@ import { getActions, getGlobal, withGlobal } from '../../../../global';
import type { IAnchorPosition } from '../../../../types'; import type { IAnchorPosition } from '../../../../types';
import { import {
type ApiAvailableEffect, type ApiAvailableEffect,
type ApiMessage, type ApiMessageEntity, type ApiMessage,
type ApiReaction, type ApiReactionCustomEmoji, type ApiSticker, type ApiStory, type ApiStorySkipped, type ApiMessageEntity,
type ApiReaction,
type ApiReactionCustomEmoji,
type ApiSticker,
type ApiStory,
type ApiStorySkipped,
MAIN_THREAD_ID, MAIN_THREAD_ID,
} from '../../../../api/types'; } from '../../../../api/types';
import { getReactionKey, getStoryKey, isUserId } from '../../../../global/helpers'; import { getReactionKey, getStoryKey, isUserId } from '../../../../global/helpers';
import { import {
selectChat, selectChatFullInfo, selectChatMessage, selectIsContextMenuTranslucent, selectIsCurrentUserPremium, selectChat,
selectPeerStory, selectTabState, selectChatFullInfo,
selectChatMessage,
selectIsContextMenuTranslucent,
selectIsCurrentUserPremium,
selectPeerStory,
selectTabState,
} from '../../../../global/selectors'; } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText'; import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
@ -23,7 +33,6 @@ import { buildCustomEmojiHtml } from '../../composer/helpers/customEmoji';
import { getIsMobile } from '../../../../hooks/useAppLayout'; import { getIsMobile } from '../../../../hooks/useAppLayout';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useLastCallback from '../../../../hooks/useLastCallback'; import useLastCallback from '../../../../hooks/useLastCallback';
import useMenuPosition from '../../../../hooks/useMenuPosition';
import useOldLang from '../../../../hooks/useOldLang'; import useOldLang from '../../../../hooks/useOldLang';
import CustomEmojiPicker from '../../../common/CustomEmojiPicker'; import CustomEmojiPicker from '../../../common/CustomEmojiPicker';
@ -104,9 +113,6 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
? -(menuRef.current.offsetWidth - REACTION_SELECTOR_WIDTH) / 2 - FULL_PICKER_SHIFT_DELTA.x / 2 ? -(menuRef.current.offsetWidth - REACTION_SELECTOR_WIDTH) / 2 - FULL_PICKER_SHIFT_DELTA.x / 2
: 0, : 0,
})); }));
const {
positionX, positionY, transformOriginX, transformOriginY, style,
} = useMenuPosition(renderingPosition, getTriggerElement, getRootElement, getMenuElement, getLayout);
const handleToggleCustomReaction = useLastCallback((sticker: ApiSticker) => { const handleToggleCustomReaction = useLastCallback((sticker: ApiSticker) => {
if (!renderedChatId || !renderedMessageId) { if (!renderedChatId || !renderedMessageId) {
@ -213,11 +219,11 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
)} )}
withPortal withPortal
noCompact noCompact
positionX={positionX} anchor={renderingPosition}
positionY={positionY} getTriggerElement={getTriggerElement}
transformOriginX={transformOriginX} getRootElement={getRootElement}
transformOriginY={transformOriginY} getMenuElement={getMenuElement}
style={style} getLayout={getLayout}
backdropExcludedSelector=".Modal.confirm" backdropExcludedSelector=".Modal.confirm"
onClose={closeReactionPicker} onClose={closeReactionPicker}
> >

View File

@ -1,9 +1,7 @@
import React, { memo, useRef } from '../../../../lib/teact/teact'; import React, { memo, useRef } from '../../../../lib/teact/teact';
import { getActions } from '../../../../global'; import { getActions } from '../../../../global';
import type { import type { ApiReaction, ApiSavedReactionTag } from '../../../../api/types';
ApiReaction, ApiSavedReactionTag,
} from '../../../../api/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
@ -12,7 +10,6 @@ import { REM } from '../../../common/helpers/mediaDimensions';
import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers';
import useFlag from '../../../../hooks/useFlag'; import useFlag from '../../../../hooks/useFlag';
import useLastCallback from '../../../../hooks/useLastCallback'; import useLastCallback from '../../../../hooks/useLastCallback';
import useMenuPosition from '../../../../hooks/useMenuPosition';
import useOldLang from '../../../../hooks/useOldLang'; import useOldLang from '../../../../hooks/useOldLang';
import ReactionAnimatedEmoji from '../../../common/reactions/ReactionAnimatedEmoji'; import ReactionAnimatedEmoji from '../../../common/reactions/ReactionAnimatedEmoji';
@ -88,7 +85,7 @@ const SavedTagButton = ({
const { const {
isContextMenuOpen, isContextMenuOpen,
contextMenuPosition, contextMenuAnchor,
handleBeforeContextMenu, handleBeforeContextMenu,
handleContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuClose,
@ -98,18 +95,7 @@ const SavedTagButton = ({
const getTriggerElement = useLastCallback(() => ref.current); const getTriggerElement = useLastCallback(() => ref.current);
const getRootElement = useLastCallback(() => document.body); const getRootElement = useLastCallback(() => document.body);
const getMenuElement = useLastCallback(() => menuRef.current); const getMenuElement = useLastCallback(() => menuRef.current);
const getLayout = useLastCallback(() => ({ withPortal: true, shouldAvoidNegativePosition: true }));
const getLayout = () => ({ withPortal: true, shouldAvoidNegativePosition: true });
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
getLayout,
);
if (withCount && count === 0) { if (withCount && count === 0) {
return undefined; return undefined;
@ -166,15 +152,15 @@ const SavedTagButton = ({
onSubmit={handleRenameTag} onSubmit={handleRenameTag}
/> />
)} )}
{withContextMenu && contextMenuPosition && ( {withContextMenu && contextMenuAnchor && (
<Menu <Menu
ref={menuRef} ref={menuRef}
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
transformOriginX={transformOriginX} anchor={contextMenuAnchor}
transformOriginY={transformOriginY} getTriggerElement={getTriggerElement}
positionX={positionX} getRootElement={getRootElement}
positionY={positionY} getMenuElement={getMenuElement}
style={menuStyle} getLayout={getLayout}
autoClose autoClose
withPortal withPortal
onClose={handleContextMenuClose} onClose={handleContextMenuClose}

View File

@ -15,7 +15,6 @@ import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMe
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
import useMenuPosition from '../../hooks/useMenuPosition';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import Icon from '../common/icons/Icon'; import Icon from '../common/icons/Icon';
@ -73,19 +72,10 @@ function MediaStory({
}, [isDeleted, isFullyLoaded, story]); }, [isDeleted, isFullyLoaded, story]);
const { const {
isContextMenuOpen, contextMenuPosition, isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu, handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(containerRef, !isOwn); } = useContextMenuHandlers(containerRef, !isOwn);
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
getLayout,
);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
openStoryViewer({ openStoryViewer({
@ -156,14 +146,14 @@ function MediaStory({
{isFullyLoaded && <MediaAreaOverlay story={story} />} {isFullyLoaded && <MediaAreaOverlay story={story} />}
{isProtected && <span className="protector" />} {isProtected && <span className="protector" />}
</div> </div>
{contextMenuPosition !== undefined && ( {contextMenuAnchor !== undefined && (
<Menu <Menu
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
transformOriginX={transformOriginX} anchor={contextMenuAnchor}
transformOriginY={transformOriginY} getTriggerElement={getTriggerElement}
positionX={positionX} getRootElement={getRootElement}
positionY={positionY} getMenuElement={getMenuElement}
style={menuStyle} getLayout={getLayout}
className={buildClassName(styles.contextMenu, 'story-context-menu')} className={buildClassName(styles.contextMenu, 'story-context-menu')}
autoClose autoClose
onClose={handleContextMenuClose} onClose={handleContextMenuClose}

View File

@ -10,7 +10,6 @@ import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMe
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useMenuPosition from '../../hooks/useMenuPosition';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import useStoryPreloader from './hooks/useStoryPreloader'; import useStoryPreloader from './hooks/useStoryPreloader';
@ -43,7 +42,7 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
useStoryPreloader(peer.id); useStoryPreloader(peer.id);
const { const {
isContextMenuOpen, contextMenuPosition, isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu, handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref); } = useContextMenuHandlers(ref);
@ -53,16 +52,6 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
const getMenuElement = useLastCallback(() => ref.current!.querySelector('.story-peer-context-menu .bubble')); const getMenuElement = useLastCallback(() => ref.current!.querySelector('.story-peer-context-menu .bubble'));
const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true })); const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true }));
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
getLayout,
);
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
if (isContextMenuOpen) return; if (isContextMenuOpen) return;
@ -114,14 +103,14 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
<div className={buildClassName(styles.name, peer.hasUnreadStories && styles.name_hasUnreadStory)}> <div className={buildClassName(styles.name, peer.hasUnreadStories && styles.name_hasUnreadStory)}>
{isSelf ? lang('MyStory') : getSenderTitle(lang, peer)} {isSelf ? lang('MyStory') : getSenderTitle(lang, peer)}
</div> </div>
{contextMenuPosition !== undefined && ( {contextMenuAnchor !== undefined && (
<Menu <Menu
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
transformOriginX={transformOriginX} anchor={contextMenuAnchor}
transformOriginY={transformOriginY} getTriggerElement={getTriggerElement}
positionX={positionX} getRootElement={getRootElement}
positionY={positionY} getMenuElement={getMenuElement}
style={menuStyle} getLayout={getLayout}
className={buildClassName('story-peer-context-menu', styles.contextMenu)} className={buildClassName('story-peer-context-menu', styles.contextMenu)}
autoClose autoClose
withPortal withPortal

View File

@ -1,7 +1,6 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { import React, {
useCallback, useMemo, useCallback, useMemo, useRef, useState,
useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import Button from './Button'; import Button from './Button';
@ -44,12 +43,11 @@ const DropdownMenu: FC<OwnProps> = ({
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const dropdownRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const toggleIsOpen = () => { const toggleIsOpen = () => {
setIsOpen(!isOpen); setIsOpen(!isOpen);
if (isOpen) { if (isOpen) {
onClose?.(); onClose?.();
} else { } else {
@ -96,7 +94,6 @@ const DropdownMenu: FC<OwnProps> = ({
return ( return (
<div <div
ref={dropdownRef}
className={`DropdownMenu ${className || ''}`} className={`DropdownMenu ${className || ''}`}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onTransitionEnd={onTransitionEnd} onTransitionEnd={onTransitionEnd}
@ -105,7 +102,6 @@ const DropdownMenu: FC<OwnProps> = ({
<Menu <Menu
ref={menuRef} ref={menuRef}
containerRef={dropdownRef}
isOpen={isOpen || Boolean(forceOpen)} isOpen={isOpen || Boolean(forceOpen)}
className={className || ''} className={className || ''}
transformOriginX={transformOriginX} transformOriginX={transformOriginX}

View File

@ -13,7 +13,6 @@ import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import { useFastClick } from '../../hooks/useFastClick'; import { useFastClick } from '../../hooks/useFastClick';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useMenuPosition from '../../hooks/useMenuPosition';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import Icon from '../common/icons/Icon'; import Icon from '../common/icons/Icon';
@ -120,7 +119,7 @@ const ListItem: FC<OwnProps> = ({
const [isTouched, markIsTouched, unmarkIsTouched] = useFlag(); const [isTouched, markIsTouched, unmarkIsTouched] = useFlag();
const { const {
isContextMenuOpen, contextMenuPosition, isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu, handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(containerRef, !contextActions); } = useContextMenuHandlers(containerRef, !contextActions);
@ -133,16 +132,6 @@ const ListItem: FC<OwnProps> = ({
}); });
const getLayout = useLastCallback(() => ({ withPortal: withPortalForMenu })); const getLayout = useLastCallback(() => ({ withPortal: withPortalForMenu }));
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
getLayout,
);
const handleClickEvent = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => { const handleClickEvent = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
const hasModifierKey = e.ctrlKey || e.metaKey || e.shiftKey; const hasModifierKey = e.ctrlKey || e.metaKey || e.shiftKey;
if (!hasModifierKey && e.button === MouseButton.Main) { if (!hasModifierKey && e.button === MouseButton.Main) {
@ -215,7 +204,7 @@ const ListItem: FC<OwnProps> = ({
disabled && 'disabled', disabled && 'disabled',
allowDisabledClick && 'click-allowed', allowDisabledClick && 'click-allowed',
inactive && 'inactive', inactive && 'inactive',
contextMenuPosition && 'has-menu-open', contextMenuAnchor && 'has-menu-open',
focus && 'focus', focus && 'focus',
destructive && 'destructive', destructive && 'destructive',
multiline && 'multiline', multiline && 'multiline',
@ -267,14 +256,14 @@ const ListItem: FC<OwnProps> = ({
)} )}
{rightElement} {rightElement}
</ButtonElementTag> </ButtonElementTag>
{contextActions && contextMenuPosition !== undefined && ( {contextActions && contextMenuAnchor !== undefined && (
<Menu <Menu
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
transformOriginX={transformOriginX} anchor={contextMenuAnchor}
transformOriginY={transformOriginY} getTriggerElement={getTriggerElement}
positionX={positionX} getRootElement={getRootElement}
positionY={positionY} getMenuElement={getMenuElement}
style={menuStyle} getLayout={getLayout}
className="ListItem-context-menu with-menu-transitions" className="ListItem-context-menu with-menu-transitions"
autoClose autoClose
onClose={handleContextMenuClose} onClose={handleContextMenuClose}

View File

@ -1,7 +1,10 @@
import React, { type FC, memo, useEffect } from '../../lib/teact/teact'; import React, {
type FC, memo, useEffect, useRef,
} from '../../lib/teact/teact';
import type { MenuPositionOptions } from '../../hooks/useMenuPosition';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import captureEscKeyListener from '../../util/captureEscKeyListener'; import captureEscKeyListener from '../../util/captureEscKeyListener';
import freezeWhenClosed from '../../util/hoc/freezeWhenClosed'; import freezeWhenClosed from '../../util/hoc/freezeWhenClosed';
import { IS_BACKDROP_BLUR_SUPPORTED } from '../../util/windowEnvironment'; import { IS_BACKDROP_BLUR_SUPPORTED } from '../../util/windowEnvironment';
@ -13,6 +16,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'
import useHistoryBack from '../../hooks/useHistoryBack'; import useHistoryBack from '../../hooks/useHistoryBack';
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation'; import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useMenuPosition from '../../hooks/useMenuPosition';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import useVirtualBackdrop from '../../hooks/useVirtualBackdrop'; import useVirtualBackdrop from '../../hooks/useVirtualBackdrop';
@ -20,54 +24,44 @@ import Portal from './Portal';
import './Menu.scss'; import './Menu.scss';
type OwnProps = { export type { MenuPositionOptions } from '../../hooks/useMenuPosition';
ref?: React.RefObject<HTMLDivElement>;
containerRef?: React.RefObject<HTMLElement>; type OwnProps =
isOpen: boolean; {
shouldCloseFast?: boolean; ref?: React.RefObject<HTMLDivElement>;
id?: string; isOpen: boolean;
className?: string; shouldCloseFast?: boolean;
bubbleClassName?: string; id?: string;
style?: string; className?: string;
bubbleStyle?: string; bubbleClassName?: string;
ariaLabelledBy?: string; ariaLabelledBy?: string;
transformOriginX?: number; autoClose?: boolean;
transformOriginY?: number; footer?: string;
positionX?: 'left' | 'right'; noCloseOnBackdrop?: boolean;
positionY?: 'top' | 'bottom'; backdropExcludedSelector?: string;
autoClose?: boolean; noCompact?: boolean;
footer?: string; onKeyDown?: (e: React.KeyboardEvent<any>) => void;
noCloseOnBackdrop?: boolean; onCloseAnimationEnd?: () => void;
backdropExcludedSelector?: string; onClose: () => void;
noCompact?: boolean; onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onKeyDown?: (e: React.KeyboardEvent<any>) => void; onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onCloseAnimationEnd?: () => void; onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onClose: () => void; withPortal?: boolean;
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; children?: React.ReactNode;
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; }
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; & MenuPositionOptions;
withPortal?: boolean;
children?: React.ReactNode;
};
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;
const Menu: FC<OwnProps> = ({ const Menu: FC<OwnProps> = ({
ref: externalRef, ref: externalRef,
containerRef,
shouldCloseFast, shouldCloseFast,
isOpen, isOpen,
id, id,
className, className,
bubbleClassName, bubbleClassName,
style,
bubbleStyle,
ariaLabelledBy, ariaLabelledBy,
children, children,
transformOriginX,
transformOriginY,
positionX = 'left',
positionY = 'top',
autoClose = false, autoClose = false,
footer, footer,
noCloseOnBackdrop = false, noCloseOnBackdrop = false,
@ -79,16 +73,20 @@ const Menu: FC<OwnProps> = ({
onMouseLeave, onMouseLeave,
withPortal, withPortal,
onMouseEnterBackdrop, onMouseEnterBackdrop,
...positionOptions
}) => { }) => {
const { isTouchScreen } = useAppLayout(); const { isTouchScreen } = useAppLayout();
const { ref: menuRef } = useShowTransition({ // eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const { ref: bubbleRef } = useShowTransition({
isOpen, isOpen,
ref: externalRef, ref: externalRef,
onCloseAnimationEnd, onCloseAnimationEnd,
}); });
const backdropContainerRef = containerRef || menuRef; useMenuPosition(isOpen, containerRef, bubbleRef, positionOptions);
useEffect( useEffect(
() => (isOpen ? captureEscKeyListener(onClose) : undefined), () => (isOpen ? captureEscKeyListener(onClose) : undefined),
@ -107,11 +105,11 @@ const Menu: FC<OwnProps> = ({
} }
}, [isOpen]); }, [isOpen]);
const handleKeyDown = useKeyboardListNavigation(menuRef, isOpen, autoClose ? onClose : undefined, undefined, true); const handleKeyDown = useKeyboardListNavigation(bubbleRef, isOpen, autoClose ? onClose : undefined, undefined, true);
useVirtualBackdrop( useVirtualBackdrop(
isOpen, isOpen,
backdropContainerRef, containerRef,
noCloseOnBackdrop ? undefined : onClose, noCloseOnBackdrop ? undefined : onClose,
undefined, undefined,
backdropExcludedSelector, backdropExcludedSelector,
@ -119,16 +117,11 @@ const Menu: FC<OwnProps> = ({
const bubbleFullClassName = buildClassName( const bubbleFullClassName = buildClassName(
'bubble menu-container custom-scroll', 'bubble menu-container custom-scroll',
positionY,
positionX,
footer && 'with-footer', footer && 'with-footer',
bubbleClassName, bubbleClassName,
shouldCloseFast && 'close-fast', shouldCloseFast && 'close-fast',
); );
const transformOriginYStyle = transformOriginY !== undefined ? `${transformOriginY}px` : undefined;
const transformOriginXStyle = transformOriginX !== undefined ? `${transformOriginX}px` : undefined;
const handleClick = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => { const handleClick = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation(); e.stopPropagation();
if (autoClose) { if (autoClose) {
@ -138,6 +131,7 @@ const Menu: FC<OwnProps> = ({
const menu = ( const menu = (
<div <div
ref={containerRef}
id={id} id={id}
className={buildClassName( className={buildClassName(
'Menu', 'Menu',
@ -146,7 +140,6 @@ const Menu: FC<OwnProps> = ({
withPortal && 'in-portal', withPortal && 'in-portal',
className, className,
)} )}
style={style}
aria-labelledby={ariaLabelledBy} aria-labelledby={ariaLabelledBy}
role={ariaLabelledBy ? 'menu' : undefined} role={ariaLabelledBy ? 'menu' : undefined}
onKeyDown={isOpen ? handleKeyDown : undefined} onKeyDown={isOpen ? handleKeyDown : undefined}
@ -163,12 +156,8 @@ const Menu: FC<OwnProps> = ({
)} )}
<div <div
role="presentation" role="presentation"
ref={menuRef} ref={bubbleRef}
className={bubbleFullClassName} className={bubbleFullClassName}
style={buildStyle(
`transform-origin: ${transformOriginXStyle || positionX} ${transformOriginYStyle || positionY}`,
bubbleStyle,
)}
onClick={handleClick} onClick={handleClick}
> >
{children} {children}

View File

@ -12,7 +12,6 @@ import renderText from '../common/helpers/renderText';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import { useFastClick } from '../../hooks/useFastClick'; import { useFastClick } from '../../hooks/useFastClick';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useMenuPosition from '../../hooks/useMenuPosition';
import Menu from './Menu'; import Menu from './Menu';
import MenuItem from './MenuItem'; import MenuItem from './MenuItem';
@ -106,7 +105,7 @@ const Tab: FC<OwnProps> = ({
}, [isActive, previousActiveTab]); }, [isActive, previousActiveTab]);
const { const {
contextMenuPosition, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose, contextMenuAnchor, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose,
handleContextMenuHide, isContextMenuOpen, handleContextMenuHide, isContextMenuOpen,
} = useContextMenuHandlers(tabRef, !contextActions); } = useContextMenuHandlers(tabRef, !contextActions);
@ -131,16 +130,6 @@ const Tab: FC<OwnProps> = ({
); );
const getLayout = useLastCallback(() => ({ withPortal: true })); const getLayout = useLastCallback(() => ({ withPortal: true }));
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
getLayout,
);
return ( return (
<div <div
className={buildClassName('Tab', onClick && 'Tab--interactive', className)} className={buildClassName('Tab', onClick && 'Tab--interactive', className)}
@ -158,14 +147,14 @@ const Tab: FC<OwnProps> = ({
<i className="platform" /> <i className="platform" />
</span> </span>
{contextActions && contextMenuPosition !== undefined && ( {contextActions && contextMenuAnchor !== undefined && (
<Menu <Menu
isOpen={isContextMenuOpen} isOpen={isContextMenuOpen}
transformOriginX={transformOriginX} anchor={contextMenuAnchor}
transformOriginY={transformOriginY} getTriggerElement={getTriggerElement}
positionX={positionX} getRootElement={getRootElement}
positionY={positionY} getMenuElement={getMenuElement}
style={menuStyle} getLayout={getLayout}
className="Tab-context-menu" className="Tab-context-menu"
autoClose autoClose
onClose={handleContextMenuClose} onClose={handleContextMenuClose}

View File

@ -6,10 +6,7 @@ import type { IAnchorPosition } from '../types';
import type { Signal } from '../util/signals'; import type { Signal } from '../util/signals';
import { requestMutation } from '../lib/fasterdom/fasterdom'; import { requestMutation } from '../lib/fasterdom/fasterdom';
import { import { IS_IOS, IS_PWA, IS_TOUCH_ENV } from '../util/windowEnvironment';
IS_IOS,
IS_PWA, IS_TOUCH_ENV,
} from '../util/windowEnvironment';
import useLastCallback from './useLastCallback'; import useLastCallback from './useLastCallback';
const LONG_TAP_DURATION_MS = 200; const LONG_TAP_DURATION_MS = 200;
@ -29,7 +26,7 @@ const useContextMenuHandlers = (
getIsReady?: Signal<boolean>, getIsReady?: Signal<boolean>,
) => { ) => {
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState<IAnchorPosition | undefined>(undefined); const [contextMenuAnchor, setContextMenuAnchor] = useState<IAnchorPosition | undefined>(undefined);
const [contextMenuTarget, setContextMenuTarget] = useState<HTMLElement | undefined>(undefined); const [contextMenuTarget, setContextMenuTarget] = useState<HTMLElement | undefined>(undefined);
const handleBeforeContextMenu = useLastCallback((e: React.MouseEvent) => { const handleBeforeContextMenu = useLastCallback((e: React.MouseEvent) => {
@ -51,12 +48,12 @@ const useContextMenuHandlers = (
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (contextMenuPosition) { if (contextMenuAnchor) {
return; return;
} }
setIsContextMenuOpen(true); setIsContextMenuOpen(true);
setContextMenuPosition({ x: e.clientX, y: e.clientY }); setContextMenuAnchor({ x: e.clientX, y: e.clientY });
setContextMenuTarget(e.target as HTMLElement); setContextMenuTarget(e.target as HTMLElement);
}); });
@ -65,7 +62,7 @@ const useContextMenuHandlers = (
}); });
const handleContextMenuHide = useLastCallback(() => { const handleContextMenuHide = useLastCallback(() => {
setContextMenuPosition(undefined); setContextMenuAnchor(undefined);
}); });
// Support context menu on touch devices // Support context menu on touch devices
@ -93,7 +90,7 @@ const useContextMenuHandlers = (
const { clientX, clientY, target } = originalEvent.touches[0]; const { clientX, clientY, target } = originalEvent.touches[0];
if (contextMenuPosition || (shouldDisableOnLink && (target as HTMLElement).matches('a[href]'))) { if (contextMenuAnchor || (shouldDisableOnLink && (target as HTMLElement).matches('a[href]'))) {
return; return;
} }
@ -129,7 +126,7 @@ const useContextMenuHandlers = (
} }
setIsContextMenuOpen(true); setIsContextMenuOpen(true);
setContextMenuPosition({ x: clientX, y: clientY }); setContextMenuAnchor({ x: clientX, y: clientY });
}; };
const startLongPressTimer = (e: TouchEvent) => { const startLongPressTimer = (e: TouchEvent) => {
@ -156,12 +153,12 @@ const useContextMenuHandlers = (
element.removeEventListener('touchmove', clearLongPressTimer); element.removeEventListener('touchmove', clearLongPressTimer);
}; };
}, [ }, [
contextMenuPosition, isMenuDisabled, shouldDisableOnLongTap, elementRef, shouldDisableOnLink, getIsReady, contextMenuAnchor, isMenuDisabled, shouldDisableOnLongTap, elementRef, shouldDisableOnLink, getIsReady,
]); ]);
return { return {
isContextMenuOpen, isContextMenuOpen,
contextMenuPosition, contextMenuAnchor,
contextMenuTarget, contextMenuTarget,
handleBeforeContextMenu, handleBeforeContextMenu,
handleContextMenu, handleContextMenu,

View File

@ -1,8 +1,35 @@
import { useEffect, useState } from '../lib/teact/teact'; import type React from '../lib/teact/teact';
import { useLayoutEffect } from '../lib/teact/teact';
import { addExtraClass, setExtraStyles } from '../lib/teact/teact-dom';
import type { IAnchorPosition } from '../types'; import type { IAnchorPosition } from '../types';
interface Layout { import { requestForcedReflow } from '../lib/fasterdom/fasterdom';
import { useStateRef } from './useStateRef';
interface StaticPositionOptions {
anchor?: IAnchorPosition;
positionX?: 'left' | 'right';
positionY?: 'top' | 'bottom';
transformOriginX?: number;
transformOriginY?: number;
style?: string;
bubbleStyle?: string;
}
interface DynamicPositionOptions {
anchor: IAnchorPosition;
getTriggerElement: () => HTMLElement | null;
getRootElement: () => HTMLElement | null;
getMenuElement: () => HTMLElement | null;
getLayout?: () => Layout;
}
export type MenuPositionOptions =
StaticPositionOptions
| DynamicPositionOptions;
export interface Layout {
extraPaddingX?: number; extraPaddingX?: number;
extraTopPadding?: number; extraTopPadding?: number;
extraMarginTop?: number; extraMarginTop?: number;
@ -21,124 +48,175 @@ const EMPTY_RECT = {
}; };
export default function useMenuPosition( export default function useMenuPosition(
anchor: IAnchorPosition | undefined, isOpen: boolean,
getTriggerElement: () => HTMLElement | null, containerRef: React.RefObject<HTMLDivElement>,
getRootElement: () => HTMLElement | null, bubbleRef: React.RefObject<HTMLDivElement>,
getMenuElement: () => HTMLElement | null, options: MenuPositionOptions,
getLayout?: () => Layout,
) { ) {
const [positionX, setPositionX] = useState<'right' | 'left'>('right'); const optionsRef = useStateRef(options);
const [positionY, setPositionY] = useState<'top' | 'bottom'>('bottom');
const [transformOriginX, setTransformOriginX] = useState<number>();
const [transformOriginY, setTransformOriginY] = useState<number>();
const [withScroll, setWithScroll] = useState(false);
const [style, setStyle] = useState('');
const [menuStyle, setMenuStyle] = useState('opacity: 0;');
useEffect(() => { useLayoutEffect(() => {
const triggerEl = getTriggerElement(); if (!isOpen) return;
if (!anchor || !triggerEl) {
return;
}
let { x, y } = anchor; const options2 = optionsRef.current;
const anchorX = x;
const anchorY = y;
const menuEl = getMenuElement(); if (!('getTriggerElement' in options2)) {
const rootEl = getRootElement(); applyStaticOptions(containerRef, bubbleRef, options2);
const {
extraPaddingX = 0,
extraTopPadding = 0,
extraMarginTop = 0,
topShiftY = 0,
menuElMinWidth = 0,
deltaX = 0,
shouldAvoidNegativePosition = false,
withPortal = false,
isDense = false,
} = getLayout?.() || {};
const marginTop = menuEl ? parseInt(getComputedStyle(menuEl).marginTop, 10) + extraMarginTop : undefined;
const { offsetWidth: menuElWidth, offsetHeight: menuElHeight } = menuEl || { offsetWidth: 0, offsetHeight: 0 };
const menuRect = menuEl ? {
width: Math.max(menuElWidth, menuElMinWidth),
height: menuElHeight + marginTop!,
} : EMPTY_RECT;
const rootRect = rootEl ? rootEl.getBoundingClientRect() : EMPTY_RECT;
let horizontalPosition: 'left' | 'right';
let verticalPosition: 'top' | 'bottom';
if (isDense || (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left)) {
x += 3;
horizontalPosition = 'left';
} else if (x - menuRect.width - rootRect.left > 0) {
horizontalPosition = 'right';
x -= 3;
} else { } else {
horizontalPosition = 'left'; requestForcedReflow(() => {
x = 16; const staticOptions = processDynamically(containerRef, bubbleRef, options2);
return () => {
applyStaticOptions(containerRef, bubbleRef, staticOptions);
};
});
} }
setPositionX(horizontalPosition); }, [isOpen, containerRef, bubbleRef, optionsRef]);
}
x += deltaX; function applyStaticOptions(
containerRef: React.RefObject<HTMLDivElement>,
bubbleRef: React.RefObject<HTMLDivElement>,
{
positionX = 'left',
positionY = 'top',
transformOriginX,
transformOriginY,
style,
bubbleStyle,
}: StaticPositionOptions,
) {
const containerEl = containerRef.current!;
const bubbleEl = bubbleRef.current!;
const yWithTopShift = y + topShiftY; if (style) {
containerEl.style.cssText = style;
}
if (isDense || (yWithTopShift + menuRect.height < rootRect.height + rootRect.top)) { if (bubbleStyle) {
verticalPosition = 'top'; bubbleEl.style.cssText = bubbleStyle;
y = yWithTopShift; }
} else {
verticalPosition = 'bottom';
if (y - menuRect.height < rootRect.top + extraTopPadding) { if (positionX) {
y = rootRect.top + rootRect.height; addExtraClass(bubbleEl, positionX);
} }
if (positionY) {
addExtraClass(bubbleEl, positionY);
}
setExtraStyles(bubbleEl, {
transformOrigin: [
transformOriginX ? `${transformOriginX}px` : positionX,
transformOriginY ? `${transformOriginY}px` : positionY,
].join(' '),
});
}
function processDynamically(
containerRef: React.RefObject<HTMLDivElement>,
bubbleRef: React.RefObject<HTMLDivElement>,
{
anchor,
getRootElement,
getMenuElement,
getTriggerElement,
getLayout,
}: DynamicPositionOptions,
) {
const triggerEl = getTriggerElement()!;
let { x, y } = anchor;
const anchorX = x;
const anchorY = y;
const menuEl = getMenuElement();
const rootEl = getRootElement();
const {
extraPaddingX = 0,
extraTopPadding = 0,
extraMarginTop = 0,
topShiftY = 0,
menuElMinWidth = 0,
deltaX = 0,
shouldAvoidNegativePosition = false,
withPortal = false,
isDense = false,
} = getLayout?.() || {};
const marginTop = menuEl ? parseInt(getComputedStyle(menuEl).marginTop, 10) + extraMarginTop : undefined;
const { offsetWidth: menuElWidth, offsetHeight: menuElHeight } = menuEl || { offsetWidth: 0, offsetHeight: 0 };
const menuRect = menuEl ? {
width: Math.max(menuElWidth, menuElMinWidth),
height: menuElHeight + marginTop!,
} : EMPTY_RECT;
const rootRect = rootEl ? rootEl.getBoundingClientRect() : EMPTY_RECT;
let positionX: 'left' | 'right';
let positionY: 'top' | 'bottom';
if (isDense || (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left)) {
x += 3;
positionX = 'left';
} else if (x - menuRect.width - rootRect.left > 0) {
positionX = 'right';
x -= 3;
} else {
positionX = 'left';
x = 16;
}
x += deltaX;
const yWithTopShift = y + topShiftY;
if (isDense || (yWithTopShift + menuRect.height < rootRect.height + rootRect.top)) {
positionY = 'top';
y = yWithTopShift;
} else {
positionY = 'bottom';
if (y - menuRect.height < rootRect.top + extraTopPadding) {
y = rootRect.top + rootRect.height;
} }
}
setPositionY(verticalPosition); const triggerRect = triggerEl.getBoundingClientRect();
const triggerRect = triggerEl.getBoundingClientRect(); const addedYForPortalPositioning = (withPortal ? triggerRect.top : 0);
const addedXForPortalPositioning = (withPortal ? triggerRect.left : 0);
const addedYForPortalPositioning = (withPortal ? triggerRect.top : 0); const leftWithPossibleNegative = Math.min(
const addedXForPortalPositioning = (withPortal ? triggerRect.left : 0); x - triggerRect.left,
rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX,
);
let left = (positionX === 'left'
? (withPortal || shouldAvoidNegativePosition
? Math.max(MENU_POSITION_VISUAL_COMFORT_SPACE_PX, leftWithPossibleNegative)
: leftWithPossibleNegative)
: (x - triggerRect.left)) + addedXForPortalPositioning;
let top = y - triggerRect.top + addedYForPortalPositioning;
const leftWithPossibleNegative = Math.min( if (isDense) {
x - triggerRect.left, left = Math.min(left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX);
rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX, top = Math.min(top, rootRect.height - menuRect.height - MENU_POSITION_VISUAL_COMFORT_SPACE_PX);
); }
let left = (horizontalPosition === 'left'
? (withPortal || shouldAvoidNegativePosition
? Math.max(MENU_POSITION_VISUAL_COMFORT_SPACE_PX, leftWithPossibleNegative)
: leftWithPossibleNegative)
: (x - triggerRect.left)) + addedXForPortalPositioning;
let top = y - triggerRect.top + addedYForPortalPositioning;
if (isDense) { // Avoid hiding external parts of menus on mobile devices behind the edges of the screen (ReactionSelector for example)
left = Math.min(left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX); const addedXForMenuPositioning = menuElMinWidth ? Math.max(0, (menuElMinWidth - menuElWidth) / 2) : 0;
top = Math.min(top, rootRect.height - menuRect.height - MENU_POSITION_VISUAL_COMFORT_SPACE_PX); if (left - addedXForMenuPositioning < 0 && shouldAvoidNegativePosition) {
} left = addedXForMenuPositioning + MENU_POSITION_VISUAL_COMFORT_SPACE_PX;
}
// Avoid hiding external parts of menus on mobile devices behind the edges of the screen (ReactionSelector for example) const offsetX = (anchorX + addedXForPortalPositioning - triggerRect.left) - left;
const addedXForMenuPositioning = menuElMinWidth ? Math.max(0, (menuElMinWidth - menuElWidth) / 2) : 0; const offsetY = (anchorY + addedYForPortalPositioning - triggerRect.top) - top - (marginTop || 0);
if (left - addedXForMenuPositioning < 0 && shouldAvoidNegativePosition) { const transformOriginX = positionX === 'left' ? offsetX : menuRect.width + offsetX;
left = addedXForMenuPositioning + MENU_POSITION_VISUAL_COMFORT_SPACE_PX; const transformOriginY = positionY === 'bottom' ? menuRect.height + offsetY : offsetY;
}
const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN - (marginTop || 0); const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN - (marginTop || 0);
const bubbleStyle = `max-height: ${menuMaxHeight}px;`;
setWithScroll(menuMaxHeight < menuRect.height); const style = `left: ${left}px; top: ${top}px`;
setMenuStyle(`max-height: ${menuMaxHeight}px;`);
setStyle(`left: ${left}px; top: ${top}px`);
const offsetX = (anchorX + addedXForPortalPositioning - triggerRect.left) - left;
const offsetY = (anchorY + addedYForPortalPositioning - triggerRect.top) - top - (marginTop || 0);
setTransformOriginX(horizontalPosition === 'left' ? offsetX : menuRect.width + offsetX);
setTransformOriginY(verticalPosition === 'bottom' ? menuRect.height + offsetY : offsetY);
}, [
anchor, getMenuElement, getRootElement, getTriggerElement, getLayout,
]);
return { return {
positionX, positionX,
@ -146,7 +224,6 @@ export default function useMenuPosition(
transformOriginX, transformOriginX,
transformOriginY, transformOriginY,
style, style,
menuStyle, bubbleStyle,
withScroll,
}; };
} }

View File

@ -7,7 +7,7 @@ const BACKDROP_CLASSNAME = 'backdrop';
// without adding extra elements to the DOM // without adding extra elements to the DOM
export default function useVirtualBackdrop( export default function useVirtualBackdrop(
isOpen: boolean, isOpen: boolean,
menuRef: RefObject<HTMLElement>, containerRef: RefObject<HTMLElement>,
onClose?: () => void | undefined, onClose?: () => void | undefined,
ignoreRightClick?: boolean, ignoreRightClick?: boolean,
excludedClosestSelector?: string, excludedClosestSelector?: string,
@ -18,14 +18,14 @@ export default function useVirtualBackdrop(
} }
const handleEvent = (e: MouseEvent) => { const handleEvent = (e: MouseEvent) => {
const menu = menuRef.current; const container = containerRef.current;
const target = e.target as HTMLElement | null; const target = e.target as HTMLElement | null;
if (!menu || !target || (ignoreRightClick && e.button === 2)) { if (!container || !target || (ignoreRightClick && e.button === 2)) {
return; return;
} }
if (( if ((
!menu.contains(e.target as Node | null) !container.contains(e.target as Node | null)
|| target.classList.contains(BACKDROP_CLASSNAME) || target.classList.contains(BACKDROP_CLASSNAME)
) && !(excludedClosestSelector && ( ) && !(excludedClosestSelector && (
target.matches(excludedClosestSelector) || target.closest(excludedClosestSelector) target.matches(excludedClosestSelector) || target.closest(excludedClosestSelector)
@ -41,5 +41,5 @@ export default function useVirtualBackdrop(
return () => { return () => {
document.removeEventListener('mousedown', handleEvent); document.removeEventListener('mousedown', handleEvent);
}; };
}, [excludedClosestSelector, ignoreRightClick, isOpen, menuRef, onClose]); }, [excludedClosestSelector, ignoreRightClick, isOpen, containerRef, onClose]);
} }