TabsProvider.tsx105 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import { xor } from 'lodash' |
| 4 | import { |
| 5 | createContext, |
| 6 | useCallback, |
| 7 | useContext, |
| 8 | useMemo, |
| 9 | useState, |
| 10 | type Dispatch, |
| 11 | type PropsWithChildren, |
| 12 | type SetStateAction, |
| 13 | } from 'react' |
| 14 | |
| 15 | export interface TabGroup { |
| 16 | tabIds: string[] |
| 17 | activeId: string |
| 18 | } |
| 19 | |
| 20 | interface TabsContextValue { |
| 21 | tabGroups: TabGroup[] |
| 22 | setTabGroups: Dispatch<SetStateAction<TabGroup[]>> |
| 23 | } |
| 24 | |
| 25 | const 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 | */ |
| 34 | const TabsProvider = ({ children }: PropsWithChildren<{}>) => { |
| 35 | const [tabGroups, setTabGroups] = useState<TabGroup[]>([]) |
| 36 | |
| 37 | return <TabsContext.Provider value={{ tabGroups, setTabGroups }}>{children}</TabsContext.Provider> |
| 38 | } |
| 39 | |
| 40 | export 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 | */ |
| 64 | export 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 | |
| 105 | export default TabsProvider |