UNPKG

3.64 kBPlain TextView Raw
1import * as React from "react";
2import { createComponent } from "reakit-system/createComponent";
3import { createHook } from "reakit-system/createHook";
4import { useForkRef } from "reakit-utils/useForkRef";
5import {
6 DisclosureContentOptions,
7 DisclosureContentHTMLProps,
8 useDisclosureContent,
9} from "../Disclosure/DisclosureContent";
10import {
11 unstable_useId,
12 unstable_IdOptions,
13 unstable_IdHTMLProps,
14} from "../Id/Id";
15import { TabStateReturn } from "./TabState";
16import { TAB_PANEL_KEYS } from "./__keys";
17
18export type TabPanelOptions = DisclosureContentOptions &
19 unstable_IdOptions &
20 Pick<
21 TabStateReturn,
22 "selectedId" | "registerPanel" | "unregisterPanel" | "panels" | "items"
23 > & {
24 /**
25 * Tab's id
26 */
27 tabId?: string;
28 };
29
30export type TabPanelHTMLProps = DisclosureContentHTMLProps &
31 unstable_IdHTMLProps;
32
33export type TabPanelProps = TabPanelOptions & TabPanelHTMLProps;
34
35function getTabsWithoutPanel(
36 tabs: TabPanelOptions["items"],
37 panels: TabPanelOptions["panels"]
38) {
39 const panelsTabIds = panels.map((panel) => panel.groupId).filter(Boolean);
40 return tabs.filter(
41 (item) => panelsTabIds.indexOf(item.id || undefined) === -1
42 );
43}
44
45function getPanelIndex(
46 panels: TabPanelOptions["panels"],
47 panel: typeof panels[number]
48) {
49 const panelsWithoutTabId = panels.filter((p) => !p.groupId);
50 return panelsWithoutTabId.indexOf(panel);
51}
52
53/**
54 * When <TabPanel> is used without tabId:
55 *
56 * - First render: getTabId will return undefined because options.panels
57 * doesn't contain the current panel yet (registerPanel wasn't called yet).
58 * Thus registerPanel will be called without groupId (tabId).
59 *
60 * - Second render: options.panels already contains the current panel
61 * (because registerPanel was called in the previous render). This means that
62 * we'll be able to get the related tabId with the tab panel index. Basically,
63 * we filter out all the tabs and panels that have already matched. In this
64 * phase, registerPanel will be called again with the proper groupId (tabId).
65 *
66 * - In the third render, panel.groupId will be already defined, so we just
67 * return it. registerPanel is not called.
68 */
69function getTabId(options: TabPanelOptions) {
70 const panel = options.panels?.find((p) => p.id === options.id);
71 const tabId = options.tabId || panel?.groupId;
72 if (tabId || !panel || !options.panels || !options.items) {
73 return tabId;
74 }
75 const panelIndex = getPanelIndex(options.panels, panel);
76 const tabsWithoutPanel = getTabsWithoutPanel(options.items, options.panels);
77 return tabsWithoutPanel[panelIndex]?.id || undefined;
78}
79
80export const useTabPanel = createHook<TabPanelOptions, TabPanelHTMLProps>({
81 name: "TabPanel",
82 compose: [unstable_useId, useDisclosureContent],
83 keys: TAB_PANEL_KEYS,
84
85 useProps(options, { ref: htmlRef, ...htmlProps }) {
86 const ref = React.useRef<HTMLElement>(null);
87 const tabId = getTabId(options);
88 const { id, registerPanel, unregisterPanel } = options;
89
90 React.useEffect(() => {
91 if (!id) return undefined;
92 registerPanel?.({ id, ref, groupId: tabId });
93 return () => {
94 unregisterPanel?.(id);
95 };
96 }, [tabId, id, registerPanel, unregisterPanel]);
97
98 return {
99 ref: useForkRef(ref, htmlRef),
100 role: "tabpanel",
101 tabIndex: 0,
102 "aria-labelledby": tabId,
103 ...htmlProps,
104 };
105 },
106
107 useComposeOptions(options) {
108 const tabId = getTabId(options);
109 return {
110 visible: tabId ? options.selectedId === tabId : false,
111 ...options,
112 };
113 },
114});
115
116export const TabPanel = createComponent({
117 as: "div",
118 useHook: useTabPanel,
119});