Skip to content

Commit

Permalink
feat(components): listButton and children creation
Browse files Browse the repository at this point in the history
closes AUTH-657
  • Loading branch information
jerader committed Aug 12, 2024
1 parent b6be7bf commit 1dbf083
Show file tree
Hide file tree
Showing 9 changed files with 485 additions and 0 deletions.
132 changes: 132 additions & 0 deletions components/src/atoms/ListButton/ListButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as React from 'react'

import { ListButton as ListButtonComponent } from './index'
import {
ListButtonAccordion,
ListButtonAccordionContainer,
ListButtonRadioButton,
} from './ListButtonChildren/index'
import { StyledText } from '../..'
import type { Meta, StoryObj } from '@storybook/react'

const meta: Meta<typeof ListButtonComponent> = {
title: 'Library/Atoms/ListButton',
component: ListButtonComponent,
argTypes: {
type: {
control: {
type: 'select',
options: ['noActive', 'success', 'warning'],
},
},
},
}

export default meta

type Story = StoryObj<typeof ListButtonComponent>

const Template = (args: any): JSX.Element => {
const [containerExpand, setContainerExpand] = React.useState<boolean>(false)
const [buttonValue, setButtonValue] = React.useState<string | null>(null)
const [nestedButtonValue, setNestedButtonValue] = React.useState<
string | null
>(null)

return (
<ListButtonComponent
{...args}
onClick={() => {
setContainerExpand(!containerExpand)
}}
>
<ListButtonAccordion
key="main"
mainHeadline="Main heading"
headline="accordion heading"
isExpanded={containerExpand}
>
<ListButtonAccordionContainer id="mainAccordionContainer">
<>
<ListButtonRadioButton
key="buttonNested"
isSelected={buttonValue === 'radio button nested'}
buttonValue="radio button nested"
buttonText="Radio button with nested"
onChange={e => {
e.stopPropagation()
setButtonValue('radio button nested')
}}
/>

{buttonValue === 'radio button nested' ? (
<ListButtonAccordionContainer id="nestedAccordionContainer">
<ListButtonAccordion
key="child"
isNested
headline="nested accordion heading"
isExpanded={buttonValue === 'radio button nested'}
>
<>
<ListButtonRadioButton
isSelected={nestedButtonValue === 'radio button1'}
buttonValue="radio button1"
buttonText="nested button"
onChange={() => {
setNestedButtonValue('radio button1')
}}
/>
{nestedButtonValue === 'radio button1' ? (
<StyledText desktop="bodyDefaultRegular">
Nested button option
</StyledText>
) : null}
</>
<ListButtonRadioButton
isSelected={nestedButtonValue === 'radio button2'}
buttonValue="radio button2"
buttonText="nested button 2"
onChange={() => {
setNestedButtonValue('radio button2')
}}
/>
<ListButtonRadioButton
isSelected={nestedButtonValue === 'radio button3'}
buttonValue="radio button3"
buttonText="nested button 3"
onChange={() => {
setNestedButtonValue('radio button3')
}}
/>
</ListButtonAccordion>
</ListButtonAccordionContainer>
) : null}
</>
<>
<ListButtonRadioButton
key="buttonNonNested"
isSelected={buttonValue === 'radio button non nest'}
buttonValue="radio button non nest"
buttonText="Radio button without nested"
onChange={() => {
setButtonValue('radio button non nest')
}}
/>

{buttonValue === 'radio button non nest' ? (
<StyledText desktop="bodyDefaultRegular">
Non nested button option
</StyledText>
) : null}
</>
</ListButtonAccordionContainer>
</ListButtonAccordion>
</ListButtonComponent>
)
}
export const ListButton: Story = {
render: Template,
args: {
type: 'noActive',
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react'
import { Flex } from '../../../primitives'
import { DIRECTION_COLUMN } from '../../../styles'
import { SPACING } from '../../../ui-style-constants'
import { StyledText } from '../../StyledText'

interface ListButtonAccordionProps {
headline: string
children: React.ReactNode
// determines if the accordion is expanded or not
isExpanded?: boolean
// is it nested into another accordion?
isNested?: boolean
// optional main headline for the top level accordion
mainHeadline?: string
}

/*
To be used with ListButton, ListButtonAccordion and ListButtonRadioButton
This is the accordion component to use both as just an accordion or nested accordion
**/
export function ListButtonAccordion(
props: ListButtonAccordionProps
): JSX.Element {
const {
headline,
children,
mainHeadline,
isExpanded = false,
isNested = false,
} = props

return (
<Flex flexDirection={DIRECTION_COLUMN} width="100%">
{mainHeadline != null ? (
<Flex marginBottom={isExpanded ? SPACING.spacing40 : '0'}>
<StyledText desktopStyle="bodyDefaultSemiBold">
{mainHeadline}
</StyledText>
</Flex>
) : null}
{isExpanded ? (
<Flex
marginTop={isNested ? SPACING.spacing4 : '0'}
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing4}
marginLeft={isNested ? SPACING.spacing40 : '0'}
>
<Flex marginBottom={SPACING.spacing4}>
<StyledText desktopStyle="bodyDefaultSemiBold">
{headline}
</StyledText>
</Flex>
<Flex flexDirection={DIRECTION_COLUMN}>{children}</Flex>
</Flex>
) : null}
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react'
import { Flex } from '../../../primitives'
import { DIRECTION_COLUMN } from '../../../styles'

interface ListButtonAccordionContainerProps {
children: React.ReactNode
id: string
}
/*
To be used with ListButtonAccordion to stop propagation since multiple
layers have a CTA
**/
export function ListButtonAccordionContainer(
props: ListButtonAccordionContainerProps
): JSX.Element {
const { id, children } = props

return (
<Flex
key={id}
flexDirection={DIRECTION_COLUMN}
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
}}
>
{children}
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as React from 'react'
import styled, { css } from 'styled-components'
import { SPACING } from '../../../ui-style-constants'
import { BORDERS, COLORS } from '../../../helix-design-system'
import { Flex } from '../../../primitives'
import { StyledText } from '../../StyledText'

import type { StyleProps } from '../../../primitives'

interface ListButtonRadioButtonProps extends StyleProps {
buttonText: string
buttonValue: string | number
onChange: React.ChangeEventHandler<HTMLInputElement>
disabled?: boolean
isSelected?: boolean
id?: string
}

// used for helix and as a child button to ListButtonAccordion
export function ListButtonRadioButton(
props: ListButtonRadioButtonProps
): JSX.Element {
const {
buttonText,
buttonValue,
isSelected = false,
onChange,
disabled = false,
id = buttonText,
} = props

const SettingButton = styled.input`
display: none;
`

const AVAILABLE_BUTTON_STYLE = css`
background: ${COLORS.white};
color: ${COLORS.black90};
&:hover {
background-color: ${COLORS.grey10};
}
`

const SELECTED_BUTTON_STYLE = css`
background: ${COLORS.blue50};
color: ${COLORS.white};
&:active {
background-color: ${COLORS.blue60};
}
`

const DISABLED_STYLE = css`
color: ${COLORS.grey40};
background-color: ${COLORS.grey10};
`

const SettingButtonLabel = styled.label`
border-radius: ${BORDERS.borderRadius8};
cursor: pointer;
padding: 14px ${SPACING.spacing12};
width: 100%;
${isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE}
${disabled && DISABLED_STYLE}
`

return (
<Flex width="100%" margin={SPACING.spacing4}>
<SettingButton
checked={isSelected}
id={id}
disabled={disabled}
onChange={onChange}
type="radio"
value={buttonValue}
/>
<SettingButtonLabel role="label" htmlFor={id}>
<StyledText desktopStyle="bodyDefaultRegular">{buttonText}</StyledText>
</SettingButtonLabel>
</Flex>
)
}
3 changes: 3 additions & 0 deletions components/src/atoms/ListButton/ListButtonChildren/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './ListButtonAccordion'
export * from './ListButtonAccordionContainer'
export * from './ListButtonRadioButton'
46 changes: 46 additions & 0 deletions components/src/atoms/ListButton/__tests__/ListButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react'
import { vi, describe, it, expect, beforeEach } from 'vitest'
import '@testing-library/jest-dom/vitest'
import { fireEvent, screen } from '@testing-library/react'
import { renderWithProviders } from '../../../testing/utils'
import { COLORS } from '../../../helix-design-system'

import { ListButton } from '..'

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

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

beforeEach(() => {
props = {
type: 'noActive',
children: <div>mock ListButton content</div>,
onClick: vi.fn(),
}
})

it('should render correct style - noActive', () => {
render(props)
const listButton = screen.getByTestId('ListButton_noActive')
expect(listButton).toHaveStyle(`backgroundColor: ${COLORS.grey35}`)
})
it('should render correct style - connected', () => {
props.type = 'connected'
render(props)
const listButton = screen.getByTestId('ListButton_connected')
expect(listButton).toHaveStyle(`backgroundColor: ${COLORS.green35}`)
})
it('should render correct style - notConnected', () => {
props.type = 'notConnected'
render(props)
const listButton = screen.getByTestId('ListButton_notConnected')
expect(listButton).toHaveStyle(`backgroundColor: ${COLORS.yellow35}`)
})
it('should call on click when pressed', () => {
render(props)
fireEvent.click(screen.getByText('mock ListButton content'))
expect(props.onClick).toHaveBeenCalled()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from 'react'
import { describe, it, beforeEach } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithProviders } from '../../../testing/utils'

import { ListButtonAccordion } from '..'

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

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

beforeEach(() => {
props = {
children: <div>mock ListButtonAccordion content</div>,
headline: 'mock headline',
isExpanded: true,
}
})

it('should render non nested accordion', () => {
render(props)
screen.getByText('mock headline')
screen.getByText('mock ListButtonAccordion content')
})
it('should render non nested accordion with main headline', () => {
props.isNested = true
props.mainHeadline = 'mock main headline'
render(props)
screen.getByText('mock main headline')
screen.getByText('mock headline')
screen.getByText('mock ListButtonAccordion content')
})
})
Loading

0 comments on commit 1dbf083

Please sign in to comment.