Skip to content

Commit

Permalink
Tabs (future): add carousel functionality when tabs overflow container (
Browse files Browse the repository at this point in the history
#5355)

* Tabs (future): add carousel functionality when tabs overflow container

* Replace Button with div onClick :O

* Add a couple more tabs to story

* Stylelint

* Add stickersheets

* Limit tab selector to unique container

* Add unique ids for stickersheet examples

* Support for RTL

* Adjust tab hover back to gray

* Add SB/Chromatic test for carousel functionality

* Add cleanup to disconnect observers

* Add root element to observers to avoid arrows showing when cut off vertically

* Adjust SB test to assert arrows are in the document

* When selected tab changes, scroll it into view

* Use new spacing var

* Add typeof Tabs to spec stories meta

* Add prefix to testids and only render when testid passed in props

* Move scrollAmount to a constant

* Extract isRTL into a util

* Add a show/hide arrow SB test for RTL
  • Loading branch information
dougmacknz authored Dec 12, 2024
1 parent 87ae545 commit f6bd88c
Show file tree
Hide file tree
Showing 11 changed files with 457 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/thin-actors-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kaizen/components': patch
---

Tabs (future): add carousel functionality when tabs overflow container width
118 changes: 118 additions & 0 deletions packages/components/src/__future__/Tabs/_docs/Tabs.spec.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from 'react'
import { Meta, StoryObj } from '@storybook/react'
import { within, userEvent, expect } from '@storybook/test'
import { Text } from '~components/Text'
import { Tab, TabList, TabPanel, Tabs } from '../index'

const meta = {
title: 'Components/Tabs/Tabs (Future)/Tests',
parameters: {
controls: { disable: true },
},
args: {
children: (
<>
<TabList aria-label="Tabs" data-testid="sb-arrows">
<Tab id="one">Tab 1</Tab>
<Tab id="two">Tab 2</Tab>
<Tab id="three" badge="3">
Tab 3
</Tab>
<Tab id="disabled" isDisabled>
Disabled Tab
</Tab>
<Tab id="four">Tab 4</Tab>
<Tab id="five">Tab 5</Tab>
</TabList>
<TabPanel id="one" className="p-24">
<Text variant="body">Content 1</Text>
</TabPanel>
<TabPanel id="two" className="p-24">
<Text variant="body">Content 2</Text>
</TabPanel>
<TabPanel id="three" className="p-24">
<Text variant="body">Content 3</Text>
</TabPanel>
<TabPanel id="disabled" className="p-24">
<Text variant="body">Disabled content</Text>
</TabPanel>
<TabPanel id="four" className="p-24">
<Text variant="body">Content 4</Text>
</TabPanel>
<TabPanel id="five" className="p-24">
<Text variant="body">Content 5</Text>
</TabPanel>
</>
),
},
} satisfies Meta<typeof Tabs>

export default meta

type Story = StoryObj<typeof meta>

export const ArrowsShowingAndHiding: Story = {
render: (args) => {
return (
<div style={{ maxWidth: '500px' }}>
<Tabs {...args} />
</div>
)
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.parentElement!)

expect(canvas.queryByTestId('kz-tablist-left-arrow')).not.toBeInTheDocument()

const rightArrow = await canvas.findByTestId('sb-arrows-kz-tablist-right-arrow')

await userEvent.click(rightArrow)
await new Promise((r) => setTimeout(r, 500))

const leftArrow = await canvas.findByTestId('sb-arrows-kz-tablist-left-arrow')

expect(leftArrow).toBeInTheDocument()
expect(rightArrow).toBeInTheDocument()

await userEvent.click(rightArrow)
await new Promise((r) => setTimeout(r, 500))

expect(leftArrow).toBeInTheDocument()
expect(rightArrow).not.toBeInTheDocument()
},
}

