diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 9efba47b6..adbd99cbe 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -53,11 +53,15 @@ const ProfileInfo: FC = ({ 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 = ({ } }, [currentPhotoIndex, photos.length]); - const lang = useLang(); - useEffect(() => { if (connectionState === 'connectionStateReady' && userId && !forceShowSelf) { loadFullUser({ userId }); diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 5641ddd54..51f8f98c4 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -228,7 +228,7 @@ const ChatFolders: FC = ({ ) : undefined} diff --git a/src/components/left/search/LeftSearch.tsx b/src/components/left/search/LeftSearch.tsx index 7523c0271..155ff83a6 100644 --- a/src/components/left/search/LeftSearch.tsx +++ b/src/components/left/search/LeftSearch.tsx @@ -87,7 +87,7 @@ const LeftSearch: FC = ({
diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index f375ecda6..1b3b78311 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -403,7 +403,9 @@ const MediaViewer: FC = ({ // Support for swipe gestures and closing on click useEffect(() => { - const element = document.querySelector('.slide-container > .active, .slide-container > .to'); + const element = document.querySelector( + '.slide-container > .Transition__slide--active, .slide-container > .to', + ); if (!element) { return undefined; } diff --git a/src/components/mediaViewer/PanZoom.tsx b/src/components/mediaViewer/PanZoom.tsx index d92c9824b..6ab922378 100644 --- a/src/components/mediaViewer/PanZoom.tsx +++ b/src/components/mediaViewer/PanZoom.tsx @@ -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; } diff --git a/src/components/mediaViewer/helpers/ghostAnimation.ts b/src/components/mediaViewer/helpers/ghostAnimation.ts index 595a459f2..c77e1ade9 100644 --- a/src/components/mediaViewer/helpers/ghostAnimation.ts +++ b/src/components/mediaViewer/helpers/ghostAnimation.ts @@ -96,7 +96,7 @@ export function animateClosing(origin: MediaViewerOrigin, bestImageData: string, } const fromImage = document.getElementById('MediaViewer')!.querySelector( - '.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('.active > .MessageList')!; + const messageListElement = document.querySelector('.Transition__slide--active > .MessageList')!; let imgOffsetTop = container.offsetTop + imageEl.closest('.content-inner, .WebPage')!.offsetTop; if (container.id.includes('album-media-')) { imgOffsetTop += container.parentElement!.offsetTop + container.closest('.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'; } diff --git a/src/components/middle/MiddleHeader.scss b/src/components/middle/MiddleHeader.scss index daa2f48e4..b60ae43a8 100644 --- a/src/components/middle/MiddleHeader.scss +++ b/src/components/middle/MiddleHeader.scss @@ -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; diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index df567b9a0..f9e19f788 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -97,11 +97,11 @@ const MessageContextMenu: FC = ({ 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'), [], ); diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index f0bdbaba6..90146a532 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -431,7 +431,7 @@ const Profile: FC = ({ > 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(', '); } diff --git a/src/components/ui/Tab.scss b/src/components/ui/Tab.scss index be0aa17ab..ba3de5f0b 100644 --- a/src/components/ui/Tab.scss +++ b/src/components/ui/Tab.scss @@ -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 { diff --git a/src/components/ui/Tab.tsx b/src/components/ui/Tab.tsx index fa9ad6a80..eb5850a32 100644 --- a/src/components/ui/Tab.tsx +++ b/src/components/ui/Tab.tsx @@ -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 = ({ className, title, - active, + isActive, badgeCount, isBadgeActive, previousActiveTab, @@ -29,56 +35,51 @@ const Tab: FC = ({ clickArg, }) => { // eslint-disable-next-line no-null/no-null - const tabRef = useRef(null); + const tabRef = useRef(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 ( - +
); }; diff --git a/src/components/ui/TabList.scss b/src/components/ui/TabList.scss index 814021043..2f247138f 100644 --- a/src/components/ui/TabList.scss +++ b/src/components/ui/TabList.scss @@ -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); } } diff --git a/src/components/ui/TabList.tsx b/src/components/ui/TabList.tsx index acb5f6872..3516eb751 100644 --- a/src/components/ui/TabList.tsx +++ b/src/components/ui/TabList.tsx @@ -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 = ({ tabs, activeTab, big, onSwitchTab, @@ -38,21 +41,24 @@ const TabList: FC = ({ // 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 = ({ 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%); } diff --git a/src/components/ui/Transition.tsx b/src/components/ui/Transition.tsx index 34d54b7a1..e3a3add54 100644 --- a/src/components/ui/Transition.tsx +++ b/src/components/ui/Transition.tsx @@ -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; 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 = ({ ref, activeKey, @@ -85,7 +90,11 @@ const Transition: FC = ({ 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 = ({ 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 = ({ 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 = ({ requestAnimationFrame(() => { container.classList.add('animating'); - if (onStart) { - onStart(); - } + onStart?.(); function onAnimationEnd() { requestAnimationFrame(() => { @@ -153,12 +180,12 @@ const Transition: FC = ({ 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('.active'); + const activeElement = container.querySelector(`.${classNames.active}`); if (activeElement) { activeElement.style.height = 'auto'; @@ -166,13 +193,9 @@ const Transition: FC = ({ } } + onStop?.(); dispatchHeavyAnimationStop(); - cleanup(); - - if (onStop) { - onStop(); - } }); } @@ -182,8 +205,6 @@ const Transition: FC = ({ ? childNodes[prevActiveIndex] : childNodes[activeIndex]; - currentKeyRef.current = activeKey; - if (watchedNode) { waitForAnimationEnd(watchedNode, onAnimationEnd); } else { @@ -209,7 +230,7 @@ const Transition: FC = ({ useLayoutEffect(() => { if (shouldRestoreHeight) { const container = containerRef.current!; - const activeElement = container.querySelector('.active') + const activeElement = container.querySelector(`.${classNames.active}`) || container.querySelector('.from'); if (activeElement) { @@ -222,27 +243,85 @@ const Transition: FC = ({ 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' - ?
{render(key === activeKey, key === prevActiveKey, activeKey)}
- : undefined + typeof render === 'function' ? ( +
{render(key === activeKey, key === prevActiveKey, activeKey)}
+ ) : undefined ); }); - const fullClassName = buildClassName( - 'Transition', - className, - name, - ); - return ( -
+
{contents}
); }; 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(); + }); + }); +} diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index afd0416d9..57f3bcb40 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -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; } } diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 6554a7647..19fe6a0a2 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -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; } diff --git a/src/styles/index.scss b/src/styles/index.scss index e7b7ce856..6f31cf742 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -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 { diff --git a/src/util/fastSmoothScrollHorizontal.ts b/src/util/fastSmoothScrollHorizontal.ts index 1d4463c2d..01552fb39 100644 --- a/src/util/fastSmoothScrollHorizontal.ts +++ b/src/util/fastSmoothScrollHorizontal.ts @@ -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) { diff --git a/src/util/forceReflow.ts b/src/util/forceReflow.ts new file mode 100644 index 000000000..8d5c72ee2 --- /dev/null +++ b/src/util/forceReflow.ts @@ -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; +}