You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

228 lines
7.4 KiB

import HelpAndAccountMenu from '@/components/HelpAndAccountMenu'
import ScrollToTopButton from '@/components/ScrollToTopButton'
import { ReadOnlySessionIndicator } from '@/components/ReadOnlySessionIndicator'
import { Titlebar } from '@/components/Titlebar'
import { Button } from '@/components/ui/button'
import {
FOCUS_SECONDARY_SCROLL_SHORTCUT_KEY,
isRadixDialogOpen,
shouldIgnoreKeyboardShortcutEvent
} from '@/lib/keyboard-shortcuts'
import { useSecondaryPage } from '@/PageManager'
import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { cn } from '@/lib/utils'
import { ChevronLeft } from 'lucide-react'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { useTranslation } from 'react-i18next'
const SecondaryPageLayout = forwardRef(
(
{
children,
index,
title,
controls,
hideBackButton = false,
hideTitlebarBottomBorder = false,
displayScrollToTopButton = false,
titlebar
}: {
children?: React.ReactNode
index?: number
title?: React.ReactNode
controls?: React.ReactNode
hideBackButton?: boolean
hideTitlebarBottomBorder?: boolean
displayScrollToTopButton?: boolean
titlebar?: React.ReactNode
},
ref
) => {
const scrollAreaRef = useRef<HTMLDivElement>(null)
const { isSmallScreen } = useScreenSize()
const { currentIndex } = useSecondaryPage()
const shouldRenderTitlebar =
titlebar != null || (title != null && title !== '') || !hideBackButton
useImperativeHandle(
ref,
() => ({
scrollToTop: (behavior: ScrollBehavior = 'smooth') => {
setTimeout(() => {
if (scrollAreaRef.current) {
return scrollAreaRef.current.scrollTo({ top: 0, behavior })
}
window.scrollTo({ top: 0, behavior })
}, 10)
}
}),
[]
)
useEffect(() => {
if (!isSmallScreen) return
setTimeout(() => scrollAreaRef.current?.scrollTo({ top: 0 }), 10)
}, [isSmallScreen])
useEffect(() => {
if (isSmallScreen) return
if (currentIndex !== index) return
const onKeyDown = (e: KeyboardEvent) => {
if (!e.altKey || !e.shiftKey || e.key.toLowerCase() !== FOCUS_SECONDARY_SCROLL_SHORTCUT_KEY) return
if (e.metaKey || e.ctrlKey) return
if (shouldIgnoreKeyboardShortcutEvent(e.target)) return
if (isRadixDialogOpen()) return
e.preventDefault()
scrollAreaRef.current?.focus({ preventScroll: true })
}
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [isSmallScreen, currentIndex, index])
if (isSmallScreen) {
return (
<DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}>
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{shouldRenderTitlebar ? (
<SecondaryPageTitlebar
title={title}
controls={controls}
hideBackButton={hideBackButton}
hideBottomBorder={hideTitlebarBottomBorder}
titlebar={titlebar}
/>
) : null}
<div
ref={scrollAreaRef}
className="page-scroll-y min-h-0 min-w-0 flex-1 basis-0 overflow-y-scroll overflow-x-hidden overscroll-y-contain touch-pan-y"
style={{
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
>
{children}
</div>
</div>
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider>
)
}
return (
<DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}>
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{shouldRenderTitlebar ? (
<SecondaryPageTitlebar
title={title}
controls={controls}
hideBackButton={hideBackButton}
hideBottomBorder={hideTitlebarBottomBorder}
titlebar={titlebar}
/>
) : null}
<div
ref={scrollAreaRef}
tabIndex={-1}
className="page-scroll-y min-h-0 min-w-0 flex-1 basis-0 overflow-y-scroll overflow-x-auto overscroll-y-contain"
>
{children}
<div className="h-12" />
</div>
</div>
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider>
)
}
)
SecondaryPageLayout.displayName = 'SecondaryPageLayout'
export default SecondaryPageLayout
function SecondaryPageTitlebar({
title,
controls,
hideBackButton = false,
hideBottomBorder = false,
titlebar,
sticky = false
}: {
title?: React.ReactNode
controls?: React.ReactNode
hideBackButton?: boolean
hideBottomBorder?: boolean
titlebar?: React.ReactNode
/** Keep back visible while the page scrolls (mobile secondary stack). */
sticky?: boolean
}): JSX.Element {
const { isSmallScreen } = useScreenSize()
const { t } = useTranslation()
const titlebarInset = isSmallScreen
? 'py-1 pl-2 pr-[max(0.75rem,env(safe-area-inset-right,0px))]'
: 'p-1'
const stickyClass = sticky
? 'sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80'
: ''
if (titlebar) {
return (
<Titlebar
className={cn(titlebarInset, stickyClass)}
hideBottomBorder={hideBottomBorder}
>
<div className="flex w-full min-w-0 items-center gap-2">
<ReadOnlySessionIndicator variant="titlebar" />
<div className="min-w-0 flex-1">{titlebar}</div>
{isSmallScreen ? <HelpAndAccountMenu variant="titlebar" /> : null}
</div>
</Titlebar>
)
}
return (
<Titlebar
className={cn(titlebarInset, stickyClass)}
hideBottomBorder={hideBottomBorder}
>
<div className="flex w-full min-w-0 items-center gap-2 font-semibold">
<ReadOnlySessionIndicator variant="titlebar" />
<div className="flex min-w-0 flex-1 items-center justify-between gap-1">
{hideBackButton ? (
title ? (
<div className="app-chrome-title flex w-fit items-center gap-2 truncate pl-2">
{title}
</div>
) : null
) : (
<div className="flex min-w-0 items-center">
<BackButton>{title ?? t('back')}</BackButton>
</div>
)}
<div className="flex shrink-0 flex-wrap items-center justify-end gap-0.5 min-w-0 max-w-[min(100%,14rem)] sm:max-w-none">
{controls}
</div>
</div>
{isSmallScreen ? <HelpAndAccountMenu variant="titlebar" /> : null}
</div>
</Titlebar>
)
}
function BackButton({ children }: { children?: React.ReactNode }) {
const { t } = useTranslation()
const { pop } = useSecondaryPage()
return (
<Button
className="flex gap-1 items-center w-fit max-w-full justify-start pl-2 pr-3"
variant="ghost"
size="titlebar-icon"
title={t('back')}
onClick={() => pop()}
>
<ChevronLeft />
<div className="app-chrome-title truncate">{children}</div>
</Button>
)
}