From 828b1de5e73946c736a93f5e82af19e06bb22e42 Mon Sep 17 00:00:00 2001 From: Daniil Suvorov Date: Tue, 3 Sep 2024 14:48:21 +0300 Subject: [PATCH 1/3] feat(Skeleton): use js sync animation --- .../components/Skeleton/Skeleton.module.css | 62 ++++++------- .../vkui/src/components/Skeleton/Skeleton.tsx | 92 ++++++++++++++++++- 2 files changed, 120 insertions(+), 34 deletions(-) diff --git a/packages/vkui/src/components/Skeleton/Skeleton.module.css b/packages/vkui/src/components/Skeleton/Skeleton.module.css index 5cae8a66bc..05280c0a8a 100644 --- a/packages/vkui/src/components/Skeleton/Skeleton.module.css +++ b/packages/vkui/src/components/Skeleton/Skeleton.module.css @@ -1,18 +1,8 @@ -@keyframes skeleton { - from { - transform: translateX(0); - } - - to { - transform: translateX(var(--vkui_internal--skeleton_width)); - } -} - .Skeleton { - --vkui_internal--skeleton_width: 300px; --vkui_internal--skeleton_color_from: var(--vkui--color_skeleton_from); --vkui_internal--skeleton_color_to: var(--vkui--color_skeleton_to); --vkui_internal--skeleton_animation_duration: 1.5s; + --vkui_internal--skeleton_gradient_left: 0; display: inline-flex; position: relative; @@ -21,26 +11,7 @@ line-height: 1; border-radius: 6px; overflow: hidden; -} - -.Skeleton::before { - content: ''; - position: absolute; - inset: 0; - inset-inline-start: calc(-1 * var(--vkui_internal--skeleton_width)); - animation-duration: var(--vkui_internal--skeleton_animation_duration); - animation-iteration-count: infinite; - animation-name: skeleton; - background-attachment: fixed; - animation-timing-function: var(--vkui--animation_easing_platform); - background-size: var(--vkui_internal--skeleton_width) 100%; background-color: var(--vkui_internal--skeleton_color_from); - background-image: linear-gradient( - to right, - var(--vkui_internal--skeleton_color_from) 0, - var(--vkui_internal--skeleton_color_to) 50%, - var(--vkui_internal--skeleton_color_from) 75% - ); } /* Если скелетон находится внутри другого скелетона он меняет цвет */ @@ -54,7 +25,34 @@ --vkui_internal--skeleton_color_to: var(--vkui--color_skeleton_to); } -.Skeleton--disableAnimation { +.Skeleton::before { + position: absolute; + inset-inline-start: var(--vkui_internal--skeleton_gradient_left); + inset-block-start: 0; + content: ' '; + inline-size: 100vw; + block-size: 100%; + background-image: linear-gradient( + 90deg, + var(--vkui_internal--skeleton_color_from), + var(--vkui_internal--skeleton_color_to), + var(--vkui_internal--skeleton_color_from) + ); + transform: translateX(-100vw); + animation-name: animation-skeleton; + animation-direction: normal /*rtl:reverse*/; + animation-duration: 1.5s; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; +} + +@keyframes animation-skeleton { + 100% { + transform: translateX(100vw); + } +} + +.Skeleton--disableAnimation::before { /** * Safari тратит время не пересчет анимации даже если элемент скрыт * Для повышения производительности анимацию необходимо выключить @@ -64,7 +62,7 @@ } @media (--reduce-motion) { - .Skeleton { + .Skeleton::before { animation-name: none; background-image: none; } diff --git a/packages/vkui/src/components/Skeleton/Skeleton.tsx b/packages/vkui/src/components/Skeleton/Skeleton.tsx index 13099e5a0d..6363e0a7de 100644 --- a/packages/vkui/src/components/Skeleton/Skeleton.tsx +++ b/packages/vkui/src/components/Skeleton/Skeleton.tsx @@ -1,9 +1,89 @@ import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; +import { millisecondsInSecond } from 'date-fns/constants'; +import { useExternRef } from '../../hooks/useExternRef'; +import { useGlobalEventListener } from '../../hooks/useGlobalEventListener'; +import { usePrevious } from '../../hooks/usePrevious'; +import { useDOM } from '../../lib/dom'; import type { CSSCustomProperties, HTMLAttributesWithRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; import styles from './Skeleton.module.css'; +const CUSTOM_PROPERTY_GRADIENT_LEFT = '--vkui_internal--skeleton_gradient_left'; + +/** + * Синхронизирует анимацию скелетонов с помощью временных отрезков + * + * ## visibilitychange + * + * В синхронизацию не заложен механизм перехода на оптимизации браузеров при + * переходе на другую вкладку, поскольку нет уверенности в реальности таких + * кейсов со скелетонами. Если такой кейс принесут, необходимо обработать + * событие `visibilitychange` используя функцию `syncAnimation` + * + * https://developer.chrome.com/blog/page-lifecycle-api/ + * + * @param duration длительность анимации в секундах + */ +function useSkeletonSyncAnimation(disableAnimation: boolean, duration = 1.5) { + const [isAnimationStarted, setIsAnimationStarted] = React.useState(false); + const timer = React.useRef(undefined); + + const syncAnimation = React.useCallback(() => { + clearTimeout(timer.current); + setIsAnimationStarted(false); + + const durationInMilliseconds = duration * millisecondsInSecond; + const delay = durationInMilliseconds - (performance.now() % durationInMilliseconds); + + timer.current = setTimeout(() => setIsAnimationStarted(true), delay); + + return () => clearTimeout(timer.current); + }, [duration]); + + React.useEffect(() => { + if (disableAnimation) { + setIsAnimationStarted(false); + return; + } + + if (isAnimationStarted) { + return; + } + + return syncAnimation(); + }, [disableAnimation, isAnimationStarted, syncAnimation]); + + return isAnimationStarted; +} + +/** + * Вычисляет позицию скелетона + */ +function useSkeletonPosition(rootRef: React.MutableRefObject) { + const { document, window } = useDOM(); + const [skeletonGradientLeft, setSkeletonGradientLeft] = React.useState('0'); + const prevSkeletonGradientLeft = usePrevious(skeletonGradientLeft); + + const updatePosition = React.useCallback(() => { + const el = rootRef.current; + if (!el || !document) { + return; + } + + const value = -(el.getBoundingClientRect().left - document.body.getBoundingClientRect().left); + const gradientValue = value === 0 ? '0' : `${value}px`; + if (prevSkeletonGradientLeft !== gradientValue) { + setSkeletonGradientLeft(gradientValue); + } + }, [document, prevSkeletonGradientLeft, rootRef]); + + React.useEffect(updatePosition, [updatePosition]); + useGlobalEventListener(window, 'resize', updatePosition); + + return skeletonGradientLeft; +} + export interface SkeletonProps extends HTMLAttributesWithRootRef, Pick< @@ -61,11 +141,17 @@ export const Skeleton = ({ children, colorFrom, colorTo, - noAnimation, + noAnimation = false, duration, margin, + getRootRef, ...restProps }: SkeletonProps): React.ReactNode => { + const rootRef = useExternRef(getRootRef); + + const disableAnimation = !useSkeletonSyncAnimation(noAnimation, duration); + const skeletonGradientLeft = useSkeletonPosition(rootRef); + const skeletonStyle: React.CSSProperties & CSSCustomProperties = { width, height, @@ -75,6 +161,7 @@ export const Skeleton = ({ maxInlineSize, borderRadius, margin, + [CUSTOM_PROPERTY_GRADIENT_LEFT]: skeletonGradientLeft, }; if (colorFrom) { @@ -91,10 +178,11 @@ export const Skeleton = ({ return ( Date: Tue, 3 Sep 2024 15:13:47 +0300 Subject: [PATCH 2/3] chore: update screenshots --- .../skeleton-android-chromium-dark-1-snap.png | 4 ++-- .../skeleton-android-chromium-light-1-snap.png | 4 ++-- .../__image_snapshots__/skeleton-ios-webkit-dark-1-snap.png | 4 ++-- .../__image_snapshots__/skeleton-ios-webkit-light-1-snap.png | 4 ++-- .../skeleton-vkcom-chromium-dark-1-snap.png | 4 ++-- .../skeleton-vkcom-chromium-light-1-snap.png | 4 ++-- .../skeleton-vkcom-firefox-dark-1-snap.png | 4 ++-- .../skeleton-vkcom-firefox-light-1-snap.png | 4 ++-- .../__image_snapshots__/skeleton-vkcom-webkit-dark-1-snap.png | 4 ++-- .../skeleton-vkcom-webkit-light-1-snap.png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-android-chromium-dark-1-snap.png b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-android-chromium-dark-1-snap.png index 4dc5cb9835..3221808a9a 100644 --- a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-android-chromium-dark-1-snap.png +++ b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-android-chromium-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55bff43c7b6781f2abff92407307e8665f0a5ba7cb692f644bc99452025225c5 -size 21227 +oid sha256:a881dcad297a286597b567853f0828ccc2c8797889bfd532f63153fec47e790a +size 17004 diff --git a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-android-chromium-light-1-snap.png b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-android-chromium-light-1-snap.png index e242d5fdda..9d0cc6ac24 100644 --- a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-android-chromium-light-1-snap.png +++ b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-android-chromium-light-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:552605907191674b5c4929d2e3298ce384cd79c206c8a200daafb2a31c350277 -size 21686 +oid sha256:0595aacbe494da00f407f7274848c0043f2228229af01a1f4405c2cacf746156 +size 17094 diff --git a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-ios-webkit-dark-1-snap.png b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-ios-webkit-dark-1-snap.png index 5cde1dbc7e..a7be066afe 100644 --- a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-ios-webkit-dark-1-snap.png +++ b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-ios-webkit-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c62ec685fa9abe64e4a78dc0d928ad4e1f620c2dcff25e0972786217c4729626 -size 17815 +oid sha256:0968198198e34fe8e5faab8f042c297c81a547a7f4250ddc01fc61ca8705daec +size 17637 diff --git a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-ios-webkit-light-1-snap.png b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-ios-webkit-light-1-snap.png index 995025fd46..09ab0bfc57 100644 --- a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-ios-webkit-light-1-snap.png +++ b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-ios-webkit-light-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc715bd492ebe712a47d379558e2fc6f09ed6d22b37a707ddac3e0b094f76df8 -size 17914 +oid sha256:d760103f247a8d4f77569157784297c970c74481609130f99fab63978e809075 +size 17496 diff --git a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-chromium-dark-1-snap.png b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-chromium-dark-1-snap.png index e2d5038b46..a4f7e0e68f 100644 --- a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-chromium-dark-1-snap.png +++ b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-chromium-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1a6b82008dad48fc7697c5b0b6bac8fa2fad0380f31a5bb0e9483547615012d -size 20088 +oid sha256:dc86b4807aae7b5dc7ecfd1c53fcf7e5d47622346da7735d791157c78fd15f43 +size 16413 diff --git a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-chromium-light-1-snap.png b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-chromium-light-1-snap.png index bb4c6f63b4..8c0afc4a24 100644 --- a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-chromium-light-1-snap.png +++ b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-chromium-light-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47ffaf00e484979e033ba22338897485ddf02342170d48693a95d57c3aadc1b0 -size 20726 +oid sha256:d942e665372d72f0f0d432b92271c65021437b413c629fbe4eef920983939364 +size 16840 diff --git a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-firefox-dark-1-snap.png b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-firefox-dark-1-snap.png index 2e51efe114..9bcd96da83 100644 --- a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-firefox-dark-1-snap.png +++ b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-firefox-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dc751d644c8e00ff33a499a934928874439d9598dc1884eb6fcc743e8218369 -size 21671 +oid sha256:48aa3eae7839f54701562f80f9d4f7a3566cfec38bbd1b6a71e84ecbd4d5776f +size 21544 diff --git a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-firefox-light-1-snap.png b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-firefox-light-1-snap.png index 8cbfaf4158..58cd88ba19 100644 --- a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-firefox-light-1-snap.png +++ b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-firefox-light-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5e0341b2a1ed62d8dba4a11330652b5451909e8e929e47cbaa89ddf2747f5cf -size 22032 +oid sha256:5552f30b98e8a0886e6288e0832d0e8db7b8972fc50c6544c7c090d4a5ba9362 +size 21899 diff --git a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-webkit-dark-1-snap.png b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-webkit-dark-1-snap.png index 83f38fcba7..73657f8d00 100644 --- a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-webkit-dark-1-snap.png +++ b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-webkit-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f1bc7b9229119b3e07ff6998485f47ad0b16dacc9d8f752258910da04f5a6fc -size 17066 +oid sha256:01b57fe656bb49a4abb69622d5389006d3d23317d9e211241567b45f4427ec59 +size 16918 diff --git a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-webkit-light-1-snap.png b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-webkit-light-1-snap.png index 460c8e483d..79aa25f1c2 100644 --- a/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-webkit-light-1-snap.png +++ b/packages/vkui/src/components/Skeleton/__image_snapshots__/skeleton-vkcom-webkit-light-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37e56fead2696ee27f0e9f207c829cb3fbd76f5dfa222dc252a7998ecf9e60ee -size 17383 +oid sha256:b65ba1df0a3f10fc6b1c365cd10307aa5770e6c327929b07ad2e8166d95812d5 +size 17082 From 7e3485b546dc71b8dd3f2df2b67ca437f0622442 Mon Sep 17 00:00:00 2001 From: Daniil Suvorov Date: Tue, 10 Sep 2024 11:14:48 +0300 Subject: [PATCH 3/3] fix: types setTimeout --- packages/vkui/src/components/Skeleton/Skeleton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vkui/src/components/Skeleton/Skeleton.tsx b/packages/vkui/src/components/Skeleton/Skeleton.tsx index 6363e0a7de..0f2935d45b 100644 --- a/packages/vkui/src/components/Skeleton/Skeleton.tsx +++ b/packages/vkui/src/components/Skeleton/Skeleton.tsx @@ -27,7 +27,7 @@ const CUSTOM_PROPERTY_GRADIENT_LEFT = '--vkui_internal--skeleton_gradient_left'; */ function useSkeletonSyncAnimation(disableAnimation: boolean, duration = 1.5) { const [isAnimationStarted, setIsAnimationStarted] = React.useState(false); - const timer = React.useRef(undefined); + const timer = React.useRef | undefined>(undefined); const syncAnimation = React.useCallback(() => { clearTimeout(timer.current);