export const ArrowsShowingAndHidingRTL: Story = {
name: 'Arrows Showing and Hiding (RTL)',
parameters: {
textDirection: 'rtl',
},
render: (args) => {
return (
<div style={{ maxWidth: '500px' }}>
<Tabs {...args} />
</div>
)
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.parentElement!)

expect(canvas.queryByTestId('kz-tablist-right-arrow')).not.toBeInTheDocument()

const leftArrow = await canvas.findByTestId('sb-arrows-kz-tablist-left-arrow')

await userEvent.click(leftArrow)
await new Promise((r) => setTimeout(r, 500))

const rightArrow = await canvas.findByTestId('sb-arrows-kz-tablist-right-arrow')

expect(leftArrow).toBeInTheDocument()
expect(rightArrow).toBeInTheDocument()

await userEvent.click(leftArrow)
await new Promise((r) => setTimeout(r, 500))

expect(rightArrow).toBeInTheDocument()
expect(leftArrow).not.toBeInTheDocument()
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react'
import { Meta } from '@storybook/react'
import { Text } from '~components/Text'
import { StickerSheet, StickerSheetStory } from '~storybook/components/StickerSheet'
import { Tab, TabList, TabPanel, Tabs } from '../index'

export default {
title: 'Components/Tabs/Tabs (Future)',
parameters: {
chromatic: { disable: false },
controls: { disable: true },
},
} satisfies Meta

const ExampleTabs = ({ id }: { id: string }): JSX.Element => (
<Tabs>
<TabList aria-label="Tabs">
<Tab id={`${id}-1`}>Tab 1</Tab>
<Tab id={`${id}-2`}>Tab 2</Tab>
<Tab id={`${id}-3`} badge="3">
Tab 3
</Tab>
<Tab id={`${id}-disabled`} isDisabled>
Disabled Tab
</Tab>
<Tab id={`${id}-4`}>Tab 4</Tab>
<Tab id={`${id}-5`}>Tab 5</Tab>
</TabList>
<TabPanel id={`${id}-1`} className="p-24">
<Text variant="body">Content 1</Text>
</TabPanel>
<TabPanel id={`${id}-2`} className="p-24">
<Text variant="body">Content 2</Text>
</TabPanel>
<TabPanel id={`${id}-3`} className="p-24">
<Text variant="body">Content 3</Text>
</TabPanel>
<TabPanel id={`${id}-disabled`} className="p-24">
<Text variant="body">Disabled content</Text>
</TabPanel>
<TabPanel id={`${id}-4`} className="p-24">
<Text variant="body">Content 4</Text>
</TabPanel>
<TabPanel id={`${id}-5`} className="p-24">
<Text variant="body">Content 5</Text>
</TabPanel>
</Tabs>
)

const StickerSheetTemplate: StickerSheetStory = {
render: () => {
return (
<>
<StickerSheet title="Tabs" layout="stretch">
<StickerSheet.Row>
<ExampleTabs id="fullwidth" />
</StickerSheet.Row>
</StickerSheet>

<StickerSheet title="Overflow (container 500px)" layout="stretch">
<StickerSheet.Row>
<div style={{ maxWidth: '500px' }}>
<ExampleTabs id="overflow" />
</div>
</StickerSheet.Row>
</StickerSheet>
</>
)
},
}

export const StickerSheetDefault: StickerSheetStory = {
...StickerSheetTemplate,
name: 'Sticker Sheet (Default)',
}

export const StickerSheetRTL: StickerSheetStory = {
...StickerSheetTemplate,
name: 'Sticker Sheet (RTL)',
parameters: {
...StickerSheetTemplate.parameters,
textDirection: 'rtl',
},
}
13 changes: 12 additions & 1 deletion packages/components/src/__future__/Tabs/_docs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ const meta = {
<Tab id="three" badge="3">
Tab 3
</Tab>
<Tab id="four" isDisabled>
<Tab id="disabled" isDisabled>
Disabled Tab
</Tab>
<Tab id="four">Tab 4</Tab>
<Tab id="five">Tab 5</Tab>
</TabList>
<TabPanel id="one" className="p-24">
<Text variant="body">Content 1</Text>
Expand All @@ -29,6 +31,15 @@ const meta = {
<TabPanel id="three" className="p-24">
<Text variant="body">Content 3</Text>
</TabPanel>
<TabPanel id="disabled" className="p-24">
<Text variant="body">Content 4</Text>
</TabPanel>
<TabPanel id="four" className="p-24">
<Text variant="body">Content 4</Text>
</TabPanel>
<TabPanel id="five" className="p-24">
<Text variant="body">Content 5</Text>
</TabPanel>
</>
),
},
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/__future__/Tabs/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SCROLL_AMOUNT = 120
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const Tab = (props: TabProps): JSX.Element => {
}

return (
<RACTab {...tabProps}>
<RACTab data-kz-tab {...tabProps}>
{({ isSelected, isFocusVisible, isHovered }) => (
<>
{children}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,60 @@
.container {
position: relative;
}

.tabList {
border-bottom: 1px solid rgba(var(--color-gray-600-rgb), 0.1);
padding: var(--spacing-xs) var(--spacing-md) 0;
padding: var(--spacing-6) 0 0;
width: 100%;
height: 100%;
overflow-x: scroll;
white-space: nowrap;
scrollbar-width: none;
scroll-behavior: smooth;
}

.noPadding {
padding: 0;
}

.leftArrow,
.rightArrow {
--icon-size: 24;

display: flex;
align-items: center;
justify-content: center;
position: absolute;
z-index: 10000;
background: var(--color-white);
inset-block: 0 1px;
width: 48px;
cursor: default;
user-select: none;
}

/*
* Note: we're purposefully using directional properties instead of start/end for positioning and styling related to the carousel arrows
*/
.leftArrow {
left: 0;
}

.leftArrow,
.leftArrow:hover {
border-right: 1px solid rgba(var(--color-gray-600-rgb), 0.1);
}

.rightArrow {
right: 0;
}

.rightArrow,
.rightArrow:hover {
border-left: 1px solid rgba(var(--color-gray-600-rgb), 0.1);
}

.leftArrow:hover,
.rightArrow:hover {
background: var(--color-gray-200);
}
Loading

0 comments on commit f6bd88c

Please sign in to comment.