Restore native mobile page scrolling
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState, type UIEvent } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { BookOpen, Moon, Sun } from "lucide-react";
|
import { BookOpen, Moon, Sun } from "lucide-react";
|
||||||
import { Outlet, useLocation, useNavigate, useParams } from "@/lib/router";
|
import { Outlet, useLocation, useNavigate, useParams } from "@/lib/router";
|
||||||
@@ -177,11 +177,7 @@ export function Layout() {
|
|||||||
};
|
};
|
||||||
}, [isMobile, sidebarOpen, setSidebarOpen]);
|
}, [isMobile, sidebarOpen, setSidebarOpen]);
|
||||||
|
|
||||||
const handleMainScroll = useCallback(
|
const updateMobileNavVisibility = useCallback((currentTop: number) => {
|
||||||
(event: UIEvent<HTMLElement>) => {
|
|
||||||
if (!isMobile) return;
|
|
||||||
|
|
||||||
const currentTop = event.currentTarget.scrollTop;
|
|
||||||
const delta = currentTop - lastMainScrollTop.current;
|
const delta = currentTop - lastMainScrollTop.current;
|
||||||
|
|
||||||
if (currentTop <= 24) {
|
if (currentTop <= 24) {
|
||||||
@@ -193,12 +189,44 @@ export function Layout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastMainScrollTop.current = currentTop;
|
lastMainScrollTop.current = currentTop;
|
||||||
},
|
}, []);
|
||||||
[isMobile],
|
|
||||||
);
|
useEffect(() => {
|
||||||
|
if (!isMobile) {
|
||||||
|
setMobileNavVisible(true);
|
||||||
|
lastMainScrollTop.current = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
updateMobileNavVisibility(window.scrollY || document.documentElement.scrollTop || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
onScroll();
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", onScroll);
|
||||||
|
};
|
||||||
|
}, [isMobile, updateMobileNavVisibility]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const previousOverflow = document.body.style.overflow;
|
||||||
|
|
||||||
|
document.body.style.overflow = isMobile ? "visible" : "hidden";
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = previousOverflow;
|
||||||
|
};
|
||||||
|
}, [isMobile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-dvh bg-background text-foreground overflow-hidden pt-[env(safe-area-inset-top)]">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-background text-foreground pt-[env(safe-area-inset-top)]",
|
||||||
|
isMobile ? "min-h-dvh" : "flex h-dvh overflow-hidden",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="#main-content"
|
href="#main-content"
|
||||||
className="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-[200] focus:rounded-md focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-[200] focus:rounded-md focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
@@ -287,14 +315,22 @@ export function Layout() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className="flex-1 flex flex-col min-w-0 h-full">
|
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "h-full flex-1")}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
isMobile && "sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/85",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<BreadcrumbBar />
|
<BreadcrumbBar />
|
||||||
<div className="flex flex-1 min-h-0">
|
</div>
|
||||||
|
<div className={cn(isMobile ? "block" : "flex flex-1 min-h-0")}>
|
||||||
<main
|
<main
|
||||||
id="main-content"
|
id="main-content"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={cn("flex-1 overflow-auto p-4 md:p-6", isMobile && "pb-[calc(5rem+env(safe-area-inset-bottom))]")}
|
className={cn(
|
||||||
onScroll={handleMainScroll}
|
"flex-1 p-4 md:p-6",
|
||||||
|
isMobile ? "overflow-visible pb-[calc(5rem+env(safe-area-inset-bottom))]" : "overflow-auto",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{hasUnknownCompanyPrefix ? (
|
{hasUnknownCompanyPrefix ? (
|
||||||
<NotFoundPage
|
<NotFoundPage
|
||||||
|
|||||||
@@ -1,29 +1,68 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { ArrowDown } from "lucide-react";
|
import { ArrowDown } from "lucide-react";
|
||||||
|
|
||||||
|
function resolveScrollTarget() {
|
||||||
|
const mainContent = document.getElementById("main-content");
|
||||||
|
|
||||||
|
if (mainContent instanceof HTMLElement) {
|
||||||
|
const overflowY = window.getComputedStyle(mainContent).overflowY;
|
||||||
|
const usesOwnScroll =
|
||||||
|
(overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay")
|
||||||
|
&& mainContent.scrollHeight > mainContent.clientHeight + 1;
|
||||||
|
|
||||||
|
if (usesOwnScroll) {
|
||||||
|
return { type: "element" as const, element: mainContent };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: "window" as const };
|
||||||
|
}
|
||||||
|
|
||||||
|
function distanceFromBottom(target: ReturnType<typeof resolveScrollTarget>) {
|
||||||
|
if (target.type === "element") {
|
||||||
|
return target.element.scrollHeight - target.element.scrollTop - target.element.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scroller = document.scrollingElement ?? document.documentElement;
|
||||||
|
return scroller.scrollHeight - window.scrollY - window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Floating scroll-to-bottom button that appears when the user is far from the
|
* Floating scroll-to-bottom button that follows the active page scroller.
|
||||||
* bottom of the `#main-content` scroll container. Hides when within 300px of
|
* On desktop that is `#main-content`; on mobile it falls back to window/page scroll.
|
||||||
* the bottom. Positioned to avoid the mobile bottom nav.
|
|
||||||
*/
|
*/
|
||||||
export function ScrollToBottom() {
|
export function ScrollToBottom() {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = document.getElementById("main-content");
|
|
||||||
if (!el) return;
|
|
||||||
const check = () => {
|
const check = () => {
|
||||||
const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
|
setVisible(distanceFromBottom(resolveScrollTarget()) > 300);
|
||||||
setVisible(distance > 300);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mainContent = document.getElementById("main-content");
|
||||||
|
|
||||||
check();
|
check();
|
||||||
el.addEventListener("scroll", check, { passive: true });
|
mainContent?.addEventListener("scroll", check, { passive: true });
|
||||||
return () => el.removeEventListener("scroll", check);
|
window.addEventListener("scroll", check, { passive: true });
|
||||||
|
window.addEventListener("resize", check);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mainContent?.removeEventListener("scroll", check);
|
||||||
|
window.removeEventListener("scroll", check);
|
||||||
|
window.removeEventListener("resize", check);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const scroll = useCallback(() => {
|
const scroll = useCallback(() => {
|
||||||
const el = document.getElementById("main-content");
|
const target = resolveScrollTarget();
|
||||||
if (el) el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
|
|
||||||
|
if (target.type === "element") {
|
||||||
|
target.element.scrollTo({ top: target.element.scrollHeight, behavior: "smooth" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scroller = document.scrollingElement ?? document.documentElement;
|
||||||
|
window.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user