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.
 
 
 
 

158 lines
5.1 KiB

import ScrollToTopButton from '@/components/ScrollToTopButton'
import { Titlebar } from '@/components/Titlebar'
import { TPrimaryPageName, usePrimaryPage } from '@/PageManager'
import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import {
FOCUS_PRIMARY_SCROLL_SHORTCUT_KEY,
isRadixDialogOpen,
shouldIgnoreKeyboardShortcutEvent
} from '@/lib/keyboard-shortcuts'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
const PrimaryPageLayout = forwardRef(
(
{
children,
titlebar,
pageName,
displayScrollToTopButton = false,
hideTitlebarBottomBorder = false,
subHeader
}: {
children?: React.ReactNode
titlebar: React.ReactNode
pageName: TPrimaryPageName
displayScrollToTopButton?: boolean
hideTitlebarBottomBorder?: boolean
/** Rendered between titlebar and scroll area; not in scroll flow so it never overlaps content */
subHeader?: React.ReactNode
},
ref
) => {
const scrollAreaRef = useRef<HTMLDivElement>(null)
const smallScreenScrollAreaRef = useRef<HTMLDivElement>(null)
const smallScreenLastScrollTopRef = useRef(0)
const { isSmallScreen } = useScreenSize()
const { current, display } = usePrimaryPage()
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
const isVisible = () => {
return smallScreenScrollAreaRef.current?.checkVisibility
? smallScreenScrollAreaRef.current?.checkVisibility()
: false
}
if (isVisible()) {
window.scrollTo({ top: smallScreenLastScrollTopRef.current, behavior: 'instant' })
}
const handleScroll = () => {
if (isVisible()) {
smallScreenLastScrollTopRef.current = window.scrollY
}
}
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [current, isSmallScreen, display])
useEffect(() => {
if (isSmallScreen) return
if (current !== pageName || !display) return
const onKeyDown = (e: KeyboardEvent) => {
if (!e.altKey || !e.shiftKey || e.key.toLowerCase() !== FOCUS_PRIMARY_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, current, pageName, display])
if (isSmallScreen) {
return (
<DeepBrowsingProvider active={current === pageName && display}>
<div
ref={smallScreenScrollAreaRef}
className="min-w-0 w-full overflow-x-hidden"
style={{
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
>
<PrimaryPageTitlebar hideBottomBorder={hideTitlebarBottomBorder}>
{titlebar}
</PrimaryPageTitlebar>
{subHeader && <div className="shrink-0 w-full min-w-0 bg-background">{subHeader}</div>}
<div className="min-w-0 w-full">
{children}
</div>
</div>
{displayScrollToTopButton && <ScrollToTopButton />}
</DeepBrowsingProvider>
)
}
return (
<DeepBrowsingProvider active={current === pageName && display} scrollAreaRef={scrollAreaRef}>
<div className="relative h-full min-h-0 flex flex-col">
<PrimaryPageTitlebar hideBottomBorder={hideTitlebarBottomBorder}>
{titlebar}
</PrimaryPageTitlebar>
{subHeader && <div className="shrink-0 bg-background">{subHeader}</div>}
<div
ref={scrollAreaRef}
tabIndex={-1}
className={subHeader ? 'flex-1 min-h-0 overflow-y-auto overflow-x-hidden' : 'absolute top-12 left-0 right-0 bottom-0 overflow-y-auto overflow-x-hidden'}
>
{children}
<div className="h-4" />
</div>
</div>
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider>
)
}
)
PrimaryPageLayout.displayName = 'PrimaryPageLayout'
export default PrimaryPageLayout
export type TPrimaryPageLayoutRef = {
scrollToTop: (behavior?: ScrollBehavior) => void
}
function PrimaryPageTitlebar({
children,
hideBottomBorder = false
}: {
children?: React.ReactNode
hideBottomBorder?: boolean
}) {
return (
<Titlebar className="p-1" hideBottomBorder={hideBottomBorder}>
{children}
</Titlebar>
)
}