Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/app/components/LeftSidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Button from "@app/components/ui/Button";
import Icon from "@app/components/ui/Icon";
import { useAppLayout } from "@app/providers/AppLayout";
import { useMobileScrollReveal } from "@app/providers/AppLayout/useMobileScrollReveal";
import { useSidebarSwipe } from "@app/providers/AppLayout/useSidebarSwipe";
import { cn } from "@app/utils/cn";
import React from "react";
Expand All @@ -20,6 +21,7 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
hideContentWhenClosed = false,
}) => {
const { appLayout, updateAppLayout } = useAppLayout();
const scrollVisible = useMobileScrollReveal();

const sidebarOpen = appLayout.leftSidebarOpen;

Expand Down Expand Up @@ -70,10 +72,12 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
onPress={toggleSidebar}
aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
className={cn(
"flex-none m-2 z-10",
"flex-none m-2 z-10 transition-opacity duration-500",
sidebarOpen
? ""
: "fixed left-0 bottom-[env(safe-area-inset-bottom)] md:bottom-0 md:relative",
: "fixed left-0 bottom-[env(safe-area-inset-bottom)] md:bottom-0 md:relative md:opacity-100",
!sidebarOpen &&
(scrollVisible ? "opacity-100" : "opacity-0 md:opacity-100"),
)}
>
<Icon
Expand Down
8 changes: 6 additions & 2 deletions src/app/components/RightSidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Button from "@app/components/ui/Button";
import Icon from "@app/components/ui/Icon";
import { useAppLayout } from "@app/providers/AppLayout";
import { useMobileScrollReveal } from "@app/providers/AppLayout/useMobileScrollReveal";
import { useSidebarSwipe } from "@app/providers/AppLayout/useSidebarSwipe";
import { cn } from "@app/utils/cn";
import React from "react";
Expand All @@ -23,6 +24,7 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
} = useAppLayout();

const sidebarOpen = rightSidebarOpen;
const scrollVisible = useMobileScrollReveal();

const bind = useSidebarSwipe({
isOpen: sidebarOpen,
Expand Down Expand Up @@ -68,10 +70,12 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
onPress={toggleSidebar}
aria-label={rightSidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
className={cn(
"flex-none m-2 z-10 pointer-events-auto",
"flex-none m-2 z-10 pointer-events-auto transition-opacity duration-500",
rightSidebarOpen
? ""
: "fixed right-0 bottom-[env(safe-area-inset-bottom)] lg:bottom-0 lg:relative lg:self-center",
: "fixed right-0 bottom-[env(safe-area-inset-bottom)] lg:bottom-0 lg:relative lg:self-center lg:opacity-100",
!rightSidebarOpen &&
(scrollVisible ? "opacity-100" : "opacity-0 lg:opacity-100"),
)}
>
<Icon
Expand Down
41 changes: 41 additions & 0 deletions src/app/providers/AppLayout/useMobileScrollReveal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useCallback, useEffect, useRef, useState } from "react";

const FADE_DELAY_MS = 3000;

/**
* On mobile, returns a `visible` boolean that is `true` when the user is idle
* and `false` while scrolling. After scrolling stops, waits FADE_DELAY_MS then
* fades back in.
*
* Always returns `true` on non-touch / desktop viewports.
*/
export const useMobileScrollReveal = (): boolean => {
const [visible, setVisible] = useState(true);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const handleScroll = useCallback(() => {
setVisible(false);
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setVisible(true);
}, FADE_DELAY_MS);
}, []);

useEffect(() => {
// Use capture so we catch scroll events from any scrollable child element
window.addEventListener("scroll", handleScroll, {
capture: true,
passive: true,
});
return () => {
window.removeEventListener("scroll", handleScroll, { capture: true });
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
};
}, [handleScroll]);

return visible;
};
7 changes: 6 additions & 1 deletion src/app/providers/AppPage/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Button from "@app/components/ui/Button";
import { useAppLayout } from "@app/providers/AppLayout";
import { useMobileScrollReveal } from "@app/providers/AppLayout/useMobileScrollReveal";
import {
SWIPE_THRESHOLD_DISTANCE,
SWIPE_THRESHOLD_VELOCITY,
Expand All @@ -21,6 +22,7 @@ export const AppPageProvider: React.FC<{
const [hasChildren, setHasChildren] = React.useState(false);
const [showArticleFixedInfo, setShowArticleFixedInfo] = React.useState(false);
const { pathname } = useLocation();
const scrollVisible = useMobileScrollReveal();
const {
appLayout: { leftSidebarOpen, rightSidebarOpen },
updateAppLayout,
Expand Down Expand Up @@ -110,7 +112,10 @@ export const AppPageProvider: React.FC<{
isIconOnly
size="sm"
variant="flat"
className="absolute top-2 right-3"
className={cn(
"absolute top-2 right-3 transition-opacity duration-500",
scrollVisible ? "opacity-100" : "opacity-0 md:opacity-100",
)}
onPress={() => setShowArticleFixedInfo(true)}
onMouseEnter={() => setShowArticleFixedInfo(true)}
>
Expand Down
Loading