From 18ace589b1fe6a90cba786c5c6196ea88a66c396 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Fri, 3 Nov 2023 23:56:12 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20header=20op?= =?UTF-8?q?timize=20rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[lang]/Header.tsx | 77 +++++++++++++++------------------------ 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/src/app/[lang]/Header.tsx b/src/app/[lang]/Header.tsx index ad79bfe..e5618c1 100644 --- a/src/app/[lang]/Header.tsx +++ b/src/app/[lang]/Header.tsx @@ -18,16 +18,13 @@ type HeaderProps = { function Header({ avatar, children, scrollThreshold = 100 }: HeaderProps) { const [avatarScale, setAvatarScale] = useRafState(0); const [willChange, setWillChange] = useState(true); - const [avatarTranslateY, setAvatarTranslateY] = useState(0); const [headerTranslateY, setHeaderTranslateY] = useState(0); const [headerFixed, setHeaderFixed] = useState(true); - const headerRef = useRef(null); const previousScrollY = useRef(0); const scrollHandler = useCallback( ({ y }) => { const currentScrollY = Math.floor(y); - const headerHeight = headerRef.current?.clientHeight || 0; const deltaScrollY = currentScrollY - previousScrollY.current; const isScrollingDown = deltaScrollY > 0; @@ -37,17 +34,15 @@ function Header({ avatar, children, scrollThreshold = 100 }: HeaderProps) { if (currentScrollY < scrollThreshold) { setHeaderFixed(true); } else if (headerFixed) { - setHeaderTranslateY(currentScrollY); - setAvatarTranslateY(currentScrollY - headerHeight); + setHeaderTranslateY(currentScrollY - scrollThreshold); setHeaderFixed(false); } } else { - const newHeaderTranslateY = currentScrollY - headerHeight; + const newHeaderTranslateY = currentScrollY - scrollThreshold * 2; if (newHeaderTranslateY > headerTranslateY) { setHeaderTranslateY(newHeaderTranslateY); - setAvatarTranslateY(newHeaderTranslateY - headerHeight); - } else if (currentScrollY < headerTranslateY) { + } else if (currentScrollY - scrollThreshold < headerTranslateY) { setHeaderFixed(true); } } @@ -69,39 +64,26 @@ function Header({ avatar, children, scrollThreshold = 100 }: HeaderProps) { useScroll({ handler: scrollHandler, initial: true }); const headerStyles = { + '--header-height': `-${scrollThreshold}px`, '--header-translate-y': `${headerTranslateY}px`, - } as CSSProperties; - - const avatarStyles = { - '--avatar-translate-y': `${avatarTranslateY}px`, '--avatar-scale': avatarScale, } as CSSProperties; return ( - <> - + +
{children} - - - {avatar} - - +
+ {avatar} +
); } @@ -112,19 +94,18 @@ type AvatarProps = { export const Avatar = ({ src, alt }: AvatarProps) => { return ( - - {alt} - +
+ + {alt} + +
); }; From dd24638b39785e6579423916ba1a36ed4bd8dcbb Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Fri, 3 Nov 2023 23:57:49 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=94=A5=20remove=20container=20compone?= =?UTF-8?q?nt=20forwardRef?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Container/Container.tsx | 33 ++++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/Container/Container.tsx b/src/components/Container/Container.tsx index 5a30749..5ddc3be 100644 --- a/src/components/Container/Container.tsx +++ b/src/components/Container/Container.tsx @@ -1,27 +1,24 @@ -import { HTMLAttributes, forwardRef } from 'react'; +import type { HTMLAttributes } from 'react'; import cn from '@/utils/cn'; type ContainerProps = { as?: 'main' | 'header' | 'footer' | 'div'; } & HTMLAttributes; -const Container = forwardRef( - function InternalContainer({ as = 'div', className, ...props }, ref) { - const Component = as; +function Container({ as = 'div', className, ...props }: ContainerProps) { + const Component = as; - return ( - - ); - } -); + return ( + + ); +} export default Container; From 86abc6d5437959590dcb46bf8baef1dbf2ea1a8b Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Fri, 3 Nov 2023 23:58:15 +0800 Subject: [PATCH 3/5] =?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/app/[lang]/__tests__/header.test.tsx | 38 +++++++----------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/src/app/[lang]/__tests__/header.test.tsx b/src/app/[lang]/__tests__/header.test.tsx index 226d627..fdb2fb4 100644 --- a/src/app/[lang]/__tests__/header.test.tsx +++ b/src/app/[lang]/__tests__/header.test.tsx @@ -16,11 +16,6 @@ describe('Header component', () => { }); it('should hide header on scroll down and show on scroll up', async () => { - Object.defineProperty(HTMLElement.prototype, 'clientHeight', { - configurable: true, - value: 50, - }); - render(
); const header = screen.getByRole('banner'); @@ -57,51 +52,40 @@ describe('Header component', () => { }); await waitFor(() => { - expect(header).toHaveStyle({ '--header-translate-y': '150px' }); + expect(header).toHaveStyle({ '--header-translate-y': '50px' }); }); act(() => { - window.scrollY = 251; + window.scrollY = 299; window.dispatchEvent(new Event('scroll')); - window.scrollY = 250; + window.scrollY = 300; window.dispatchEvent(new Event('scroll')); }); await waitFor(() => { - expect(header).toHaveStyle({ '--header-translate-y': '200px' }); + expect(header).toHaveStyle({ '--header-translate-y': '50px' }); }); act(() => { - window.scrollY = 1; + window.scrollY = 501; window.dispatchEvent(new Event('scroll')); - window.scrollY = 0; + window.scrollY = 500; window.dispatchEvent(new Event('scroll')); }); await waitFor(() => { - expect(header).toHaveStyle({ '--header-translate-y': '200px' }); + expect(header).toHaveStyle({ '--header-translate-y': '300px' }); }); - }); - - it('should calculate the correct styles even when `clientHeight` is `undefined`', async () => { - Object.defineProperty(HTMLElement.prototype, 'clientHeight', { - configurable: true, - value: undefined, - }); - - render(
); - - const header = screen.getByRole('banner'); - - expect(header.tagName).toBe('HEADER'); act(() => { - window.scrollY = 200; + window.scrollY = 1; + window.dispatchEvent(new Event('scroll')); + window.scrollY = 0; window.dispatchEvent(new Event('scroll')); }); await waitFor(() => { - expect(header).toHaveStyle({ '--header-translate-y': '200px' }); + expect(header).toHaveStyle({ '--header-translate-y': '300px' }); }); }); }); From 73d29787748fb63971077b97ad8a13a56468fd2e Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 4 Nov 2023 16:55:37 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20optimize=20scroll=20ha?= =?UTF-8?q?ndler=20for=20better=20performance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[lang]/Header.tsx | 113 +++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 44 deletions(-) diff --git a/src/app/[lang]/Header.tsx b/src/app/[lang]/Header.tsx index e5618c1..1e46f3e 100644 --- a/src/app/[lang]/Header.tsx +++ b/src/app/[lang]/Header.tsx @@ -8,7 +8,7 @@ import Link from '@/components/Link'; import useScroll, { ScrollHandler } from '@/hooks/useScroll'; import useRafState from '@/hooks/useRafState'; import cn from '@/utils/cn'; -import { clamp, pipe, toFixedNumber } from '@/utils/math'; +import { toFixedNumber } from '@/utils/math'; type HeaderProps = { avatar: React.ReactNode; @@ -17,11 +17,50 @@ type HeaderProps = { function Header({ avatar, children, scrollThreshold = 100 }: HeaderProps) { const [avatarScale, setAvatarScale] = useRafState(0); - const [willChange, setWillChange] = useState(true); - const [headerTranslateY, setHeaderTranslateY] = useState(0); const [headerFixed, setHeaderFixed] = useState(true); + const [headerTranslateY, setHeaderTranslateY] = useState(0); + const [willChange, setWillChange] = useState(true); const previousScrollY = useRef(0); + const handleScrollDown = useCallback( + (scrollY: number) => { + if (scrollY < scrollThreshold) { + setHeaderFixed(true); + } else if (headerFixed) { + setHeaderTranslateY(scrollY - scrollThreshold); + setHeaderFixed(false); + } + }, + [scrollThreshold, headerFixed] + ); + + const handleScrollUp = useCallback( + (scrollY: number) => { + const newHeaderTranslateY = scrollY - scrollThreshold * 2; + + if (newHeaderTranslateY > headerTranslateY) { + setHeaderTranslateY(newHeaderTranslateY); + } else if (scrollY - scrollThreshold < headerTranslateY) { + setHeaderFixed(true); + } + }, + [scrollThreshold, headerTranslateY] + ); + + const handleAvatarScale = useCallback( + (scrollY: number) => { + setWillChange(scrollY < scrollThreshold + 100); + if (scrollY > scrollThreshold) { + setAvatarScale(1); + } else { + setAvatarScale( + toFixedNumber(2)(1.5 - scrollY / (scrollThreshold * 2)) + ); + } + }, + [scrollThreshold, setAvatarScale] + ); + const scrollHandler = useCallback( ({ y }) => { const currentScrollY = Math.floor(y); @@ -31,40 +70,20 @@ function Header({ avatar, children, scrollThreshold = 100 }: HeaderProps) { previousScrollY.current = currentScrollY; if (isScrollingDown) { - if (currentScrollY < scrollThreshold) { - setHeaderFixed(true); - } else if (headerFixed) { - setHeaderTranslateY(currentScrollY - scrollThreshold); - setHeaderFixed(false); - } + handleScrollDown(currentScrollY); } else { - const newHeaderTranslateY = currentScrollY - scrollThreshold * 2; - - if (newHeaderTranslateY > headerTranslateY) { - setHeaderTranslateY(newHeaderTranslateY); - } else if (currentScrollY - scrollThreshold < headerTranslateY) { - setHeaderFixed(true); - } + handleScrollUp(currentScrollY); } - setWillChange(currentScrollY < scrollThreshold + 100); - setAvatarScale( - pipe( - currentScrollY, - clamp(0, scrollThreshold), - (y) => y / (scrollThreshold * 2), - (y) => 1.5 - y, - toFixedNumber(2) - ) - ); + handleAvatarScale(currentScrollY); }, - [scrollThreshold, headerFixed, headerTranslateY, setAvatarScale] + [handleScrollDown, handleScrollUp, handleAvatarScale] ); useScroll({ handler: scrollHandler, initial: true }); const headerStyles = { - '--header-height': `-${scrollThreshold}px`, + '--scroll-threshold': `-${scrollThreshold}px`, '--header-translate-y': `${headerTranslateY}px`, '--avatar-scale': avatarScale, } as CSSProperties; @@ -73,16 +92,24 @@ function Header({ avatar, children, scrollThreshold = 100 }: HeaderProps) {
{children}
- {avatar} +
+
+ {avatar} +
+
); } @@ -94,18 +121,16 @@ type AvatarProps = { export const Avatar = ({ src, alt }: AvatarProps) => { return ( -
- - {alt} - -
+ + {alt} + ); }; From 49939b56849c451cdabee6e819f236f2ff9568bf Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 4 Nov 2023 16:56:02 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=92=84=20update=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[lang]/page.tsx | 2 +- src/app/[lang]/posts/[postId]/page.tsx | 2 +- src/app/[lang]/posts/page.tsx | 2 +- src/components/Container/Container.tsx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx index 983e7eb..763bff8 100644 --- a/src/app/[lang]/page.tsx +++ b/src/app/[lang]/page.tsx @@ -24,7 +24,7 @@ async function RootPage({ params: { lang } }: RootParams) { return ( <> - +

{metadata.title}

diff --git a/src/app/[lang]/posts/[postId]/page.tsx b/src/app/[lang]/posts/[postId]/page.tsx index 0995456..25641e2 100644 --- a/src/app/[lang]/posts/[postId]/page.tsx +++ b/src/app/[lang]/posts/[postId]/page.tsx @@ -39,7 +39,7 @@ async function PostPage({ params: { postId } }: PostParams) { return ( <> - +

{frontmatter.title}

diff --git a/src/app/[lang]/posts/page.tsx b/src/app/[lang]/posts/page.tsx index 71c3cbd..f711d44 100644 --- a/src/app/[lang]/posts/page.tsx +++ b/src/app/[lang]/posts/page.tsx @@ -24,7 +24,7 @@ async function RootPage({ params: { lang } }: RootParams) { return ( <> - +

{metadata.title}

diff --git a/src/components/Container/Container.tsx b/src/components/Container/Container.tsx index 5ddc3be..63e4c17 100644 --- a/src/components/Container/Container.tsx +++ b/src/components/Container/Container.tsx @@ -11,9 +11,9 @@ function Container({ as = 'div', className, ...props }: ContainerProps) { return (