Skip to content

Commit

Permalink
Merge pull request #716 from Atom-Learning/HMP-1508-create-ds-segment…
Browse files Browse the repository at this point in the history
…ed-control-component

Feat: SegmentedControl component
  • Loading branch information
avirati authored Dec 12, 2024
2 parents e2e052c + ee958c2 commit c2d730f
Show file tree
Hide file tree
Showing 28 changed files with 3,529 additions and 0 deletions.
465 changes: 465 additions & 0 deletions documentation/content/components.segmented-control.md

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions lib/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ export { TopBar } from './top-bar'
export { Tree } from './tree'
export { Video } from './video'
export { KeyboardShortcut } from './keyboard-shortcut'
export { SegmentedControl } from './segmented-control'
152 changes: 152 additions & 0 deletions lib/src/components/segmented-control/SegmentedControl.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import * as React from 'react'
import { SegmentedControl } from './SegmentedControl'
import { Alarm, Anchor } from '@atom-learning/icons'
import { render, screen, waitFor } from '@testing-library/react'
import { axe } from 'jest-axe'
import userEvent from '@testing-library/user-event'

const SegmentedControlComponent = ({
children,
...props
}: React.PropsWithChildren<
React.ComponentProps<typeof SegmentedControl.Root>
>) => {
return (
<SegmentedControl.Root
size="sm"
defaultValue="one"
theme="marsh"
{...props}
>
<SegmentedControl.ItemList>
<SegmentedControl.Item value="one">
<SegmentedControl.Icon is={Alarm} />
<SegmentedControl.Heading>Heading One</SegmentedControl.Heading>
<SegmentedControl.Description>
Description One
</SegmentedControl.Description>
<SegmentedControl.Badge>Status One</SegmentedControl.Badge>
</SegmentedControl.Item>
<SegmentedControl.Item value="two">
<SegmentedControl.Icon is={Anchor} />
<SegmentedControl.Heading>Heading Two</SegmentedControl.Heading>
<SegmentedControl.Description>
Description Two
</SegmentedControl.Description>
<SegmentedControl.Badge>Status Two</SegmentedControl.Badge>
</SegmentedControl.Item>
</SegmentedControl.ItemList>
<SegmentedControl.Content value="one">
Content One
</SegmentedControl.Content>
<SegmentedControl.Content value="two">
Content One
</SegmentedControl.Content>
</SegmentedControl.Root>
)
}

describe('SegmentedControl component', () => {
it('renders', async () => {
const { container } = render(<SegmentedControlComponent />)

expect(screen.getByRole('heading', { name: 'Heading One' })).toBeVisible()
expect(screen.getByText('Description One')).toBeVisible()
expect(screen.getByText('Status One')).toBeVisible()

expect(screen.getByRole('heading', { name: 'Heading Two' })).toBeVisible()
expect(screen.getByText('Description Two')).toBeVisible()
expect(screen.getByText('Status Two')).toBeVisible()

expect(screen.getByText('Content One')).toBeVisible()
await expect(
waitFor(() => screen.getByText('Content Two'))
).rejects.toThrow()

expect(container).toMatchSnapshot()
})

it('has no programmatically detectable a11y issues', async () => {
const { container } = render(<SegmentedControlComponent />)
expect(await axe(container)).toHaveNoViolations()
})

it('renders md variant with primary theme', () => {
const { container } = render(
<SegmentedControlComponent size="md" theme="primary" />
)
expect(container).toMatchSnapshot()
})

it('renders lg variant with marsh theme', () => {
const { container } = render(
<SegmentedControlComponent size="lg" theme="marsh" />
)
expect(container).toMatchSnapshot()
})

it('renders with a default tab selected', () => {
render(<SegmentedControlComponent defaultValue="two" />)

const tabOne = screen.getByRole('tab', { name: /Heading One/ })
const tabTwo = screen.getByRole('tab', { name: /Heading Two/ })

expect(tabOne).toHaveAttribute('aria-selected', 'false')
expect(tabTwo).toHaveAttribute('aria-selected', 'true')
})

it('allows switching between tabs', async () => {
render(<SegmentedControlComponent />)

const tabOne = screen.getByRole('tab', { name: /Heading One/ })
const tabTwo = screen.getByRole('tab', { name: /Heading Two/ })

expect(tabOne).toHaveAttribute('aria-selected', 'true')
expect(tabTwo).toHaveAttribute('aria-selected', 'false')

await userEvent.click(tabTwo)

expect(tabOne).toHaveAttribute('aria-selected', 'false')
expect(tabTwo).toHaveAttribute('aria-selected', 'true')

await userEvent.click(tabOne)

expect(tabOne).toHaveAttribute('aria-selected', 'true')
expect(tabTwo).toHaveAttribute('aria-selected', 'false')
})

it('does not allow clicking on disabled tab', async () => {
render(
<SegmentedControl.Root size="sm" defaultValue="one" theme="marsh">
<SegmentedControl.ItemList>
<SegmentedControl.Item value="one">
<SegmentedControl.Heading>Heading One</SegmentedControl.Heading>
</SegmentedControl.Item>
<SegmentedControl.Item value="two" disabled>
<SegmentedControl.Heading>Heading Two</SegmentedControl.Heading>
</SegmentedControl.Item>
</SegmentedControl.ItemList>
<SegmentedControl.Content value="one">
Content One
</SegmentedControl.Content>
<SegmentedControl.Content value="two">
Content One
</SegmentedControl.Content>
</SegmentedControl.Root>
)

const tabOne = screen.getByRole('tab', { name: 'Heading One' })
const tabTwo = screen.getByRole('tab', { name: 'Heading Two' })

expect(tabOne).toBeEnabled()
expect(tabTwo).toBeDisabled()

expect(tabOne).toHaveAttribute('aria-selected', 'true')
expect(tabTwo).toHaveAttribute('aria-selected', 'false')

await userEvent.click(tabTwo)

expect(tabOne).toHaveAttribute('aria-selected', 'true')
expect(tabTwo).toHaveAttribute('aria-selected', 'false')
})
})
19 changes: 19 additions & 0 deletions lib/src/components/segmented-control/SegmentedControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SegmentedControlBadge } from './SegmentedControlBadge'
import { SegmentedControlContent } from './SegmentedControlContent'
import { SegmentedControlDescription } from './SegmentedControlDescription'
import { SegmentedControlHeading } from './SegmentedControlHeading'
import { SegmentedControlIcon } from './SegmentedControlIcon'
import { SegmentedControlItem } from './SegmentedControlItem'
import { SegmentedControlItemList } from './SegmentedControlItemList'
import { SegmentedControlRoot } from './SegmentedControlRoot'

