From 680553c6c339a68b7880d0adeee09af047d149eb Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Fri, 20 Oct 2023 23:37:59 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20add=20pipe=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/__tests__/math.test.ts | 2 +- src/utils/math.ts | 40 ++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/utils/__tests__/math.test.ts b/src/utils/__tests__/math.test.ts index 450ab3b..1bae05f 100644 --- a/src/utils/__tests__/math.test.ts +++ b/src/utils/__tests__/math.test.ts @@ -1,6 +1,6 @@ import { clamp } from '../math'; -describe('get locale function', () => { +describe('clamp function', () => { it.each<[number, number, number, number | undefined]>([ [15, 15, 20, undefined], [-5, -5, -20, undefined], diff --git a/src/utils/math.ts b/src/utils/math.ts index 3e0a8a3..98fdcc8 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -1,6 +1,38 @@ +type UnaryFunction = (a: A) => B; + +/** Processes an input value through a series of functions in sequence. */ +export function pipe(input: T): T; +export function pipe(input: T, fn1: UnaryFunction): A; +export function pipe( + input: T, + fn1: UnaryFunction, + fn2: UnaryFunction +): B; +export function pipe( + input: T, + fn1: UnaryFunction, + fn2: UnaryFunction, + fn3: UnaryFunction +): C; +export function pipe( + input: T, + fn1: UnaryFunction, + fn2: UnaryFunction, + fn3: UnaryFunction, + fn4: UnaryFunction, +): D; + +export function pipe( + input: unknown, + ...functions: UnaryFunction[] +) { + return functions.reduce((value, fn) => fn(value), input); +} + +/** Restricts the given value within a specified range. */ export function clamp(value: number, a: number, b = 0) { - const min = Math.min(a, b) - const max = Math.max(a, b) + const min = Math.min(a, b); + const max = Math.max(a, b); - return Math.min(Math.max(value, min), max) -} \ No newline at end of file + return Math.min(Math.max(value, min), max); +} From 5a0bac65a7868fc60f1d536360dcab6d3a8960e8 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 21 Oct 2023 21:46:45 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E2=9C=A8=20add=20toFixedNumber=20function?= =?UTF-8?q?=20and=20refactor=20clamp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/math.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/utils/math.ts b/src/utils/math.ts index 98fdcc8..a49f384 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -1,4 +1,4 @@ -type UnaryFunction = (a: A) => B; +export type UnaryFunction = (a: A) => B; /** Processes an input value through a series of functions in sequence. */ export function pipe(input: T): T; @@ -19,7 +19,7 @@ export function pipe( fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction, - fn4: UnaryFunction, + fn4: UnaryFunction ): D; export function pipe( @@ -30,9 +30,16 @@ export function pipe( } /** Restricts the given value within a specified range. */ -export function clamp(value: number, a: number, b = 0) { +export const clamp = (a: number, b: number) => (value: number) => { const min = Math.min(a, b); const max = Math.max(a, b); return Math.min(Math.max(value, min), max); -} +}; + +/** Round a number to a specified number of decimal places. */ +export const toFixedNumber = (digits: number) => (value: number) => { + const pow = Math.pow(10, digits); + + return Math.round(value * pow) / pow; +}; From f298b3f82455c24b103d2714b1d69c93a771bde0 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 21 Oct 2023 21:47:38 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=85=20add=20pipe=20and=20toFixedNumbe?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/__tests__/math.test.ts | 50 +++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/utils/__tests__/math.test.ts b/src/utils/__tests__/math.test.ts index 1bae05f..4065b63 100644 --- a/src/utils/__tests__/math.test.ts +++ b/src/utils/__tests__/math.test.ts @@ -1,13 +1,13 @@ -import { clamp } from '../math'; +import { clamp, pipe, toFixedNumber } from '../math'; -describe('clamp function', () => { - it.each<[number, number, number, number | undefined]>([ - [15, 15, 20, undefined], - [-5, -5, -20, undefined], - [0, -5, 20, undefined], - [0, 15, -20, undefined], - [20, 50, 20, undefined], - [-20, -50, -20, undefined], +describe('math function', () => { + it.each<[number, number, number, number]>([ + [15, 15, 20, 0], + [-5, -5, -20, 0], + [0, -5, 20, 0], + [0, 15, -20, 0], + [20, 50, 20, 0], + [-20, -50, -20, 0], [-10, 0, -20, -10], [-10, 0, -10, -20], [10, 0, 10, 20], @@ -15,8 +15,36 @@ describe('clamp function', () => { [5, 5, 20, -20], ])( '%#. should correctly clamp a value (%o) within the specified range', - (expected, ...params) => { - expect(clamp(...params)).toBe(expected); + (expected, input, ...params) => { + expect(clamp(...params)(input)).toBe(expected); } ); + + it('should process an input value through a series of functions in sequence', () => { + expect(pipe(1)).toBe(1); + expect(pipe(2, (n) => n * 2)).toBe(4); + expect( + pipe( + 3, + (n) => n * 3, + (n) => n + 2 + ) + ).toBe(11); + expect( + pipe( + 4, + (n) => n * 2, + (n) => n / 3, + (n) => n.toFixed(2) + ) + ).toBe('2.67'); + }); + + it('should round a number to the specified number of decimal places', () => { + const input = 1.3456; + expect(toFixedNumber(0)(input)).toBe(1); + expect(toFixedNumber(1)(input)).toBe(1.3); + expect(toFixedNumber(2)(input)).toBe(1.35); + expect(toFixedNumber(3)(input)).toBe(1.346); + }); }); From a11d0a660d7e86754ef721c91c4db3b18103589e Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 21 Oct 2023 23:15:13 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20update=20scroll=20hook=20functi?= =?UTF-8?q?onality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useScroll.ts | 57 ++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/hooks/useScroll.ts b/src/hooks/useScroll.ts index ce16aa6..a5b582f 100644 --- a/src/hooks/useScroll.ts +++ b/src/hooks/useScroll.ts @@ -1,46 +1,65 @@ import { RefObject, useCallback, useEffect, useRef } from 'react'; -import useRafState from './useRafState'; type ScrollElement = HTMLElement | Window | null; +export type ScrollHandler = ( + scroll: Record<'x' | 'y', number>, + element: ScrollElement +) => void; + +export type UseScroll = { + ref?: RefObject; + initial?: boolean; + handler: ScrollHandler; +}; + /** * This hook allows tracking the scroll position of a specified DOM element or the window. */ -function useScroll(ref?: RefObject) { - const [scroll, setScroll] = useRafState({ x: 0, y: 0 }); +function useScroll({ ref, initial, handler }: UseScroll) { const internalElementRef = useRef(null); - const handleScroll = useCallback(() => { + const scrollHandler = useCallback(() => { const element = internalElementRef.current; + const scroll = { x: 0, y: 0 }; if (element === window) { - setScroll({ x: element.scrollX, y: element.scrollY }); + scroll.x = element.scrollX; + scroll.y = element.scrollY; } else if (element instanceof Element) { - setScroll({ x: element.scrollLeft, y: element.scrollTop }); + scroll.x = element.scrollLeft; + scroll.y = element.scrollTop; } - }, [setScroll]); + handler(scroll, element); + }, [handler]); + + const remove = useCallback(() => { + internalElementRef.current?.removeEventListener('scroll', scrollHandler); + }, [scrollHandler]); - const setInternalElementRef = useCallback( + const register = useCallback( (element: ScrollElement) => { - internalElementRef.current?.removeEventListener('scroll', handleScroll); + internalElementRef.current?.removeEventListener('scroll', scrollHandler); internalElementRef.current = element; - internalElementRef.current?.addEventListener('scroll', handleScroll); - handleScroll(); + internalElementRef.current?.addEventListener('scroll', scrollHandler); }, - [handleScroll] + [scrollHandler] ); useEffect(() => { - internalElementRef.current = ref?.current || window; - - const element = internalElementRef.current; + const element = ref?.current || window; - element.addEventListener('scroll', handleScroll); + internalElementRef.current = element; + element.addEventListener('scroll', scrollHandler); + if (initial) scrollHandler(); - return () => element.removeEventListener('scroll', handleScroll); - }, [ref, handleScroll]); + return () => element.removeEventListener('scroll', scrollHandler); + }, [ref, initial, scrollHandler]); - return [scroll, setInternalElementRef] as const; + return { + remove, + register, + }; } export default useScroll; From 3e74fb0b1c2880fbec57abcea2ac7e9f91fb56ff Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 21 Oct 2023 23:25:35 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=85=20update=20scroll=20hook=20case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/__tests__/useScroll.test.ts | 118 +++++++++++++++++++++++--- 1 file changed, 105 insertions(+), 13 deletions(-) diff --git a/src/hooks/__tests__/useScroll.test.ts b/src/hooks/__tests__/useScroll.test.ts index b234a2d..3f204d2 100644 --- a/src/hooks/__tests__/useScroll.test.ts +++ b/src/hooks/__tests__/useScroll.test.ts @@ -2,10 +2,18 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import useScroll from '../useScroll'; describe('useScroll hook', () => { - it('should monitor and update scroll position for the window', async () => { - const { result } = renderHook(() => useScroll()); + const scrollHandler = jest.fn(); - expect(result.current[0]).toEqual({ x: 0, y: 0 }); + beforeEach(() => { + window.scrollX = 0; + window.scrollY = 0; + scrollHandler.mockClear(); + }); + + it('should call the scroll handler when window is scrolled', async () => { + renderHook(() => useScroll({ handler: scrollHandler })); + + expect(scrollHandler).not.toBeCalled(); act(() => { window.scrollX = 100; @@ -14,15 +22,23 @@ describe('useScroll hook', () => { }); await waitFor(() => { - expect(result.current[0]).toEqual({ x: 100, y: 200 }); + expect(scrollHandler).toBeCalled(); + expect(scrollHandler).toBeCalledTimes(1); + expect(scrollHandler).toBeCalledWith({ x: 100, y: 200 }, window); }); }); - it('should monitor and update scroll position for a specific DOM element', async () => { + it('should call the scroll handler when a specified element is scrolled', async () => { const element = document.createElement('div'); - const { result } = renderHook(() => useScroll({ current: element })); - expect(result.current[0]).toEqual({ x: 0, y: 0 }); + renderHook(() => + useScroll({ + ref: { current: element }, + handler: scrollHandler, + }) + ); + + expect(scrollHandler).not.toBeCalled(); act(() => { element.scrollLeft = 150; @@ -31,25 +47,101 @@ describe('useScroll hook', () => { }); await waitFor(() => { - expect(result.current[0]).toEqual({ x: 150, y: 250 }); + expect(scrollHandler).toBeCalled(); + expect(scrollHandler).toBeCalledTimes(1); + expect(scrollHandler).toBeCalledWith({ x: 150, y: 250 }, element); }); }); - it('should monitor and update scroll position after setting the monitored element', async () => { + it('should switch the element being listened to for scrolling when using register', async () => { const element = document.createElement('main'); - const { result } = renderHook(() => useScroll()); + const { result } = renderHook(() => + useScroll({ + handler: scrollHandler, + }) + ); + + act(() => { + window.scrollX = 0; + window.scrollY = 80; + window.dispatchEvent(new Event('scroll')); + }); - expect(result.current[0]).toEqual({ x: 0, y: 0 }); + await waitFor(() => { + expect(scrollHandler).toBeCalled(); + expect(scrollHandler).toBeCalledTimes(1); + expect(scrollHandler).toBeCalledWith({ x: 0, y: 80 }, window); + }); act(() => { - result.current[1](element); + result.current.register(element); element.scrollLeft = 180; element.scrollTop = 280; element.dispatchEvent(new Event('scroll')); }); await waitFor(() => { - expect(result.current[0]).toEqual({ x: 180, y: 280 }); + expect(scrollHandler).toBeCalled(); + expect(scrollHandler).toBeCalledTimes(2); + expect(scrollHandler).toBeCalledWith({ x: 180, y: 280 }, element); + }); + }); + + it('should stop listening to scroll events when remove is called', async () => { + const { result } = renderHook(() => + useScroll({ + handler: scrollHandler, + }) + ); + + expect(scrollHandler).not.toBeCalled(); + + act(() => { + result.current.remove(); + window.scrollX = 100; + window.scrollY = 0; + window.dispatchEvent(new Event('scroll')); + }); + + await waitFor(() => { + expect(scrollHandler).not.toBeCalled(); + }); + }); + + it('should stop listening to scroll events when the component is unmounted', async () => { + const { unmount } = renderHook(() => + useScroll({ + handler: scrollHandler, + }) + ); + + expect(scrollHandler).not.toBeCalled(); + + unmount(); + + act(() => { + window.scrollX = 100; + window.scrollY = 0; + window.dispatchEvent(new Event('scroll')); + }); + + await waitFor(() => { + expect(scrollHandler).not.toBeCalled(); + }); + }); + + it('should trigger the scroll handler on initial render', async () => { + renderHook(() => + useScroll({ + handler: scrollHandler, + initial: true, + }) + ); + + await waitFor(() => { + expect(scrollHandler).toBeCalled(); + expect(scrollHandler).toBeCalledTimes(1); + expect(scrollHandler).toBeCalledWith({ x: 0, y: 0 }, window); }); }); }); From 60934ef3f2e33a0eefff8b1a2cab753c4015bd80 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 21 Oct 2023 23:27:30 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20header=20re?= =?UTF-8?q?nder=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Header/Header.tsx | 106 +++++++++++++++++++++---------- src/components/Header/Menu.tsx | 2 +- 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index f202a94..5e38940 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,10 +1,12 @@ 'use client'; import type { StaticImport } from 'next/dist/shared/lib/get-img-props'; -import { CSSProperties, useRef } from 'react'; +import { CSSProperties, useCallback, useRef } from 'react'; -import useScroll from '@/hooks/useScroll'; -import { clamp } from '@/utils/math'; +import useScroll, { ScrollHandler } from '@/hooks/useScroll'; +import useRafState from '@/hooks/useRafState'; +import cn from '@/utils/cn'; +import { clamp, pipe, toFixedNumber } from '@/utils/math'; import Container from '../Container'; import Image from '../Image'; @@ -23,40 +25,77 @@ export type HeaderProps = { } & MenuProps; function Header({ avatar, menu, scrollThreshold = 100 }: HeaderProps) { - const [scroll] = useScroll(); + const [headerFixed, setHeaderFixed] = useRafState(true); + const [headerTranslateY, setHeaderTranslateY] = useRafState(0); + const [avatarTranslateY, setAvatarTranslateY] = useRafState(0); + const [avatarScale, setAvatarScale] = useRafState(0); const headerRef = useRef(null); - const headerTranslateY = useRef(0); const previousScrollY = useRef(0); - const currentScrollY = Math.floor(scroll.y); - const avatarTranslateY = ( - clamp(currentScrollY * -1, scrollThreshold * -1) + scrollThreshold - ).toFixed(2); - const avatarScale = ( - 1.5 - - clamp(currentScrollY, scrollThreshold) / (scrollThreshold * 2) - ).toFixed(2); - - const calcHeaderTranslateY = () => { - const scrollPosition = currentScrollY - scrollThreshold; - - if (scrollPosition <= 0) { - headerTranslateY.current = 0; - } else { - const deltaScrollY = previousScrollY.current - scrollPosition; - const headerHeight = headerRef.current?.clientHeight || 0; - - previousScrollY.current = scrollPosition; - headerTranslateY.current = clamp( - headerTranslateY.current + deltaScrollY, - headerHeight * -1 + + const handleHeader = useCallback( + (currentScrollY: number, threshold: number) => { + const deltaScrollY = currentScrollY - previousScrollY.current; + const isScrollingDown = deltaScrollY > 0; + + previousScrollY.current = currentScrollY; + + if (isScrollingDown) { + if (currentScrollY < threshold) { + setHeaderFixed(true); + } else if (headerFixed) { + setHeaderTranslateY(currentScrollY); + setHeaderFixed(false); + } + } else { + const headerHeight = headerRef.current?.clientHeight || 0; + const newHeaderTranslateY = currentScrollY - headerHeight; + + if (newHeaderTranslateY > headerTranslateY) { + setHeaderTranslateY(newHeaderTranslateY); + } else if (currentScrollY < headerTranslateY) { + setHeaderFixed(true); + } + } + }, + [headerFixed, setHeaderFixed, headerTranslateY, setHeaderTranslateY] + ); + + const handleAvatar = useCallback( + (currentScrollY: number, threshold: number) => { + setAvatarTranslateY( + pipe( + currentScrollY * -1, + clamp(0, threshold * -1), + (y) => y + threshold, + toFixedNumber(2) + ) + ); + setAvatarScale( + pipe( + currentScrollY, + clamp(0, threshold), + (y) => y / (threshold * 2), + (y) => 1.5 - y, + toFixedNumber(2) + ) ); - } + }, + [setAvatarTranslateY, setAvatarScale] + ); + + const scrollHandler = useCallback( + ({ y }) => { + const currentScrollY = Math.floor(y); + handleHeader(currentScrollY, scrollThreshold); + handleAvatar(currentScrollY, scrollThreshold); + }, + [scrollThreshold, handleHeader, handleAvatar] + ); - return headerTranslateY.current; - }; + useScroll({ handler: scrollHandler, initial: true }); const headerStyles = { - '--header-translate-y': `${calcHeaderTranslateY()}px`, + '--header-translate-y': `${headerTranslateY}px`, '--avatar-translate-y': `${avatarTranslateY}px`, '--avatar-scale': avatarScale, } as CSSProperties; @@ -65,7 +104,10 @@ function Header({ avatar, menu, scrollThreshold = 100 }: HeaderProps) { -
    +
      {menu.map(({ text, href }) => (
    • Date: Sat, 21 Oct 2023 23:27:55 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E2=9C=85=20update=20header=20component=20t?= =?UTF-8?q?est?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Header/header.test.tsx | 54 ++++++++++++++++----------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/src/components/Header/header.test.tsx b/src/components/Header/header.test.tsx index 9046a67..fd9eef8 100644 --- a/src/components/Header/header.test.tsx +++ b/src/components/Header/header.test.tsx @@ -26,65 +26,75 @@ describe('Header component', () => { value: 50, }); - render(
      ); + render(
      ); const header = screen.getByRole('banner'); expect(header.tagName).toBe('HEADER'); act(() => { + window.scrollY = 19; + window.dispatchEvent(new Event('scroll')); window.scrollY = 20; window.dispatchEvent(new Event('scroll')); }); await waitFor(() => { expect(header).toHaveStyle({ '--header-translate-y': '0px' }); - expect(header).toHaveStyle({ '--avatar-translate-y': '100.00px' }); - expect(header).toHaveStyle({ '--avatar-scale': '1.42' }); + expect(header).toHaveStyle({ '--avatar-translate-y': '80px' }); + expect(header).toHaveStyle({ '--avatar-scale': '1.4' }); }); act(() => { - window.scrollY = 170; + window.scrollY = 98; + window.dispatchEvent(new Event('scroll')); + window.scrollY = 99; window.dispatchEvent(new Event('scroll')); }); await waitFor(() => { - expect(header).toHaveStyle({ '--header-translate-y': '-50px' }); - expect(header).toHaveStyle({ '--avatar-translate-y': '0.00px' }); - expect(header).toHaveStyle({ '--avatar-scale': '1.00' }); + expect(header).toHaveStyle({ '--header-translate-y': '0px' }); + expect(header).toHaveStyle({ '--avatar-translate-y': '1px' }); + expect(header).toHaveStyle({ '--avatar-scale': '1' }); }); act(() => { - window.scrollY = 140; + window.scrollY = 149; + window.dispatchEvent(new Event('scroll')); + window.scrollY = 150; window.dispatchEvent(new Event('scroll')); }); await waitFor(() => { - expect(header).toHaveStyle({ '--header-translate-y': '-20px' }); - expect(header).toHaveStyle({ '--avatar-translate-y': '0.00px' }); - expect(header).toHaveStyle({ '--avatar-scale': '1.00' }); + expect(header).toHaveStyle({ '--header-translate-y': '150px' }); + expect(header).toHaveStyle({ '--avatar-translate-y': '0px' }); + expect(header).toHaveStyle({ '--avatar-scale': '1' }); }); act(() => { - window.scrollY = 80; + window.scrollY = 251; + window.dispatchEvent(new Event('scroll')); + window.scrollY = 250; window.dispatchEvent(new Event('scroll')); }); await waitFor(() => { - expect(header).toHaveStyle({ '--header-translate-y': '0px' }); - expect(header).toHaveStyle({ '--avatar-translate-y': '40.00px' }); - expect(header).toHaveStyle({ '--avatar-scale': '1.17' }); + expect(header).toHaveStyle({ '--header-translate-y': '200px' }); + expect(header).toHaveStyle({ '--avatar-translate-y': '0px' }); + expect(header).toHaveStyle({ '--avatar-scale': '1' }); }); act(() => { + window.scrollY = 1; + window.dispatchEvent(new Event('scroll')); window.scrollY = 0; window.dispatchEvent(new Event('scroll')); }); await waitFor(() => { - expect(header).toHaveStyle({ '--header-translate-y': '0px' }); - expect(header).toHaveStyle({ '--avatar-translate-y': '120.00px' }); - expect(header).toHaveStyle({ '--avatar-scale': '1.50' }); + expect(header).toHaveStyle({ '--header-translate-y': '200px' }); + expect(header).toHaveStyle({ '--avatar-translate-y': '100px' }); + expect(header).toHaveStyle({ '--avatar-scale': '1.5' }); }); }); @@ -94,7 +104,7 @@ describe('Header component', () => { value: undefined, }); - render(
      ); + render(
      ); const header = screen.getByRole('banner'); @@ -106,9 +116,9 @@ describe('Header component', () => { }); await waitFor(() => { - expect(header).toHaveStyle({ '--header-translate-y': '0px' }); - expect(header).toHaveStyle({ '--avatar-translate-y': '0.00px' }); - expect(header).toHaveStyle({ '--avatar-scale': '1.00' }); + expect(header).toHaveStyle({ '--header-translate-y': '200px' }); + expect(header).toHaveStyle({ '--avatar-translate-y': '0px' }); + expect(header).toHaveStyle({ '--avatar-scale': '1' }); }); }); });