[Perf] Transition: Optimize slide transitions (#1562)
This commit is contained in:
parent
dee7d2163f
commit
3375954d5f
@ -53,11 +53,15 @@ const ProfileInfo: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
loadFullUser,
|
loadFullUser,
|
||||||
openMediaViewer,
|
openMediaViewer,
|
||||||
}) => {
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
const { id: userId } = user || {};
|
const { id: userId } = user || {};
|
||||||
const { id: chatId } = chat || {};
|
const { id: chatId } = chat || {};
|
||||||
const fullName = user ? getUserFullName(user) : (chat ? chat.title : '');
|
const fullName = user ? getUserFullName(user) : (chat ? chat.title : '');
|
||||||
const photos = user?.photos || chat?.photos || [];
|
const photos = user?.photos || chat?.photos || [];
|
||||||
const slideAnimation = animationLevel >= 1 ? 'slide' : 'none';
|
const slideAnimation = animationLevel >= 1
|
||||||
|
? (lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized')
|
||||||
|
: 'none';
|
||||||
|
|
||||||
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
||||||
const isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0;
|
const isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0;
|
||||||
@ -70,8 +74,6 @@ const ProfileInfo: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
}
|
}
|
||||||
}, [currentPhotoIndex, photos.length]);
|
}, [currentPhotoIndex, photos.length]);
|
||||||
|
|
||||||
const lang = useLang();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connectionState === 'connectionStateReady' && userId && !forceShowSelf) {
|
if (connectionState === 'connectionStateReady' && userId && !forceShowSelf) {
|
||||||
loadFullUser({ userId });
|
loadFullUser({ userId });
|
||||||
|
|||||||
@ -228,7 +228,7 @@ const ChatFolders: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
<Transition
|
<Transition
|
||||||
ref={transitionRef}
|
ref={transitionRef}
|
||||||
name={shouldSkipHistoryAnimations ? 'none' : lang.isRtl ? 'slide-reversed' : 'slide'}
|
name={shouldSkipHistoryAnimations ? 'none' : lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized'}
|
||||||
activeKey={activeChatFolder}
|
activeKey={activeChatFolder}
|
||||||
renderCount={folderTabs ? folderTabs.length : undefined}
|
renderCount={folderTabs ? folderTabs.length : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -87,7 +87,7 @@ const LeftSearch: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
<div className="LeftSearch" ref={containerRef} onKeyDown={handleKeyDown}>
|
<div className="LeftSearch" ref={containerRef} onKeyDown={handleKeyDown}>
|
||||||
<TabList activeTab={activeTab} tabs={chatId ? CHAT_TABS : TABS} onSwitchTab={handleSwitchTab} />
|
<TabList activeTab={activeTab} tabs={chatId ? CHAT_TABS : TABS} onSwitchTab={handleSwitchTab} />
|
||||||
<Transition
|
<Transition
|
||||||
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
name={lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized'}
|
||||||
renderCount={TRANSITION_RENDER_COUNT}
|
renderCount={TRANSITION_RENDER_COUNT}
|
||||||
activeKey={currentContent}
|
activeKey={currentContent}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -403,7 +403,9 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
// Support for swipe gestures and closing on click
|
// Support for swipe gestures and closing on click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = document.querySelector<HTMLDivElement>('.slide-container > .active, .slide-container > .to');
|
const element = document.querySelector<HTMLDivElement>(
|
||||||
|
'.slide-container > .Transition__slide--active, .slide-container > .to',
|
||||||
|
);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ const SCALE_VALUES = {
|
|||||||
const ZOOM_SAFE_AREA = 150;
|
const ZOOM_SAFE_AREA = 150;
|
||||||
|
|
||||||
function calculateSafeZoneOnZoom(oldScale: number, matrixData: number[], wrapper: HTMLDivElement | null) {
|
function calculateSafeZoneOnZoom(oldScale: number, matrixData: number[], wrapper: HTMLDivElement | null) {
|
||||||
const image = wrapper && wrapper.querySelector('.active img');
|
const image = wrapper && wrapper.querySelector('.Transition__slide--active img');
|
||||||
if (!wrapper || !image) {
|
if (!wrapper || !image) {
|
||||||
return matrixData;
|
return matrixData;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,7 +96,7 @@ export function animateClosing(origin: MediaViewerOrigin, bestImageData: string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fromImage = document.getElementById('MediaViewer')!.querySelector<HTMLImageElement>(
|
const fromImage = document.getElementById('MediaViewer')!.querySelector<HTMLImageElement>(
|
||||||
'.active .media-viewer-content img, .active .media-viewer-content video',
|
'.Transition__slide--active .media-viewer-content img, .Transition__slide--active .media-viewer-content video',
|
||||||
);
|
);
|
||||||
if (!fromImage || !toImage) {
|
if (!fromImage || !toImage) {
|
||||||
return;
|
return;
|
||||||
@ -271,7 +271,7 @@ function isElementInViewport(el: HTMLElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isMessageImageFullyVisible(container: HTMLElement, imageEl: HTMLElement) {
|
function isMessageImageFullyVisible(container: HTMLElement, imageEl: HTMLElement) {
|
||||||
const messageListElement = document.querySelector<HTMLDivElement>('.active > .MessageList')!;
|
const messageListElement = document.querySelector<HTMLDivElement>('.Transition__slide--active > .MessageList')!;
|
||||||
let imgOffsetTop = container.offsetTop + imageEl.closest<HTMLDivElement>('.content-inner, .WebPage')!.offsetTop;
|
let imgOffsetTop = container.offsetTop + imageEl.closest<HTMLDivElement>('.content-inner, .WebPage')!.offsetTop;
|
||||||
if (container.id.includes('album-media-')) {
|
if (container.id.includes('album-media-')) {
|
||||||
imgOffsetTop += container.parentElement!.offsetTop + container.closest<HTMLDivElement>('.Message')!.offsetTop;
|
imgOffsetTop += container.parentElement!.offsetTop + container.closest<HTMLDivElement>('.Message')!.offsetTop;
|
||||||
@ -302,7 +302,7 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
|
|||||||
switch (origin) {
|
switch (origin) {
|
||||||
case MediaViewerOrigin.Album:
|
case MediaViewerOrigin.Album:
|
||||||
case MediaViewerOrigin.ScheduledAlbum:
|
case MediaViewerOrigin.ScheduledAlbum:
|
||||||
containerSelector = `.active > .MessageList #album-media-${message!.id}`;
|
containerSelector = `.Transition__slide--active > .MessageList #album-media-${message!.id}`;
|
||||||
mediaSelector = '.full-media';
|
mediaSelector = '.full-media';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -322,19 +322,19 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MediaViewerOrigin.SettingsAvatar:
|
case MediaViewerOrigin.SettingsAvatar:
|
||||||
containerSelector = '#Settings .ProfileInfo .active .ProfilePhoto';
|
containerSelector = '#Settings .ProfileInfo .Transition__slide--active .ProfilePhoto';
|
||||||
mediaSelector = 'img.avatar-media';
|
mediaSelector = 'img.avatar-media';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MediaViewerOrigin.ProfileAvatar:
|
case MediaViewerOrigin.ProfileAvatar:
|
||||||
containerSelector = '#RightColumn .ProfileInfo .active .ProfilePhoto';
|
containerSelector = '#RightColumn .ProfileInfo .Transition__slide--active .ProfilePhoto';
|
||||||
mediaSelector = 'img.avatar-media';
|
mediaSelector = 'img.avatar-media';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MediaViewerOrigin.ScheduledInline:
|
case MediaViewerOrigin.ScheduledInline:
|
||||||
case MediaViewerOrigin.Inline:
|
case MediaViewerOrigin.Inline:
|
||||||
default:
|
default:
|
||||||
containerSelector = `.active > .MessageList #message${message!.id}`;
|
containerSelector = `.Transition__slide--active > .MessageList #message${message!.id}`;
|
||||||
mediaSelector = '.message-content .full-media, .message-content .thumbnail';
|
mediaSelector = '.message-content .full-media, .message-content .thumbnail';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -302,7 +302,7 @@
|
|||||||
|
|
||||||
.Avatar {
|
.Avatar {
|
||||||
margin-right: .625rem;
|
margin-right: .625rem;
|
||||||
// TODO For some reason webpack imports `Audio.scss` second time when loading calls bundle
|
// TODO For some reason webpack imports `Audio.scss` second time when loading calls bundle
|
||||||
width: 2.5rem !important;
|
width: 2.5rem !important;
|
||||||
height: 2.5rem !important;
|
height: 2.5rem !important;
|
||||||
font-size: 1.0625rem;
|
font-size: 1.0625rem;
|
||||||
|
|||||||
@ -97,11 +97,11 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
const copyOptions = getMessageCopyOptions(message, onClose, canCopyLink ? onCopyLink : undefined);
|
const copyOptions = getMessageCopyOptions(message, onClose, canCopyLink ? onCopyLink : undefined);
|
||||||
|
|
||||||
const getTriggerElement = useCallback(() => {
|
const getTriggerElement = useCallback(() => {
|
||||||
return document.querySelector(`.active > .MessageList div[data-message-id="${message.id}"]`);
|
return document.querySelector(`.Transition__slide--active > .MessageList div[data-message-id="${message.id}"]`);
|
||||||
}, [message.id]);
|
}, [message.id]);
|
||||||
|
|
||||||
const getRootElement = useCallback(
|
const getRootElement = useCallback(
|
||||||
() => document.querySelector('.active > .MessageList'),
|
() => document.querySelector('.Transition__slide--active > .MessageList'),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -431,7 +431,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
>
|
>
|
||||||
<Transition
|
<Transition
|
||||||
ref={transitionRef}
|
ref={transitionRef}
|
||||||
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
name={lang.isRtl ? 'slide-rtl' : 'slide'}
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
renderCount={tabs.length}
|
renderCount={tabs.length}
|
||||||
shouldRestoreHeight
|
shouldRestoreHeight
|
||||||
@ -479,7 +479,7 @@ function buildInfiniteScrollItemSelector(resultType: string) {
|
|||||||
// Used on first render
|
// Used on first render
|
||||||
`.shared-media-transition > div:only-child > .${resultType}-list > .scroll-item`,
|
`.shared-media-transition > div:only-child > .${resultType}-list > .scroll-item`,
|
||||||
// Used after transition
|
// Used after transition
|
||||||
`.shared-media-transition > div.active > .${resultType}-list > .scroll-item`,
|
`.shared-media-transition > .Transition__slide--active > .${resultType}-list > .scroll-item`,
|
||||||
].join(', ');
|
].join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,16 +20,16 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&--active {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
|
|
||||||
i {
|
i {
|
||||||
opacity: 1;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.active):hover {
|
&:not(&--active):hover {
|
||||||
background: var(--color-interactive-element-hover);
|
background: var(--color-interactive-element-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,10 +57,10 @@
|
|||||||
body.is-macos & {
|
body.is-macos & {
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&__badge--active {
|
||||||
background: var(--color-primary);
|
background: var(--color-primary) !important;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, useLayoutEffect, useRef, memo,
|
FC, useRef, memo, useEffect,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import forceReflow from '../../util/forceReflow';
|
||||||
import renderText from '../common/helpers/renderText';
|
import renderText from '../common/helpers/renderText';
|
||||||
|
|
||||||
import './Tab.scss';
|
import './Tab.scss';
|
||||||
@ -10,7 +11,7 @@ import './Tab.scss';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
title: string;
|
title: string;
|
||||||
active?: boolean;
|
isActive?: boolean;
|
||||||
badgeCount?: number;
|
badgeCount?: number;
|
||||||
isBadgeActive?: boolean;
|
isBadgeActive?: boolean;
|
||||||
previousActiveTab?: number;
|
previousActiveTab?: number;
|
||||||
@ -18,10 +19,15 @@ type OwnProps = {
|
|||||||
clickArg: number;
|
clickArg: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const classNames = {
|
||||||
|
active: 'Tab--active',
|
||||||
|
badgeActive: 'Tab__badge--active',
|
||||||
|
};
|
||||||
|
|
||||||
const Tab: FC<OwnProps> = ({
|
const Tab: FC<OwnProps> = ({
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
active,
|
isActive,
|
||||||
badgeCount,
|
badgeCount,
|
||||||
isBadgeActive,
|
isBadgeActive,
|
||||||
previousActiveTab,
|
previousActiveTab,
|
||||||
@ -29,56 +35,51 @@ const Tab: FC<OwnProps> = ({
|
|||||||
clickArg,
|
clickArg,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const tabRef = useRef<HTMLButtonElement>(null);
|
const tabRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
if (!active || previousActiveTab === undefined) {
|
if (!isActive || previousActiveTab === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tab = tabRef.current!;
|
const tabEl = tabRef.current!;
|
||||||
const indicator = tab.querySelector('i')!;
|
const prevTabEl = tabEl.parentElement!.children[previousActiveTab];
|
||||||
const prevTab = tab.parentElement!.children[previousActiveTab];
|
if (!prevTabEl) {
|
||||||
if (!prevTab) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentIndicator = prevTab.querySelector('i')!;
|
|
||||||
|
|
||||||
currentIndicator.classList.remove('animate');
|
const platformEl = tabEl.querySelector('i')!;
|
||||||
indicator.classList.remove('animate');
|
const prevPlatformEl = prevTabEl.querySelector('i')!;
|
||||||
|
|
||||||
// We move and resize our indicator so it repeats the position and size of the previous one.
|
// We move and resize the platform, so it repeats the position and size of the previous one
|
||||||
const shiftLeft = currentIndicator.parentElement!.offsetLeft - indicator.parentElement!.offsetLeft;
|
const shiftLeft = prevPlatformEl.parentElement!.offsetLeft - platformEl.parentElement!.offsetLeft;
|
||||||
const scaleFactor = currentIndicator.clientWidth / indicator.clientWidth;
|
const scaleFactor = prevPlatformEl.clientWidth / platformEl.clientWidth;
|
||||||
indicator.style.transform = `translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`;
|
|
||||||
|
|
||||||
// 3 AFs needed here to synchronize animations with Transition component
|
prevPlatformEl.classList.remove('animate');
|
||||||
requestAnimationFrame(() => {
|
platformEl.classList.remove('animate');
|
||||||
requestAnimationFrame(() => {
|
platformEl.style.transform = `translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`;
|
||||||
requestAnimationFrame(() => {
|
forceReflow(platformEl);
|
||||||
// Now we remove the transform to let it animate to its own position and size.
|
platformEl.classList.add('animate');
|
||||||
indicator.classList.add('animate');
|
platformEl.style.transform = 'none';
|
||||||
indicator.style.transform = 'none';
|
|
||||||
});
|
prevTabEl.classList.remove(classNames.active);
|
||||||
});
|
tabEl.classList.add(classNames.active);
|
||||||
});
|
}, [isActive, previousActiveTab]);
|
||||||
}, [active, previousActiveTab]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
type="button"
|
className={buildClassName('Tab', className)}
|
||||||
className={buildClassName('Tab', className, active && 'active')}
|
|
||||||
onClick={() => onClick(clickArg)}
|
onClick={() => onClick(clickArg)}
|
||||||
ref={tabRef}
|
ref={tabRef}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{renderText(title)}
|
{renderText(title)}
|
||||||
{!!badgeCount && (
|
{!!badgeCount && (
|
||||||
<span className={buildClassName('badge', isBadgeActive && 'active')}>{badgeCount}</span>
|
<span className={buildClassName('badge', isBadgeActive && classNames.badgeActive)}>{badgeCount}</span>
|
||||||
)}
|
)}
|
||||||
<i />
|
<i />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -26,5 +26,7 @@
|
|||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
// `box-shadow` prevents repaint on macOS when hovering out of scrollable container
|
||||||
|
box-shadow: 0 0 1px rgba(255, 255, 255, .01);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React, {
|
|||||||
FC, memo, useRef, useEffect,
|
FC, memo, useRef, useEffect,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { IS_ANDROID, IS_IOS } from '../../util/environment';
|
||||||
import fastSmoothScrollHorizontal from '../../util/fastSmoothScrollHorizontal';
|
import fastSmoothScrollHorizontal from '../../util/fastSmoothScrollHorizontal';
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||||
@ -25,6 +26,8 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TAB_SCROLL_THRESHOLD_PX = 16;
|
const TAB_SCROLL_THRESHOLD_PX = 16;
|
||||||
|
// Should match duration from `--slide-transition` CSS variable
|
||||||
|
const SCROLL_DURATION = IS_IOS || IS_ANDROID ? 450 : 300;
|
||||||
|
|
||||||
const TabList: FC<OwnProps> = ({
|
const TabList: FC<OwnProps> = ({
|
||||||
tabs, activeTab, big, onSwitchTab,
|
tabs, activeTab, big, onSwitchTab,
|
||||||
@ -38,21 +41,24 @@ const TabList: FC<OwnProps> = ({
|
|||||||
// Scroll container to place active tab in the center
|
// Scroll container to place active tab in the center
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current!;
|
const container = containerRef.current!;
|
||||||
if (container.scrollWidth <= container.offsetWidth) {
|
const { scrollWidth, offsetWidth, scrollLeft } = container;
|
||||||
|
if (scrollWidth <= offsetWidth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeTabElement = container.querySelector('.Tab.active') as HTMLElement | null;
|
const activeTabElement = container.childNodes[activeTab] as HTMLElement | null;
|
||||||
if (activeTabElement) {
|
if (!activeTabElement) {
|
||||||
const newLeft = activeTabElement.offsetLeft - (container.offsetWidth / 2) + (activeTabElement.offsetWidth / 2);
|
return;
|
||||||
|
|
||||||
// Prevent scrolling by only a couple of pixels, which doesn't look smooth
|
|
||||||
if (Math.abs(newLeft - container.scrollLeft) < TAB_SCROLL_THRESHOLD_PX) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fastSmoothScrollHorizontal(container, newLeft);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newLeft = activeTabElement.offsetLeft - (offsetWidth / 2) + (activeTabElement.offsetWidth / 2);
|
||||||
|
|
||||||
|
// Prevent scrolling by only a couple of pixels, which doesn't look smooth
|
||||||
|
if (Math.abs(newLeft - scrollLeft) < TAB_SCROLL_THRESHOLD_PX) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fastSmoothScrollHorizontal(container, newLeft, SCROLL_DURATION);
|
||||||
}, [activeTab]);
|
}, [activeTab]);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -67,7 +73,7 @@ const TabList: FC<OwnProps> = ({
|
|||||||
<Tab
|
<Tab
|
||||||
key={tab.title}
|
key={tab.title}
|
||||||
title={lang(tab.title)}
|
title={lang(tab.title)}
|
||||||
active={i === activeTab}
|
isActive={i === activeTab}
|
||||||
badgeCount={tab.badgeCount}
|
badgeCount={tab.badgeCount}
|
||||||
isBadgeActive={tab.isBadgeActive}
|
isBadgeActive={tab.isBadgeActive}
|
||||||
previousActiveTab={previousActiveTab}
|
previousActiveTab={previousActiveTab}
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.active):not(.from):not(.to) {
|
&:not(.Transition__slide--active):not(.from):not(.to) {
|
||||||
display: none !important; // Best performance when animating container
|
display: none !important; // Best performance when animating container
|
||||||
//transform: scale(0); // Shortest initial delay
|
//transform: scale(0); // Shortest initial delay
|
||||||
}
|
}
|
||||||
@ -26,6 +26,20 @@
|
|||||||
/*
|
/*
|
||||||
* slide
|
* slide
|
||||||
*/
|
*/
|
||||||
|
&.slide-optimized,
|
||||||
|
&.slide-optimized-rtl {
|
||||||
|
// Create a new composite layer to reduce the page repaint area when switching a tab
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
|
||||||
|
#root & > div {
|
||||||
|
position: absolute;
|
||||||
|
display: block !important;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
transition: transform var(--slide-transition);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.slide {
|
&.slide {
|
||||||
> .to {
|
> .to {
|
||||||
@ -60,7 +74,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&.slide-reversed {
|
&.slide-rtl {
|
||||||
> .to {
|
> .to {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,12 @@ import React, {
|
|||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getGlobal } from '../../lib/teact/teactn';
|
import { getGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import forceReflow from '../../util/forceReflow';
|
||||||
|
import { waitForAnimationEnd, waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
||||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
import buildClassName from '../../util/buildClassName';
|
|
||||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||||
import { waitForAnimationEnd } from '../../util/cssAnimationEndListeners';
|
|
||||||
|
|
||||||
import './Transition.scss';
|
import './Transition.scss';
|
||||||
|
|
||||||
@ -17,8 +18,8 @@ type OwnProps = {
|
|||||||
ref?: RefObject<HTMLDivElement>;
|
ref?: RefObject<HTMLDivElement>;
|
||||||
activeKey: number;
|
activeKey: number;
|
||||||
name: (
|
name: (
|
||||||
'none' | 'slide' | 'slide-reversed' | 'mv-slide' | 'slide-fade' | 'zoom-fade' | 'slide-layers'
|
'none' | 'slide' | 'slide-rtl' | 'mv-slide' | 'slide-fade' | 'zoom-fade' | 'slide-layers'
|
||||||
| 'fade' | 'push-slide' | 'reveal'
|
| 'fade' | 'push-slide' | 'reveal' | 'slide-optimized' | 'slide-optimized-rtl'
|
||||||
);
|
);
|
||||||
direction?: 'auto' | 'inverse' | 1 | -1;
|
direction?: 'auto' | 'inverse' | 1 | -1;
|
||||||
renderCount?: number;
|
renderCount?: number;
|
||||||
@ -27,13 +28,17 @@ type OwnProps = {
|
|||||||
cleanupExceptionKey?: number;
|
cleanupExceptionKey?: number;
|
||||||
id?: string;
|
id?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onStart?: () => void;
|
onStart?: NoneToVoidFunction;
|
||||||
onStop?: () => void;
|
onStop?: NoneToVoidFunction;
|
||||||
children: ChildrenFn;
|
children: ChildrenFn;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CLEANED_UP = Symbol('CLEANED_UP');
|
const CLEANED_UP = Symbol('CLEANED_UP');
|
||||||
|
|
||||||
|
const classNames = {
|
||||||
|
active: 'Transition__slide--active',
|
||||||
|
};
|
||||||
|
|
||||||
const Transition: FC<OwnProps> = ({
|
const Transition: FC<OwnProps> = ({
|
||||||
ref,
|
ref,
|
||||||
activeKey,
|
activeKey,
|
||||||
@ -85,7 +90,11 @@ const Transition: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const childElements = container.children;
|
const childElements = container.children;
|
||||||
if (childElements.length === 1 && !activeKeyChanged) {
|
if (childElements.length === 1 && !activeKeyChanged) {
|
||||||
childElements[0].classList.add('active');
|
if (name.startsWith('slide-optimized')) {
|
||||||
|
(childElements[0] as HTMLElement).style.transform = 'translate3d(0, 0, 0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
childElements[0].classList.add(classNames.active);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -96,24 +105,44 @@ const Transition: FC<OwnProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentKeyRef.current = activeKey;
|
||||||
|
|
||||||
const isBackwards = (
|
const isBackwards = (
|
||||||
direction === -1
|
direction === -1
|
||||||
|| (direction === 'auto' && prevActiveKey > activeKey)
|
|| (direction === 'auto' && prevActiveKey > activeKey)
|
||||||
|| (direction === 'inverse' && prevActiveKey < activeKey)
|
|| (direction === 'inverse' && prevActiveKey < activeKey)
|
||||||
);
|
);
|
||||||
|
|
||||||
container.classList.remove('animating');
|
|
||||||
container.classList.toggle('backwards', isBackwards);
|
|
||||||
|
|
||||||
const keys = Object.keys(rendersRef.current).map(Number);
|
const keys = Object.keys(rendersRef.current).map(Number);
|
||||||
const prevActiveIndex = renderCount ? prevActiveKey : keys.indexOf(prevActiveKey);
|
const prevActiveIndex = renderCount ? prevActiveKey : keys.indexOf(prevActiveKey);
|
||||||
const activeIndex = renderCount ? activeKey : keys.indexOf(activeKey);
|
const activeIndex = renderCount ? activeKey : keys.indexOf(activeKey);
|
||||||
|
|
||||||
|
if (name === 'slide-optimized' || name === 'slide-optimized-rtl') {
|
||||||
|
performSlideOptimized(
|
||||||
|
name,
|
||||||
|
isBackwards,
|
||||||
|
cleanup,
|
||||||
|
activeKey,
|
||||||
|
currentKeyRef,
|
||||||
|
container,
|
||||||
|
shouldRestoreHeight,
|
||||||
|
onStart,
|
||||||
|
onStop,
|
||||||
|
childNodes[activeIndex] as HTMLElement,
|
||||||
|
childNodes[prevActiveIndex] as HTMLElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.classList.remove('animating');
|
||||||
|
container.classList.toggle('backwards', isBackwards);
|
||||||
|
|
||||||
if (name === 'none' || animationLevel === 0) {
|
if (name === 'none' || animationLevel === 0) {
|
||||||
childNodes.forEach((node, i) => {
|
childNodes.forEach((node, i) => {
|
||||||
if (node instanceof HTMLElement) {
|
if (node instanceof HTMLElement) {
|
||||||
node.classList.remove('from', 'through', 'to');
|
node.classList.remove('from', 'through', 'to');
|
||||||
node.classList.toggle('active', i === activeIndex);
|
node.classList.toggle(classNames.active, i === activeIndex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -124,7 +153,7 @@ const Transition: FC<OwnProps> = ({
|
|||||||
|
|
||||||
childNodes.forEach((node, i) => {
|
childNodes.forEach((node, i) => {
|
||||||
if (node instanceof HTMLElement) {
|
if (node instanceof HTMLElement) {
|
||||||
node.classList.remove('active');
|
node.classList.remove(classNames.active);
|
||||||
node.classList.toggle('from', i === prevActiveIndex);
|
node.classList.toggle('from', i === prevActiveIndex);
|
||||||
node.classList.toggle('through', (
|
node.classList.toggle('through', (
|
||||||
(i > prevActiveIndex && i < activeIndex) || (i < prevActiveIndex && i > activeIndex)
|
(i > prevActiveIndex && i < activeIndex) || (i < prevActiveIndex && i > activeIndex)
|
||||||
@ -138,9 +167,7 @@ const Transition: FC<OwnProps> = ({
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
container.classList.add('animating');
|
container.classList.add('animating');
|
||||||
|
|
||||||
if (onStart) {
|
onStart?.();
|
||||||
onStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAnimationEnd() {
|
function onAnimationEnd() {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@ -153,12 +180,12 @@ const Transition: FC<OwnProps> = ({
|
|||||||
childNodes.forEach((node, i) => {
|
childNodes.forEach((node, i) => {
|
||||||
if (node instanceof HTMLElement) {
|
if (node instanceof HTMLElement) {
|
||||||
node.classList.remove('from', 'through', 'to');
|
node.classList.remove('from', 'through', 'to');
|
||||||
node.classList.toggle('active', i === activeIndex);
|
node.classList.toggle(classNames.active, i === activeIndex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shouldRestoreHeight) {
|
if (shouldRestoreHeight) {
|
||||||
const activeElement = container.querySelector<HTMLDivElement>('.active');
|
const activeElement = container.querySelector<HTMLDivElement>(`.${classNames.active}`);
|
||||||
|
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
activeElement.style.height = 'auto';
|
activeElement.style.height = 'auto';
|
||||||
@ -166,13 +193,9 @@ const Transition: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onStop?.();
|
||||||
dispatchHeavyAnimationStop();
|
dispatchHeavyAnimationStop();
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
if (onStop) {
|
|
||||||
onStop();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,8 +205,6 @@ const Transition: FC<OwnProps> = ({
|
|||||||
? childNodes[prevActiveIndex]
|
? childNodes[prevActiveIndex]
|
||||||
: childNodes[activeIndex];
|
: childNodes[activeIndex];
|
||||||
|
|
||||||
currentKeyRef.current = activeKey;
|
|
||||||
|
|
||||||
if (watchedNode) {
|
if (watchedNode) {
|
||||||
waitForAnimationEnd(watchedNode, onAnimationEnd);
|
waitForAnimationEnd(watchedNode, onAnimationEnd);
|
||||||
} else {
|
} else {
|
||||||
@ -209,7 +230,7 @@ const Transition: FC<OwnProps> = ({
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (shouldRestoreHeight) {
|
if (shouldRestoreHeight) {
|
||||||
const container = containerRef.current!;
|
const container = containerRef.current!;
|
||||||
const activeElement = container.querySelector<HTMLDivElement>('.active')
|
const activeElement = container.querySelector<HTMLDivElement>(`.${classNames.active}`)
|
||||||
|| container.querySelector<HTMLDivElement>('.from');
|
|| container.querySelector<HTMLDivElement>('.from');
|
||||||
|
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
@ -222,27 +243,85 @@ const Transition: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const renders = rendersRef.current;
|
const renders = rendersRef.current;
|
||||||
const collection = Object.keys(renderCount ? new Array(renderCount).fill(undefined) : renders).map(Number);
|
const collection = Object.keys(renderCount ? new Array(renderCount).fill(undefined) : renders).map(Number);
|
||||||
|
|
||||||
const contents = collection.map((key) => {
|
const contents = collection.map((key) => {
|
||||||
const render = renders[key];
|
const render = renders[key];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
typeof render === 'function'
|
typeof render === 'function' ? (
|
||||||
? <div key={key}>{render(key === activeKey, key === prevActiveKey, activeKey)}</div>
|
<div key={key} teactOrderKey={key}>{render(key === activeKey, key === prevActiveKey, activeKey)}</div>
|
||||||
: undefined
|
) : undefined
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullClassName = buildClassName(
|
|
||||||
'Transition',
|
|
||||||
className,
|
|
||||||
name,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} id={id} className={fullClassName}>
|
<div ref={containerRef} id={id} className={buildClassName('Transition', className, name)} teactFastList>
|
||||||
{contents}
|
{contents}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Transition;
|
export default Transition;
|
||||||
|
|
||||||
|
function performSlideOptimized(
|
||||||
|
name: 'slide-optimized' | 'slide-optimized-rtl',
|
||||||
|
isBackwards: boolean,
|
||||||
|
cleanup: NoneToVoidFunction,
|
||||||
|
activeKey: number,
|
||||||
|
currentKeyRef: { current: number | undefined },
|
||||||
|
container: HTMLElement,
|
||||||
|
shouldRestoreHeight?: boolean,
|
||||||
|
onStart?: NoneToVoidFunction,
|
||||||
|
onStop?: NoneToVoidFunction,
|
||||||
|
currentSlide?: HTMLElement,
|
||||||
|
prevSlide?: HTMLElement,
|
||||||
|
) {
|
||||||
|
if (!prevSlide || !currentSlide) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'slide-optimized-rtl') {
|
||||||
|
isBackwards = !isBackwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent();
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
onStart?.();
|
||||||
|
|
||||||
|
prevSlide.style.transition = 'none';
|
||||||
|
prevSlide.style.transform = 'translate3d(0, 0, 0)';
|
||||||
|
|
||||||
|
currentSlide.style.transition = 'none';
|
||||||
|
currentSlide.style.transform = `translate3d(${isBackwards ? '-' : ''}100%, 0, 0)`;
|
||||||
|
|
||||||
|
forceReflow(currentSlide);
|
||||||
|
|
||||||
|
prevSlide.style.transition = '';
|
||||||
|
prevSlide.style.transform = `translate3d(${isBackwards ? '' : '-'}100%, 0, 0)`;
|
||||||
|
|
||||||
|
currentSlide.style.transition = '';
|
||||||
|
currentSlide.style.transform = 'translate3d(0, 0, 0)';
|
||||||
|
|
||||||
|
prevSlide.classList.remove(classNames.active);
|
||||||
|
currentSlide.classList.add(classNames.active);
|
||||||
|
|
||||||
|
waitForTransitionEnd(prevSlide, () => {
|
||||||
|
if (activeKey !== currentKeyRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSlide.style.transition = 'none';
|
||||||
|
prevSlide.style.transform = '';
|
||||||
|
|
||||||
|
if (shouldRestoreHeight) {
|
||||||
|
currentSlide.style.height = 'auto';
|
||||||
|
container.style.height = `${currentSlide.clientHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onStop?.();
|
||||||
|
dispatchHeavyAnimationStop();
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// @optimization
|
// @optimization
|
||||||
@mixin while-transition() {
|
@mixin while-transition() {
|
||||||
.Transition > div:not(.active) &, body.animating-right-column & {
|
.Transition > div:not(.Transition__slide--active) &, body.animating-right-column & {
|
||||||
@content;
|
@content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -210,21 +210,15 @@ $color-user-8: #faa774;
|
|||||||
|
|
||||||
--layer-transition: 300ms cubic-bezier(0.33, 1, 0.68, 1);
|
--layer-transition: 300ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||||
--layer-blackout-opacity: 0.3;
|
--layer-blackout-opacity: 0.3;
|
||||||
|
|
||||||
// For some reason these parameters cause worse animation in desktop Chrome
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
--layer-transition: 450ms cubic-bezier(0.33, 1, 0.68, 1);
|
|
||||||
|
|
||||||
body.is-android {
|
|
||||||
--layer-transition: 450ms cubic-bezier(0.25, 1, 0.5, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
--slide-transition: 300ms cubic-bezier(0.25, 1, 0.5, 1);
|
--slide-transition: 300ms cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
--select-transition: 200ms ease-out;
|
||||||
|
|
||||||
|
body.is-ios {
|
||||||
|
--layer-transition: 450ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||||
|
}
|
||||||
|
|
||||||
body.is-android {
|
body.is-android {
|
||||||
--slide-transition: 200ms ease-in-out;
|
--layer-transition: 450ms cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
--slide-transition: 450ms cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
--select-transition: 200ms ease-out;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,7 +137,9 @@ body.cursor-ew-resize {
|
|||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(90, 90, 90, 0);
|
background-color: rgba(90, 90, 90, 0);
|
||||||
border-radius: 0.35rem;
|
border-radius: 0.375rem;
|
||||||
|
// `box-shadow` prevents repaint on macOS when hovering out of scrollable container
|
||||||
|
box-shadow: 0 0 1px rgba(255, 255, 255, .01);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover, &:focus, &:focus-within {
|
&:hover, &:focus, &:focus-within {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { getGlobal } from '../lib/teact/teactn';
|
import { getGlobal } from '../lib/teact/teactn';
|
||||||
|
|
||||||
import { ANIMATION_LEVEL_MIN } from '../config';
|
import { ANIMATION_LEVEL_MIN } from '../config';
|
||||||
import { IS_IOS } from './environment';
|
|
||||||
import { animate } from './animation';
|
import { animate } from './animation';
|
||||||
|
|
||||||
const DEFAULT_DURATION = 300;
|
const DEFAULT_DURATION = 300;
|
||||||
@ -11,15 +10,7 @@ export default function fastSmoothScrollHorizontal(container: HTMLElement, left:
|
|||||||
duration = 0;
|
duration = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native way seems to be smoother in Chrome
|
scrollWithJs(container, left, duration);
|
||||||
if (!IS_IOS) {
|
|
||||||
container.scrollTo({
|
|
||||||
left,
|
|
||||||
...(duration && { behavior: 'smooth' }),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
scrollWithJs(container, left, duration);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollWithJs(container: HTMLElement, left: number, duration: number) {
|
function scrollWithJs(container: HTMLElement, left: number, duration: number) {
|
||||||
|
|||||||
5
src/util/forceReflow.ts
Normal file
5
src/util/forceReflow.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// Used before applying CSS transition
|
||||||
|
export default function forceReflow(element: HTMLElement) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
|
element.offsetWidth;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user