Skip to content

Commit

Permalink
Create 'Bullets' molecule (#786)
Browse files Browse the repository at this point in the history
* Create Bullets component

* Add tests and Storybook story for Bullets molecule

* Add mdx.d.ts file
  • Loading branch information
victorhmp authored Jun 25, 2021
1 parent d366c38 commit 396df0f
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 0 deletions.
77 changes: 77 additions & 0 deletions packages/store-ui/src/molecules/Bullets/Bullets.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { render, act, fireEvent } from '@testing-library/react'
import React from 'react'

import Bullets from './Bullets'

describe('Bullets', () => {
it('should have `data-store-bullets` attribute', () => {
const { getByTestId } = render(
<Bullets totalQuantity={5} activeBullet={2} onClick={() => {}} />
)

expect(getByTestId('store-bullets')).toHaveAttribute('data-store-bullets')
})

it('should render 5 bullets with `data-bullet-item` attribute', () => {
const { queryAllByTestId } = render(
<Bullets totalQuantity={5} activeBullet={2} onClick={() => {}} />
)

const bulletItems = queryAllByTestId('store-bullets-item')

expect(bulletItems).toHaveLength(5)

bulletItems.forEach((bullet) =>
expect(bullet).toHaveAttribute('data-bullet-item')
)
})

it('should render only the currently active bullet with a `data-active` attribute', () => {
const { queryAllByTestId } = render(
<Bullets totalQuantity={5} activeBullet={2} onClick={() => {}} />
)

const bulletItems = queryAllByTestId('store-bullets-item')

// eslint-disable-next-line prefer-destructuring
const expectedActiveBullet = bulletItems[2]

expect(bulletItems).toHaveLength(5)
expect(expectedActiveBullet).toHaveAttribute('data-active')

// Remove the currently active bullet, at index 2
bulletItems.splice(2, 1)
// Validate that no other element has the 'data-active' attribute
bulletItems.forEach((bullet) => {
expect(bullet).not.toHaveAttribute('data-active')
})
})

it('should ensure that onClick is called with the correct bullet index', () => {
const updateCurrentBulletMock = jest.fn()

const { queryAllByTestId } = render(
<Bullets
totalQuantity={5}
activeBullet={2}
onClick={updateCurrentBulletMock}
/>
)

const bulletItems = queryAllByTestId('store-bullets-item')

expect(bulletItems).toHaveLength(5)

// Each bullet is rendered with an <Button /> inside, and the button gets
// the onClick handler.
const bullets = queryAllByTestId('store-button')

act(() => {
// 'click' the bullet at index 3 (the 4th visible bullet)
fireEvent.click(bullets[3])
})

expect(updateCurrentBulletMock).toHaveBeenCalledTimes(1)
expect(updateCurrentBulletMock).toHaveBeenCalledWith(expect.any(Object), 3)
})
})
87 changes: 87 additions & 0 deletions packages/store-ui/src/molecules/Bullets/Bullets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { MouseEvent, PropsWithChildren } from 'react'
import React, { useMemo } from 'react'

import Button from '../../atoms/Button'

export interface BulletsProps {
/**
* Number of bullets that should be rendered.
*/
totalQuantity: number
/**
* The currently active bullet (zero-indexed).
*/
activeBullet: number
/**
* Event handler for clicks on each bullet. The handler will be called with
* the index of the bullet that received the click.
*/
onClick: (e: MouseEvent, bulletIdx: number) => void
/**
* ID to find this component in testing tools (e.g.: cypress,
* testing-library, and jest).
*/
testId?: string
/**
* Function that should be used to generate the aria-label attribute added
* to each bullet that is inactive. It receives the bullet index as an
* argument so that it can be interpolated in the generated string.
*/
ariaLabelGenerator?: (index: number, isActive: boolean) => string
}

interface BulletProps {
isActive: boolean
testId: string
}

function Bullet({
isActive,
testId,
children,
}: PropsWithChildren<BulletProps>) {
return (
<li
data-testid={testId}
data-bullet-item
data-active={isActive || undefined}
>
{children}
</li>
)
}

const defaultAriaLabel = (idx: number, isActive: boolean) =>
isActive ? 'Current page' : `Go to page ${idx + 1}`

function Bullets({
totalQuantity,
activeBullet,
onClick,
testId = 'store-bullets',
ariaLabelGenerator = defaultAriaLabel,
}: BulletsProps) {
const bulletIndexes = useMemo(() => [...new Array(totalQuantity).keys()], [
totalQuantity,
])

return (
<ol data-store-bullets data-testid={testId}>
{bulletIndexes.map((idx) => {
const isActive = activeBullet === idx

return (
<Bullet key={idx} testId={`${testId}-item`} isActive={isActive}>
<Button
aria-label={ariaLabelGenerator(idx, isActive)}
onClick={(e) => onClick(e, idx)}
disabled={isActive}
/>
</Bullet>
)
})}
</ol>
)
}

export default Bullets
2 changes: 2 additions & 0 deletions packages/store-ui/src/molecules/Bullets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './Bullets'
export * from './Bullets'
13 changes: 13 additions & 0 deletions packages/store-ui/src/molecules/Bullets/stories/Bullets.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Preview, Props, Story, ArgsTable } from '@storybook/addon-docs/blocks'

import Bullets from '../Bullets'

# Default

<Preview>
<Story id="molecules-bullets--bullets" />
</Preview>

# Props

<ArgsTable of={Bullets} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Story } from '@storybook/react'
import React from 'react'

import type { BulletsProps } from '../Bullets'
import Component from '../Bullets'
import mdx from './Bullets.mdx'

const BulletsTemplate: Story<BulletsProps> = ({
onClick,
activeBullet,
totalQuantity,
ariaLabelGenerator,
testId,
}) => (
<Component
onClick={onClick}
activeBullet={activeBullet}
totalQuantity={totalQuantity}
ariaLabelGenerator={ariaLabelGenerator}
testId={testId}
/>
)

export const Bullets = BulletsTemplate.bind({})
Bullets.args = {
totalQuantity: 5,
activeBullet: 3,
}

export default {
title: 'Molecules/Bullets',
component: Bullets,
argTypes: {
totalQuantity: {
control: { type: 'number', min: 1 },
defaultValue: 5,
min: 1,
},
activeBullet: {
control: { type: 'number', min: 0 },
defaultValue: 3,
min: 0,
},
onClick: {
action: 'Bullet clicked',
},
},
parameters: {
docs: {
page: mdx,
},
},
}
4 changes: 4 additions & 0 deletions packages/store-ui/src/typings/mdx.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.mdx' {
const MDXComponent: (props) => JSX.Element
export default MDXComponent
}

0 comments on commit 396df0f

Please sign in to comment.