Skip to content

Instantly share code, notes, and snippets.

@SherryH
Created December 6, 2021 15:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SherryH/fe4ba0edb2a06c592e9cd096bee174ec to your computer and use it in GitHub Desktop.
Save SherryH/fe4ba0edb2a06c592e9cd096bee174ec to your computer and use it in GitHub Desktop.
Tabs Types rabbit hole
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