From 012333451cc36383cd4dc6f79fb1e678da676d66 Mon Sep 17 00:00:00 2001 From: Andi Oneto Date: Wed, 8 Sep 2021 15:59:34 -0700 Subject: [PATCH] feat(tabs): add element customization (#1840) --- .changeset/cool-cats-fly.md | 6 + .../components/tabs/__tests__/tabs.test.tsx | 337 ++++++++++++++++++ .../paste-core/components/tabs/src/Tab.tsx | 20 +- .../components/tabs/src/TabList.tsx | 23 +- .../components/tabs/src/TabPanel.tsx | 11 +- .../components/tabs/src/TabPanels.tsx | 18 +- .../paste-core/components/tabs/src/Tabs.tsx | 32 +- .../components/tabs/src/TabsContext.tsx | 9 +- .../paste-core/components/tabs/src/utils.ts | 5 + .../components/tabs/stories/index.stories.tsx | 299 ++++++++++++++++ 10 files changed, 729 insertions(+), 31 deletions(-) create mode 100644 .changeset/cool-cats-fly.md create mode 100644 packages/paste-core/components/tabs/src/utils.ts diff --git a/.changeset/cool-cats-fly.md b/.changeset/cool-cats-fly.md new file mode 100644 index 0000000000..28afff4bb3 --- /dev/null +++ b/.changeset/cool-cats-fly.md @@ -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. diff --git a/packages/paste-core/components/tabs/__tests__/tabs.test.tsx b/packages/paste-core/components/tabs/__tests__/tabs.test.tsx index 6ced28215c..5b0133660f 100644 --- a/packages/paste-core/components/tabs/__tests__/tabs.test.tsx +++ b/packages/paste-core/components/tabs/__tests__/tabs.test.tsx @@ -1,11 +1,37 @@ import * as React from 'react'; +import {matchers} from 'jest-emotion'; import {render, screen} from '@testing-library/react'; +import {CustomizationProvider} from '@twilio-paste/customization'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore typescript doesn't like js imports import axe from '../../../../../.jest/axe-helper'; import {HorizontalTabs, StateHookTabs} from '../stories/index.stories'; import {Tabs, Tab, TabList, TabPanels, TabPanel} from '../src'; +import {getElementName} from '../src/utils'; + +expect.extend(matchers); describe('Tabs', () => { + describe('Utils', () => { + describe('getElementName', () => { + const mockFallbackName = 'TEST_ELEMENT_FALLBACK'; + + it('should return the correct string when element is undefined', () => { + expect(getElementName('horizontal', mockFallbackName)).toEqual('HORIZONTAL_TEST_ELEMENT_FALLBACK'); + expect(getElementName('vertical', mockFallbackName)).toEqual('VERTICAL_TEST_ELEMENT_FALLBACK'); + }); + + it('should return the correct string when element is defined', () => { + expect(getElementName('horizontal', mockFallbackName, 'CUSTOM_NAME')).toEqual('CUSTOM_NAME'); + expect(getElementName('vertical', mockFallbackName, 'CUSTOM_NAME')).toEqual('CUSTOM_NAME'); + }); + + it('should return the correct string when element is an empty string', () => { + expect(getElementName('horizontal', mockFallbackName, '')).toEqual(''); + expect(getElementName('vertical', mockFallbackName, '')).toEqual(''); + }); + }); + }); describe('Render', () => { it('relevant html and aria attributes', () => { const [tabOneId, tabTwoId, tabThreeId, panelOneId, panelTwoId, panelThreeId] = [...new Array(6)].map( @@ -92,6 +118,317 @@ describe('Tabs', () => { }); }); + describe('HTML Attribute', () => { + it('should set an element data attribute for Horizontal Tabs (default)', () => { + render( + + + Tab 1 is a long tab name because the server sent a long tab name + Tab 2 + Tab 3 + + + Tab 1 + Tab 2 + Tab 3 + + + ); + + const outerDiv = screen.getByTestId('tab-list').parentElement as HTMLElement; + + expect(outerDiv.getAttribute('data-paste-element')).toEqual('HORIZONTAL_TABS'); + expect(screen.getByTestId('tab-list').getAttribute('data-paste-element')).toEqual('HORIZONTAL_TAB_LIST'); + expect((screen.getByTestId('tab-list').firstChild as HTMLElement).getAttribute('data-paste-element')).toEqual( + 'HORIZONTAL_TAB_LIST_CHILD' + ); + expect(screen.getByTestId('tab-1').getAttribute('data-paste-element')).toEqual('HORIZONTAL_TAB'); + expect(screen.getByTestId('tab-2').getAttribute('data-paste-element')).toEqual('HORIZONTAL_TAB'); + expect(screen.getByTestId('tab-3').getAttribute('data-paste-element')).toEqual('HORIZONTAL_TAB'); + expect(screen.getByTestId('tab-panels').getAttribute('data-paste-element')).toEqual('HORIZONTAL_TAB_PANELS'); + expect(screen.getByTestId('tab-panel-1').getAttribute('data-paste-element')).toEqual('HORIZONTAL_TAB_PANEL'); + expect(screen.getByTestId('tab-panel-2').getAttribute('data-paste-element')).toEqual('HORIZONTAL_TAB_PANEL'); + expect(screen.getByTestId('tab-panel-3').getAttribute('data-paste-element')).toEqual('HORIZONTAL_TAB_PANEL'); + }); + + it('should set an element data attribute for Vertical Tabs (default)', () => { + render( + + + Tab 1 is a long tab name because the server sent a long tab name + Tab 2 + Tab 3 + + + Tab 1 + Tab 2 + Tab 3 + + + ); + + const outerDiv = screen.getByTestId('tab-list').parentElement as HTMLElement; + + expect(outerDiv.getAttribute('data-paste-element')).toEqual('VERTICAL_TABS'); + expect(screen.getByTestId('tab-list').getAttribute('data-paste-element')).toEqual('VERTICAL_TAB_LIST'); + expect((screen.getByTestId('tab-list').firstChild as HTMLElement).getAttribute('data-paste-element')).toEqual( + 'VERTICAL_TAB_LIST_CHILD' + ); + expect(screen.getByTestId('tab-1').getAttribute('data-paste-element')).toEqual('VERTICAL_TAB'); + expect(screen.getByTestId('tab-2').getAttribute('data-paste-element')).toEqual('VERTICAL_TAB'); + expect(screen.getByTestId('tab-3').getAttribute('data-paste-element')).toEqual('VERTICAL_TAB'); + expect(screen.getByTestId('tab-panels').getAttribute('data-paste-element')).toEqual('VERTICAL_TAB_PANELS'); + expect(screen.getByTestId('tab-panel-1').getAttribute('data-paste-element')).toEqual('VERTICAL_TAB_PANEL'); + expect(screen.getByTestId('tab-panel-2').getAttribute('data-paste-element')).toEqual('VERTICAL_TAB_PANEL'); + expect(screen.getByTestId('tab-panel-3').getAttribute('data-paste-element')).toEqual('VERTICAL_TAB_PANEL'); + }); + + it('should set an element data attribute Horizontal Tabs', () => { + render( + + + + Tab 1 is a long tab name because the server sent a long tab name + + + Tab 2 + + + Tab 3 + + + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + + ); + + const outerDiv = screen.getByTestId('tab-list').parentElement as HTMLElement; + + expect(outerDiv.getAttribute('data-paste-element')).toEqual('HORSE'); + expect(screen.getByTestId('tab-list').getAttribute('data-paste-element')).toEqual('CAT'); + expect((screen.getByTestId('tab-list').firstChild as HTMLElement).getAttribute('data-paste-element')).toEqual( + 'CAT_CHILD' + ); + expect(screen.getByTestId('tab-1').getAttribute('data-paste-element')).toEqual('TIGER'); + expect(screen.getByTestId('tab-2').getAttribute('data-paste-element')).toEqual('PANTHER'); + expect(screen.getByTestId('tab-3').getAttribute('data-paste-element')).toEqual('SCOTTISH_FOLD'); + expect(screen.getByTestId('tab-panels').getAttribute('data-paste-element')).toEqual('DOG'); + expect(screen.getByTestId('tab-panel-1').getAttribute('data-paste-element')).toEqual('CORGI'); + expect(screen.getByTestId('tab-panel-2').getAttribute('data-paste-element')).toEqual('GOLDEN_DOODLE'); + expect(screen.getByTestId('tab-panel-3').getAttribute('data-paste-element')).toEqual('PUG'); + }); + + it('should set an element data attribute for Vertical Tabs', () => { + render( + + + + Tab 1 is a long tab name because the server sent a long tab name + + + Tab 2 + + + Tab 3 + + + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + + ); + + const outerDiv = screen.getByTestId('tab-list').parentElement as HTMLElement; + + expect(outerDiv.getAttribute('data-paste-element')).toEqual('HORSE'); + expect(screen.getByTestId('tab-list').getAttribute('data-paste-element')).toEqual('CAT'); + expect((screen.getByTestId('tab-list').firstChild as HTMLElement).getAttribute('data-paste-element')).toEqual( + 'CAT_CHILD' + ); + expect(screen.getByTestId('tab-1').getAttribute('data-paste-element')).toEqual('TIGER'); + expect(screen.getByTestId('tab-2').getAttribute('data-paste-element')).toEqual('PANTHER'); + expect(screen.getByTestId('tab-3').getAttribute('data-paste-element')).toEqual('SCOTTISH_FOLD'); + expect(screen.getByTestId('tab-panels').getAttribute('data-paste-element')).toEqual('DOG'); + expect(screen.getByTestId('tab-panel-1').getAttribute('data-paste-element')).toEqual('CORGI'); + expect(screen.getByTestId('tab-panel-2').getAttribute('data-paste-element')).toEqual('GOLDEN_DOODLE'); + expect(screen.getByTestId('tab-panel-3').getAttribute('data-paste-element')).toEqual('PUG'); + }); + }); + + describe('Customization', () => { + it('should add custom styles to Tabs', () => { + render( + + + + Tab 1 is a long tab name because the server sent a long tab name + Tab 2 + Tab 3 + + + Tab 1 + Tab 2 + Tab 3 + + + + ); + + expect(screen.getByTestId('tab-list').parentElement).toHaveStyleRule('margin', '2.25rem'); + expect(screen.getByTestId('tab-list').parentElement).toHaveStyleRule('padding', '2.25rem'); + expect(screen.getByTestId('tab-list').parentElement).toHaveStyleRule('border-style', 'solid'); + expect(screen.getByTestId('tab-list').parentElement).toHaveStyleRule('border-width', '4px'); + expect(screen.getByTestId('tab-list').parentElement).toHaveStyleRule('border-color', 'rgb(2,99,224)'); + + expect(screen.getByTestId('tab-list')).toHaveStyleRule('margin-top', '2.25rem'); + expect(screen.getByTestId('tab-list')).toHaveStyleRule('margin-bottom', '2.25rem'); + + expect(screen.getByTestId('tab-list').firstChild).toHaveStyleRule('border-color', 'rgb(214,31,31)'); + + expect(screen.getByTestId('tab-1')).toHaveStyleRule('font-family', "'Fira Mono','Courier New',Courier,monospace"); + expect(screen.getByTestId('tab-2')).toHaveStyleRule('font-family', "'Fira Mono','Courier New',Courier,monospace"); + expect(screen.getByTestId('tab-3')).toHaveStyleRule('font-family', "'Fira Mono','Courier New',Courier,monospace"); + + expect(screen.getByTestId('tab-panels')).toHaveStyleRule('margin-top', '0.125rem'); + expect(screen.getByTestId('tab-panels')).toHaveStyleRule('margin-bottom', '0.125rem'); + + expect(screen.getByTestId('tab-panel-1')).toHaveStyleRule('padding-right', '0.5rem'); + expect(screen.getByTestId('tab-panel-2')).toHaveStyleRule('padding-left', '0.5rem'); + }); + + it('should add custom styles to Tabs with a custom element data attribute', () => { + render( + + + + + Tab 1 is a long tab name because the server sent a long tab name + + + Tab 2 + + + Tab 3 + + + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + + + ); + + expect(screen.getByTestId('tab-list').parentElement).toHaveStyleRule('margin', '2.25rem'); + expect(screen.getByTestId('tab-list').parentElement).toHaveStyleRule('padding', '2.25rem'); + expect(screen.getByTestId('tab-list').parentElement).toHaveStyleRule('border-style', 'solid'); + expect(screen.getByTestId('tab-list').parentElement).toHaveStyleRule('border-width', '4px'); + expect(screen.getByTestId('tab-list').parentElement).toHaveStyleRule('border-color', 'rgb(2,99,224)'); + + expect(screen.getByTestId('tab-list')).toHaveStyleRule('margin-top', '2.25rem'); + expect(screen.getByTestId('tab-list')).toHaveStyleRule('margin-bottom', '2.25rem'); + + expect(screen.getByTestId('tab-list').firstChild).toHaveStyleRule('border-color', 'rgb(214,31,31)'); + + expect(screen.getByTestId('tab-1')).toHaveStyleRule('font-family', "'Fira Mono','Courier New',Courier,monospace"); + expect(screen.getByTestId('tab-2')).toHaveStyleRule('font-family', "'Fira Mono','Courier New',Courier,monospace"); + expect(screen.getByTestId('tab-3')).toHaveStyleRule('font-family', "'Fira Mono','Courier New',Courier,monospace"); + + expect(screen.getByTestId('tab-panels')).toHaveStyleRule('margin-top', '0.125rem'); + expect(screen.getByTestId('tab-panels')).toHaveStyleRule('margin-bottom', '0.125rem'); + + expect(screen.getByTestId('tab-panel-1')).toHaveStyleRule('padding-right', '0'); + expect(screen.getByTestId('tab-panel-2')).toHaveStyleRule('padding-left', '0.25rem'); + expect(screen.getByTestId('tab-panel-3')).toHaveStyleRule('padding-left', '0.75rem'); + }); + }); + describe('Accessibility', () => { it('Should have no accessibility violations', async () => { const {container} = render(); diff --git a/packages/paste-core/components/tabs/src/Tab.tsx b/packages/paste-core/components/tabs/src/Tab.tsx index cb8a0bf82b..1534ca276c 100644 --- a/packages/paste-core/components/tabs/src/Tab.tsx +++ b/packages/paste-core/components/tabs/src/Tab.tsx @@ -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 @@ -94,13 +96,18 @@ export interface TabProps extends React.HTMLAttributes { id?: string | undefined; focusable?: boolean | undefined; disabled?: boolean | undefined; + element?: BoxProps['element']; children: React.ReactNode; 'aria-disabled'?: boolean; } -const Tab = React.forwardRef(({children, ...tabProps}, ref) => { +const Tab = React.forwardRef(({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 ( {(props: TabProps) => { @@ -110,13 +117,14 @@ const Tab = React.forwardRef(({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} diff --git a/packages/paste-core/components/tabs/src/TabList.tsx b/packages/paste-core/components/tabs/src/TabList.tsx index 94743d0b31..d235595dc7 100644 --- a/packages/paste-core/components/tabs/src/TabList.tsx +++ b/packages/paste-core/components/tabs/src/TabList.tsx @@ -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}) => ( {children} ); -const VerticalTabList: React.FC = ({children}) => ( +const VerticalTabList: React.FC<{element?: BoxProps['element']}> = ({children, element}) => ( ( ); -const TabList = React.forwardRef(({children, variant, ...props}, ref) => { +const TabList = React.forwardRef(({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 ( - - {children} + + {children} ); }); @@ -53,6 +61,7 @@ if (process.env.NODE_ENV === 'development') { 'aria-label': PropTypes.string.isRequired, focusable: PropTypes.bool, disabled: PropTypes.bool, + element: PropTypes.string, }; } diff --git a/packages/paste-core/components/tabs/src/TabPanel.tsx b/packages/paste-core/components/tabs/src/TabPanel.tsx index 2fcc8071ca..0744bbb77c 100644 --- a/packages/paste-core/components/tabs/src/TabPanel.tsx +++ b/packages/paste-core/components/tabs/src/TabPanel.tsx @@ -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', @@ -17,12 +18,15 @@ export interface TabPanelProps { id?: string | undefined; tabId?: string | undefined; children: React.ReactNode; + element?: BoxProps['element']; } -const TabPanel = React.forwardRef(({children, ...props}, ref) => { +const TabPanel = React.forwardRef(({children, element, ...props}, ref) => { const tab = React.useContext(TabsContext); + const elementName = getElementName(tab.orientation, 'TAB_PANEL', element); + return ( - + {children} ); @@ -30,6 +34,7 @@ const TabPanel = React.forwardRef(({children, ... if (process.env.NODE_ENV === 'development') { TabPanel.propTypes = { + element: PropTypes.string, id: PropTypes.string, tabId: PropTypes.string, }; diff --git a/packages/paste-core/components/tabs/src/TabPanels.tsx b/packages/paste-core/components/tabs/src/TabPanels.tsx index c5f6f77293..e0b3076f2f 100644 --- a/packages/paste-core/components/tabs/src/TabPanels.tsx +++ b/packages/paste-core/components/tabs/src/TabPanels.tsx @@ -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(({children, ...props}, ref) => { +const TabPanels = React.forwardRef(({children, element, ...props}, ref) => { + const {orientation} = React.useContext(TabsContext); + const elementName = getElementName(orientation, 'TAB_PANELS', element); return ( - + {children} ); }); +if (process.env.NODE_ENV === 'development') { + TabPanels.propTypes = { + element: PropTypes.string, + }; +} + TabPanels.displayName = 'TabPanels'; export {TabPanels}; diff --git a/packages/paste-core/components/tabs/src/Tabs.tsx b/packages/paste-core/components/tabs/src/Tabs.tsx index 7781249d25..b14f3c4296 100644 --- a/packages/paste-core/components/tabs/src/Tabs.tsx +++ b/packages/paste-core/components/tabs/src/Tabs.tsx @@ -1,9 +1,13 @@ 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; @@ -11,32 +15,42 @@ export interface TabStateReturn extends TabPrimitiveStateReturn { 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( - ({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 = {children}; - if (tab.orientation === 'vertical') { + if (tabOrientation === 'vertical') { return ( - + {returnValue} ); } - return returnValue; + + return {returnValue}; } ); 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]), }; } diff --git a/packages/paste-core/components/tabs/src/TabsContext.tsx b/packages/paste-core/components/tabs/src/TabsContext.tsx index ef92556bef..16319a23bf 100644 --- a/packages/paste-core/components/tabs/src/TabsContext.tsx +++ b/packages/paste-core/components/tabs/src/TabsContext.tsx @@ -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 { variant?: Variants; + orientation: 'horizontal' | 'vertical'; } -const TabsContext = React.createContext>({}); +const TabsContext = React.createContext({} as TabState); export {TabsContext}; diff --git a/packages/paste-core/components/tabs/src/utils.ts b/packages/paste-core/components/tabs/src/utils.ts new file mode 100644 index 0000000000..1394626cf5 --- /dev/null +++ b/packages/paste-core/components/tabs/src/utils.ts @@ -0,0 +1,5 @@ +export const getElementName = ( + orientation: 'horizontal' | 'vertical', + fallback: string, + elementName?: string | undefined +): string => (elementName != null ? elementName : `${orientation.toUpperCase()}_${fallback}`); diff --git a/packages/paste-core/components/tabs/stories/index.stories.tsx b/packages/paste-core/components/tabs/stories/index.stories.tsx index 55b77b4f31..d5ba030f95 100644 --- a/packages/paste-core/components/tabs/stories/index.stories.tsx +++ b/packages/paste-core/components/tabs/stories/index.stories.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; +import {CustomizationProvider} from '@twilio-paste/customization'; import {useUID} from '@twilio-paste/uid-library'; import {Button} from '@twilio-paste/button'; import {Heading} from '@twilio-paste/heading'; import {Anchor} from '@twilio-paste/anchor'; import {Paragraph} from '@twilio-paste/paragraph'; +import {useTheme} from '@twilio-paste/theme'; import {useTabState, Tabs, TabList, Tab, TabPanels, TabPanel} from '../src'; import type {TabStateReturn} from '../src'; @@ -188,6 +190,7 @@ const useButtonClickTabState = (): TabStateReturn => { export const StateHookTabs: React.FC = () => { const {...tab} = useButtonClickTabState(); + return ( @@ -241,6 +244,302 @@ CenterAlignTabTest.story = { name: 'Testing Center Alignment', }; +export const CustomHorizontalTabs: React.FC = () => { + const currentTheme = useTheme(); + const selectedId = useUID(); + + return ( + + + + Inside Out + Transgender District + + Audre Lorde Project + + Coming soon... + + + + + Inside Out + + + Inside Out empowers, educates, and advocates for LGBTQ+ of youth from the Pikes Peak Region in Southern + Colorado. Inside Out does this by creating safe spaces, support systems and teaching life skills to all + youth in the community and working to make the community safer and more accepting of gender and sexual + orientation diversity. + + Support Inside Out + + + + Transgender District + + + The mission of the Transgender District is to create an urban environment that fosters the rich history, + culture, legacy, and empowerment of transgender people and its deep roots in the southeastern Tenderloin + neighborhood. The transgender district aims to stabilize and economically empower the transgender + community through ownership of homes, businesses, historic and cultural sites, and safe community spaces. + + Support The Transgender District + + + + Audre Lorde Project + + + The Audre Lorde Project is a Lesbian, Gay, Bisexual, Two Spirit, Trans and Gender Non Conforming People of + Color center for community organizing, focusing on the New York City area. Through mobilization, education + and capacity-building, they work for community wellness and progressive social and economic justice. + Committed to struggling across differences, they seek to responsibly reflect, represent and serve their + various communities. + + Support The Audre Lorde Project + + + + + ); +}; + +export const CustomVerticalTabs: React.FC = () => { + const currentTheme = useTheme(); + const selectedId = useUID(); + + return ( + + + + Inside Out + Transgender District + Audre Lorde Project + + Coming soon... + + + + + + Inside Out + + + Inside Out empowers, educates, and advocates for LGBTQ+ of youth from the Pikes Peak Region in Southern + Colorado. Inside Out does this by creating safe spaces, support systems and teaching life skills to all + youth in the community and working to make the community safer and more accepting of gender and sexual + orientation diversity. + + Support Inside Out + + + + Transgender District + + + The mission of the Transgender District is to create an urban environment that fosters the rich history, + culture, legacy, and empowerment of transgender people and its deep roots in the southeastern Tenderloin + neighborhood. The transgender district aims to stabilize and economically empower the transgender + community through ownership of homes, businesses, historic and cultural sites, and safe community spaces. + + Support The Transgender District + + + + Audre Lorde Project + + + The Audre Lorde Project is a Lesbian, Gay, Bisexual, Two Spirit, Trans and Gender Non Conforming People of + Color center for community organizing, focusing on the New York City area. Through mobilization, education + and capacity-building, they work for community wellness and progressive social and economic justice. + Committed to struggling across differences, they seek to responsibly reflect, represent and serve their + various communities. + + Support The Audre Lorde Project + + + + + ); +}; + +// @TODO +export const CustomFittedTabs: React.FC = () => { + const currentTheme = useTheme(); + const selectedId = useUID(); + + return ( + + + + Inside Out + Transgender District + Audre Lorde Project + Coming soon... + + + + + Inside Out + + + Inside Out empowers, educates, and advocates for LGBTQ+ of youth from the Pikes Peak Region in Southern + Colorado. Inside Out does this by creating safe spaces, support systems and teaching life skills to all + youth in the community and working to make the community safer and more accepting of gender and sexual + orientation diversity. + + Support Inside Out + + + + Transgender District + + + The mission of the Transgender District is to create an urban environment that fosters the rich history, + culture, legacy, and empowerment of transgender people and its deep roots in the southeastern Tenderloin + neighborhood. The transgender district aims to stabilize and economically empower the transgender + community through ownership of homes, businesses, historic and cultural sites, and safe community spaces. + + Support The Transgender District + + + + Audre Lorde Project + + + The Audre Lorde Project is a Lesbian, Gay, Bisexual, Two Spirit, Trans and Gender Non Conforming People of + Color center for community organizing, focusing on the New York City area. Through mobilization, education + and capacity-building, they work for community wellness and progressive social and economic justice. + Committed to struggling across differences, they seek to responsibly reflect, represent and serve their + various communities. + + Support The Audre Lorde Project + + + + + ); +}; + // eslint-disable-next-line import/no-default-export export default { title: 'Components/Tabs',