Skip to content

Commit

Permalink
feat(tabs): add element customization (#1840)
Browse files Browse the repository at this point in the history
  • Loading branch information
andioneto committed Sep 8, 2021
1 parent a157911 commit 0123334
Show file tree
Hide file tree
Showing 10 changed files with 729 additions and 31 deletions.
6 changes: 6 additions & 0 deletions .changeset/cool-cats-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/tabs': minor
'@twilio-paste/core': minor
---

[Tabs]: Enable Component to respect element customizations set on the customization provider. Component now enables setting an element name on the underlying HTML element and checks the emotion theme object to determine whether it should merge in custom styles to the ones set by the component author.
337 changes: 337 additions & 0 deletions packages/paste-core/components/tabs/__tests__/tabs.test.tsx

Large diffs are not rendered by default.

20 changes: 14 additions & 6 deletions packages/paste-core/components/tabs/src/Tab.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {Box, safelySpreadBoxProps} from '@twilio-paste/box';
import type {BoxStyleProps} from '@twilio-paste/box';
import type {BoxStyleProps, BoxProps} from '@twilio-paste/box';
import {TabPrimitive} from '@twilio-paste/tabs-primitive';
import {TabsContext} from './TabsContext';
import {Orientation, Variants} from './types';
import type {Orientation, Variants} from './types';

import {getElementName} from './utils';

// TODO:
// Split vertical tabs into a separate component
Expand Down Expand Up @@ -94,13 +96,18 @@ export interface TabProps extends React.HTMLAttributes<HTMLElement> {
id?: string | undefined;
focusable?: boolean | undefined;
disabled?: boolean | undefined;
element?: BoxProps['element'];
children: React.ReactNode;
'aria-disabled'?: boolean;
}

const Tab = React.forwardRef<HTMLDivElement, TabProps>(({children, ...tabProps}, ref) => {
const Tab = React.forwardRef<HTMLDivElement, TabProps>(({children, element, ...tabProps}, ref) => {
const tab = React.useContext(TabsContext);
const boxStyles = React.useMemo(() => getTabBoxStyles(tab.orientation, tab.variant), [tab.orientation, tab.variant]);

const {orientation} = tab;
const elementName = getElementName(orientation, 'TAB', element);

return (
<TabPrimitive {...(tab as any)} {...tabProps} ref={ref}>
{(props: TabProps) => {
Expand All @@ -110,13 +117,14 @@ const Tab = React.forwardRef<HTMLDivElement, TabProps>(({children, ...tabProps},
{...boxStyles}
as="span"
cursor={props['aria-disabled'] ? 'not-allowed' : 'pointer'}
element={elementName}
fontSize="fontSize30"
fontWeight="fontWeightSemibold"
overflow={tab.orientation !== 'vertical' ? 'hidden' : undefined}
overflow={orientation !== 'vertical' ? 'hidden' : undefined}
position="relative"
textOverflow={tab.orientation !== 'vertical' ? 'ellipsis' : undefined}
textOverflow={orientation !== 'vertical' ? 'ellipsis' : undefined}
transition="border-color 100ms ease, color 100ms ease"
whiteSpace={tab.orientation !== 'vertical' ? 'nowrap' : undefined}
whiteSpace={orientation !== 'vertical' ? 'nowrap' : undefined}
>
{children}
</Box>
Expand Down
23 changes: 16 additions & 7 deletions packages/paste-core/components/tabs/src/TabList.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {Box} from '@twilio-paste/box';
import type {BoxProps} from '@twilio-paste/box';
import {TabPrimitiveList} from '@twilio-paste/tabs-primitive';
import {TabsContext} from './TabsContext';
import {Variants} from './types';
import type {Variants} from './types';
import {getElementName} from './utils';

export interface TabListProps {
'aria-label': string;
disabled?: boolean | undefined;
element?: BoxProps['element'];
focusable?: boolean | undefined;
children: React.ReactNode;
variant?: Variants;
}

const HorizontalTabList: React.FC = ({children}) => (
const HorizontalTabList: React.FC<{element?: BoxProps['element']}> = ({children, element}) => (
<Box
display="flex"
borderBottomWidth="borderWidth10"
borderBottomColor="colorBorderWeak"
borderBottomStyle="solid"
element={element}
marginBottom="space60"
>
{children}
</Box>
);

const VerticalTabList: React.FC = ({children}) => (
const VerticalTabList: React.FC<{element?: BoxProps['element']}> = ({children, element}) => (
<Box
borderLeftWidth="borderWidth10"
borderLeftColor="colorBorderWeak"
borderLeftStyle="solid"
element={element}
marginRight="space110"
minWidth="size20"
maxWidth="size40"
Expand All @@ -38,12 +43,15 @@ const VerticalTabList: React.FC = ({children}) => (
</Box>
);

const TabList = React.forwardRef<HTMLDivElement, TabListProps>(({children, variant, ...props}, ref) => {
const TabList = React.forwardRef<HTMLDivElement, TabListProps>(({children, element, variant, ...props}, ref) => {
const tab = React.useContext(TabsContext);
const TabListWrapper = tab.orientation === 'vertical' ? VerticalTabList : HorizontalTabList;
const {orientation} = tab;
const elementName = getElementName(orientation, 'TAB_LIST', element);
const TabListWrapper = orientation === 'vertical' ? VerticalTabList : HorizontalTabList;

return (
<TabPrimitiveList {...(tab as any)} {...props} ref={ref}>
<TabListWrapper>{children}</TabListWrapper>
<TabPrimitiveList {...(tab as any)} as={Box} {...props} element={elementName} ref={ref}>
<TabListWrapper element={`${elementName}_CHILD`}>{children}</TabListWrapper>
</TabPrimitiveList>
);
});
Expand All @@ -53,6 +61,7 @@ if (process.env.NODE_ENV === 'development') {
'aria-label': PropTypes.string.isRequired,
focusable: PropTypes.bool,
disabled: PropTypes.bool,
element: PropTypes.string,
};
}

Expand Down
11 changes: 8 additions & 3 deletions packages/paste-core/components/tabs/src/TabPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {Box} from '@twilio-paste/box';
import type {BoxStyleProps} from '@twilio-paste/box';
import type {BoxStyleProps, BoxProps} from '@twilio-paste/box';
import {TabPrimitivePanel} from '@twilio-paste/tabs-primitive';
import {TabsContext} from './TabsContext';
import {getElementName} from './utils';

export const tabPanelStyles = {
borderRadius: 'borderRadius20',
Expand All @@ -17,19 +18,23 @@ export interface TabPanelProps {
id?: string | undefined;
tabId?: string | undefined;
children: React.ReactNode;
element?: BoxProps['element'];
}

const TabPanel = React.forwardRef<HTMLDivElement, TabPanelProps>(({children, ...props}, ref) => {
const TabPanel = React.forwardRef<HTMLDivElement, TabPanelProps>(({children, element, ...props}, ref) => {
const tab = React.useContext(TabsContext);
const elementName = getElementName(tab.orientation, 'TAB_PANEL', element);

return (
<TabPrimitivePanel {...(tab as any)} {...tabPanelStyles} {...props} as={Box} ref={ref}>
<TabPrimitivePanel {...(tab as any)} {...tabPanelStyles} {...props} element={elementName} as={Box} ref={ref}>
{children}
</TabPrimitivePanel>
);
});

if (process.env.NODE_ENV === 'development') {
TabPanel.propTypes = {
element: PropTypes.string,
id: PropTypes.string,
tabId: PropTypes.string,
};
Expand Down
18 changes: 16 additions & 2 deletions packages/paste-core/components/tabs/src/TabPanels.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {Box, safelySpreadBoxProps} from '@twilio-paste/box';
import type {BoxProps} from '@twilio-paste/box';

import {TabsContext} from './TabsContext';
import {getElementName} from './utils';

export interface TabPanelsProps {
children: React.ReactNode;
element?: BoxProps['element'];
}

const TabPanels = React.forwardRef<HTMLDivElement, TabPanelsProps>(({children, ...props}, ref) => {
const TabPanels = React.forwardRef<HTMLDivElement, TabPanelsProps>(({children, element, ...props}, ref) => {
const {orientation} = React.useContext(TabsContext);
const elementName = getElementName(orientation, 'TAB_PANELS', element);
return (
<Box {...safelySpreadBoxProps(props)} width="100%" ref={ref}>
<Box {...safelySpreadBoxProps(props)} element={elementName} width="100%" ref={ref}>
{children}
</Box>
);
});

if (process.env.NODE_ENV === 'development') {
TabPanels.propTypes = {
element: PropTypes.string,
};
}

TabPanels.displayName = 'TabPanels';
export {TabPanels};
32 changes: 23 additions & 9 deletions packages/paste-core/components/tabs/src/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,56 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import type {BoxProps} from '@twilio-paste/box';
import {Flex} from '@twilio-paste/flex';
import {useTabPrimitiveState, TabPrimitiveInitialState, TabPrimitiveStateReturn} from '@twilio-paste/tabs-primitive';
import {Box} from '@twilio-paste/box';
import {useTabPrimitiveState} from '@twilio-paste/tabs-primitive';
import type {TabPrimitiveInitialState, TabPrimitiveStateReturn} from '@twilio-paste/tabs-primitive';
import {TabsContext} from './TabsContext';
import {Variants} from './types';
import type {Variants} from './types';
import {getElementName} from './utils';

export interface TabStateReturn extends TabPrimitiveStateReturn {
[key: string]: any;
}

export interface TabsProps extends TabPrimitiveInitialState {
children?: React.ReactNode;
element?: BoxProps['element'];
state?: TabStateReturn;
variant?: Variants;
}

// Set orientation to horizontal because undefined enables all arrow key movement
const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
({children, orientation = 'horizontal', state, variant, ...initialState}, ref) => {
const tab = state || useTabPrimitiveState({orientation, ...initialState});
const value = React.useMemo(() => ({...tab, variant}), [...Object.values(tab), variant]);
({children, element, orientation = 'horizontal', state, variant, ...initialState}, ref) => {
// If returned state from primitive has orientation set to undefined, use the default "horizontal"
const {orientation: tabOrientation = orientation, ...tab} =
state || useTabPrimitiveState({orientation, ...initialState});
const elementName = getElementName(tabOrientation, 'TABS', element);
const value = React.useMemo(() => ({...tab, orientation: tabOrientation, variant}), [
...Object.values(tab),
tabOrientation,
variant,
]);
const returnValue = <TabsContext.Provider value={value}>{children}</TabsContext.Provider>;

if (tab.orientation === 'vertical') {
if (tabOrientation === 'vertical') {
return (
<Flex ref={ref} wrap={false} vAlignContent="stretch">
<Flex element={elementName} ref={ref} wrap={false} vAlignContent="stretch">
{returnValue}
</Flex>
);
}
return returnValue;

return <Box element={elementName}>{returnValue}</Box>;
}
);

if (process.env.NODE_ENV === 'development') {
Tabs.propTypes = {
element: PropTypes.string,
selectedId: PropTypes.string,
orientation: PropTypes.oneOf(['horizontal', 'vertical', null]),
orientation: PropTypes.oneOf(['horizontal', 'vertical', undefined]),
variant: PropTypes.oneOf(['fitted', null]),
};
}
Expand Down
9 changes: 5 additions & 4 deletions packages/paste-core/components/tabs/src/TabsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as React from 'react';
import {TabPrimitiveState} from '@twilio-paste/tabs-primitive';
import {Variants} from './types';
import type {TabPrimitiveState} from '@twilio-paste/tabs-primitive';
import type {Variants} from './types';

interface TabState extends TabPrimitiveState {
interface TabState extends Omit<TabPrimitiveState, 'orientation'> {
variant?: Variants;
orientation: 'horizontal' | 'vertical';
}

const TabsContext = React.createContext<Partial<TabState>>({});
const TabsContext = React.createContext<TabState>({} as TabState);

export {TabsContext};
5 changes: 5 additions & 0 deletions packages/paste-core/components/tabs/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const getElementName = (
orientation: 'horizontal' | 'vertical',
fallback: string,
elementName?: string | undefined
): string => (elementName != null ? elementName : `${orientation.toUpperCase()}_${fallback}`);
Loading

0 comments on commit 0123334

Please sign in to comment.