withSticky.tsx63 lines · main
1'use client'
2
3import { PropsWithChildren, useCallback, type FC } from 'react'
4import { type TabsProps } from 'ui/src/components/Tabs'
5
6import { useSticky } from './withSticky.utils'
7
8interface StickyProps {
9 stickyTabList?: {
10 scrollMarginTop?: string
11 style?: CSSStyleDeclaration
12 }
13}
14
15/**
16 * Wrapper around Tabs to make the tab list sticky while the tab content is in
17 * view.
18 */
19const withSticky =
20 <Props extends PropsWithChildren<TabsProps>>(
21 Component: FC<Omit<Props, 'stickyTabList' | 'onClick'>>
22 ) =>
23 ({ stickyTabList, onClick, ...props }: Props & StickyProps) => {
24 const { inView, observedRef, stickyRef } = useSticky<HTMLDivElement>({
25 enabled: !!stickyTabList,
26 style: stickyTabList?.style,
27 })
28
29 const onClickInternal = useCallback(
30 (id: string) => {
31 if (stickyTabList && inView && stickyRef.current) {
32 let elem = stickyRef.current as Element | null
33 while (elem && !elem.matches('[role="tabpanel"][data-state="active"]')) {
34 elem = elem.nextElementSibling
35 }
36 if (!elem) return
37
38 const top = elem.getBoundingClientRect().top
39 ;(elem as HTMLElement).style.scrollMarginTop = stickyTabList?.scrollMarginTop || '0px'
40 if (top < 0) {
41 elem.scrollIntoView({
42 behavior: window.matchMedia('(prefers-reduced-motion: no-preference)').matches
43 ? 'smooth'
44 : 'instant',
45 })
46 }
47 }
48
49 onClick?.(id)
50 },
51 [!!stickyTabList, inView, onClick]
52 )
53
54 return (
55 <Component
56 {...props}
57 onClick={onClickInternal}
58 refs={{ base: observedRef, list: stickyRef }}
59 />
60 )
61 }
62
63export { withSticky }