Skip to content

Commit

Permalink
feat(store-ui): Breadcrumb molecule (#986)
Browse files Browse the repository at this point in the history
* Breadcrumb molecule

* use Icon component and export level typing

* reviews improvements

* typing, naming and description improvements

* unnecessary styles
  • Loading branch information
Gabriel Antiqueira authored Oct 25, 2021
1 parent 87a7014 commit 325c738
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 0 deletions.
47 changes: 47 additions & 0 deletions packages/store-ui/src/molecules/Breadcrumb/Breadcrumb.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { render } from '@testing-library/react'
import React from 'react'

import Breadcrumb from './Breadcrumb'

describe('Breadcrumb', () => {
it('`data-store-breadcrumb` is present', () => {
const { getByTestId } = render(<Breadcrumb breadcrumb={[]} />)

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

it('has the correct breadcrumb size and active item is always the last breadcrumb item', () => {
const { queryAllByTestId, rerender } = render(
<Breadcrumb breadcrumb={[]} />
)

// Test 10 different breadcrumbs, from 1 level to 10 levels.
for (let breadcrumbSize = 1; breadcrumbSize <= 10; breadcrumbSize += 1) {
// Creates a generic breadcrumb data.
const breadcrumb = Array.from(
{ length: breadcrumbSize },
(_: undefined, index: number) => ({ href: '/', text: `Level ${index}` })
)

rerender(<Breadcrumb breadcrumb={breadcrumb} />)

const breadcrumbItems = queryAllByTestId('store-breadcrumb-item')

// Validate that breadcrumb is rendering with the correct size.
expect(breadcrumbItems).toHaveLength(breadcrumbSize)
// Validate if the last element has the active attribute.
expect(breadcrumbItems[breadcrumbSize - 1]).toHaveAttribute(
'data-store-breadcrumb-item-active'
)

// Get all elements except the last one.
breadcrumbItems.pop()
// Validate that no other element has the 'data-store-breadcrumb-item-active' attribute
breadcrumbItems.forEach((item) => {
expect(item).not.toHaveAttribute('data-store-breadcrumb-item-active')
})
}
})
})
104 changes: 104 additions & 0 deletions packages/store-ui/src/molecules/Breadcrumb/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type {
HTMLAttributes,
AnchorHTMLAttributes,
ElementType,
ReactNode,
PropsWithChildren,
} from 'react'
import React, { forwardRef, Fragment } from 'react'

import Icon from '../../atoms/Icon'

export type BreadcrumbLevelType = {
href: string
text: string
}

const DefaultLink = ({
children,
...otherProps
}: AnchorHTMLAttributes<HTMLAnchorElement>) => <a {...otherProps}>{children}</a>

const DefaultHomeIcon = () => (
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M21 13v10h-6v-6h-6v6h-6v-10h-3l12-12 12 12h-3zm-1-5.907v-5.093h-3v2.093l3 3z" />
</svg>
)

const DefaultDividerIcon = () => (
<svg width="24" height="24" viewBox="0 0 180 180">
<path d="M51.707,185.343c-2.741,0-5.493-1.044-7.593-3.149c-4.194-4.194-4.194-10.981,0-15.175 l74.352-74.347L44.114,18.32c-4.194-4.194-4.194-10.987,0-15.175c4.194-4.194,10.987-4.194,15.18,0l81.934,81.934 c4.194,4.194,4.194,10.987,0,15.175l-81.934,81.939C57.201,184.293,54.454,185.343,51.707,185.343z" />
</svg>
)

export interface BreadcrumbProps extends HTMLAttributes<HTMLDivElement> {
/**
* Array with each level of the breadcrumb, containing link and text.
*/
breadcrumb: BreadcrumbLevelType[]
/**
* A component that will be rendered as the Divider icon.
*/
dividerIcon?: ReactNode
/**
* A component that will be rendered as the Home icon.
*/
homeIcon?: ReactNode
/**
* A component link that will replace the HTML Anchor.
*/
linkComponent?: ElementType<
PropsWithChildren<{ href?: string; to?: string; 'data-testid'?: string }>
>
/**
* ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
*/
testId?: string
}

const Breadcrumb = forwardRef<HTMLDivElement, BreadcrumbProps>(
function Breadcrumb(
{
breadcrumb,
children,
homeIcon = <DefaultHomeIcon />,
dividerIcon = <DefaultDividerIcon />,
linkComponent: Link = DefaultLink,
testId = 'store-breadcrumb',
...otherProps
},
ref
) {
return (
<div ref={ref} data-store-breadcrumb data-testid={testId} {...otherProps}>
<Link
data-testid={`${testId}-home`}
data-store-breadcrumb-home
href="/"
to="/"
>
<Icon component={homeIcon} />
</Link>
{breadcrumb.map(({ href, text }, index) => (
<Fragment key={index}>
<Icon component={dividerIcon} />
<Link
data-testid={`${testId}-item`}
data-store-breadcrumb-item
data-store-breadcrumb-item-active={
index === breadcrumb.length - 1 || undefined
}
key={index}
href={href}
to={href}
>
{text}
</Link>
</Fragment>
))}
</div>
)
}
)

export default Breadcrumb
2 changes: 2 additions & 0 deletions packages/store-ui/src/molecules/Breadcrumb/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './Breadcrumb'
export type { BreadcrumbProps, BreadcrumbLevelType } from './Breadcrumb'
23 changes: 23 additions & 0 deletions packages/store-ui/src/molecules/Breadcrumb/stories/Breadcrumb.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Canvas, Props, Story, ArgsTable } from '@storybook/addon-docs'

import Breadcrumb from '../Breadcrumb'

# Bullets

<Canvas>
<Story id="molecules-breadcrumb--breadcrumb" />
</Canvas>

## Props

<ArgsTable of={Breadcrumb} />

## CSS Selectors

```css
[data-store-breadcrumb] {}

[data-breadcrumb-item] {}

[data-breadcrumb-item-active] {}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Story } from '@storybook/react'
import React from 'react'

import type { ComponentArgTypes } from '../../../typings/utils'
import type { BreadcrumbProps } from '../Breadcrumb'
import Component from '../Breadcrumb'
import mdx from './Breadcrumb.mdx'

const BreadcrumbTemplate: Story<BreadcrumbProps> = ({
LinkComponent,
breadcrumb,
testId,
}) => {
return (
<Component
breadcrumb={breadcrumb}
LinkComponent={LinkComponent}
testId={testId}
/>
)
}

export const Breadcrumb = BreadcrumbTemplate.bind({})

const argTypes: ComponentArgTypes<BreadcrumbProps> = {
breadcrumb: {
control: { type: 'object' },
defaultValue: [
{ href: '', text: 'Level 1' },
{ href: '', text: 'Level 2' },
{ href: '', text: 'Level 3' },
],
},
}

export default {
title: 'Molecules/Breadcrumb',
argTypes,
parameters: {
docs: {
page: mdx,
},
},
}
15 changes: 15 additions & 0 deletions themes/theme-b2c-tailwind/src/molecules/breadcrumb.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[data-store-breadcrumb] {
@apply flex items-center;
}

[data-store-breadcrumb-home] {
@apply mr-2;
}

[data-store-breadcrumb-item] {
@apply flex text-black no-underline hover:underline mx-2;
}

[data-store-breadcrumb-item-active] {
@apply font-bold text-xl;
}
1 change: 1 addition & 0 deletions themes/theme-b2c-tailwind/src/molecules/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import "./breadcrumb.css";
@import "./bullets.css";
@import "./carousel.css";
@import "./icon-button.css";
Expand Down

0 comments on commit 325c738

Please sign in to comment.