Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(components): listButton and accordion children creation #15967

Merged
merged 3 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions components/src/atoms/ListButton/ListButton.stories.tsx
Copy link
Contributor

@koji koji Aug 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding text like 'click the button to see Accordion, Radio, Nested' would be user-friendly for the designers.
we can notice that easily since we read code, but they don't check code.
probably adding text might DQA smooth.

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 => {
jerader marked this conversation as resolved.
Show resolved Hide resolved
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
jerader marked this conversation as resolved.
Show resolved Hide resolved
isExpanded?: boolean
// is it nested into another accordion?
jerader marked this conversation as resolved.
Show resolved Hide resolved
isNested?: boolean
// optional main headline for the top level accordion
jerader marked this conversation as resolved.
Show resolved Hide resolved
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
Loading