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