Skip to content

Commit

Permalink
feat: add breadcrumbs component (#353)
Browse files Browse the repository at this point in the history
* feat: add breadcrumbs component

* feat: add a11y properites

---------

Co-authored-by: Leonardo Di Vittorio <leonardo.divittorio@Leonardos-MacBook-Pro.local>
  • Loading branch information
div-Leo and Leonardo Di Vittorio committed Aug 18, 2023
1 parent 510d918 commit ed82e05
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 0 deletions.
38 changes: 38 additions & 0 deletions src/components/Breadcrumbs/Breadcrumbs.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { render, screen } from '@testing-library/react';
import * as React from 'react';
import { Breadcrumbs } from './Breadcrumbs';

const renderBreadCrumbs = () =>
render(
<Breadcrumbs>
<Breadcrumbs.Link href="/path">Path</Breadcrumbs.Link>
<Breadcrumbs.Link href="/to">to</Breadcrumbs.Link>
<Breadcrumbs.Item>Glory</Breadcrumbs.Item>
</Breadcrumbs>
);

describe('Breadcrumbs', () => {
it('renders a <a> if we use Link', () => {
expect(render(<Breadcrumbs.Link>Children</Breadcrumbs.Link>)).toMatchHtmlTag('a');
});

it('renders a <span> if we use Item', () => {
expect(render(<Breadcrumbs.Item>Children</Breadcrumbs.Item>)).toMatchHtmlTag('span');
});

it('renders the children', () => {
renderBreadCrumbs();
expect(screen.getByText('Path')).toBeInTheDocument();
expect(screen.getByText('to')).toBeInTheDocument();
expect(screen.getByText('Glory')).toBeInTheDocument();
});

it('passes href to underlying element', () => {
const expectedHref = 'https://free-now.com/';
const anchorElement = render(
<Breadcrumbs.Link href={expectedHref}>Path</Breadcrumbs.Link>
).container.querySelector('a');

expect(anchorElement).toHaveAttribute('href', expectedHref);
});
});
92 changes: 92 additions & 0 deletions src/components/Breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { Children, ComponentPropsWithoutRef, ReactElement, ReactNode, cloneElement } from 'react';
import styled from 'styled-components';

import { ChevronRightIcon } from '../../icons';
import { Text } from '../Text/Text';

import { Colors } from '../../essentials';
import { theme } from '../../essentials/theme';
import { get } from '../../utils/themeGet';
import { Box } from '../Box/Box';

interface InvertedStyle {
/**
* Adjust color for display on a dark background
* @default false
*/
inverted?: boolean;
}

interface BreadcrumbsProps extends InvertedStyle {
/**
* Content of the Breadcrumbs
* @required
*/
children: ReactNode;
}

interface LinkProps extends ComponentPropsWithoutRef<'a'>, InvertedStyle {}

type ItemProps = InvertedStyle;

const BreadcrumbsList = styled.ul`
padding: 0;
list-style: none;
display: flex;
`;

const BreadcrumbsListItem = styled.li`
display: flex;
`;

const Breadcrumbs = ({ children, inverted }: BreadcrumbsProps): JSX.Element => {
const arrayChildren = Children.toArray(children);

return (
<BreadcrumbsList>
{Children.map(arrayChildren, (child, index) => (
<BreadcrumbsListItem>
<nav aria-label="breadcrumbs">
{cloneElement(child as ReactElement, {
inverted
})}
</nav>
{index < arrayChildren.length - 1 ? (
<Box height={16} mt="0.125rem">
<ChevronRightIcon size={16} color={Colors.AUTHENTIC_BLUE_350} />
</Box>
) : // eslint-disable-next-line unicorn/no-null
null}
</BreadcrumbsListItem>
))}
</BreadcrumbsList>
);
};

const Link = styled.a.attrs({ theme })<LinkProps>`
display: inline-block;
color: ${p => (p.inverted ? Colors.WHITE : Colors.ACTION_BLUE_900)};
cursor: pointer;
line-height: 1.4;
font-family: ${get('fonts.normal')};
font-size: ${get('fontSizes.1')};
text-decoration: none;
padding: 0 0.25rem 0 0.25rem;
&:hover,
&:active {
color: ${p => (p.inverted ? Colors.AUTHENTIC_BLUE_350 : Colors.ACTION_BLUE_1000)};
text-decoration: underline;
}
`;

const Item = styled(Text).attrs(({ inverted }: ItemProps) => ({
secondary: inverted,
fontSize: 'small',
padding: '0 0.25rem 0 0.25rem'
}))<ItemProps>``;

Breadcrumbs.Item = Item;
Breadcrumbs.Link = Link;

export { Breadcrumbs };
46 changes: 46 additions & 0 deletions src/components/Breadcrumbs/docs/Breadcrumbs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { StoryObj, Meta } from '@storybook/react';

import { onDarkBackground } from '../../../docs/parameters';
import { Breadcrumbs } from '../Breadcrumbs';
import { DefaultBreadcrumbs } from './DefaultBreadcrumbs';

const meta: Meta = {
title: 'Components/Breadcrumbs',
component: Breadcrumbs,
argTypes: {
children: {
table: {
type: {
summary: 'ReactNode'
}
}
},
inverted: {
options: [true, false],
control: 'select',
table: {
type: {
summary: 'boolean'
}
}
}
}
};

export default meta;

type Story = StoryObj<typeof Breadcrumbs>;

export const Default: Story = {
render: DefaultBreadcrumbs
};

export const Inverted: Story = {
args: {
inverted: true
},
render: DefaultBreadcrumbs,
parameters: {
...onDarkBackground
}
};
39 changes: 39 additions & 0 deletions src/components/Breadcrumbs/docs/Breadcrumbs.storybook.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Primary, Stories, ArgTypes, Meta } from '@storybook/blocks';
import { StyledSystemLinks } from '../../../docs/StyledSystemLinks';
import * as Breadcrumbs from './Breadcrumbs.stories';

<Meta of={Breadcrumbs} />

# Breadcrumbs

<Primary />

## Properties

<ArgTypes of={Breadcrumbs} />

## Compound components approach

You can create your Breadcrumbs with active links and text using the following components"

- **Breadcrumbs** (Wrapper)
- **Breadcrumbs.Link**
- **Breadcrumbs.Text**

The wrapper component (**Breadcrumbs**) expects a list of children and will render them with the separator. If Inverted variant is passed it will afftect the children.

We can pass to the Link our favourite router passing the RouterLink in the `as` prop, avoiding the reload of the `a` tags.

```tsx
<Breadcrumbs>
<Breadcrumbs.Link href="/path">Path</Breadcrumbs.Link>
<Breadcrumbs.Link as={RouterLink} to="/path/to">
to
</Breadcrumbs.Link>
<Breadcrumbs.Item>Glory</Breadcrumbs.Item>
</Breadcrumbs>
```

{/* <StyledSystemLinks component="Link" supportedProps={['margin', 'fontSize', 'textAlign']} /> */}

<Stories includePrimary={false} />
10 changes: 10 additions & 0 deletions src/components/Breadcrumbs/docs/DefaultBreadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { Breadcrumbs } from '../Breadcrumbs';

export const DefaultBreadcrumbs = ({ ...props }) => (
<Breadcrumbs {...props}>
<Breadcrumbs.Link href="/path">Path</Breadcrumbs.Link>
<Breadcrumbs.Link href="/path/to">to</Breadcrumbs.Link>
<Breadcrumbs.Item>Glory</Breadcrumbs.Item>
</Breadcrumbs>
);

0 comments on commit ed82e05

Please sign in to comment.