TabsProvider.tsx105 lines · main
1'use client'
2
3import { xor } from 'lodash'
4import {
5 createContext,
6 useCallback,
7 useContext,
8 useMemo,
9 useState,
10 type Dispatch,
11 type PropsWithChildren,
12 type SetStateAction,
13} from 'react'
14
15export interface TabGroup {
16 tabIds: string[]
17 activeId: string
18}
19
20interface TabsContextValue {
21 tabGroups: TabGroup[]
22 setTabGroups: Dispatch<SetStateAction<TabGroup[]>>
23}
24
25const TabsContext = createContext<TabsContextValue>({
26 tabGroups: [],
27 setTabGroups: () => {},
28})
29
30/**
31 * Tracks active Tab IDs across the site so that tabs
32 * with the same ID stay in sync (eg. JS vs TS tabs).
33 */
34const TabsProvider = ({ children }: PropsWithChildren<{}>) => {
35 const [tabGroups, setTabGroups] = useState<TabGroup[]>([])
36
37 return <TabsContext.Provider value={{ tabGroups, setTabGroups }}>{children}</TabsContext.Provider>
38}
39
40export interface UseTabGroupValue {
41 /**
42 * The active tab ID for the tab group.
43 * This value is shared with all matching tab
44 * groups within the context.
45 */
46 groupActiveId?: string
47
48 /**
49 * Set the active tab ID for the tab group.
50 * Value will be shared with all matching tab
51 * groups within the context.
52 */
53 setGroupActiveId?(id: string): void
54}
55
56/**
57 * Hook to retrieve and set the active tab ID for
58 * the current tab group.
59 * The value will be shared with all matching tab
60 * groups within the context.
61 *
62 * Silently fails if no `TabsProvider` is set.
63 */
64export const useTabGroup = (tabIds: string[]): UseTabGroupValue => {
65 const tabsContext = useContext(TabsContext)
66
67 if (!tabsContext) {
68 return {}
69 }
70
71 const { tabGroups, setTabGroups } = tabsContext
72
73 const groupActiveId = useMemo(
74 () => tabGroups.find((group) => xor(group.tabIds, tabIds).length === 0)?.activeId,
75 [tabGroups]
76 )
77
78 const setGroupActiveId = useCallback((id: string) => {
79 setTabGroups((groups) => {
80 // Clone the array
81 const newGroups = groups.concat()
82
83 const existingGroupIndex = groups.findIndex((group) => xor(group.tabIds, tabIds).length === 0)
84
85 const tabGroup: TabGroup = {
86 tabIds,
87 activeId: id,
88 }
89
90 // If this group already exists, replace it
91 // Otherwise add to the end
92 if (existingGroupIndex !== -1) {
93 newGroups.splice(existingGroupIndex, 1, tabGroup)
94 } else {
95 newGroups.push(tabGroup)
96 }
97
98 return newGroups
99 })
100 }, [])
101
102 return { groupActiveId, setGroupActiveId }
103}
104
105export default TabsProvider