[Perf] Message Context Menu: Avoid forced reflow

This commit is contained in:
Alexander Zinchuk 2022-01-24 04:41:46 +01:00
parent 07ac02b201
commit d0fc223f78
3 changed files with 61 additions and 50 deletions

View File

@ -145,6 +145,22 @@ const MessageContextMenu: FC<OwnProps> = ({
[], [],
); );
const getLayout = useCallback(() => {
const extraHeightAudioPlayer = (IS_SINGLE_COLUMN_LAYOUT
&& (document.querySelector<HTMLElement>('.AudioPlayer-content'))?.offsetHeight) || 0;
const pinnedElement = document.querySelector<HTMLElement>('.HeaderPinnedMessage-wrapper');
const extraHeightPinned = (((IS_SINGLE_COLUMN_LAYOUT && !extraHeightAudioPlayer)
|| (!IS_SINGLE_COLUMN_LAYOUT && pinnedElement?.classList.contains('full-width')))
&& pinnedElement?.offsetHeight) || 0;
return {
extraPaddingX: SCROLLBAR_WIDTH,
extraTopPadding: (document.querySelector<HTMLElement>('.MiddleHeader')!).offsetHeight,
marginSides: withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined,
extraMarginTop: extraHeightPinned + extraHeightAudioPlayer,
};
}, [withReactions]);
const handleRemoveReaction = useCallback(() => { const handleRemoveReaction = useCallback(() => {
onSendReaction(undefined, 0, 0); onSendReaction(undefined, 0, 0);
}, [onSendReaction]); }, [onSendReaction]);
@ -160,25 +176,9 @@ const MessageContextMenu: FC<OwnProps> = ({
}, ANIMATION_DURATION); }, ANIMATION_DURATION);
}, [isOpen, markIsReady, unmarkIsReady]); }, [isOpen, markIsReady, unmarkIsReady]);
const extraHeightAudioPlayer = (IS_SINGLE_COLUMN_LAYOUT
&& (document.querySelector<HTMLElement>('.AudioPlayer-content'))?.offsetHeight) || 0;
const pinnedElement = document.querySelector<HTMLElement>('.HeaderPinnedMessage-wrapper');
const extraHeightPinned = (((IS_SINGLE_COLUMN_LAYOUT && !extraHeightAudioPlayer)
|| (!IS_SINGLE_COLUMN_LAYOUT && pinnedElement?.classList.contains('full-width')))
&& pinnedElement?.offsetHeight) || 0;
const { const {
positionX, positionY, style, menuStyle, withScroll, positionX, positionY, style, menuStyle, withScroll,
} = useContextMenuPosition( } = useContextMenuPosition(anchor, getTriggerElement, getRootElement, getMenuElement, getLayout);
anchor,
getTriggerElement,
getRootElement,
getMenuElement,
SCROLLBAR_WIDTH,
(document.querySelector<HTMLElement>('.MiddleHeader')!).offsetHeight,
withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined,
extraHeightPinned + extraHeightAudioPlayer,
);
useEffect(() => { useEffect(() => {
disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionSelector'); disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionSelector');
@ -195,7 +195,7 @@ const MessageContextMenu: FC<OwnProps> = ({
positionX={positionX} positionX={positionX}
positionY={positionY} positionY={positionY}
style={style} style={style}
menuStyle={menuStyle} bubbleStyle={menuStyle}
className={buildClassName( className={buildClassName(
'MessageContextMenu', 'fluid', withReactions && 'with-reactions', 'MessageContextMenu', 'fluid', withReactions && 'with-reactions',
)} )}
@ -253,10 +253,12 @@ const MessageContextMenu: FC<OwnProps> = ({
{canShowReactionsCount && message.reactors?.count ? ( {canShowReactionsCount && message.reactors?.count ? (
canShowSeenBy && message.seenByUserIds?.length canShowSeenBy && message.seenByUserIds?.length
? lang('Chat.OutgoingContextMixedReactionCount', [message.reactors.count, message.seenByUserIds.length]) ? lang('Chat.OutgoingContextMixedReactionCount', [message.reactors.count, message.seenByUserIds.length])
: lang('Chat.ContextReactionCount', message.reactors.count, 'i')) : lang('Chat.ContextReactionCount', message.reactors.count, 'i')
: (message.seenByUserIds?.length ) : (
message.seenByUserIds?.length
? lang('Conversation.ContextMenuSeen', message.seenByUserIds.length, 'i') ? lang('Conversation.ContextMenuSeen', message.seenByUserIds.length, 'i')
: lang('Conversation.ContextMenuNoViews'))} : lang('Conversation.ContextMenuNoViews')
)}
<div className="avatars"> <div className="avatars">
{seenByRecentUsers?.map((user) => ( {seenByRecentUsers?.map((user) => (
<Avatar <Avatar

View File

@ -19,7 +19,7 @@ type OwnProps = {
isOpen: boolean; isOpen: boolean;
className?: string; className?: string;
style?: string; style?: string;
menuStyle?: string; bubbleStyle?: string;
positionX?: 'left' | 'right'; positionX?: 'left' | 'right';
positionY?: 'top' | 'bottom'; positionY?: 'top' | 'bottom';
autoClose?: boolean; autoClose?: boolean;
@ -42,7 +42,7 @@ const Menu: FC<OwnProps> = ({
isOpen, isOpen,
className, className,
style, style,
menuStyle, bubbleStyle,
children, children,
positionX = 'left', positionX = 'left',
positionY = 'top', positionY = 'top',
@ -118,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};${menuStyle || ''}`} style={`transform-origin: ${positionY} ${positionX};${bubbleStyle || ''}`}
onClick={autoClose ? onClose : undefined} onClick={autoClose ? onClose : undefined}
> >
{children} {children}

View File

@ -1,69 +1,78 @@
import { useState, useLayoutEffect } from '../lib/teact/teact'; import { useState, useEffect } from '../lib/teact/teact';
import { IAnchorPosition } from '../types'; import { IAnchorPosition } from '../types';
interface Layout {
extraPaddingX?: number;
extraTopPadding?: number;
marginSides?: number;
extraMarginTop?: number;
}
const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16; const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16;
const MENU_POSITION_BOTTOM_MARGIN = 12; const MENU_POSITION_BOTTOM_MARGIN = 12;
const EMPTY_RECT = {
width: 0, left: 0, height: 0, top: 0,
};
export default ( export default function useContextMenuPosition(
anchor: IAnchorPosition | undefined, anchor: IAnchorPosition | undefined,
getTriggerElement: () => HTMLElement | null, getTriggerElement: () => HTMLElement | null,
getRootElement: () => HTMLElement | null, getRootElement: () => HTMLElement | null,
getMenuElement: () => HTMLElement | null, getMenuElement: () => HTMLElement | null,
extraPaddingX = 0, getLayout?: () => Layout,
extraTopPadding = 0, ) {
marginSides = 0,
extraMarginTop = 0,
) => {
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 [withScroll, setWithScroll] = useState(false);
const [style, setStyle] = useState(''); const [style, setStyle] = useState('');
const [menuStyle, setMenuStyle] = useState(''); const [menuStyle, setMenuStyle] = useState('opacity: 0;');
useLayoutEffect(() => { useEffect(() => {
const triggerEl = getTriggerElement(); const triggerEl = getTriggerElement();
if (!anchor || !triggerEl) { if (!anchor || !triggerEl) {
return; return;
} }
let { x, y } = anchor; let { x, y } = anchor;
const emptyRect = {
width: 0, left: 0, height: 0, top: 0,
};
const menuEl = getMenuElement(); const menuEl = getMenuElement();
const rootEl = getRootElement(); const rootEl = getRootElement();
const triggerRect = triggerEl.getBoundingClientRect(); const {
extraPaddingX = 0,
extraTopPadding = 0,
marginSides = 0,
extraMarginTop = 0,
} = getLayout?.() || {};
const marginTop = menuEl ? parseInt(getComputedStyle(menuEl).marginTop, 10) + extraMarginTop : undefined; const marginTop = menuEl ? parseInt(getComputedStyle(menuEl).marginTop, 10) + extraMarginTop : undefined;
const menuRect = menuEl ? { const menuRect = menuEl ? {
width: menuEl.offsetWidth, width: menuEl.offsetWidth,
height: menuEl.offsetHeight + marginTop!, height: menuEl.offsetHeight + marginTop!,
} : emptyRect; } : EMPTY_RECT;
const rootRect = rootEl ? rootEl.getBoundingClientRect() : emptyRect; const rootRect = rootEl ? rootEl.getBoundingClientRect() : EMPTY_RECT;
let horizontalPostition: 'left' | 'right'; let horizontalPosition: 'left' | 'right';
if (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left) { if (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left) {
x += 3; x += 3;
horizontalPostition = 'left'; horizontalPosition = 'left';
} else if (x - menuRect.width > 0) { } else if (x - menuRect.width > 0) {
horizontalPostition = 'right'; horizontalPosition = 'right';
x -= 3; x -= 3;
} else { } else {
horizontalPostition = 'left'; horizontalPosition = 'left';
x = 16; x = 16;
} }
setPositionX(horizontalPostition); setPositionX(horizontalPosition);
if (marginSides if (marginSides
&& horizontalPostition === 'right' && (x + extraPaddingX + marginSides >= rootRect.width + rootRect.left)) { && horizontalPosition === 'right' && (x + extraPaddingX + marginSides >= rootRect.width + rootRect.left)) {
x -= marginSides; x -= marginSides;
} }
if (marginSides && horizontalPostition === 'left') { if (marginSides && horizontalPosition === 'left') {
if (x + extraPaddingX + marginSides + menuRect.width >= rootRect.width + rootRect.left) { if (x + extraPaddingX + marginSides + menuRect.width >= rootRect.width + rootRect.left) {
x -= marginSides; x -= marginSides;
} else if (x - marginSides <= 0) { } else if (x - marginSides <= 0) {
@ -81,7 +90,8 @@ export default (
} }
} }
const left = horizontalPostition === 'left' const triggerRect = triggerEl.getBoundingClientRect();
const left = horizontalPosition === '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( const top = Math.min(
@ -94,8 +104,7 @@ export default (
setMenuStyle(`max-height: ${menuMaxHeight}px;`); setMenuStyle(`max-height: ${menuMaxHeight}px;`);
setStyle(`left: ${left}px; top: ${top}px`); setStyle(`left: ${left}px; top: ${top}px`);
}, [ }, [
anchor, extraPaddingX, extraTopPadding, extraMarginTop, anchor, getMenuElement, getRootElement, getTriggerElement, getLayout,
getMenuElement, getRootElement, getTriggerElement, marginSides,
]); ]);
return { return {
@ -105,4 +114,4 @@ export default (
menuStyle, menuStyle,
withScroll, withScroll,
}; };
}; }