Created
December 6, 2021 15:15
-
-
Save SherryH/fe4ba0edb2a06c592e9cd096bee174ec to your computer and use it in GitHub Desktop.
Tabs Types rabbit hole
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { FC, ReactComponentElement, ReactNode } from 'react'; | |
import React, { Children } from 'react'; | |
import { Colors, space } from '@smartly/tokens'; | |
import type { FlexBoxProps } from '@smartly/container'; | |
import { Direction, FlexBox } from '@smartly/container'; | |
import styled, { css } from 'styled-components'; | |
import { useKeyboardFocus } from '@smartly/ui-hooks'; | |
import type { | |
TabsContextType, | |
HorizontalPosition, | |
VerticalPosition, | |
Position | |
} from './types'; | |
import { Orientation } from './types'; | |
import type { TabItemProps } from './types'; | |
import { Tab } from './TabItem'; | |
import { TabsContext } from './TabContext'; | |
export type HorizontalTabsProps = { | |
orientation?: Orientation.Horizontal; | |
tabsPosition?: HorizontalPosition; | |
}; | |
export type VerticalTabsProps = { | |
orientation?: Orientation.Vertical; | |
tabsPosition?: VerticalPosition; | |
}; | |
export type TabsProps = (VerticalTabsProps | HorizontalTabsProps) & | |
Pick<FlexBoxProps, 'as'> & { | |
activeTabId?: React.Key; | |
children: React.ReactComponentElement<typeof Tab>[]; | |
onIndexChange?: (activeTabId: number) => void; | |
/** | |
* A descriptive label to explain what this tab list is about. | |
*/ | |
tabListAriaLabel?: string; | |
}; | |
export type TabsPropsType = FC<TabsProps> & { | |
Item: FC<TabItemProps>; | |
}; | |
const getContainerFlexDirection = ( | |
orientation: Orientation, | |
tabsPosition: Position | |
): Direction | undefined => { | |
switch (orientation) { | |
case Orientation.Horizontal: | |
if (tabsPosition === 'bottom') { | |
return Direction.ColumnReverse; | |
} else { | |
return Direction.Column; | |
} | |
case Orientation.Vertical: | |
if (tabsPosition === 'right') { | |
return Direction.RowReverse; | |
} else { | |
return Direction.Row; | |
} | |
default: | |
return undefined; | |
} | |
}; | |
const getTabsBarPosition = ( | |
orientation: Orientation, | |
tabsPosition: Position | |
): Position => { | |
switch (orientation) { | |
case Orientation.Horizontal: | |
if (tabsPosition === 'bottom') { | |
return 'top'; | |
} else { | |
return 'bottom'; | |
} | |
case Orientation.Vertical: | |
if (tabsPosition === 'right') { | |
return 'left'; | |
} else { | |
return 'right'; | |
} | |
} | |
}; | |
const getTabsFlexDirection = ( | |
orientation: Orientation | |
): Direction | undefined => { | |
return orientation === Orientation.Vertical ? Direction.Column : undefined; | |
}; | |
const tabsBarPositionStyles = { | |
top: `border-top: 2px solid ${Colors.gray400}`, | |
right: `border-right: 1px solid ${Colors.gray400}`, | |
bottom: `border-bottom: 2px solid ${Colors.gray400}`, | |
left: `border-left: 1px solid ${Colors.gray400}` | |
}; | |
const TabList = styled(FlexBox)<{ tabsBarPosition: Position }>` | |
${({ tabsBarPosition }) => | |
tabsBarPosition && | |
css` | |
${tabsBarPositionStyles[tabsBarPosition]} | |
`} | |
`; | |
const TabPanel = styled.div` | |
&:focus-visible { | |
outline: 2px solid ${Colors.blue500}; | |
outline-offset: -2px; | |
} | |
`; | |
export const DEFAULT_ACTIVE_TAB_ID = '0'; | |
export const filterTabs = (child: ReactNode): is ReactComponentElement<typeof Tab></typeof> => { | |
if (typeof child !== 'string' && React.isValidElement<typeof Tab>(child)) { | |
if (typeof child.type !== 'string') { | |
if ('name' in child.type) { | |
return child.type.name === 'Tab'; | |
} | |
if ('displayName' in child.type) { | |
return child.type['displayName'] === 'Tab'; | |
} | |
} | |
} | |
return false; | |
}; | |
export const Tabs: TabsPropsType = ({ | |
activeTabId = DEFAULT_ACTIVE_TAB_ID, | |
as, | |
children, | |
orientation = Orientation.Horizontal, | |
tabsPosition = orientation === Orientation.Horizontal ? 'top' : 'left', | |
tabListAriaLabel | |
}) => { | |
const tabItems: ReactComponentElement<typeof Tab>[] = Children.toArray(children).filter(filterTabs); | |
const containerFlexDirection = getContainerFlexDirection( | |
orientation, | |
tabsPosition | |
); | |
const tabsFlexDirection = getTabsFlexDirection(orientation); | |
const tabsBarPosition = getTabsBarPosition(orientation, tabsPosition); | |
const { handleKeyDown, ...focus } = useKeyboardFocus({ | |
direction: orientation | |
}); | |
const context: TabsContextType = { | |
activeTabId, | |
orientation, | |
tabsBarPosition, | |
tabsPosition, | |
focusProps: focus | |
}; | |
return ( | |
<TabsContext.Provider value={context}> | |
<FlexBox | |
as={as} | |
backgroundColor={Colors.white} | |
gap={space(0)} | |
direction={containerFlexDirection} | |
> | |
<TabList | |
role="tablist" | |
aria-label={tabListAriaLabel} | |
tabsBarPosition={tabsBarPosition} | |
direction={tabsFlexDirection} | |
gap={orientation === Orientation.Horizontal ? space(2) : space(0)} | |
onKeyDown={handleKeyDown} | |
> | |
{tabItems} | |
</TabList> | |
{tabItems.map((tab) => ( | |
<TabPanel | |
role="tabpanel" | |
aria-labelledby={`tab-${tab.props.tabId}`} | |
tabIndex={0} | |
id={`panel-${tab.props.tabId}`} | |
key={tab.key} | |
hidden={tab.props.tabId !== activeTabId} | |
> | |
{tab.props.children} | |
</TabPanel> | |
))} | |
</FlexBox> | |
</TabsContext.Provider> | |
); | |
}; | |
Tabs.Item = Tab; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment