xref: /expo/docs/ui/components/Tabs/Tabs.tsx (revision da751fd5)
1import { css } from '@emotion/react';
2import { shadows, theme } from '@expo/styleguide';
3import { borderRadius, spacing } from '@expo/styleguide-base';
4import { TabList, TabPanels, Tabs as ReachTabs, TabsProps } from '@reach/tabs';
5import * as React from 'react';
6
7import { TabButton } from './TabButton';
8
9type Props = React.PropsWithChildren<TabsProps> & {
10  tabs: string[];
11};
12
13const generateTabLabels = (children: React.ReactNode) => {
14  return React.Children.map(children, child =>
15    React.isValidElement(child) ? child?.props?.label : child || '[untitled]'
16  );
17};
18
19const SharedTabsContext = React.createContext<{
20  index: number;
21  setIndex: (index: number) => void;
22} | null>(null);
23
24/**
25 * Wraps a group of tabs to share the same state. Useful for guides where one aspect of the guide is broken up into multiple tabs, e.g. Yarn vs NPM.
26 */
27export function TabsGroup({ children }: { children: React.ReactNode }) {
28  const [index, setIndex] = React.useState(0);
29  return (
30    <SharedTabsContext.Provider value={{ index, setIndex }}>{children}</SharedTabsContext.Provider>
31  );
32}
33
34export const Tabs = (props: Props) => {
35  const context = React.useContext(SharedTabsContext);
36  const [tabIndex, setTabIndex] = React.useState(0);
37
38  if (context) {
39    return <InnerTabs {...props} {...context} />;
40  }
41
42  return <InnerTabs {...props} index={tabIndex} setIndex={setTabIndex} />;
43};
44
45const InnerTabs = ({
46  children,
47  tabs,
48  index: tabIndex,
49  setIndex,
50}: Props & { index: number; setIndex: (index: number) => void }) => {
51  const tabTitles = tabs || generateTabLabels(children);
52
53  return (
54    <ReachTabs index={tabIndex} onChange={setIndex} css={tabsWrapperStyle}>
55      <TabList css={tabsListStyle}>
56        {tabTitles.map((title, index) => (
57          <TabButton key={index} selected={index === tabIndex}>
58            {title}
59          </TabButton>
60        ))}
61      </TabList>
62      <TabPanels css={tabsPanelStyle}>{children}</TabPanels>
63    </ReachTabs>
64  );
65};
66
67const tabsWrapperStyle = css({
68  border: `1px solid ${theme.border.default}`,
69  borderRadius: borderRadius.sm,
70  boxShadow: shadows.xs,
71  margin: `${spacing[4]}px 0`,
72});
73
74const tabsPanelStyle = css({
75  padding: `${spacing[4]}px ${spacing[5]}px ${spacing[1]}px`,
76
77  'pre:first-child': {
78    marginTop: spacing[1],
79  },
80
81  ul: {
82    marginBottom: spacing[3],
83  },
84});
85
86const tabsListStyle = css({
87  borderBottom: `1px solid ${theme.border.default}`,
88});
89