From 320e9f1529a0773174bba31f02a0011792dd828b Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 30 Aug 2023 10:39:59 -0400 Subject: [PATCH 1/2] smooth mobile header transitions --- .vscode/settings.json | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 8b3e46f5f..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "emeraldwalk.runonsave": { - "commands": [ - { - "match": "\\.graphql$", - "cmd": "npm run graphql:codegen" - } - ] - } -} From 7247795ea0fab226909edbfdd8761da014287ee3 Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 30 Aug 2023 10:40:48 -0400 Subject: [PATCH 2/2] mobile scroll transitions --- .../organisms/mobileHeader.module.scss | 14 +++++ src/components/organisms/mobileHeader.tsx | 41 ++++---------- .../mobileHeader.useScrollDirection.ts | 37 +++++++++++++ .../mobileHeader.useTransitionProgress.ts | 55 ------------------- src/lib/hooks/useScrollTop.ts | 23 ++++++++ 5 files changed, 86 insertions(+), 84 deletions(-) create mode 100644 src/components/organisms/mobileHeader.useScrollDirection.ts delete mode 100644 src/components/organisms/mobileHeader.useTransitionProgress.ts create mode 100644 src/lib/hooks/useScrollTop.ts diff --git a/src/components/organisms/mobileHeader.module.scss b/src/components/organisms/mobileHeader.module.scss index d0e7243c4..c7c97cfd1 100644 --- a/src/components/organisms/mobileHeader.module.scss +++ b/src/components/organisms/mobileHeader.module.scss @@ -25,12 +25,20 @@ align-items: center; justify-content: space-between; z-index: 1; + padding-top: 24px; + padding-bottom: 14px; + transition: $transition-quick padding-top, $transition-quick padding-bottom; > :first-child { margin-right: 16px; } } +.down .title { + padding-top: 16px; + padding-bottom: 8px; +} + .subnav { display: flex; align-items: flex-end; @@ -39,6 +47,12 @@ position: relative; overflow: hidden; background: $ts-cream; + height: 24px; + transition: $transition-quick height; +} + +.down .subnav { + height: 0; } .subnavItems { diff --git a/src/components/organisms/mobileHeader.tsx b/src/components/organisms/mobileHeader.tsx index 5caf69e19..d683516f4 100644 --- a/src/components/organisms/mobileHeader.tsx +++ b/src/components/organisms/mobileHeader.tsx @@ -16,24 +16,11 @@ import IconExitSmall from '~public/img/icons/icon-exit-small.svg'; import MoreIcon from '~public/img/icons/icon-more.svg'; import styles from './mobileHeader.module.scss'; -import { useTransitionProgress } from './mobileHeader.useTransitionProgress'; +import useScrollDirection, { + SCROLL_DIRECTIONS, +} from './mobileHeader.useScrollDirection'; import { EntityFilterId } from './searchResults.filters'; -type Transition = [number, number]; - -const SUBNAV_HEIGHT: Transition = [24, 0]; -const TITLE_PAD_TOP: Transition = [24, 16]; -const TITLE_PAD_BOTTOM: Transition = [14, 8]; - -const COLLAPSING_HEIGHT = [ - SUBNAV_HEIGHT, - TITLE_PAD_TOP, - TITLE_PAD_BOTTOM, -].reduce((acc, [s, e]) => acc + (s - e), 0); - -const px = (progress: number, [s, e]: Transition) => - `${s + (e - s) * progress}px`; - export default function MobileHeader({ setShowingMenu, scrollRef, @@ -53,18 +40,17 @@ export default function MobileHeader({ const lang = useLanguageRoute(); const navItems = useNavigationItems(); const { getRecording } = useContext(PlaybackContext); - const progress = useTransitionProgress(scrollRef, COLLAPSING_HEIGHT); + const scrollDirection = useScrollDirection(scrollRef); return ( -
+
-
+
-
+
{ if (!item.href) { diff --git a/src/components/organisms/mobileHeader.useScrollDirection.ts b/src/components/organisms/mobileHeader.useScrollDirection.ts new file mode 100644 index 000000000..0fc5558ce --- /dev/null +++ b/src/components/organisms/mobileHeader.useScrollDirection.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; + +import useScrollTop from '~src/lib/hooks/useScrollTop'; + +export const SCROLL_DIRECTIONS = { + UP: 'up', + DOWN: 'down', +}; + +export default function useScrollDirection( + scrollRef: React.RefObject +) { + const scrollTop = useScrollTop(scrollRef); + const [lastScrollTop, setLastScrollTop] = useState(0); + const [direction, setDirection] = useState(SCROLL_DIRECTIONS.UP); + + useEffect(() => { + setLastScrollTop(Math.max(scrollTop, 0)); + }, [scrollTop]); + + useEffect(() => { + const scrollHeight = scrollRef.current?.scrollHeight || 0; + const clientHeight = scrollRef.current?.clientHeight || 0; + const clientTop = scrollHeight - scrollTop; + const buffer = clientTop - clientHeight; + const maxHeaderHeight = 101; + if (buffer < maxHeaderHeight) return; + if (scrollTop > lastScrollTop) { + setDirection(SCROLL_DIRECTIONS.DOWN); + } + if (scrollTop < lastScrollTop) { + setDirection(SCROLL_DIRECTIONS.UP); + } + }, [scrollTop, lastScrollTop, scrollRef]); + + return direction; +} diff --git a/src/components/organisms/mobileHeader.useTransitionProgress.ts b/src/components/organisms/mobileHeader.useTransitionProgress.ts deleted file mode 100644 index 1a25d0d4c..000000000 --- a/src/components/organisms/mobileHeader.useTransitionProgress.ts +++ /dev/null @@ -1,55 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; - -import isServerSide from '~lib/isServerSide'; - -function useScrollTop(scrollRef: React.RefObject) { - const [scrollTop, setScrollTop] = useState(0); - - const listener = useCallback(() => { - if (isServerSide() || !scrollRef.current) return; - setScrollTop(scrollRef.current.scrollTop); - }, [scrollRef]); - - useEffect(() => { - if (isServerSide() || !scrollRef.current) return; - const el = scrollRef.current; - el.addEventListener('scroll', listener); - return () => el.removeEventListener('scroll', listener); - }, [listener, scrollRef]); - - return scrollTop; -} - -export function useTransitionProgress( - scrollRef: React.RefObject, - transitionLength: number -) { - const [lastScrollTop, setLastScrollTop] = useState(0); - const [startPosition, setStartPosition] = useState(0); - const [progress, setProgress] = useState(0); - const scrollTop = useScrollTop(scrollRef); - - useEffect(() => { - if (isServerSide()) return; - - const isScrollingUp = scrollTop < lastScrollTop; - - if (isScrollingUp && startPosition + transitionLength < scrollTop) { - setStartPosition(Math.max(lastScrollTop - transitionLength, 0)); - } else if (!isScrollingUp && startPosition > scrollTop) { - setStartPosition(scrollTop); - } - - const distanceScrolled = scrollTop - startPosition; - const clampedDistance = Math.max( - Math.min(distanceScrolled, transitionLength), - 0 - ); - const progress = clampedDistance / transitionLength; - - setProgress(progress); - setLastScrollTop(scrollTop); - }, [lastScrollTop, scrollTop, startPosition, transitionLength]); - - return progress; -} diff --git a/src/lib/hooks/useScrollTop.ts b/src/lib/hooks/useScrollTop.ts new file mode 100644 index 000000000..400bb8e82 --- /dev/null +++ b/src/lib/hooks/useScrollTop.ts @@ -0,0 +1,23 @@ +import { useCallback, useEffect, useState } from 'react'; + +import isServerSide from '../isServerSide'; + +export default function useScrollTop( + scrollRef: React.RefObject +) { + const [scrollTop, setScrollTop] = useState(0); + + const listener = useCallback(() => { + if (isServerSide() || !scrollRef.current) return; + setScrollTop(scrollRef.current.scrollTop); + }, [scrollRef]); + + useEffect(() => { + if (isServerSide() || !scrollRef.current) return; + const el = scrollRef.current; + el.addEventListener('scroll', listener); + return () => el.removeEventListener('scroll', listener); + }, [listener, scrollRef]); + + return scrollTop; +}