From 6cb18fb3846ea640f575ef55c2353782be8736ae Mon Sep 17 00:00:00 2001 From: Hyeseong Kim Date: Sun, 21 Apr 2024 13:28:38 +0900 Subject: [PATCH 1/3] Hide top nav on reading docs on mobile The top navigation disappears when you scroll down and reappears when you scroll up. Like browsers, and many content services like Medium, This is a necessary feature for mobile readers with a small viewport. Also made some code changes here to add memoization, because the scroll event is highly frequant. `React.useState` preserves the reference of the set dispatcher, but doesn't for the surrounding tuple. Adding a few memo, most children, except for components that are cheap to render, are not re-rendered. Also no deep re-render, the appearance state is propagated via old good CSS cascading rather than React state. --- src/Blog.res | 4 +-- src/Packages.res | 8 ++--- src/SyntaxLookup.res | 4 +-- src/Try.res | 4 +-- src/bindings/Next.res | 1 - src/bindings/Next.resi | 1 - src/bindings/Webapi.res | 1 + src/components/Footer.res | 2 ++ src/components/Markdown.res | 6 +--- src/components/Navigation.res | 12 ++++---- src/components/Navigation.resi | 6 +++- src/layouts/LandingPageLayout.res | 6 ++-- src/layouts/MainLayout.res | 4 +-- src/layouts/SidebarLayout.res | 49 ++++++++++++++++++++++++------- 14 files changed, 69 insertions(+), 39 deletions(-) diff --git a/src/Blog.res b/src/Blog.res index f2c0c57d2..b98841a1b 100644 --- a/src/Blog.res +++ b/src/Blog.res @@ -289,7 +289,7 @@ let default = (props: props): React.element => { } - let overlayState = React.useState(() => false) + let (isOverlayOpen, setOverlayOpen) = React.useState(() => false) let title = "Blog | ReScript Documentation" <> @@ -298,7 +298,7 @@ let default = (props: props): React.element => { />
- +
diff --git a/src/Packages.res b/src/Packages.res index 3b2aa851c..73ed2352d 100644 --- a/src/Packages.res +++ b/src/Packages.res @@ -328,8 +328,8 @@ type state = let scrollToTop: unit => unit = %raw(`function() { window.scroll({ - top: 0, - left: 0, + top: 0, + left: 0, behavior: 'smooth' }); } @@ -462,7 +462,7 @@ let default = (props: props) => { None }, [state]) - let overlayState = React.useState(() => false) + let (isOverlayOpen, setOverlayOpen) = React.useState(() => false) <> { />
- +
diff --git a/src/SyntaxLookup.res b/src/SyntaxLookup.res index 3d729b8e0..6b5af7ea2 100644 --- a/src/SyntaxLookup.res +++ b/src/SyntaxLookup.res @@ -336,7 +336,7 @@ let default = (props: props) => { onSearchValueChange("") } - let overlayState = React.useState(() => false) + let (isOverlayOpen, setOverlayOpen) = React.useState(() => false) let title = "Syntax Lookup | ReScript Documentation" let content = @@ -372,7 +372,7 @@ let default = (props: props) => { />
- +
diff --git a/src/Try.res b/src/Try.res index 2562b92ac..b40e82442 100644 --- a/src/Try.res +++ b/src/Try.res @@ -1,7 +1,7 @@ type props = {versions: array} let default = props => { - let overlayState = React.useState(() => false) + let (isOverlayOpen, setOverlayOpen) = React.useState(() => false) let lazyPlayground = Next.Dynamic.dynamic( async () => await Js.import(Playground.make), @@ -20,7 +20,7 @@ let default = props => {
- + playground
diff --git a/src/bindings/Next.res b/src/bindings/Next.res index 0179bf8c1..c783c5197 100644 --- a/src/bindings/Next.res +++ b/src/bindings/Next.res @@ -63,7 +63,6 @@ module Link = { ~children: React.element, ~className: string=?, ~target: string=?, - ~hrefRel: string=?, ) => React.element = "default" } diff --git a/src/bindings/Next.resi b/src/bindings/Next.resi index e225919df..00583343d 100644 --- a/src/bindings/Next.resi +++ b/src/bindings/Next.resi @@ -63,7 +63,6 @@ module Link: { ~children: React.element, ~className: string=?, ~target: string=?, - ~hrefRel: string=?, ) => React.element } diff --git a/src/bindings/Webapi.res b/src/bindings/Webapi.res index 27c3ff07a..5520fad21 100644 --- a/src/bindings/Webapi.res +++ b/src/bindings/Webapi.res @@ -37,6 +37,7 @@ module Window = { external removeEventListener: (string, 'a => unit) => unit = "removeEventListener" @scope("window") @val external innerWidth: int = "innerWidth" @scope("window") @val external innerHeight: int = "innerHeight" + @scope("window") @val external scrollY: int = "scrollY" } module Fetch = { diff --git a/src/components/Footer.res b/src/components/Footer.res index e1d4799a2..5947747f1 100644 --- a/src/components/Footer.res +++ b/src/components/Footer.res @@ -74,3 +74,5 @@ let make = () => {
} + +let make = React.memo(make) diff --git a/src/components/Markdown.res b/src/components/Markdown.res index e5792f9e5..a7ca402b6 100644 --- a/src/components/Markdown.res +++ b/src/components/Markdown.res @@ -380,11 +380,7 @@ module A = { | [pathname] => Js.String2.replaceByRe(pathname, regex, "") | _ => href } - + children } diff --git a/src/components/Navigation.res b/src/components/Navigation.res index 02bbb42c2..7ff8dbb48 100644 --- a/src/components/Navigation.res +++ b/src/components/Navigation.res @@ -385,9 +385,9 @@ module MobileNav = { /*
  • - + {React.string("Community")} - +
  • */ @@ -413,7 +413,7 @@ module MobileNav = { /* isOverlayOpen: if the mobile overlay is toggled open */ @react.component -let make = (~fixed=true, ~overlayState: (bool, (bool => bool) => unit)) => { +let make = (~fixed=true, ~isOverlayOpen: bool, ~setOverlayOpen: (bool => bool) => unit) => { let minWidth = "20rem" let router = Next.Router.useRouter() let route = router.route @@ -443,8 +443,6 @@ let make = (~fixed=true, ~overlayState: (bool, (bool => bool) => unit)) => { let isSubnavOpen = Js.Array2.find(collapsibles, c => c.state !== Closed) !== None - let (isOverlayOpen, setOverlayOpen) = overlayState - let toggleOverlay = () => setOverlayOpen(prev => !prev) let resetCollapsibles = () => @@ -518,7 +516,7 @@ let make = (~fixed=true, ~overlayState: (bool, (bool => bool) => unit)) => { ref={ReactDOM.Ref.domRef(navRef)} id="header" style={ReactDOMStyle.make(~minWidth, ())} - className={fixedNav ++ " items-center z-50 px-4 flex xs:justify-center w-full h-16 bg-gray-90 shadow text-white-80 text-14"}> + className={fixedNav ++ " items-center z-50 px-4 flex xs:justify-center w-full h-16 bg-gray-90 shadow text-white-80 text-14 transition duration-300 ease-out group-[.nav-disappear]:-translate-y-16 md:group-[.nav-disappear]:transform-none"}>
    bool) => unit)) => { /> } + +let make = React.memo(make) diff --git a/src/components/Navigation.resi b/src/components/Navigation.resi index 411584c8b..4c1f8c476 100644 --- a/src/components/Navigation.resi +++ b/src/components/Navigation.resi @@ -1,2 +1,6 @@ @react.component -let make: (~fixed: bool=?, ~overlayState: (bool, (bool => bool) => unit)) => React.element +let make: ( + ~fixed: bool=?, + ~isOverlayOpen: bool, + ~setOverlayOpen: (bool => bool) => unit, +) => React.element diff --git a/src/layouts/LandingPageLayout.res b/src/layouts/LandingPageLayout.res index c2548b9f4..6292a9976 100644 --- a/src/layouts/LandingPageLayout.res +++ b/src/layouts/LandingPageLayout.res @@ -191,7 +191,7 @@ module QuickInstall = { // and in the next tick, add the opacity-100 class, so the transition animation actually takes place. // If we don't do that, the banner will essentially pop up without any animation let bannerEl = Document.createElement("div") - bannerEl->Element.setClassName("foobar opacity-0 absolute top-0 mt-4 -mr-1 px-2 rounded right-0 + bannerEl->Element.setClassName("foobar opacity-0 absolute top-0 mt-4 -mr-1 px-2 rounded right-0 bg-turtle text-gray-80-tr body-sm transition-all duration-500 ease-in-out ") let textNode = Document.createTextNode("Copied!") @@ -694,7 +694,7 @@ module Sponsors = { @react.component let make = (~components=MarkdownComponents.default, ~children) => { - let overlayState = React.useState(() => false) + let (isOverlayOpen, setOverlayOpen) = React.useState(() => false) <> { />
    - +
    // Delete this again, when ReScript 11.1 is out for some time. diff --git a/src/layouts/MainLayout.res b/src/layouts/MainLayout.res index dd3994a9a..056a6b5ab 100644 --- a/src/layouts/MainLayout.res +++ b/src/layouts/MainLayout.res @@ -1,11 +1,11 @@ @react.component let make = (~components=MarkdownComponents.default, ~children) => { - let overlayState = React.useState(() => false) + let (isOverlayOpen, setOverlayOpen) = React.useState(() => false) <>
    - +
    children diff --git a/src/layouts/SidebarLayout.res b/src/layouts/SidebarLayout.res index f6e5665e2..a36fa8f05 100644 --- a/src/layouts/SidebarLayout.res +++ b/src/layouts/SidebarLayout.res @@ -206,6 +206,10 @@ module MobileDrawerButton = { } +type scrollDir = + | Up({scrollY: int}) + | Down({scrollY: int}) + @react.component let make = ( ~metaTitle: string, @@ -221,6 +225,8 @@ let make = ( ~children, ) => { let (isNavOpen, setNavOpen) = React.useState(() => false) + let (_, startScrollEventTransition) = React.useTransition() + let (scrollDir, setScrollDir) = React.useState(() => Up({scrollY: %raw(`Infinity`)})) let router = Next.Router.useRouter() let version = Url.parse(router.route).version @@ -254,6 +260,30 @@ let make = ( ) }, []) + React.useEffect(() => { + let onScroll = _e => { + startScrollEventTransition(() => { + setScrollDir( + prev => { + let Up({scrollY}) | Down({scrollY}) = prev + if scrollY === 0 || scrollY > Webapi.Window.scrollY { + Up({scrollY: Webapi.Window.scrollY}) + } else { + Down({scrollY: Webapi.Window.scrollY}) + } + }, + ) + }) + } + Webapi.Window.addEventListener("scroll", onScroll) + Some(() => Webapi.Window.removeEventListener("scroll", onScroll)) + }, []) + + let handleDrawerButtonClick = React.useCallback(evt => { + ReactEvent.Mouse.preventDefault(evt) + toggleSidebar() + }, []) + let editLinkEl = switch editHref { | Some(href) => @@ -297,25 +327,24 @@ let make = ( | None => React.null } + let navAppearanceCascading = switch scrollDir { + | Up(_) => "nav-appear" + | Down(_) => "nav-disappear" + } + <> -
    +
    - +
    sidebar
    //width of the right content part
    -