export const SegmentedControl = {
Root: SegmentedControlRoot,
Item: SegmentedControlItem,
Heading: SegmentedControlHeading,
Description: SegmentedControlDescription,
Icon: SegmentedControlIcon,
Content: SegmentedControlContent,
Badge: SegmentedControlBadge,
ItemList: SegmentedControlItemList
}
24 changes: 24 additions & 0 deletions lib/src/components/segmented-control/SegmentedControlBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react'

import { Badge } from '../badge'
import { useSegmentedControl } from './SegmentedControlContext'

const badgeSizeMap = {
sm: 'xs',
md: 'xs',
lg: 'sm'
}

export const SegmentedControlBadge = ({
css,
...props
}: Omit<React.ComponentProps<typeof Badge>, 'size'>): JSX.Element => {
const { size } = useSegmentedControl()
return (
<Badge
{...props}
css={{ border: 'none', ...css, fontWeight: 'normal' }}
size={badgeSizeMap[size as string]}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as React from 'react'

import { Tabs } from '../tabs'

export const SegmentedControlContent = Tabs.Content
40 changes: 40 additions & 0 deletions lib/src/components/segmented-control/SegmentedControlContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react'

import type { SegmentedControlRootProps } from './SegmentedControlRoot'

export type SegmentedControlTheme = 'primary' | 'marsh'

interface SegmentedControlContextValue {
size: SegmentedControlRootProps['size']
theme: SegmentedControlTheme
}

interface SegmentedControlProviderProps extends SegmentedControlContextValue {
children: React.ReactNode
}

const SegmentedControlContext =
React.createContext<SegmentedControlContextValue>({
size: 'md',
theme: 'primary'
})

export const SegmentedControlProvider = ({
size,
theme,
children
}: SegmentedControlProviderProps): JSX.Element => {
const value = React.useMemo<SegmentedControlContextValue>(
() => ({ size, theme }),
[size, theme]
)

return (
<SegmentedControlContext.Provider value={value}>
{children}
</SegmentedControlContext.Provider>
)
}

export const useSegmentedControl = (): SegmentedControlContextValue =>
React.useContext(SegmentedControlContext)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react'

import { styled } from '../../stitches'
import { Text } from '../text'
import { useSegmentedControl } from './SegmentedControlContext'

const StyledText = styled(Text, {
fontFamily: '$body',
color: '$textSubtle',
fontWeight: 400,
variants: {
size: {
sm: {
fontSize: '$xs'
},
md: {
fontSize: '$sm'
},
lg: {
fontSize: '$md'
}
}
}
})

export const SegmentedControlDescription = (
props: Omit<React.ComponentProps<typeof StyledText>, 'size'>
): JSX.Element => {
const { size } = useSegmentedControl()
return <StyledText {...props} size={size} />
}
29 changes: 29 additions & 0 deletions lib/src/components/segmented-control/SegmentedControlHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react'

import { styled } from '../../stitches'
import { Heading } from '../heading'
import { useSegmentedControl } from './SegmentedControlContext'

const StyledHeading = styled(Heading, {
fontFamily: '$body',
variants: {
size: {
sm: {
fontSize: '$sm'
},
md: {
fontSize: '$md'
},
lg: {
fontSize: '$lg'
}
}
}
})

export const SegmentedControlHeading = (
props: Omit<React.ComponentProps<typeof StyledHeading>, 'size'>
): JSX.Element => {
const { size } = useSegmentedControl()
return <StyledHeading {...props} size={size} />
}
17 changes: 17 additions & 0 deletions lib/src/components/segmented-control/SegmentedControlIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react'

import { Icon } from '../icon'
import { useSegmentedControl } from './SegmentedControlContext'

const sizeMap = {
sm: 'sm',
md: 'md',
lg: 'md'
}

export const SegmentedControlIcon = (
props: Omit<React.ComponentProps<typeof Icon>, 'size'>
): JSX.Element => {
const { size } = useSegmentedControl()
return <Icon {...props} size={sizeMap[size as string]} />
}
Loading

0 comments on commit c2d730f

Please sign in to comment.