[Perf] Message Context Menu: Avoid forced reflow
This commit is contained in:
parent
07ac02b201
commit
d0fc223f78
@ -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
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user