Message / Context Menu: Add scrollbars, better calculation of position and size (#1405)
This commit is contained in:
parent
4ee26fb38d
commit
fbdb13e5a2
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user