From ed82e05c35b8c8e31ed27b158f4e47a95d9199e3 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 18 Aug 2023 14:25:45 +0200 Subject: [PATCH] feat: add breadcrumbs component (#353) * feat: add breadcrumbs component * feat: add a11y properites --------- Co-authored-by: Leonardo Di Vittorio --- .../Breadcrumbs/Breadcrumbs.spec.tsx | 38 ++++++++ src/components/Breadcrumbs/Breadcrumbs.tsx | 92 +++++++++++++++++++ .../Breadcrumbs/docs/Breadcrumbs.stories.tsx | 46 ++++++++++ .../docs/Breadcrumbs.storybook.mdx | 39 ++++++++ .../Breadcrumbs/docs/DefaultBreadcrumbs.tsx | 10 ++ 5 files changed, 225 insertions(+) create mode 100644 src/components/Breadcrumbs/Breadcrumbs.spec.tsx create mode 100644 src/components/Breadcrumbs/Breadcrumbs.tsx create mode 100644 src/components/Breadcrumbs/docs/Breadcrumbs.stories.tsx create mode 100644 src/components/Breadcrumbs/docs/Breadcrumbs.storybook.mdx create mode 100644 src/components/Breadcrumbs/docs/DefaultBreadcrumbs.tsx diff --git a/src/components/Breadcrumbs/Breadcrumbs.spec.tsx b/src/components/Breadcrumbs/Breadcrumbs.spec.tsx new file mode 100644 index 000000000..2af096853 --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.spec.tsx @@ -0,0 +1,38 @@ +import { render, screen } from '@testing-library/react'; +import * as React from 'react'; +import { Breadcrumbs } from './Breadcrumbs'; + +const renderBreadCrumbs = () => + render( + + Path + to + Glory + + ); + +describe('Breadcrumbs', () => { + it('renders a if we use Link', () => { + expect(render(Children)).toMatchHtmlTag('a'); + }); + + it('renders a if we use Item', () => { + expect(render(Children)).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( + Path + ).container.querySelector('a'); + + expect(anchorElement).toHaveAttribute('href', expectedHref); + }); +}); diff --git a/src/components/Breadcrumbs/Breadcrumbs.tsx b/src/components/Breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 000000000..557b38532 --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -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 ( + + {Children.map(arrayChildren, (child, index) => ( + + + {index < arrayChildren.length - 1 ? ( + + + + ) : // eslint-disable-next-line unicorn/no-null + null} + + ))} + + ); +}; + +const Link = styled.a.attrs({ theme })` + 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' +}))``; + +Breadcrumbs.Item = Item; +Breadcrumbs.Link = Link; + +export { Breadcrumbs }; diff --git a/src/components/Breadcrumbs/docs/Breadcrumbs.stories.tsx b/src/components/Breadcrumbs/docs/Breadcrumbs.stories.tsx new file mode 100644 index 000000000..1449e56fb --- /dev/null +++ b/src/components/Breadcrumbs/docs/Breadcrumbs.stories.tsx @@ -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; + +export const Default: Story = { + render: DefaultBreadcrumbs +}; + +export const Inverted: Story = { + args: { + inverted: true + }, + render: DefaultBreadcrumbs, + parameters: { + ...onDarkBackground + } +}; diff --git a/src/components/Breadcrumbs/docs/Breadcrumbs.storybook.mdx b/src/components/Breadcrumbs/docs/Breadcrumbs.storybook.mdx new file mode 100644 index 000000000..d4f416364 --- /dev/null +++ b/src/components/Breadcrumbs/docs/Breadcrumbs.storybook.mdx @@ -0,0 +1,39 @@ +import { Primary, Stories, ArgTypes, Meta } from '@storybook/blocks'; +import { StyledSystemLinks } from '../../../docs/StyledSystemLinks'; +import * as Breadcrumbs from './Breadcrumbs.stories'; + + + +# Breadcrumbs + + + +## Properties + + + +## 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 + + Path + + to + + Glory + +``` + +{/* */} + + diff --git a/src/components/Breadcrumbs/docs/DefaultBreadcrumbs.tsx b/src/components/Breadcrumbs/docs/DefaultBreadcrumbs.tsx new file mode 100644 index 000000000..bebb7926c --- /dev/null +++ b/src/components/Breadcrumbs/docs/DefaultBreadcrumbs.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Breadcrumbs } from '../Breadcrumbs'; + +export const DefaultBreadcrumbs = ({ ...props }) => ( + + Path + to + Glory + +);