[Perf] Transition: Optimize slide transitions (#1562)

This commit is contained in:
Alexander Zinchuk 2021-12-10 18:32:30 +01:00
parent dee7d2163f
commit 3375954d5f
20 changed files with 229 additions and 131 deletions

View File

@ -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 });

View File

@ -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}
>

View File

@ -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}
>

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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';
}

View File

@ -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;

View File

@ -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'),
[],
);

View File

@ -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(', ');
}

View File

@ -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 {

View File

@ -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>
);
};

View File

@ -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);
}
}

View File

@ -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}

View File

@ -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%);
}

View File

@ -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();
});
});
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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
View 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;
}