feat: add discover back to top fab

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-09-12 13:21:26 +08:00
parent c038c791af
commit a97e60cd00
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
5 changed files with 135 additions and 4 deletions

View File

@ -0,0 +1,107 @@
import { m } from "@renderer/components/common/Motion"
import { useTypeScriptHappyCallback } from "@renderer/hooks/common/useTypescriptHappyCallback"
import { jotaiStore } from "@renderer/lib/jotai"
import { cn } from "@renderer/lib/utils"
import clsx from "clsx"
import { typescriptHappyForwardRef } from "foxact/typescript-happy-forward-ref"
import type { HTMLMotionProps } from "framer-motion"
import { AnimatePresence } from "framer-motion"
import { atom, useAtomValue } from "jotai"
import type React from "react"
import type { JSX, PropsWithChildren, ReactNode } from "react"
import { useId } from "react"
import { RootPortal } from "../portal"
const fabContainerElementAtom = atom(null as HTMLDivElement | null)
export interface FABConfig {
id: string
icon: JSX.Element
onClick: () => void
}
export const FABBase = typescriptHappyForwardRef(
(
props: PropsWithChildren<
{
id: string
show?: boolean
children: JSX.Element
} & HTMLMotionProps<"button">
>,
ref: React.ForwardedRef<HTMLButtonElement>,
) => {
const { children, show = true, ...extra } = props
const { className, ...rest } = extra
return (
<AnimatePresence>
{show && (
<m.button
type="button"
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
ref={ref}
aria-label="Floating action button"
className={cn(
"mt-2 flex items-center justify-center",
"size-9 text-lg md:text-base",
"outline-accent hover:opacity-100 focus:opacity-100 focus:outline-none",
"rounded-xl border border-zinc-400/20 backdrop-blur-lg dark:border-zinc-500/30 dark:text-zinc-200",
"bg-zinc-50/80 shadow-lg dark:bg-neutral-900/80",
"transition-all duration-500 ease-in-out",
className,
)}
{...rest}
>
{children}
</m.button>
)}
</AnimatePresence>
)
},
)
export const FABPortable = typescriptHappyForwardRef(
(
props: {
children: React.JSX.Element
onClick: () => void
show?: boolean
},
ref: React.ForwardedRef<HTMLButtonElement>,
) => {
const { onClick, children, show = true } = props
const id = useId()
const portalElement = useAtomValue(fabContainerElementAtom)
if (!portalElement) return null
return (
<RootPortal to={portalElement}>
<FABBase ref={ref} id={id} show={show} onClick={onClick}>
{children}
</FABBase>
</RootPortal>
)
},
)
export const FABContainer = (props: { children?: ReactNode }) => {
return (
<div
ref={useTypeScriptHappyCallback((ref) => jotaiStore.set(fabContainerElementAtom, ref), [])}
data-testid="fab-container"
data-hide-print
className={clsx(
"fixed bottom-[calc(2rem+env(safe-area-inset-bottom))] left-[calc(100vw-3rem-1rem)] z-[9] flex flex-col",
"transition-transform duration-300 ease-in-out",
)}
>
{props.children}
</div>
)
}

View File

@ -0,0 +1 @@
export * from "./FABContainer"

View File

@ -91,7 +91,9 @@ export const FeedForm: Component<{
<div className="center grow flex-col gap-3">
<i className="i-mgc-close-cute-re size-7 text-red-500" />
<p>Error in fetching feed.</p>
<p className="break-all px-8 text-center">{getFetchErrorMessage(feedQuery.error)}</p>
<p className="cursor-text select-text break-all px-8 text-center">
{getFetchErrorMessage(feedQuery.error)}
</p>
<div className="flex items-center gap-4">
<Button

View File

@ -41,6 +41,7 @@ export function Component() {
const [search, setSearch] = useSearchParams()
const { t } = useTranslation()
useSubViewTitle("Discover")
return (
<>
<div className="text-2xl font-bold">{t("words.discover")}</div>

View File

@ -1,7 +1,9 @@
import { getReadonlyRoute } from "@renderer/atoms/route"
import { getSidebarActiveView, setSidebarActiveView } from "@renderer/atoms/sidebar"
import { MotionButtonBase } from "@renderer/components/ui/button"
import { FABContainer, FABPortable } from "@renderer/components/ui/fab"
import { ScrollArea } from "@renderer/components/ui/scroll-area"
import { springScrollTo } from "@renderer/lib/scroller"
import { cn } from "@renderer/lib/utils"
import { useEffect, useRef, useState } from "react"
import { Outlet, useNavigate } from "react-router-dom"
@ -20,13 +22,14 @@ export function Component() {
const $scroll = scrollRef
if (!$scroll) return
$scroll.scrollTop = 0
$scroll.onscroll = () => {
springScrollTo(0, $scroll)
const handler = () => {
setIsTitleSticky($scroll.scrollTop > 120)
}
$scroll.addEventListener("scroll", handler)
return () => {
$scroll.onscroll = null
$scroll.removeEventListener("scroll", handler)
}
}, [scrollRef, title])
@ -65,6 +68,23 @@ export function Component() {
>
<Outlet />
</ScrollArea.ScrollArea>
<FABContainer>
<BackToTopFAB show={isTitleSticky} scrollRef={scrollRef} />
</FABContainer>
</div>
)
}
const BackToTopFAB = ({ show, scrollRef }: { show: boolean; scrollRef: any }) => {
return (
<FABPortable
onClick={() => {
springScrollTo(0, scrollRef)
}}
show={show}
>
<i className="i-mingcute-arow-to-up-line" />
</FABPortable>
)
}