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, {
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<OwnProps & StateProps & DispatchProps> = ({
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) {

View File

@ -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;
}
}

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

View File

@ -25,6 +25,7 @@
border-radius: var(--border-radius-default);
min-width: 13.5rem;
z-index: var(--z-menu-bubble);
overscroll-behavior: contain;
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;

View File

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

View File

@ -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,
};
};

View File

@ -1,3 +1,5 @@
let scrollLockEl: HTMLElement | null | undefined;
const IGNORED_KEYS: Record<string, boolean> = {
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;
}