文件
hyb-oa/client/src/components/mobile/MobileDetailHeader.tsx
T

60 行
2.0 KiB
TypeScript

import type { ReactNode } from "react";
import { useEffect } from "react";
import { ArrowLeft } from "lucide-react";
import { useLocation, useNavigate } from "react-router-dom";
import { Button } from "../ui/button";
type MobileLocationState = {
from?: string;
};
type MobileDetailHeaderProps = {
title: string;
subtitle?: string;
status?: ReactNode;
fallbackTo: string;
testId?: string;
};
function isSafeMobileSource(value: unknown, currentPath: string) {
return typeof value === "string" && value.startsWith("/mobile") && value !== currentPath;
}
export function MobileDetailHeader({ title, subtitle, status, fallbackTo, testId }: MobileDetailHeaderProps) {
const location = useLocation();
const navigate = useNavigate();
const state = location.state as MobileLocationState | null;
const currentPath = `${location.pathname}${location.search}`;
const from = isSafeMobileSource(state?.from, currentPath) ? state?.from : null;
useEffect(() => {
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "auto" });
document.querySelector<HTMLElement>(".hyb-mobile-shell .overflow-y-auto")?.scrollTo({ top: 0, behavior: "auto" });
};
scrollToTop();
const frameId = window.requestAnimationFrame(scrollToTop);
return () => window.cancelAnimationFrame(frameId);
}, [currentPath]);
const handleBack = () => {
navigate(from ?? fallbackTo, { replace: true });
};
return (
<header className="mobile-detail-header flex min-h-12 items-center gap-3" data-testid={testId}>
<Button className="min-h-11 shrink-0 gap-1 px-3" size="sm" type="button" variant="secondary" onClick={handleBack}>
<ArrowLeft aria-hidden="true" className="size-4" />
<span>返回</span>
</Button>
<div className="min-w-0 flex-1">
<div className="truncate text-base font-semibold text-zinc-900">{title}</div>
{subtitle ? <div className="mt-0.5 truncate text-xs text-zinc-500">{subtitle}</div> : null}
</div>
{status ? <div className="shrink-0">{status}</div> : null}
</header>
);
}