[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,
|
||||
openMediaViewer,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
const { id: userId } = user || {};
|
||||
const { id: chatId } = chat || {};
|
||||
const fullName = user ? getUserFullName(user) : (chat ? chat.title : '');
|
||||
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 isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0;
|
||||
@ -70,8 +74,6 @@ const ProfileInfo: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
}
|
||||
}, [currentPhotoIndex, photos.length]);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionState === 'connectionStateReady' && userId && !forceShowSelf) {
|
||||
loadFullUser({ userId });
|
||||
|
||||
@ -228,7 +228,7 @@ const ChatFolders: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
) : undefined}
|
||||
<Transition
|
||||
ref={transitionRef}
|
||||
name={shouldSkipHistoryAnimations ? 'none' : lang.isRtl ? 'slide-reversed' : 'slide'}
|
||||
name={shouldSkipHistoryAnimations ? 'none' : lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized'}
|
||||
activeKey={activeChatFolder}
|
||||
renderCount={folderTabs ? folderTabs.length : undefined}
|
||||
>
|
||||
|
||||
@ -87,7 +87,7 @@ const LeftSearch: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
<div className="LeftSearch" ref={containerRef} onKeyDown={handleKeyDown}>
|
||||
<TabList activeTab={activeTab} tabs={chatId ? CHAT_TABS : TABS} onSwitchTab={handleSwitchTab} />
|
||||
<Transition
|
||||
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
||||
name={lang.isRtl ? 'slide-optimized-rtl' : 'slide-optimized'}
|
||||
renderCount={TRANSITION_RENDER_COUNT}
|
||||
activeKey={currentContent}
|
||||
>
|
||||
|
||||
@ -403,7 +403,9 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
|
||||
|
||||
// Support for swipe gestures and closing on click
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ const SCALE_VALUES = {
|
||||
const ZOOM_SAFE_AREA = 150;
|
||||
|
||||
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) {
|
||||
return matrixData;
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ export function animateClosing(origin: MediaViewerOrigin, bestImageData: string,
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
@ -271,7 +271,7 @@ function isElementInViewport(el: 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;
|
||||
if (container.id.includes('album-media-')) {
|
||||
imgOffsetTop += container.parentElement!.offsetTop + container.closest<HTMLDivElement>('.Message')!.offsetTop;
|
||||
@ -302,7 +302,7 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
|
||||
switch (origin) {
|
||||
case MediaViewerOrigin.Album:
|
||||
case MediaViewerOrigin.ScheduledAlbum:
|
||||
containerSelector = `.active > .MessageList #album-media-${message!.id}`;
|
||||
containerSelector = `.Transition__slide--active > .MessageList #album-media-${message!.id}`;
|
||||
mediaSelector = '.full-media';
|
||||
break;
|
||||
|
||||
@ -322,19 +322,19 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.SettingsAvatar:
|
||||
containerSelector = '#Settings .ProfileInfo .active .ProfilePhoto';
|
||||
containerSelector = '#Settings .ProfileInfo .Transition__slide--active .ProfilePhoto';
|
||||
mediaSelector = 'img.avatar-media';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.ProfileAvatar:
|
||||
containerSelector = '#RightColumn .ProfileInfo .active .ProfilePhoto';
|
||||
containerSelector = '#RightColumn .ProfileInfo .Transition__slide--active .ProfilePhoto';
|
||||
mediaSelector = 'img.avatar-media';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.ScheduledInline:
|
||||
case MediaViewerOrigin.Inline:
|
||||
default:
|
||||
containerSelector = `.active > .MessageList #message${message!.id}`;
|
||||
containerSelector = `.Transition__slide--active > .MessageList #message${message!.id}`;
|
||||
mediaSelector = '.message-content .full-media, .message-content .thumbnail';
|
||||
}
|
||||
|
||||
|
||||
@ -302,7 +302,7 @@
|
||||
|
||||
.Avatar {
|
||||
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;
|
||||
height: 2.5rem !important;
|
||||
font-size: 1.0625rem;
|
||||
|
||||
@ -97,11 +97,11 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
const copyOptions = getMessageCopyOptions(message, onClose, canCopyLink ? onCopyLink : undefined);
|
||||
|
||||
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]);
|
||||
|
||||
const getRootElement = useCallback(
|
||||
() => document.querySelector('.active > .MessageList'),
|
||||
() => document.querySelector('.Transition__slide--active > .MessageList'),
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
@ -431,7 +431,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
>
|
||||
<Transition
|
||||
ref={transitionRef}
|
||||
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
||||
name={lang.isRtl ? 'slide-rtl' : 'slide'}
|
||||
activeKey={activeKey}
|
||||
renderCount={tabs.length}
|
||||
shouldRestoreHeight
|
||||
@ -479,7 +479,7 @@ function buildInfiniteScrollItemSelector(resultType: string) {
|
||||
// Used on first render
|
||||
`.shared-media-transition > div:only-child > .${resultType}-list > .scroll-item`,
|
||||
// Used after transition
|
||||
`.shared-media-transition > div.active > .${resultType}-list > .scroll-item`,
|
||||
`.shared-media-transition > .Transition__slide--active > .${resultType}-list > .scroll-item`,
|
||||
].join(', ');
|
||||
}
|
||||
|
||||
|
||||
@ -20,16 +20,16 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
&--active {
|
||||
cursor: default;
|
||||
color: var(--color-primary);
|
||||
|
||||
i {
|
||||
opacity: 1;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active):hover {
|
||||
&:not(&--active):hover {
|
||||
background: var(--color-interactive-element-hover);
|
||||
}
|
||||
|
||||
@ -57,10 +57,10 @@
|
||||
body.is-macos & {
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
&__badge--active {
|
||||
background: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
i {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React, {
|
||||
FC, useLayoutEffect, useRef, memo,
|
||||
FC, useRef, memo, useEffect,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import forceReflow from '../../util/forceReflow';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import './Tab.scss';
|
||||
@ -10,7 +11,7 @@ import './Tab.scss';
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
title: string;
|
||||
active?: boolean;
|
||||
isActive?: boolean;
|
||||
badgeCount?: number;
|
||||
isBadgeActive?: boolean;
|
||||
previousActiveTab?: number;
|
||||
@ -18,10 +19,15 @@ type OwnProps = {
|
||||
clickArg: number;
|
||||
};
|
||||
|
||||
const classNames = {
|
||||
active: 'Tab--active',
|
||||
badgeActive: 'Tab__badge--active',
|
||||
};
|
||||
|
||||
const Tab: FC<OwnProps> = ({
|
||||
className,
|
||||
title,
|
||||
active,
|
||||
isActive,
|
||||
badgeCount,
|
||||
isBadgeActive,
|
||||
previousActiveTab,
|
||||
@ -29,56 +35,51 @@ const Tab: FC<OwnProps> = ({
|
||||
clickArg,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const tabRef = useRef<HTMLButtonElement>(null);
|
||||
const tabRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!active || previousActiveTab === undefined) {
|
||||
useEffect(() => {
|
||||
if (!isActive || previousActiveTab === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = tabRef.current!;
|
||||
const indicator = tab.querySelector('i')!;
|
||||
const prevTab = tab.parentElement!.children[previousActiveTab];
|
||||
if (!prevTab) {
|
||||
const tabEl = tabRef.current!;
|
||||
const prevTabEl = tabEl.parentElement!.children[previousActiveTab];
|
||||
if (!prevTabEl) {
|
||||
return;
|
||||
}
|
||||
const currentIndicator = prevTab.querySelector('i')!;
|
||||
|
||||
currentIndicator.classList.remove('animate');
|
||||
indicator.classList.remove('animate');
|
||||
const platformEl = tabEl.querySelector('i')!;
|
||||
const prevPlatformEl = prevTabEl.querySelector('i')!;
|
||||
|
||||
// We move and resize our indicator so it repeats the position and size of the previous one.
|
||||
const shiftLeft = currentIndicator.parentElement!.offsetLeft - indicator.parentElement!.offsetLeft;
|
||||
const scaleFactor = currentIndicator.clientWidth / indicator.clientWidth;
|
||||
indicator.style.transform = `translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`;
|
||||
// We move and resize the platform, so it repeats the position and size of the previous one
|
||||
const shiftLeft = prevPlatformEl.parentElement!.offsetLeft - platformEl.parentElement!.offsetLeft;
|
||||
const scaleFactor = prevPlatformEl.clientWidth / platformEl.clientWidth;
|
||||
|
||||
// 3 AFs needed here to synchronize animations with Transition component
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// Now we remove the transform to let it animate to its own position and size.
|
||||
indicator.classList.add('animate');
|
||||
indicator.style.transform = 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [active, previousActiveTab]);
|
||||
prevPlatformEl.classList.remove('animate');
|
||||
platformEl.classList.remove('animate');
|
||||
platformEl.style.transform = `translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`;
|
||||
forceReflow(platformEl);
|
||||
platformEl.classList.add('animate');
|
||||
platformEl.style.transform = 'none';
|
||||
|
||||
prevTabEl.classList.remove(classNames.active);
|
||||
tabEl.classList.add(classNames.active);
|
||||
}, [isActive, previousActiveTab]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={buildClassName('Tab', className, active && 'active')}
|
||||
<div
|
||||
className={buildClassName('Tab', className)}
|
||||
onClick={() => onClick(clickArg)}
|
||||
ref={tabRef}
|
||||
>
|
||||
<span>
|
||||
{renderText(title)}
|
||||
{!!badgeCount && (
|
||||
<span className={buildClassName('badge', isBadgeActive && 'active')}>{badgeCount}</span>
|
||||
<span className={buildClassName('badge', isBadgeActive && classNames.badgeActive)}>{badgeCount}</span>
|
||||
)}
|
||||
<i />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -26,5 +26,7 @@
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
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,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import { IS_ANDROID, IS_IOS } from '../../util/environment';
|
||||
import fastSmoothScrollHorizontal from '../../util/fastSmoothScrollHorizontal';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
@ -25,6 +26,8 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
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> = ({
|
||||
tabs, activeTab, big, onSwitchTab,
|
||||
@ -38,21 +41,24 @@ const TabList: FC<OwnProps> = ({
|
||||
// Scroll container to place active tab in the center
|
||||
useEffect(() => {
|
||||
const container = containerRef.current!;
|
||||
if (container.scrollWidth <= container.offsetWidth) {
|
||||
const { scrollWidth, offsetWidth, scrollLeft } = container;
|
||||
if (scrollWidth <= offsetWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTabElement = container.querySelector('.Tab.active') as HTMLElement | null;
|
||||
if (activeTabElement) {
|
||||
const newLeft = activeTabElement.offsetLeft - (container.offsetWidth / 2) + (activeTabElement.offsetWidth / 2);
|
||||
|
||||
// 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 activeTabElement = container.childNodes[activeTab] as HTMLElement | null;
|
||||
if (!activeTabElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
const lang = useLang();
|
||||
@ -67,7 +73,7 @@ const TabList: FC<OwnProps> = ({
|
||||
<Tab
|
||||
key={tab.title}
|
||||
title={lang(tab.title)}
|
||||
active={i === activeTab}
|
||||
isActive={i === activeTab}
|
||||
badgeCount={tab.badgeCount}
|
||||
isBadgeActive={tab.isBadgeActive}
|
||||
previousActiveTab={previousActiveTab}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:not(.active):not(.from):not(.to) {
|
||||
&:not(.Transition__slide--active):not(.from):not(.to) {
|
||||
display: none !important; // Best performance when animating container
|
||||
//transform: scale(0); // Shortest initial delay
|
||||
}
|
||||
@ -26,6 +26,20 @@
|
||||
/*
|
||||
* 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 {
|
||||
> .to {
|
||||
@ -60,7 +74,7 @@
|
||||
}
|
||||
|
||||
|
||||
&.slide-reversed {
|
||||
&.slide-rtl {
|
||||
> .to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
@ -4,11 +4,12 @@ import React, {
|
||||
} from '../../lib/teact/teact';
|
||||
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 usePrevious from '../../hooks/usePrevious';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||
import { waitForAnimationEnd } from '../../util/cssAnimationEndListeners';
|
||||
|
||||
import './Transition.scss';
|
||||
|
||||
@ -17,8 +18,8 @@ type OwnProps = {
|
||||
ref?: RefObject<HTMLDivElement>;
|
||||
activeKey: number;
|
||||
name: (
|
||||
'none' | 'slide' | 'slide-reversed' | 'mv-slide' | 'slide-fade' | 'zoom-fade' | 'slide-layers'
|
||||
| 'fade' | 'push-slide' | 'reveal'
|
||||
'none' | 'slide' | 'slide-rtl' | 'mv-slide' | 'slide-fade' | 'zoom-fade' | 'slide-layers'
|
||||
| 'fade' | 'push-slide' | 'reveal' | 'slide-optimized' | 'slide-optimized-rtl'
|
||||
);
|
||||
direction?: 'auto' | 'inverse' | 1 | -1;
|
||||
renderCount?: number;
|
||||
@ -27,13 +28,17 @@ type OwnProps = {
|
||||
cleanupExceptionKey?: number;
|
||||
id?: string;
|
||||
className?: string;
|
||||
onStart?: () => void;
|
||||
onStop?: () => void;
|
||||
onStart?: NoneToVoidFunction;
|
||||
onStop?: NoneToVoidFunction;
|
||||
children: ChildrenFn;
|
||||
};
|
||||
|
||||
const CLEANED_UP = Symbol('CLEANED_UP');
|
||||
|
||||
const classNames = {
|
||||
active: 'Transition__slide--active',
|
||||
};
|
||||
|
||||
const Transition: FC<OwnProps> = ({
|
||||
ref,
|
||||
activeKey,
|
||||
@ -85,7 +90,11 @@ const Transition: FC<OwnProps> = ({
|
||||
|
||||
const childElements = container.children;
|
||||
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;
|
||||
}
|
||||
@ -96,24 +105,44 @@ const Transition: FC<OwnProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
currentKeyRef.current = activeKey;
|
||||
|
||||
const isBackwards = (
|
||||
direction === -1
|
||||
|| (direction === 'auto' && prevActiveKey > activeKey)
|
||||
|| (direction === 'inverse' && prevActiveKey < activeKey)
|
||||
);
|
||||
|
||||
container.classList.remove('animating');
|
||||
container.classList.toggle('backwards', isBackwards);
|
||||
|
||||
const keys = Object.keys(rendersRef.current).map(Number);
|
||||
const prevActiveIndex = renderCount ? prevActiveKey : keys.indexOf(prevActiveKey);
|
||||
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) {
|
||||
childNodes.forEach((node, i) => {
|
||||
if (node instanceof HTMLElement) {
|
||||
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) => {
|
||||
if (node instanceof HTMLElement) {
|
||||
node.classList.remove('active');
|
||||
node.classList.remove(classNames.active);
|
||||
node.classList.toggle('from', i === prevActiveIndex);
|
||||
node.classList.toggle('through', (
|
||||
(i > prevActiveIndex && i < activeIndex) || (i < prevActiveIndex && i > activeIndex)
|
||||
@ -138,9 +167,7 @@ const Transition: FC<OwnProps> = ({
|
||||
requestAnimationFrame(() => {
|
||||
container.classList.add('animating');
|
||||
|
||||
if (onStart) {
|
||||
onStart();
|
||||
}
|
||||
onStart?.();
|
||||
|
||||
function onAnimationEnd() {
|
||||
requestAnimationFrame(() => {
|
||||
@ -153,12 +180,12 @@ const Transition: FC<OwnProps> = ({
|
||||
childNodes.forEach((node, i) => {
|
||||
if (node instanceof HTMLElement) {
|
||||
node.classList.remove('from', 'through', 'to');
|
||||
node.classList.toggle('active', i === activeIndex);
|
||||
node.classList.toggle(classNames.active, i === activeIndex);
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldRestoreHeight) {
|
||||
const activeElement = container.querySelector<HTMLDivElement>('.active');
|
||||
const activeElement = container.querySelector<HTMLDivElement>(`.${classNames.active}`);
|
||||
|
||||
if (activeElement) {
|
||||
activeElement.style.height = 'auto';
|
||||
@ -166,13 +193,9 @@ const Transition: FC<OwnProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
onStop?.();
|
||||
dispatchHeavyAnimationStop();
|
||||
|
||||
cleanup();
|
||||
|
||||
if (onStop) {
|
||||
onStop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -182,8 +205,6 @@ const Transition: FC<OwnProps> = ({
|
||||
? childNodes[prevActiveIndex]
|
||||
: childNodes[activeIndex];
|
||||
|
||||
currentKeyRef.current = activeKey;
|
||||
|
||||
if (watchedNode) {
|
||||
waitForAnimationEnd(watchedNode, onAnimationEnd);
|
||||
} else {
|
||||
@ -209,7 +230,7 @@ const Transition: FC<OwnProps> = ({
|
||||
useLayoutEffect(() => {
|
||||
if (shouldRestoreHeight) {
|
||||
const container = containerRef.current!;
|
||||
const activeElement = container.querySelector<HTMLDivElement>('.active')
|
||||
const activeElement = container.querySelector<HTMLDivElement>(`.${classNames.active}`)
|
||||
|| container.querySelector<HTMLDivElement>('.from');
|
||||
|
||||
if (activeElement) {
|
||||
@ -222,27 +243,85 @@ const Transition: FC<OwnProps> = ({
|
||||
|
||||
const renders = rendersRef.current;
|
||||
const collection = Object.keys(renderCount ? new Array(renderCount).fill(undefined) : renders).map(Number);
|
||||
|
||||
const contents = collection.map((key) => {
|
||||
const render = renders[key];
|
||||
|
||||
return (
|
||||
typeof render === 'function'
|
||||
? <div key={key}>{render(key === activeKey, key === prevActiveKey, activeKey)}</div>
|
||||
: undefined
|
||||
typeof render === 'function' ? (
|
||||
<div key={key} teactOrderKey={key}>{render(key === activeKey, key === prevActiveKey, activeKey)}</div>
|
||||
) : undefined
|
||||
);
|
||||
});
|
||||
|
||||
const fullClassName = buildClassName(
|
||||
'Transition',
|
||||
className,
|
||||
name,
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} id={id} className={fullClassName}>
|
||||
<div ref={containerRef} id={id} className={buildClassName('Transition', className, name)} teactFastList>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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
|
||||
@mixin while-transition() {
|
||||
.Transition > div:not(.active) &, body.animating-right-column & {
|
||||
.Transition > div:not(.Transition__slide--active) &, body.animating-right-column & {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,21 +210,15 @@ $color-user-8: #faa774;
|
||||
|
||||
--layer-transition: 300ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||
--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);
|
||||
--select-transition: 200ms ease-out;
|
||||
|
||||
body.is-ios {
|
||||
--layer-transition: 450ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { getGlobal } from '../lib/teact/teactn';
|
||||
|
||||
import { ANIMATION_LEVEL_MIN } from '../config';
|
||||
import { IS_IOS } from './environment';
|
||||
import { animate } from './animation';
|
||||
|
||||
const DEFAULT_DURATION = 300;
|
||||
@ -11,15 +10,7 @@ export default function fastSmoothScrollHorizontal(container: HTMLElement, left:
|
||||
duration = 0;
|
||||
}
|
||||
|
||||
// Native way seems to be smoother in Chrome
|
||||
if (!IS_IOS) {
|
||||
container.scrollTo({
|
||||
left,
|
||||
...(duration && { behavior: 'smooth' }),
|
||||
});
|
||||
} else {
|
||||
scrollWithJs(container, left, duration);
|
||||
}
|
||||
scrollWithJs(container, left, duration);
|
||||
}
|
||||
|
||||
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