Message / Context Menu: Add scrollbars, better calculation of position and size (#1405)

This commit is contained in:
Alexander Zinchuk 2021-09-10 20:33:04 +03:00
parent 4ee26fb38d
commit fbdb13e5a2
7 changed files with 62 additions and 21 deletions

View File

@ -1,5 +1,5 @@
import React, { import React, {
FC, memo, useCallback, useEffect, useMemo, useState, FC, memo, useCallback, useMemo, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn'; import { withGlobal } from '../../../lib/teact/teactn';
@ -7,7 +7,6 @@ import { GlobalActions, MessageListType } from '../../../global/types';
import { ApiMessage } from '../../../api/types'; import { ApiMessage } from '../../../api/types';
import { IAlbum, IAnchorPosition } from '../../../types'; import { IAlbum, IAnchorPosition } from '../../../types';
import { selectAllowedMessageActions, selectCurrentMessageList } from '../../../modules/selectors'; import { selectAllowedMessageActions, selectCurrentMessageList } from '../../../modules/selectors';
import { disableScrolling, enableScrolling } from '../../../util/scrollLock';
import { pick } from '../../../util/iteratees'; import { pick } from '../../../util/iteratees';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
@ -206,12 +205,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
closeMenu(); closeMenu();
}, [chatUsername, closeMenu, message.chatId, message.id]); }, [chatUsername, closeMenu, message.chatId, message.id]);
useEffect(() => {
disableScrolling();
return enableScrolling;
}, []);
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]); const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
if (noOptions) { if (noOptions) {

View File

@ -5,10 +5,11 @@
.bubble { .bubble {
transform: scale(0.5); 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; 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 { .backdrop {
position: absolute;
touch-action: none; touch-action: none;
} }
} }

View File

@ -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 { ApiMessage } from '../../../api/types';
import { IAnchorPosition } from '../../../types'; import { IAnchorPosition } from '../../../types';
import { getMessageCopyOptions } from './helpers/copyOptions'; import { getMessageCopyOptions } from './helpers/copyOptions';
import { disableScrolling, enableScrolling } from '../../../util/scrollLock';
import useContextMenuPosition from '../../../hooks/useContextMenuPosition'; import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
@ -83,6 +86,8 @@ const MessageContextMenu: FC<OwnProps> = ({
onCloseAnimationEnd, onCloseAnimationEnd,
onCopyLink, onCopyLink,
}) => { }) => {
// eslint-disable-next-line no-null/no-null
const menuRef = useRef<HTMLDivElement>(null);
const copyOptions = getMessageCopyOptions(message, onClose, canCopyLink ? onCopyLink : undefined); const copyOptions = getMessageCopyOptions(message, onClose, canCopyLink ? onCopyLink : undefined);
const getTriggerElement = useCallback(() => { const getTriggerElement = useCallback(() => {
@ -99,7 +104,9 @@ const MessageContextMenu: FC<OwnProps> = ({
[], [],
); );
const { positionX, positionY, style } = useContextMenuPosition( const {
positionX, positionY, style, menuStyle, withScroll,
} = useContextMenuPosition(
anchor, anchor,
getTriggerElement, getTriggerElement,
getRootElement, getRootElement,
@ -108,14 +115,22 @@ const MessageContextMenu: FC<OwnProps> = ({
(document.querySelector('.MiddleHeader') as HTMLElement).offsetHeight, (document.querySelector('.MiddleHeader') as HTMLElement).offsetHeight,
); );
useEffect(() => {
disableScrolling(withScroll ? menuRef.current : undefined);
return enableScrolling;
}, [withScroll]);
const lang = useLang(); const lang = useLang();
return ( return (
<Menu <Menu
ref={menuRef}
isOpen={isOpen} isOpen={isOpen}
positionX={positionX} positionX={positionX}
positionY={positionY} positionY={positionY}
style={style} style={style}
menuStyle={menuStyle}
className="MessageContextMenu fluid" className="MessageContextMenu fluid"
onClose={onClose} onClose={onClose}
onCloseAnimationEnd={onCloseAnimationEnd} onCloseAnimationEnd={onCloseAnimationEnd}

View File

@ -25,6 +25,7 @@
border-radius: var(--border-radius-default); border-radius: var(--border-radius-default);
min-width: 13.5rem; min-width: 13.5rem;
z-index: var(--z-menu-bubble); z-index: var(--z-menu-bubble);
overscroll-behavior: contain;
transform: scale(0.2); transform: scale(0.2);
transition: opacity .2s cubic-bezier(0.2, 0, 0.2, 1), transform .2s cubic-bezier(0.2, 0, 0.2, 1) !important; transition: opacity .2s cubic-bezier(0.2, 0, 0.2, 1), transform .2s cubic-bezier(0.2, 0, 0.2, 1) !important;

View File

@ -19,6 +19,7 @@ type OwnProps = {
isOpen: boolean; isOpen: boolean;
className?: string; className?: string;
style?: string; style?: string;
menuStyle?: string;
positionX?: 'left' | 'right'; positionX?: 'left' | 'right';
positionY?: 'top' | 'bottom'; positionY?: 'top' | 'bottom';
autoClose?: boolean; autoClose?: boolean;
@ -41,6 +42,7 @@ const Menu: FC<OwnProps> = ({
isOpen, isOpen,
className, className,
style, style,
menuStyle,
children, children,
positionX = 'left', positionX = 'left',
positionY = 'top', positionY = 'top',
@ -116,7 +118,7 @@ const Menu: FC<OwnProps> = ({
ref={menuRef} ref={menuRef}
className={bubbleClassName} className={bubbleClassName}
// @ts-ignore teact feature // @ts-ignore teact feature
style={`transform-origin: ${positionY} ${positionX}`} style={`transform-origin: ${positionY} ${positionX};${menuStyle || ''}`}
onClick={autoClose ? onClose : undefined} onClick={autoClose ? onClose : undefined}
> >
{children} {children}

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from '../lib/teact/teact';
import { IAnchorPosition } from '../types'; import { IAnchorPosition } from '../types';
const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16; const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16;
const MENU_POSITION_BOTTOM_MARGIN = 12;
export default ( export default (
anchor: IAnchorPosition | undefined, anchor: IAnchorPosition | undefined,
@ -13,7 +14,9 @@ export default (
) => { ) => {
const [positionX, setPositionX] = useState<'right' | 'left'>('right'); const [positionX, setPositionX] = useState<'right' | 'left'>('right');
const [positionY, setPositionY] = useState<'top' | 'bottom'>('bottom'); const [positionY, setPositionY] = useState<'top' | 'bottom'>('bottom');
const [withScroll, setWithScroll] = useState(false);
const [style, setStyle] = useState(''); const [style, setStyle] = useState('');
const [menuStyle, setMenuStyle] = useState('');
useEffect(() => { useEffect(() => {
const triggerEl = getTriggerElement(); const triggerEl = getTriggerElement();
@ -52,15 +55,22 @@ export default (
setPositionY('bottom'); setPositionY('bottom');
if (y - menuRect.height < rootRect.top + extraTopPadding) { if (y - menuRect.height < rootRect.top + extraTopPadding) {
y = rootRect.top + extraTopPadding + menuRect.height; y = rootRect.top + rootRect.height;
} }
} }
const left = horizontalPostition === 'left' const left = horizontalPostition === 'left'
? Math.min(x - triggerRect.left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX) ? 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); : 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, anchor, extraPaddingX, extraTopPadding,
getMenuElement, getRootElement, getTriggerElement, getMenuElement, getRootElement, getTriggerElement,
@ -70,5 +80,7 @@ export default (
positionX, positionX,
positionY, positionY,
style, style,
menuStyle,
withScroll,
}; };
}; };

View File

@ -1,3 +1,5 @@
let scrollLockEl: HTMLElement | null | undefined;
const IGNORED_KEYS: Record<string, boolean> = { const IGNORED_KEYS: Record<string, boolean> = {
Down: true, Down: true,
ArrowDown: true, ArrowDown: true,
@ -30,27 +32,42 @@ function isTextBox(target: EventTarget | null) {
return inputTypes.indexOf(type.toLowerCase()) > -1; return inputTypes.indexOf(type.toLowerCase()) > -1;
} }
const preventDefault = (e: Event) => { const getTouchY = (e: WheelEvent | TouchEvent) => ('changedTouches' in e ? e.changedTouches[0].clientY : 0);
e.preventDefault();
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) { function preventDefaultForScrollKeys(e: KeyboardEvent) {
if (IGNORED_KEYS[e.key] && !isTextBox(e.target)) { 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 // Disable scrolling in Chrome
document.addEventListener('wheel', preventDefault, { passive: false }); document.addEventListener('wheel', preventDefault, { passive: false });
window.ontouchmove = preventDefault; // mobile document.addEventListener('touchmove', preventDefault, { passive: false });
document.onkeydown = preventDefaultForScrollKeys; document.onkeydown = preventDefaultForScrollKeys;
} }
export function enableScrolling() { export function enableScrolling() {
scrollLockEl = undefined;
document.removeEventListener('wheel', preventDefault); // Enable scrolling in Chrome document.removeEventListener('wheel', preventDefault); // Enable scrolling in Chrome
// eslint-disable-next-line no-null/no-null document.removeEventListener('touchmove', preventDefault);
window.ontouchmove = null;
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
document.onkeydown = null; document.onkeydown = null;
} }