Skip to content

Commit

Permalink
feat(components): create new Tag component (#15441)
Browse files Browse the repository at this point in the history
closes RSQ-84
  • Loading branch information
jerader authored Jun 20, 2024
1 parent b9e1b74 commit b69403e
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 53 deletions.
2 changes: 1 addition & 1 deletion components/src/atoms/Chip/Chip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const meta: Meta<typeof ChipComponent> = {
title: 'Library/Atoms/Chip',
argTypes: {
type: {
options: ['basic', 'error', 'info', 'neutral', 'success', 'warning'],
options: ['error', 'info', 'neutral', 'success', 'warning'],
control: {
type: 'select',
},
Expand Down
36 changes: 0 additions & 36 deletions components/src/atoms/Chip/__tests__/Chip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,6 @@ const render = (props: React.ComponentProps<typeof Chip>) => {
describe('Chip Touchscreen', () => {
let props: React.ComponentProps<typeof Chip>

it('should render text, no icon with basic colors', () => {
props = {
text: 'mockBasic',
type: 'basic',
}
render(props)
const chip = screen.getByTestId('Chip_basic')
const chipText = screen.getByText('mockBasic')
expect(chip).toHaveStyle(
`background-color: ${COLORS.black90}${COLORS.opacity20HexCode}`
)
expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`)
// ToDo (kk:03/28/2024) seems that jsdom doesn't support switching via media query
// I will keep investigating this
// expect(chipText).toHaveStyle(
// `padding: ${SPACING.spacing8} ${SPACING.spacing16}`
// )
expect(screen.queryByLabelText('icon_mockBasic')).not.toBeInTheDocument()
})

it('should render text, icon, bgcolor with success colors', () => {
props = {
text: 'mockSuccess',
Expand Down Expand Up @@ -249,22 +229,6 @@ describe('Chip Web', () => {
value: 768,
})
})

it('should render text, no icon with basic colors', () => {
props = {
text: 'mockBasic',
type: 'basic',
}
render(props)
const chip = screen.getByTestId('Chip_basic')
const chipText = screen.getByText('mockBasic')
expect(chip).toHaveStyle(
`background-color: ${COLORS.black90}${COLORS.opacity20HexCode}`
)
expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`)
expect(screen.queryByLabelText('icon_mockBasic')).not.toBeInTheDocument()
})

it('should render text, icon, bgcolor with success colors', () => {
props = {
text: 'mockSuccess',
Expand Down
19 changes: 3 additions & 16 deletions components/src/atoms/Chip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,7 @@ import { Icon } from '../../icons'
import type { IconName } from '../../icons'
import type { StyleProps } from '../../primitives'

// ToDo (kk:03/26/2024) basic will be removed when we add Tag component
export type ChipType =
| 'basic'
| 'error'
| 'info'
| 'neutral'
| 'success'
| 'warning'
export type ChipType = 'error' | 'info' | 'neutral' | 'success' | 'warning'

type ChipSize = 'medium' | 'small'

Expand Down Expand Up @@ -46,12 +39,6 @@ const CHIP_PROPS_BY_TYPE: Record<
textColor: string
}
> = {
// ToDo (kk:06/12/2024) basic will be replaced by a different component
basic: {
backgroundColor: `${COLORS.black90}${COLORS.opacity20HexCode}`,
borderRadius: BORDERS.borderRadius4,
textColor: COLORS.grey60,
},
error: {
backgroundColor: COLORS.red35,
borderRadius: BORDERS.borderRadiusFull,
Expand Down Expand Up @@ -96,7 +83,7 @@ export function Chip(props: ChipProps): JSX.Element {
...styleProps
} = props
const backgroundColor =
background === false && type !== 'basic'
background === false
? COLORS.transparent
: CHIP_PROPS_BY_TYPE[type].backgroundColor
const icon = iconName ?? CHIP_PROPS_BY_TYPE[type].iconName ?? 'ot-alert'
Expand Down Expand Up @@ -151,7 +138,7 @@ export function Chip(props: ChipProps): JSX.Element {
data-testid={`Chip_${type}`}
{...styleProps}
>
{type !== 'basic' && hasIcon ? (
{hasIcon ? (
<Icon
name={icon}
color={CHIP_PROPS_BY_TYPE[type].iconColor}
Expand Down
50 changes: 50 additions & 0 deletions components/src/atoms/Tag/Tag.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react'

import { Flex } from '../../primitives'
import { SPACING, VIEWPORT } from '../../ui-style-constants'
import { Tag as TagComponent } from './index'
import type { Meta, StoryObj } from '@storybook/react'

const meta: Meta<typeof TagComponent> = {
title: 'Library/Atoms/Tag',
argTypes: {
type: {
options: ['default', 'interactive', 'branded'],
control: {
type: 'select',
},
},
// TODO(jr, 6/18/24): make iconName and iconPosition selectable when we have real examples
// used in the app
iconName: {
table: {
disable: true,
},
},
iconPosition: {
table: {
disable: true,
},
},
},
component: TagComponent,
parameters: VIEWPORT.touchScreenViewport,
decorators: [
Story => (
<Flex padding={SPACING.spacing16} width="59rem">
<Story />
</Flex>
),
],
}

export default meta
type Story = StoryObj<typeof TagComponent>

export const Tag: Story = {
args: {
type: 'default',
text: 'Text',
iconPosition: undefined,
},
}
55 changes: 55 additions & 0 deletions components/src/atoms/Tag/__tests__/Tag.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as React from 'react'
import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { COLORS } from '../../../helix-design-system'
import { renderWithProviders } from '../../../testing/utils'
import { Tag } from '../index'

const render = (props: React.ComponentProps<typeof Tag>) => {
return renderWithProviders(<Tag {...props} />)
}

describe('Tag', () => {
let props: React.ComponentProps<typeof Tag>

it('should render text, icon with default', () => {
props = {
text: 'mockDefault',
type: 'default',
iconName: 'ot-alert',
iconPosition: 'left',
}
render(props)
const tag = screen.getByTestId('Tag_default')
screen.getByText('mockDefault')
expect(tag).toHaveStyle(
`background-color: ${COLORS.black90}${COLORS.opacity20HexCode}`
)
screen.getByLabelText('icon_left_mockDefault')
})
it('should render text, right icon with branded', () => {
props = {
text: 'mockBranded',
type: 'branded',
iconName: 'ot-alert',
iconPosition: 'right',
}
render(props)
const tag = screen.getByTestId('Tag_branded')
screen.getByText('mockBranded')
expect(tag).toHaveStyle(`background-color: ${COLORS.blue50}`)
screen.getByLabelText('icon_right_mockBranded')
})
it('should render text with interactive', () => {
props = {
text: 'mockInteractive',
type: 'interactive',
}
render(props)
const tag = screen.getByTestId('Tag_interactive')
screen.getByText('mockInteractive')
expect(tag).toHaveStyle(
`background-color: ${COLORS.black90}${COLORS.opacity20HexCode}`
)
})
})
115 changes: 115 additions & 0 deletions components/src/atoms/Tag/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as React from 'react'
import { css } from 'styled-components'
import { BORDERS, COLORS } from '../../helix-design-system'
import { Flex } from '../../primitives'
import { ALIGN_CENTER, DIRECTION_ROW } from '../../styles'
import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants'
import { Icon } from '../../icons'
import { StyledText } from '../StyledText'

import type { IconName } from '../../icons'

export type TagType = 'default' | 'interactive' | 'branded'

interface TagProps {
/** Tag content */
text: string
/** name constant of the text color and the icon color to display */
type: TagType
/** iconLocation */
iconPosition?: 'left' | 'right'
/** Tagicon */
iconName?: IconName
}

const defaultColors = {
backgroundColor: `${COLORS.black90}${COLORS.opacity20HexCode}`,
color: COLORS.black90,
}

const TAG_PROPS_BY_TYPE: Record<
TagType,
{
backgroundColor: string
color: string
}
> = {
default: defaultColors,
interactive: defaultColors,
branded: {
backgroundColor: COLORS.blue50,
color: COLORS.white,
},
}

export function Tag(props: TagProps): JSX.Element {
const { iconName, type, text, iconPosition } = props

const DEFAULT_CONTAINER_STYLE = css`
padding: ${SPACING.spacing2} ${SPACING.spacing8};
border-radius: ${BORDERS.borderRadius4};
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
border-radius: ${BORDERS.borderRadius8};
padding: ${SPACING.spacing8} ${SPACING.spacing12};
}
`

const INTERACTIVE_CONTAINER_STYLE = css`
${DEFAULT_CONTAINER_STYLE}
&:hover {
background-color: ${COLORS.black90}${COLORS.opacity40HexCode};
}
&:focus-visible {
box-shadow: 0 0 0 3px ${COLORS.blue50};
outline: none;
}
`

const ICON_STYLE = css`
width: 0.75rem;
height: 0.875rem;
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
width: 1.5rem;
height: 1.5rem;
}
`

const TEXT_STYLE = css`
${TYPOGRAPHY.h3Regular}
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
${TYPOGRAPHY.bodyTextRegular}
}
`

return (
<Flex
alignItems={ALIGN_CENTER}
flexDirection={DIRECTION_ROW}
color={TAG_PROPS_BY_TYPE[type].color}
backgroundColor={TAG_PROPS_BY_TYPE[type].backgroundColor}
css={
type === 'interactive'
? INTERACTIVE_CONTAINER_STYLE
: DEFAULT_CONTAINER_STYLE
}
gridGap={SPACING.spacing4}
data-testid={`Tag_${type}`}
>
{iconName != null && iconPosition === 'left' ? (
<Icon
name={iconName}
aria-label={`icon_left_${text}`}
css={ICON_STYLE}
/>
) : null}
<StyledText css={TEXT_STYLE}>{text}</StyledText>
{iconName != null && iconPosition === 'right' ? (
<Icon
name={iconName}
aria-label={`icon_right_${text}`}
css={ICON_STYLE}
/>
) : null}
</Flex>
)
}
1 change: 1 addition & 0 deletions components/src/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './Chip'
export * from './StepMeter'
export * from './StepMeter'
export * from './StyledText'
export * from './Tag'

0 comments on commit b69403e

Please sign in to comment.