Skip to content

Commit

Permalink
feat: add breadcrumbs component
Browse files Browse the repository at this point in the history
  • Loading branch information
Leonardo Di Vittorio authored and Leonardo Di Vittorio committed Aug 11, 2023
1 parent c67d40e commit cc587d3
Show file tree
Hide file tree
Showing 5 changed files with 223 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);
});
});
90 changes: 90 additions & 0 deletions src/components/Breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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>
{cloneElement(child as ReactElement, {
inverted
})}
{index < arrayChildren.length - 1 ? (
<Box mx="0.25rem" 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')};
font-weight: ${get('fontWeights.semibold')};
text-decoration: none;
&: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',
fontWeight: 'semibold'
}))<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 cc587d3

Please sign in to comment.