From fbdb13e5a2da4d49bf6d10c8629a3672a3e07d2b Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 10 Sep 2021 20:33:04 +0300 Subject: [PATCH] Message / Context Menu: Add scrollbars, better calculation of position and size (#1405) --- .../middle/message/ContextMenuContainer.tsx | 9 +----- .../middle/message/MessageContextMenu.scss | 3 +- .../middle/message/MessageContextMenu.tsx | 19 ++++++++++-- src/components/ui/Menu.scss | 1 + src/components/ui/Menu.tsx | 4 ++- src/hooks/useContextMenuPosition.ts | 16 ++++++++-- src/util/scrollLock.ts | 31 ++++++++++++++----- 7 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 6d068c6a8..44f22c398 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -1,5 +1,5 @@ import React, { - FC, memo, useCallback, useEffect, useMemo, useState, + FC, memo, useCallback, useMemo, useState, } from '../../../lib/teact/teact'; import { withGlobal } from '../../../lib/teact/teactn'; @@ -7,7 +7,6 @@ import { GlobalActions, MessageListType } from '../../../global/types'; import { ApiMessage } from '../../../api/types'; import { IAlbum, IAnchorPosition } from '../../../types'; import { selectAllowedMessageActions, selectCurrentMessageList } from '../../../modules/selectors'; -import { disableScrolling, enableScrolling } from '../../../util/scrollLock'; import { pick } from '../../../util/iteratees'; import useShowTransition from '../../../hooks/useShowTransition'; import useFlag from '../../../hooks/useFlag'; @@ -206,12 +205,6 @@ const ContextMenuContainer: FC = ({ closeMenu(); }, [chatUsername, closeMenu, message.chatId, message.id]); - useEffect(() => { - disableScrolling(); - - return enableScrolling; - }, []); - const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]); if (noOptions) { diff --git a/src/components/middle/message/MessageContextMenu.scss b/src/components/middle/message/MessageContextMenu.scss index 2066f00e3..3b514fbb0 100644 --- a/src/components/middle/message/MessageContextMenu.scss +++ b/src/components/middle/message/MessageContextMenu.scss @@ -5,10 +5,11 @@ .bubble { transform: scale(0.5); transition: opacity .15s cubic-bezier(0.2, 0, 0.2, 1), transform .15s cubic-bezier(0.2, 0, 0.2, 1) !important; + overflow: auto; + overflow: overlay; } .backdrop { - position: absolute; touch-action: none; } } diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index fc9e4d5c2..369aea262 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -1,9 +1,12 @@ -import React, { FC, useCallback } from '../../../lib/teact/teact'; +import React, { + FC, useCallback, useEffect, useRef, +} from '../../../lib/teact/teact'; import { ApiMessage } from '../../../api/types'; import { IAnchorPosition } from '../../../types'; import { getMessageCopyOptions } from './helpers/copyOptions'; +import { disableScrolling, enableScrolling } from '../../../util/scrollLock'; import useContextMenuPosition from '../../../hooks/useContextMenuPosition'; import useLang from '../../../hooks/useLang'; @@ -83,6 +86,8 @@ const MessageContextMenu: FC = ({ onCloseAnimationEnd, onCopyLink, }) => { + // eslint-disable-next-line no-null/no-null + const menuRef = useRef(null); const copyOptions = getMessageCopyOptions(message, onClose, canCopyLink ? onCopyLink : undefined); const getTriggerElement = useCallback(() => { @@ -99,7 +104,9 @@ const MessageContextMenu: FC = ({ [], ); - const { positionX, positionY, style } = useContextMenuPosition( + const { + positionX, positionY, style, menuStyle, withScroll, + } = useContextMenuPosition( anchor, getTriggerElement, getRootElement, @@ -108,14 +115,22 @@ const MessageContextMenu: FC = ({ (document.querySelector('.MiddleHeader') as HTMLElement).offsetHeight, ); + useEffect(() => { + disableScrolling(withScroll ? menuRef.current : undefined); + + return enableScrolling; + }, [withScroll]); + const lang = useLang(); return ( = ({ isOpen, className, style, + menuStyle, children, positionX = 'left', positionY = 'top', @@ -116,7 +118,7 @@ const Menu: FC = ({ ref={menuRef} className={bubbleClassName} // @ts-ignore teact feature - style={`transform-origin: ${positionY} ${positionX}`} + style={`transform-origin: ${positionY} ${positionX};${menuStyle || ''}`} onClick={autoClose ? onClose : undefined} > {children} diff --git a/src/hooks/useContextMenuPosition.ts b/src/hooks/useContextMenuPosition.ts index a5c820f05..3ff54397e 100644 --- a/src/hooks/useContextMenuPosition.ts +++ b/src/hooks/useContextMenuPosition.ts @@ -2,6 +2,7 @@ import { useState, useEffect } from '../lib/teact/teact'; import { IAnchorPosition } from '../types'; const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16; +const MENU_POSITION_BOTTOM_MARGIN = 12; export default ( anchor: IAnchorPosition | undefined, @@ -13,7 +14,9 @@ export default ( ) => { const [positionX, setPositionX] = useState<'right' | 'left'>('right'); const [positionY, setPositionY] = useState<'top' | 'bottom'>('bottom'); + const [withScroll, setWithScroll] = useState(false); const [style, setStyle] = useState(''); + const [menuStyle, setMenuStyle] = useState(''); useEffect(() => { const triggerEl = getTriggerElement(); @@ -52,15 +55,22 @@ export default ( setPositionY('bottom'); if (y - menuRect.height < rootRect.top + extraTopPadding) { - y = rootRect.top + extraTopPadding + menuRect.height; + y = rootRect.top + rootRect.height; } } const left = horizontalPostition === 'left' ? Math.min(x - triggerRect.left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX) : Math.max((x - triggerRect.left), menuRect.width + MENU_POSITION_VISUAL_COMFORT_SPACE_PX); + const top = Math.min( + rootRect.height - triggerRect.top + triggerRect.height - MENU_POSITION_BOTTOM_MARGIN, + y - triggerRect.top, + ); + const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN; - setStyle(`left: ${left}px; top: ${y - triggerRect.top}px;`); + setWithScroll(menuMaxHeight < menuRect.height); + setMenuStyle(`max-height: ${menuMaxHeight}px;`); + setStyle(`left: ${left}px; top: ${top}px`); }, [ anchor, extraPaddingX, extraTopPadding, getMenuElement, getRootElement, getTriggerElement, @@ -70,5 +80,7 @@ export default ( positionX, positionY, style, + menuStyle, + withScroll, }; }; diff --git a/src/util/scrollLock.ts b/src/util/scrollLock.ts index 5cfd78844..5793c1f79 100644 --- a/src/util/scrollLock.ts +++ b/src/util/scrollLock.ts @@ -1,3 +1,5 @@ +let scrollLockEl: HTMLElement | null | undefined; + const IGNORED_KEYS: Record = { Down: true, ArrowDown: true, @@ -30,27 +32,42 @@ function isTextBox(target: EventTarget | null) { return inputTypes.indexOf(type.toLowerCase()) > -1; } -const preventDefault = (e: Event) => { - e.preventDefault(); +const getTouchY = (e: WheelEvent | TouchEvent) => ('changedTouches' in e ? e.changedTouches[0].clientY : 0); + +const preventDefault = (e: WheelEvent | TouchEvent) => { + const deltaY = 'deltaY' in e ? e.deltaY : getTouchY(e); + + if ( + !scrollLockEl + // Allow overlay scrolling + || !scrollLockEl.contains(e.target as HTMLElement) + // Prevent top overscroll + || (scrollLockEl.scrollTop <= 0 && deltaY <= 0) + // Prevent bottom overscroll + || (scrollLockEl.scrollTop >= (scrollLockEl.scrollHeight - scrollLockEl.offsetHeight) && deltaY >= 0) + ) { + e.preventDefault(); + } }; function preventDefaultForScrollKeys(e: KeyboardEvent) { if (IGNORED_KEYS[e.key] && !isTextBox(e.target)) { - preventDefault(e); + e.preventDefault(); } } -export function disableScrolling() { +export function disableScrolling(el?: HTMLElement | null) { + scrollLockEl = el; // Disable scrolling in Chrome document.addEventListener('wheel', preventDefault, { passive: false }); - window.ontouchmove = preventDefault; // mobile + document.addEventListener('touchmove', preventDefault, { passive: false }); document.onkeydown = preventDefaultForScrollKeys; } export function enableScrolling() { + scrollLockEl = undefined; document.removeEventListener('wheel', preventDefault); // Enable scrolling in Chrome - // eslint-disable-next-line no-null/no-null - window.ontouchmove = null; + document.removeEventListener('touchmove', preventDefault); // eslint-disable-next-line no-null/no-null document.onkeydown = null; }