[Perf] [Refactoring] Move useMenuPosition into Menu component
This commit is contained in:
parent
8aa7eb2fcb
commit
03de021b82
@ -15,7 +15,6 @@ import renderText from '../../common/helpers/renderText';
|
||||
import formatGroupCallVolume from './helpers/formatGroupCallVolume';
|
||||
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
@ -52,7 +51,7 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
contextMenuPosition,
|
||||
contextMenuAnchor,
|
||||
handleContextMenu,
|
||||
handleBeforeContextMenu,
|
||||
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(
|
||||
!isMuted && isSpeaking && participant.volume && participant.volume !== GROUP_CALL_DEFAULT_VOLUME,
|
||||
);
|
||||
@ -147,11 +136,11 @@ const GroupCallParticipant: FC<OwnProps & StateProps> = ({
|
||||
<GroupCallParticipantMenu
|
||||
participant={participant}
|
||||
isDropdownOpen={isContextMenuOpen}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
menuRef={menuRef}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useState } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
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 { selectIsAdminInActiveGroupCall } from '../../../global/selectors/calls';
|
||||
@ -26,18 +25,15 @@ import './GroupCallParticipantMenu.scss';
|
||||
const SPEAKER_ICON_DISABLED_SEGMENT: [number, number] = [0, 17];
|
||||
const SPEAKER_ICON_ENABLED_SEGMENT: [number, number] = [17, 34];
|
||||
|
||||
type OwnProps = {
|
||||
participant?: GroupCallParticipant;
|
||||
onCloseAnimationEnd: VoidFunction;
|
||||
onClose: VoidFunction;
|
||||
isDropdownOpen: boolean;
|
||||
positionX?: 'left' | 'right';
|
||||
positionY?: 'top' | 'bottom';
|
||||
transformOriginX?: number;
|
||||
transformOriginY?: number;
|
||||
style?: string;
|
||||
menuRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
type OwnProps =
|
||||
{
|
||||
participant?: GroupCallParticipant;
|
||||
onCloseAnimationEnd: VoidFunction;
|
||||
onClose: VoidFunction;
|
||||
isDropdownOpen: boolean;
|
||||
menuRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
& MenuPositionOptions;
|
||||
|
||||
type StateProps = {
|
||||
isAdmin: boolean;
|
||||
@ -58,12 +54,8 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
||||
onClose,
|
||||
isDropdownOpen,
|
||||
isAdmin,
|
||||
positionY,
|
||||
menuRef,
|
||||
positionX,
|
||||
style,
|
||||
transformOriginY,
|
||||
transformOriginX,
|
||||
...menuPositionOptions
|
||||
}) => {
|
||||
const {
|
||||
toggleGroupCallMute,
|
||||
@ -175,16 +167,13 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
||||
<div>
|
||||
<Menu
|
||||
isOpen={isDropdownOpen}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
style={style}
|
||||
ref={menuRef}
|
||||
withPortal
|
||||
onClose={onClose}
|
||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||
className="participant-menu with-menu-transitions"
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...menuPositionOptions}
|
||||
>
|
||||
{!isSelf && !shouldRaiseHand && (
|
||||
<div className="group">
|
||||
|
||||
@ -22,7 +22,6 @@ import formatGroupCallVolume from './helpers/formatGroupCallVolume';
|
||||
import useInterval from '../../../hooks/schedulers/useInterval';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import FullNameTitle from '../../common/FullNameTitle';
|
||||
@ -209,7 +208,7 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
contextMenuPosition,
|
||||
contextMenuAnchor,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
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(() => {
|
||||
setPinned(!isPinned ? {
|
||||
id: user?.id || chat!.id,
|
||||
@ -318,11 +307,11 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
|
||||
<GroupCallParticipantMenu
|
||||
participant={participant}
|
||||
isDropdownOpen={isContextMenuOpen}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
menuRef={menuRef}
|
||||
|
||||
@ -29,8 +29,7 @@ import type {
|
||||
ApiWebPage,
|
||||
} from '../../api/types';
|
||||
import type {
|
||||
ApiDraft, GlobalState, MessageList,
|
||||
MessageListType, TabState,
|
||||
ApiDraft, GlobalState, MessageList, MessageListType, TabState,
|
||||
} from '../../global/types';
|
||||
import type {
|
||||
IAnchorPosition, InlineBotSettings, ISettings, ThreadId,
|
||||
@ -830,7 +829,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
} = useContextMenuHandlers(mainButtonRef, !(mainButtonState === MainButtonState.Send && canShowCustomSendMenu));
|
||||
|
||||
const {
|
||||
contextMenuPosition: storyReactionPickerPosition,
|
||||
contextMenuAnchor: storyReactionPickerAnchor,
|
||||
handleContextMenu: handleStoryPickerContextMenu,
|
||||
handleBeforeContextMenu: handleBeforeStoryPickerContextMenu,
|
||||
handleContextMenuHide: handleStoryPickerContextMenuHide,
|
||||
@ -839,15 +838,15 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
useEffect(() => {
|
||||
if (isReactionPickerOpen) return;
|
||||
|
||||
if (storyReactionPickerPosition) {
|
||||
if (storyReactionPickerAnchor) {
|
||||
openStoryReactionPicker({
|
||||
peerId: chatId,
|
||||
storyId: storyId!,
|
||||
position: storyReactionPickerPosition,
|
||||
position: storyReactionPickerAnchor,
|
||||
});
|
||||
handleStoryPickerContextMenuHide();
|
||||
}
|
||||
}, [chatId, handleStoryPickerContextMenuHide, isReactionPickerOpen, storyId, storyReactionPickerPosition]);
|
||||
}, [chatId, handleStoryPickerContextMenuHide, isReactionPickerOpen, storyId, storyReactionPickerAnchor]);
|
||||
|
||||
useClipboardPaste(
|
||||
isForCurrentMessageList || isInStoryViewer,
|
||||
|
||||
@ -17,7 +17,6 @@ import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
@ -69,7 +68,7 @@ const GifButton: FC<OwnProps> = ({
|
||||
const isVideoReady = loadAndPlay && isBuffered;
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref);
|
||||
@ -78,15 +77,6 @@ const GifButton: FC<OwnProps> = ({
|
||||
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll, .no-scrollbar'));
|
||||
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(() => {
|
||||
if (isContextMenuOpen || !onClick) return;
|
||||
onClick({
|
||||
@ -185,14 +175,13 @@ const GifButton: FC<OwnProps> = ({
|
||||
{shouldRenderSpinner && (
|
||||
<Spinner color={previewBlobUrl || withThumb ? 'white' : 'black'} />
|
||||
)}
|
||||
{onClick && contextMenuPosition !== undefined && (
|
||||
{onClick && contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
className="gif-context-menu"
|
||||
autoClose
|
||||
onClose={handleContextMenuClose}
|
||||
|
||||
@ -16,7 +16,6 @@ import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListene
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
@ -118,29 +117,18 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
const isIntesectingForShowing = useIsIntersecting(ref, observeIntersectionForShowing);
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref);
|
||||
const shouldRenderContextMenu = Boolean(!noContextMenu && contextMenuPosition);
|
||||
const shouldRenderContextMenu = Boolean(!noContextMenu && contextMenuAnchor);
|
||||
|
||||
const getTriggerElement = useLastCallback(() => ref.current);
|
||||
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll, .no-scrollbar'));
|
||||
const getMenuElement = useLastCallback(() => {
|
||||
return isStatusPicker ? menuRef.current : ref.current!.querySelector('.sticker-context-menu .bubble');
|
||||
});
|
||||
|
||||
const getLayout = () => ({ withPortal: isStatusPicker, shouldAvoidNegativePosition: true });
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
getLayout,
|
||||
);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: isStatusPicker, shouldAvoidNegativePosition: true }));
|
||||
|
||||
useEffect(() => {
|
||||
if (isContextMenuOpen) {
|
||||
@ -345,11 +333,11 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className="sticker-context-menu"
|
||||
autoClose
|
||||
withPortal={isStatusPicker}
|
||||
|
||||
@ -12,9 +12,7 @@ import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { FocusDirection, ThreadId } from '../../types';
|
||||
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
|
||||
|
||||
import {
|
||||
getChatTitle, getMessageHtmlId, isJoinedChannelMessage,
|
||||
} from '../../global/helpers';
|
||||
import { getChatTitle, getMessageHtmlId, isJoinedChannelMessage } from '../../global/helpers';
|
||||
import { getMessageReplyInfo } from '../../global/helpers/replies';
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
@ -191,11 +189,11 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
]);
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref);
|
||||
const isContextMenuShown = contextMenuPosition !== undefined;
|
||||
const isContextMenuShown = contextMenuAnchor !== undefined;
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
preventMessageInputBlur(e);
|
||||
@ -372,10 +370,10 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
<ActionMessageSuggestedAvatar message={message} renderContent={renderContent} />
|
||||
)}
|
||||
{isJoinedMessage && <SimilarChannels chatId={targetChatId!} />}
|
||||
{contextMenuPosition && (
|
||||
{contextMenuAnchor && (
|
||||
<ContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuPosition}
|
||||
anchor={contextMenuAnchor}
|
||||
message={message}
|
||||
messageListType="thread"
|
||||
onClose={handleContextMenuClose}
|
||||
|
||||
@ -142,12 +142,12 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const lang = useOldLang();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [menuPosition, setMenuPosition] = useState<IAnchorPosition | undefined>(undefined);
|
||||
const [menuAnchor, setMenuAnchor] = useState<IAnchorPosition | undefined>(undefined);
|
||||
|
||||
const handleHeaderMenuOpen = useLastCallback(() => {
|
||||
setIsMenuOpen(true);
|
||||
const rect = menuButtonRef.current!.getBoundingClientRect();
|
||||
setMenuPosition({ x: rect.right, y: rect.bottom });
|
||||
setMenuAnchor({ x: rect.right, y: rect.bottom });
|
||||
});
|
||||
|
||||
const handleHeaderMenuClose = useLastCallback(() => {
|
||||
@ -155,7 +155,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleHeaderMenuHide = useLastCallback(() => {
|
||||
setMenuPosition(undefined);
|
||||
setMenuAnchor(undefined);
|
||||
});
|
||||
|
||||
const handleSubscribeClick = useLastCallback(() => {
|
||||
@ -420,12 +420,12 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<i className="icon icon-more" aria-hidden />
|
||||
</Button>
|
||||
{menuPosition && (
|
||||
{menuAnchor && (
|
||||
<HeaderMenuContainer
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
isOpen={isMenuOpen}
|
||||
anchor={menuPosition}
|
||||
anchor={menuAnchor}
|
||||
withExtraActions={isMobile || !canExpandActions}
|
||||
isChannel={isChannel}
|
||||
canStartBot={canStartBot}
|
||||
|
||||
@ -34,7 +34,6 @@ import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated';
|
||||
|
||||
@ -160,7 +159,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
||||
useEffect(() => (isShown ? captureEscKeyListener(clearEmbedded) : undefined), [isShown, clearEmbedded]);
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition, handleContextMenu,
|
||||
isContextMenuOpen, contextMenuAnchor, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref);
|
||||
|
||||
@ -199,15 +198,6 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
||||
const getRootElement = useLastCallback(() => ref.current!);
|
||||
const getMenuElement = useLastCallback(() => ref.current!.querySelector('.forward-context-menu .bubble'));
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldRender) {
|
||||
handleContextMenuClose();
|
||||
@ -296,11 +286,10 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
||||
{(isShowingReply || isForwarding) && !isContextMenuDisabled && (
|
||||
<Menu
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
className="forward-context-menu"
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
|
||||
@ -7,6 +7,7 @@ import { withGlobal } from '../../../global';
|
||||
import type { ApiSticker, ApiVideo } from '../../../api/types';
|
||||
import type { GlobalActions } from '../../../global';
|
||||
import type { ThreadId } from '../../../types';
|
||||
import type { MenuPositionOptions } from '../../ui/Menu';
|
||||
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { selectIsContextMenuTranslucent, selectTabState } from '../../../global/selectors';
|
||||
@ -61,12 +62,8 @@ export type OwnProps = {
|
||||
className?: string;
|
||||
isAttachmentModal?: boolean;
|
||||
canSendPlainText?: boolean;
|
||||
positionX?: 'left' | 'right';
|
||||
positionY?: 'top' | 'bottom';
|
||||
transformOriginX?: number;
|
||||
transformOriginY?: number;
|
||||
style?: string;
|
||||
};
|
||||
}
|
||||
& MenuPositionOptions;
|
||||
|
||||
type StateProps = {
|
||||
isLeftColumnShown: boolean;
|
||||
@ -87,11 +84,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
isAttachmentModal,
|
||||
canSendPlainText,
|
||||
className,
|
||||
positionX,
|
||||
positionY,
|
||||
transformOriginX,
|
||||
transformOriginY,
|
||||
style,
|
||||
isBackgroundTranslucent,
|
||||
onLoad,
|
||||
onClose,
|
||||
@ -103,6 +95,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
onSearchOpen,
|
||||
addRecentEmoji,
|
||||
addRecentCustomEmoji,
|
||||
...menuPositionOptions
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<number>(0);
|
||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||
@ -325,8 +318,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<UnfreezableMenu
|
||||
isOpen={isOpen}
|
||||
positionX={isAttachmentModal ? positionX : 'left'}
|
||||
positionY={isAttachmentModal ? positionY : 'bottom'}
|
||||
onClose={onClose}
|
||||
withPortal={isAttachmentModal}
|
||||
className={buildClassName('SymbolMenu', className)}
|
||||
@ -335,9 +326,11 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
|
||||
noCloseOnBackdrop={!IS_TOUCH_ENV}
|
||||
noCompact
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
style={style}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...(isAttachmentModal ? menuPositionOptions : {
|
||||
positionX: 'left',
|
||||
positionY: 'bottom',
|
||||
})}
|
||||
>
|
||||
{content}
|
||||
</UnfreezableMenu>
|
||||
|
||||
@ -10,7 +10,6 @@ import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
|
||||
@ -89,7 +88,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isSymbolMenuLoaded, onSymbolMenuLoadingComplete] = useFlag();
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<IAnchorPosition | undefined>(undefined);
|
||||
const [contextMenuAnchor, setContextMenuAnchor] = useState<IAnchorPosition | undefined>(undefined);
|
||||
|
||||
const symbolMenuButtonClassName = buildClassName(
|
||||
'mobile-symbol-menu-button',
|
||||
@ -106,7 +105,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
||||
const triggerEl = triggerRef.current;
|
||||
if (!triggerEl) return;
|
||||
const { x, y } = triggerEl.getBoundingClientRect();
|
||||
setContextMenuPosition({ x, y });
|
||||
setContextMenuAnchor({ x, y });
|
||||
});
|
||||
|
||||
const handleSearchOpen = useLastCallback((type: 'stickers' | 'gifs') => {
|
||||
@ -141,16 +140,6 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
||||
const getMenuElement = useLastCallback(() => document.querySelector('#portals .SymbolMenu .bubble'));
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
getLayout,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMobile ? (
|
||||
@ -199,11 +188,11 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
||||
isAttachmentModal={isAttachmentModal}
|
||||
canSendPlainText={canSendPlainText}
|
||||
className={buildClassName(className, forceDarkTheme && 'component-theme-dark')}
|
||||
positionX={isAttachmentModal ? positionX : undefined}
|
||||
positionY={isAttachmentModal ? positionY : undefined}
|
||||
transformOriginX={isAttachmentModal ? transformOriginX : undefined}
|
||||
transformOriginY={isAttachmentModal ? transformOriginY : undefined}
|
||||
style={isAttachmentModal ? menuStyle : undefined}
|
||||
anchor={isAttachmentModal ? contextMenuAnchor : undefined}
|
||||
getTriggerElement={isAttachmentModal ? getTriggerElement : undefined}
|
||||
getRootElement={isAttachmentModal ? getRootElement : undefined}
|
||||
getMenuElement={isAttachmentModal ? getMenuElement : undefined}
|
||||
getLayout={isAttachmentModal ? getLayout : undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
@ -23,7 +21,6 @@ import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useDerivedSignal from '../../../hooks/useDerivedSignal';
|
||||
import useDerivedState from '../../../hooks/useDerivedState';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
@ -122,7 +119,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition, handleContextMenu,
|
||||
isContextMenuOpen, contextMenuAnchor, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, isEditing, true);
|
||||
|
||||
@ -132,15 +129,6 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
||||
() => 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 => {
|
||||
handleContextMenu(e);
|
||||
});
|
||||
@ -172,11 +160,10 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<Menu
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
className="web-page-preview-context-menu"
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
@ -185,7 +172,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
||||
<>
|
||||
{
|
||||
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)}>
|
||||
{lang('PreviewSender.MoveTextUp')}
|
||||
</MenuItem>
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
@ -20,15 +25,10 @@ import type {
|
||||
ApiTypeStory,
|
||||
ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type {
|
||||
ActiveEmojiInteraction,
|
||||
ChatTranslatedMessages,
|
||||
MessageListType,
|
||||
} from '../../../global/types';
|
||||
import type { ActiveEmojiInteraction, ChatTranslatedMessages, MessageListType } from '../../../global/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type {
|
||||
FocusDirection, IAlbum, ISettings, ScrollTargetPosition,
|
||||
ThreadId,
|
||||
FocusDirection, IAlbum, ISettings, ScrollTargetPosition, ThreadId,
|
||||
} from '../../../types';
|
||||
import type { Signal } from '../../../util/signals';
|
||||
import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage';
|
||||
@ -66,7 +66,8 @@ import {
|
||||
selectActiveDownloads,
|
||||
selectAnimatedEmoji,
|
||||
selectCanAutoLoadMedia,
|
||||
selectCanAutoPlayMedia, selectCanReplyToMessage,
|
||||
selectCanAutoPlayMedia,
|
||||
selectCanReplyToMessage,
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
selectChatMessage,
|
||||
@ -443,7 +444,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
contextMenuPosition,
|
||||
contextMenuAnchor,
|
||||
contextMenuTarget,
|
||||
handleBeforeContextMenu,
|
||||
handleContextMenu: onContextMenu,
|
||||
@ -535,7 +536,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
) && !album.messages.some((msg) => Object.keys(msg.content).length === 0);
|
||||
const isInDocumentGroupNotFirst = isInDocumentGroup && !isFirstInDocumentGroup;
|
||||
const isInDocumentGroupNotLast = isInDocumentGroup && !isLastInDocumentGroup;
|
||||
const isContextMenuShown = contextMenuPosition !== undefined;
|
||||
const isContextMenuShown = contextMenuAnchor !== undefined;
|
||||
const canShowActionButton = (
|
||||
!(isContextMenuShown || isInSelectMode || isForwarding)
|
||||
&& !isInDocumentGroupNotLast
|
||||
@ -1650,10 +1651,10 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{contextMenuPosition && (
|
||||
{contextMenuAnchor && (
|
||||
<ContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuPosition}
|
||||
anchor={contextMenuAnchor}
|
||||
targetHref={contextMenuTarget?.matches('a[href]') ? (contextMenuTarget as HTMLAnchorElement).href : undefined}
|
||||
message={message}
|
||||
album={album}
|
||||
|
||||
@ -28,7 +28,6 @@ import { getMessageCopyOptions } from './helpers/copyOptions';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
@ -323,15 +322,11 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
}, ANIMATION_DURATION);
|
||||
}, [isOpen, markIsReady, unmarkIsReady]);
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style, menuStyle, withScroll,
|
||||
} = useMenuPosition(anchor, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
||||
|
||||
useEffect(() => {
|
||||
disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionPicker');
|
||||
disableScrolling(scrollableRef.current, '.ReactionPicker');
|
||||
|
||||
return enableScrolling;
|
||||
}, [withScroll]);
|
||||
}, [isOpen]);
|
||||
|
||||
const handleOpenMessageReactionPicker = useLastCallback((position: IAnchorPosition) => {
|
||||
onReactionPickerOpen!(position);
|
||||
@ -342,12 +337,11 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={style}
|
||||
bubbleStyle={menuStyle}
|
||||
anchor={anchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className={buildClassName(
|
||||
'MessageContextMenu', 'fluid', withReactions && 'with-reactions',
|
||||
)}
|
||||
@ -376,13 +370,12 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={scrollableRef}
|
||||
className={buildClassName(
|
||||
'MessageContextMenu_items scrollable-content custom-scroll',
|
||||
areItemsHidden && 'MessageContextMenu_items-hidden',
|
||||
)}
|
||||
style={menuStyle}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
ref={scrollableRef}
|
||||
>
|
||||
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>}
|
||||
{canReschedule && (
|
||||
|
||||
@ -65,7 +65,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
threshold: 1,
|
||||
});
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, undefined, true, IS_ANDROID);
|
||||
@ -183,10 +183,10 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{contextMenuPosition && (
|
||||
{contextMenuAnchor && (
|
||||
<SponsoredMessageContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuPosition}
|
||||
anchor={contextMenuAnchor}
|
||||
message={message!}
|
||||
onAboutAdsClick={openAboutAdsModal}
|
||||
onReportAd={handleReportSponsoredMessage}
|
||||
|
||||
@ -5,15 +5,25 @@ import { getActions, getGlobal, withGlobal } from '../../../../global';
|
||||
import type { IAnchorPosition } from '../../../../types';
|
||||
import {
|
||||
type ApiAvailableEffect,
|
||||
type ApiMessage, type ApiMessageEntity,
|
||||
type ApiReaction, type ApiReactionCustomEmoji, type ApiSticker, type ApiStory, type ApiStorySkipped,
|
||||
type ApiMessage,
|
||||
type ApiMessageEntity,
|
||||
type ApiReaction,
|
||||
type ApiReactionCustomEmoji,
|
||||
type ApiSticker,
|
||||
type ApiStory,
|
||||
type ApiStorySkipped,
|
||||
MAIN_THREAD_ID,
|
||||
} from '../../../../api/types';
|
||||
|
||||
import { getReactionKey, getStoryKey, isUserId } from '../../../../global/helpers';
|
||||
import {
|
||||
selectChat, selectChatFullInfo, selectChatMessage, selectIsContextMenuTranslucent, selectIsCurrentUserPremium,
|
||||
selectPeerStory, selectTabState,
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
selectChatMessage,
|
||||
selectIsContextMenuTranslucent,
|
||||
selectIsCurrentUserPremium,
|
||||
selectPeerStory,
|
||||
selectTabState,
|
||||
} from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
|
||||
@ -23,7 +33,6 @@ import { buildCustomEmojiHtml } from '../../composer/helpers/customEmoji';
|
||||
import { getIsMobile } from '../../../../hooks/useAppLayout';
|
||||
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../../../hooks/useOldLang';
|
||||
|
||||
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
|
||||
: 0,
|
||||
}));
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style,
|
||||
} = useMenuPosition(renderingPosition, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
||||
|
||||
const handleToggleCustomReaction = useLastCallback((sticker: ApiSticker) => {
|
||||
if (!renderedChatId || !renderedMessageId) {
|
||||
@ -213,11 +219,11 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
withPortal
|
||||
noCompact
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
style={style}
|
||||
anchor={renderingPosition}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
backdropExcludedSelector=".Modal.confirm"
|
||||
onClose={closeReactionPicker}
|
||||
>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import React, { memo, useRef } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiReaction, ApiSavedReactionTag,
|
||||
} from '../../../../api/types';
|
||||
import type { ApiReaction, ApiSavedReactionTag } from '../../../../api/types';
|
||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
@ -12,7 +10,6 @@ import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers';
|
||||
import useFlag from '../../../../hooks/useFlag';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../../../hooks/useOldLang';
|
||||
|
||||
import ReactionAnimatedEmoji from '../../../common/reactions/ReactionAnimatedEmoji';
|
||||
@ -88,7 +85,7 @@ const SavedTagButton = ({
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
contextMenuPosition,
|
||||
contextMenuAnchor,
|
||||
handleBeforeContextMenu,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
@ -98,18 +95,7 @@ const SavedTagButton = ({
|
||||
const getTriggerElement = useLastCallback(() => ref.current);
|
||||
const getRootElement = useLastCallback(() => document.body);
|
||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||
|
||||
const getLayout = () => ({ withPortal: true, shouldAvoidNegativePosition: true });
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
getLayout,
|
||||
);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true, shouldAvoidNegativePosition: true }));
|
||||
|
||||
if (withCount && count === 0) {
|
||||
return undefined;
|
||||
@ -166,15 +152,15 @@ const SavedTagButton = ({
|
||||
onSubmit={handleRenameTag}
|
||||
/>
|
||||
)}
|
||||
{withContextMenu && contextMenuPosition && (
|
||||
{withContextMenu && contextMenuAnchor && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
autoClose
|
||||
withPortal
|
||||
onClose={handleContextMenuClose}
|
||||
|
||||
@ -15,7 +15,6 @@ import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMe
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Icon from '../common/icons/Icon';
|
||||
@ -73,19 +72,10 @@ function MediaStory({
|
||||
}, [isDeleted, isFullyLoaded, story]);
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(containerRef, !isOwn);
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
getLayout,
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
openStoryViewer({
|
||||
@ -156,14 +146,14 @@ function MediaStory({
|
||||
{isFullyLoaded && <MediaAreaOverlay story={story} />}
|
||||
{isProtected && <span className="protector" />}
|
||||
</div>
|
||||
{contextMenuPosition !== undefined && (
|
||||
{contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className={buildClassName(styles.contextMenu, 'story-context-menu')}
|
||||
autoClose
|
||||
onClose={handleContextMenuClose}
|
||||
|
||||
@ -10,7 +10,6 @@ import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMe
|
||||
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useStoryPreloader from './hooks/useStoryPreloader';
|
||||
|
||||
@ -43,7 +42,7 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
|
||||
useStoryPreloader(peer.id);
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref);
|
||||
@ -53,16 +52,6 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
|
||||
const getMenuElement = useLastCallback(() => ref.current!.querySelector('.story-peer-context-menu .bubble'));
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true }));
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
getLayout,
|
||||
);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (isContextMenuOpen) return;
|
||||
|
||||
@ -114,14 +103,14 @@ function StoryRibbonButton({ peer, isArchived }: OwnProps) {
|
||||
<div className={buildClassName(styles.name, peer.hasUnreadStories && styles.name_hasUnreadStory)}>
|
||||
{isSelf ? lang('MyStory') : getSenderTitle(lang, peer)}
|
||||
</div>
|
||||
{contextMenuPosition !== undefined && (
|
||||
{contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className={buildClassName('story-peer-context-menu', styles.contextMenu)}
|
||||
autoClose
|
||||
withPortal
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useCallback, useMemo,
|
||||
useRef, useState,
|
||||
useCallback, useMemo, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import Button from './Button';
|
||||
@ -44,12 +43,11 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-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 toggleIsOpen = () => {
|
||||
setIsOpen(!isOpen);
|
||||
|
||||
if (isOpen) {
|
||||
onClose?.();
|
||||
} else {
|
||||
@ -96,7 +94,6 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className={`DropdownMenu ${className || ''}`}
|
||||
onKeyDown={handleKeyDown}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
@ -105,7 +102,6 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
containerRef={dropdownRef}
|
||||
isOpen={isOpen || Boolean(forceOpen)}
|
||||
className={className || ''}
|
||||
transformOriginX={transformOriginX}
|
||||
|
||||
@ -13,7 +13,6 @@ import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import { useFastClick } from '../../hooks/useFastClick';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Icon from '../common/icons/Icon';
|
||||
@ -120,7 +119,7 @@ const ListItem: FC<OwnProps> = ({
|
||||
const [isTouched, markIsTouched, unmarkIsTouched] = useFlag();
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(containerRef, !contextActions);
|
||||
@ -133,16 +132,6 @@ const ListItem: FC<OwnProps> = ({
|
||||
});
|
||||
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 hasModifierKey = e.ctrlKey || e.metaKey || e.shiftKey;
|
||||
if (!hasModifierKey && e.button === MouseButton.Main) {
|
||||
@ -215,7 +204,7 @@ const ListItem: FC<OwnProps> = ({
|
||||
disabled && 'disabled',
|
||||
allowDisabledClick && 'click-allowed',
|
||||
inactive && 'inactive',
|
||||
contextMenuPosition && 'has-menu-open',
|
||||
contextMenuAnchor && 'has-menu-open',
|
||||
focus && 'focus',
|
||||
destructive && 'destructive',
|
||||
multiline && 'multiline',
|
||||
@ -267,14 +256,14 @@ const ListItem: FC<OwnProps> = ({
|
||||
)}
|
||||
{rightElement}
|
||||
</ButtonElementTag>
|
||||
{contextActions && contextMenuPosition !== undefined && (
|
||||
{contextActions && contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className="ListItem-context-menu with-menu-transitions"
|
||||
autoClose
|
||||
onClose={handleContextMenuClose}
|
||||
|
||||
@ -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 buildStyle from '../../util/buildStyle';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import freezeWhenClosed from '../../util/hoc/freezeWhenClosed';
|
||||
import { IS_BACKDROP_BLUR_SUPPORTED } from '../../util/windowEnvironment';
|
||||
@ -13,6 +16,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
import useVirtualBackdrop from '../../hooks/useVirtualBackdrop';
|
||||
|
||||
@ -20,54 +24,44 @@ import Portal from './Portal';
|
||||
|
||||
import './Menu.scss';
|
||||
|
||||
type OwnProps = {
|
||||
ref?: React.RefObject<HTMLDivElement>;
|
||||
containerRef?: React.RefObject<HTMLElement>;
|
||||
isOpen: boolean;
|
||||
shouldCloseFast?: boolean;
|
||||
id?: string;
|
||||
className?: string;
|
||||
bubbleClassName?: string;
|
||||
style?: string;
|
||||
bubbleStyle?: string;
|
||||
ariaLabelledBy?: string;
|
||||
transformOriginX?: number;
|
||||
transformOriginY?: number;
|
||||
positionX?: 'left' | 'right';
|
||||
positionY?: 'top' | 'bottom';
|
||||
autoClose?: boolean;
|
||||
footer?: string;
|
||||
noCloseOnBackdrop?: boolean;
|
||||
backdropExcludedSelector?: string;
|
||||
noCompact?: boolean;
|
||||
onKeyDown?: (e: React.KeyboardEvent<any>) => void;
|
||||
onCloseAnimationEnd?: () => void;
|
||||
onClose: () => void;
|
||||
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
withPortal?: boolean;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export type { MenuPositionOptions } from '../../hooks/useMenuPosition';
|
||||
|
||||
type OwnProps =
|
||||
{
|
||||
ref?: React.RefObject<HTMLDivElement>;
|
||||
isOpen: boolean;
|
||||
shouldCloseFast?: boolean;
|
||||
id?: string;
|
||||
className?: string;
|
||||
bubbleClassName?: string;
|
||||
ariaLabelledBy?: string;
|
||||
autoClose?: boolean;
|
||||
footer?: string;
|
||||
noCloseOnBackdrop?: boolean;
|
||||
backdropExcludedSelector?: string;
|
||||
noCompact?: boolean;
|
||||
onKeyDown?: (e: React.KeyboardEvent<any>) => void;
|
||||
onCloseAnimationEnd?: () => void;
|
||||
onClose: () => void;
|
||||
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
withPortal?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
& MenuPositionOptions;
|
||||
|
||||
const ANIMATION_DURATION = 200;
|
||||
|
||||
const Menu: FC<OwnProps> = ({
|
||||
ref: externalRef,
|
||||
containerRef,
|
||||
shouldCloseFast,
|
||||
isOpen,
|
||||
id,
|
||||
className,
|
||||
bubbleClassName,
|
||||
style,
|
||||
bubbleStyle,
|
||||
ariaLabelledBy,
|
||||
children,
|
||||
transformOriginX,
|
||||
transformOriginY,
|
||||
positionX = 'left',
|
||||
positionY = 'top',
|
||||
autoClose = false,
|
||||
footer,
|
||||
noCloseOnBackdrop = false,
|
||||
@ -79,16 +73,20 @@ const Menu: FC<OwnProps> = ({
|
||||
onMouseLeave,
|
||||
withPortal,
|
||||
onMouseEnterBackdrop,
|
||||
...positionOptions
|
||||
}) => {
|
||||
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,
|
||||
ref: externalRef,
|
||||
onCloseAnimationEnd,
|
||||
});
|
||||
|
||||
const backdropContainerRef = containerRef || menuRef;
|
||||
useMenuPosition(isOpen, containerRef, bubbleRef, positionOptions);
|
||||
|
||||
useEffect(
|
||||
() => (isOpen ? captureEscKeyListener(onClose) : undefined),
|
||||
@ -107,11 +105,11 @@ const Menu: FC<OwnProps> = ({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleKeyDown = useKeyboardListNavigation(menuRef, isOpen, autoClose ? onClose : undefined, undefined, true);
|
||||
const handleKeyDown = useKeyboardListNavigation(bubbleRef, isOpen, autoClose ? onClose : undefined, undefined, true);
|
||||
|
||||
useVirtualBackdrop(
|
||||
isOpen,
|
||||
backdropContainerRef,
|
||||
containerRef,
|
||||
noCloseOnBackdrop ? undefined : onClose,
|
||||
undefined,
|
||||
backdropExcludedSelector,
|
||||
@ -119,16 +117,11 @@ const Menu: FC<OwnProps> = ({
|
||||
|
||||
const bubbleFullClassName = buildClassName(
|
||||
'bubble menu-container custom-scroll',
|
||||
positionY,
|
||||
positionX,
|
||||
footer && 'with-footer',
|
||||
bubbleClassName,
|
||||
shouldCloseFast && 'close-fast',
|
||||
);
|
||||
|
||||
const transformOriginYStyle = transformOriginY !== undefined ? `${transformOriginY}px` : undefined;
|
||||
const transformOriginXStyle = transformOriginX !== undefined ? `${transformOriginX}px` : undefined;
|
||||
|
||||
const handleClick = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
if (autoClose) {
|
||||
@ -138,6 +131,7 @@ const Menu: FC<OwnProps> = ({
|
||||
|
||||
const menu = (
|
||||
<div
|
||||
ref={containerRef}
|
||||
id={id}
|
||||
className={buildClassName(
|
||||
'Menu',
|
||||
@ -146,7 +140,6 @@ const Menu: FC<OwnProps> = ({
|
||||
withPortal && 'in-portal',
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
role={ariaLabelledBy ? 'menu' : undefined}
|
||||
onKeyDown={isOpen ? handleKeyDown : undefined}
|
||||
@ -163,12 +156,8 @@ const Menu: FC<OwnProps> = ({
|
||||
)}
|
||||
<div
|
||||
role="presentation"
|
||||
ref={menuRef}
|
||||
ref={bubbleRef}
|
||||
className={bubbleFullClassName}
|
||||
style={buildStyle(
|
||||
`transform-origin: ${transformOriginXStyle || positionX} ${transformOriginYStyle || positionY}`,
|
||||
bubbleStyle,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -12,7 +12,6 @@ import renderText from '../common/helpers/renderText';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import { useFastClick } from '../../hooks/useFastClick';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
|
||||
import Menu from './Menu';
|
||||
import MenuItem from './MenuItem';
|
||||
@ -106,7 +105,7 @@ const Tab: FC<OwnProps> = ({
|
||||
}, [isActive, previousActiveTab]);
|
||||
|
||||
const {
|
||||
contextMenuPosition, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose,
|
||||
contextMenuAnchor, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose,
|
||||
handleContextMenuHide, isContextMenuOpen,
|
||||
} = useContextMenuHandlers(tabRef, !contextActions);
|
||||
|
||||
@ -131,16 +130,6 @@ const Tab: FC<OwnProps> = ({
|
||||
);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
getLayout,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('Tab', onClick && 'Tab--interactive', className)}
|
||||
@ -158,14 +147,14 @@ const Tab: FC<OwnProps> = ({
|
||||
<i className="platform" />
|
||||
</span>
|
||||
|
||||
{contextActions && contextMenuPosition !== undefined && (
|
||||
{contextActions && contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className="Tab-context-menu"
|
||||
autoClose
|
||||
onClose={handleContextMenuClose}
|
||||
|
||||
@ -6,10 +6,7 @@ import type { IAnchorPosition } from '../types';
|
||||
import type { Signal } from '../util/signals';
|
||||
|
||||
import { requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
IS_IOS,
|
||||
IS_PWA, IS_TOUCH_ENV,
|
||||
} from '../util/windowEnvironment';
|
||||
import { IS_IOS, IS_PWA, IS_TOUCH_ENV } from '../util/windowEnvironment';
|
||||
import useLastCallback from './useLastCallback';
|
||||
|
||||
const LONG_TAP_DURATION_MS = 200;
|
||||
@ -29,7 +26,7 @@ const useContextMenuHandlers = (
|
||||
getIsReady?: Signal<boolean>,
|
||||
) => {
|
||||
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 handleBeforeContextMenu = useLastCallback((e: React.MouseEvent) => {
|
||||
@ -51,12 +48,12 @@ const useContextMenuHandlers = (
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (contextMenuPosition) {
|
||||
if (contextMenuAnchor) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsContextMenuOpen(true);
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
||||
setContextMenuAnchor({ x: e.clientX, y: e.clientY });
|
||||
setContextMenuTarget(e.target as HTMLElement);
|
||||
});
|
||||
|
||||
@ -65,7 +62,7 @@ const useContextMenuHandlers = (
|
||||
});
|
||||
|
||||
const handleContextMenuHide = useLastCallback(() => {
|
||||
setContextMenuPosition(undefined);
|
||||
setContextMenuAnchor(undefined);
|
||||
});
|
||||
|
||||
// Support context menu on touch devices
|
||||
@ -93,7 +90,7 @@ const useContextMenuHandlers = (
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -129,7 +126,7 @@ const useContextMenuHandlers = (
|
||||
}
|
||||
|
||||
setIsContextMenuOpen(true);
|
||||
setContextMenuPosition({ x: clientX, y: clientY });
|
||||
setContextMenuAnchor({ x: clientX, y: clientY });
|
||||
};
|
||||
|
||||
const startLongPressTimer = (e: TouchEvent) => {
|
||||
@ -156,12 +153,12 @@ const useContextMenuHandlers = (
|
||||
element.removeEventListener('touchmove', clearLongPressTimer);
|
||||
};
|
||||
}, [
|
||||
contextMenuPosition, isMenuDisabled, shouldDisableOnLongTap, elementRef, shouldDisableOnLink, getIsReady,
|
||||
contextMenuAnchor, isMenuDisabled, shouldDisableOnLongTap, elementRef, shouldDisableOnLink, getIsReady,
|
||||
]);
|
||||
|
||||
return {
|
||||
isContextMenuOpen,
|
||||
contextMenuPosition,
|
||||
contextMenuAnchor,
|
||||
contextMenuTarget,
|
||||
handleBeforeContextMenu,
|
||||
handleContextMenu,
|
||||
|
||||
@ -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';
|
||||
|
||||
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;
|
||||
extraTopPadding?: number;
|
||||
extraMarginTop?: number;
|
||||
@ -21,124 +48,175 @@ const EMPTY_RECT = {
|
||||
};
|
||||
|
||||
export default function useMenuPosition(
|
||||
anchor: IAnchorPosition | undefined,
|
||||
getTriggerElement: () => HTMLElement | null,
|
||||
getRootElement: () => HTMLElement | null,
|
||||
getMenuElement: () => HTMLElement | null,
|
||||
getLayout?: () => Layout,
|
||||
isOpen: boolean,
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
bubbleRef: React.RefObject<HTMLDivElement>,
|
||||
options: MenuPositionOptions,
|
||||
) {
|
||||
const [positionX, setPositionX] = useState<'right' | 'left'>('right');
|
||||
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;');
|
||||
const optionsRef = useStateRef(options);
|
||||
|
||||
useEffect(() => {
|
||||
const triggerEl = getTriggerElement();
|
||||
if (!anchor || !triggerEl) {
|
||||
return;
|
||||
}
|
||||
useLayoutEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
let { x, y } = anchor;
|
||||
const anchorX = x;
|
||||
const anchorY = y;
|
||||
const options2 = optionsRef.current;
|
||||
|
||||
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 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;
|
||||
if (!('getTriggerElement' in options2)) {
|
||||
applyStaticOptions(containerRef, bubbleRef, options2);
|
||||
} else {
|
||||
horizontalPosition = 'left';
|
||||
x = 16;
|
||||
requestForcedReflow(() => {
|
||||
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)) {
|
||||
verticalPosition = 'top';
|
||||
y = yWithTopShift;
|
||||
} else {
|
||||
verticalPosition = 'bottom';
|
||||
if (bubbleStyle) {
|
||||
bubbleEl.style.cssText = bubbleStyle;
|
||||
}
|
||||
|
||||
if (y - menuRect.height < rootRect.top + extraTopPadding) {
|
||||
y = rootRect.top + rootRect.height;
|
||||
}
|
||||
if (positionX) {
|
||||
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 addedXForPortalPositioning = (withPortal ? triggerRect.left : 0);
|
||||
const leftWithPossibleNegative = Math.min(
|
||||
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(
|
||||
x - triggerRect.left,
|
||||
rootRect.width - menuRect.width - 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) {
|
||||
left = Math.min(left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX);
|
||||
top = Math.min(top, rootRect.height - menuRect.height - MENU_POSITION_VISUAL_COMFORT_SPACE_PX);
|
||||
}
|
||||
|
||||
if (isDense) {
|
||||
left = Math.min(left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX);
|
||||
top = Math.min(top, rootRect.height - menuRect.height - 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 addedXForMenuPositioning = menuElMinWidth ? Math.max(0, (menuElMinWidth - menuElWidth) / 2) : 0;
|
||||
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 addedXForMenuPositioning = menuElMinWidth ? Math.max(0, (menuElMinWidth - menuElWidth) / 2) : 0;
|
||||
if (left - addedXForMenuPositioning < 0 && shouldAvoidNegativePosition) {
|
||||
left = addedXForMenuPositioning + MENU_POSITION_VISUAL_COMFORT_SPACE_PX;
|
||||
}
|
||||
const offsetX = (anchorX + addedXForPortalPositioning - triggerRect.left) - left;
|
||||
const offsetY = (anchorY + addedYForPortalPositioning - triggerRect.top) - top - (marginTop || 0);
|
||||
const transformOriginX = positionX === 'left' ? offsetX : menuRect.width + offsetX;
|
||||
const transformOriginY = positionY === 'bottom' ? menuRect.height + offsetY : offsetY;
|
||||
|
||||
const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN - (marginTop || 0);
|
||||
|
||||
setWithScroll(menuMaxHeight < menuRect.height);
|
||||
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,
|
||||
]);
|
||||
const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN - (marginTop || 0);
|
||||
const bubbleStyle = `max-height: ${menuMaxHeight}px;`;
|
||||
const style = `left: ${left}px; top: ${top}px`;
|
||||
|
||||
return {
|
||||
positionX,
|
||||
@ -146,7 +224,6 @@ export default function useMenuPosition(
|
||||
transformOriginX,
|
||||
transformOriginY,
|
||||
style,
|
||||
menuStyle,
|
||||
withScroll,
|
||||
bubbleStyle,
|
||||
};
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ const BACKDROP_CLASSNAME = 'backdrop';
|
||||
// without adding extra elements to the DOM
|
||||
export default function useVirtualBackdrop(
|
||||
isOpen: boolean,
|
||||
menuRef: RefObject<HTMLElement>,
|
||||
containerRef: RefObject<HTMLElement>,
|
||||
onClose?: () => void | undefined,
|
||||
ignoreRightClick?: boolean,
|
||||
excludedClosestSelector?: string,
|
||||
@ -18,14 +18,14 @@ export default function useVirtualBackdrop(
|
||||
}
|
||||
|
||||
const handleEvent = (e: MouseEvent) => {
|
||||
const menu = menuRef.current;
|
||||
const container = containerRef.current;
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (!menu || !target || (ignoreRightClick && e.button === 2)) {
|
||||
if (!container || !target || (ignoreRightClick && e.button === 2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((
|
||||
!menu.contains(e.target as Node | null)
|
||||
!container.contains(e.target as Node | null)
|
||||
|| target.classList.contains(BACKDROP_CLASSNAME)
|
||||
) && !(excludedClosestSelector && (
|
||||
target.matches(excludedClosestSelector) || target.closest(excludedClosestSelector)
|
||||
@ -41,5 +41,5 @@ export default function useVirtualBackdrop(
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleEvent);
|
||||
};
|
||||
}, [excludedClosestSelector, ignoreRightClick, isOpen, menuRef, onClose]);
|
||||
}, [excludedClosestSelector, ignoreRightClick, isOpen, containerRef, onClose]);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user