Skip to content

Commit

Permalink
feat: added Accordion component (#172)
Browse files Browse the repository at this point in the history
* feat: add Accordion component and basic documentation

* feat: added test

* chore: remove default export and add PropsWithChildren

* chore: update api table

* chore: chheck place holder to not be in document

* chore: added onExpand and onCollapse

* chore: solve lint issues

* chore: solve lint issues

* chore: solve lint issues

Co-authored-by: David Barrera <d.barrera@mytaxi.com>
  • Loading branch information
pgr1 and David Barrera committed Oct 25, 2021
1 parent 14cf21b commit 439b67c
Show file tree
Hide file tree
Showing 13 changed files with 534 additions and 0 deletions.
73 changes: 73 additions & 0 deletions src/components/Accordion/Accordion.spec.tsx
@@ -0,0 +1,73 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { Accordion } from './Accordion';

describe('Accordion', () => {
it('allows rendering react nodes as children', () => {
render(
<Accordion defaultExpanded>
<p>paragraph</p>
</Accordion>
);
expect(screen.getByText('paragraph')).toBeInTheDocument();
});

it('render default variant closed', () => {
render(
<Accordion heading="Some heading" info="some info" description="some description" buttonLabel="button">
<p>Place holder</p>
</Accordion>
);
expect(screen.getByText('Some heading')).toBeInTheDocument();
expect(screen.getByText('some info')).toBeInTheDocument();
expect(screen.getByText('some description')).toBeInTheDocument();
expect(screen.getByText('button')).toBeInTheDocument();
expect(screen.queryByText('Place holder')).not.toBeInTheDocument();
});

it('render default variant open', () => {
render(
<Accordion heading="Some heading" info="some info" description="some description" buttonLabel="button">
<p>Place holder</p>
</Accordion>
);
userEvent.click(screen.getByText('Some heading'));
expect(screen.getByText('Place holder')).toBeInTheDocument();
expect(screen.getByText('some description')).toBeInTheDocument();
expect(screen.getByText('button')).toBeInTheDocument();
userEvent.click(screen.getByText('button'));
expect(screen.queryByText('Place holder')).toBeFalsy();
});

it('render compact variant correct', () => {
render(
<Accordion
heading="Some heading"
info="some info"
description="some description"
buttonLabel="button"
variant="compact"
>
<p>Place holder</p>
</Accordion>
);
expect(screen.getByText('Some heading')).toBeInTheDocument();
expect(screen.getByText('some description')).toBeInTheDocument();
expect(screen.queryByText('some info')).toBeFalsy();
expect(screen.queryByText('paragraph')).toBeFalsy();
});

it('render compact variant open', () => {
render(
<Accordion heading="Some heading" description="some description" variant="compact">
<p>Place holder</p>
</Accordion>
);
userEvent.click(screen.getByText('Some heading'));
expect(screen.getByText('Place holder')).toBeInTheDocument();
expect(screen.getByText('some description')).toBeInTheDocument();
userEvent.click(screen.getByText('Some heading'));
expect(screen.queryByText('Place holder')).toBeFalsy();
});
});
67 changes: 67 additions & 0 deletions src/components/Accordion/Accordion.tsx
@@ -0,0 +1,67 @@
import React, { ReactNode } from 'react';
import styled from 'styled-components';

import { Colors } from '../../essentials';
import { Box } from '../Box/Box';
import { Compact } from './components/Compact';
import { DefaultPanel } from './components/Default';
import { AccordionProps } from './types';

const HorizontalDivider = styled(Box)`
border: 0;
border-top: solid 0.0625rem ${Colors.AUTHENTIC_BLUE_200};
`;

const HorizontalDividerTop = HorizontalDivider;

const HorizontalDividerBottom = styled(HorizontalDivider)`
display: none;
`;

const RenderedSection = styled(Box)`
:last-child ${HorizontalDividerBottom} {
display: inherit;
}
`;

const Accordion = ({
heading,
description,
info,
buttonLabel,
variant,
defaultExpanded,
children,
onExpand = () => undefined,
onCollapse = () => undefined
}: AccordionProps) => (
<RenderedSection role="group">
<HorizontalDividerTop />
{variant === 'compact' ? (
<Compact
heading={heading}
description={description}
defaultExpanded={defaultExpanded}
onExpand={onExpand}
onCollapse={onCollapse}
>
{children}
</Compact>
) : (
<DefaultPanel
heading={heading}
description={description}
buttonLabel={buttonLabel}
info={info}
defaultExpanded={defaultExpanded}
onExpand={onExpand}
onCollapse={onCollapse}
>
{children}
</DefaultPanel>
)}
<HorizontalDividerBottom />
</RenderedSection>
);

export { Accordion, AccordionProps };
7 changes: 7 additions & 0 deletions src/components/Accordion/components/ChevronDown.tsx
@@ -0,0 +1,7 @@
import styled from 'styled-components';
import { ChevronDownIcon } from '../../../icons';
import { Colors } from '../../../essentials';

export const ChevronDown = styled(ChevronDownIcon)`
color: ${props => (props.color ? props.color : Colors.AUTHENTIC_BLUE_900)};
`;
7 changes: 7 additions & 0 deletions src/components/Accordion/components/ChevronUp.tsx
@@ -0,0 +1,7 @@
import styled from 'styled-components';
import { ChevronUpIcon } from '../../../icons';
import { Colors } from '../../../essentials';

export const ChevronUp = styled(ChevronUpIcon)`
color: ${props => (props.color ? props.color : Colors.AUTHENTIC_BLUE_900)};
`;
69 changes: 69 additions & 0 deletions src/components/Accordion/components/Compact.tsx
@@ -0,0 +1,69 @@
import * as React from 'react';
import styled from 'styled-components';

import { Colors } from '../../../essentials';
import { Box } from '../../Box/Box';
import { Headline } from '../../Headline/Headline';
import { Header } from './Header';
import { ChevronUp } from './ChevronUp';
import { ChevronDown } from './ChevronDown';
import { Description } from './Description';
import { AccordionProps } from '../types';

type Props = Pick<
AccordionProps,
'heading' | 'description' | 'defaultExpanded' | 'children' | 'onExpand' | 'onCollapse'
>;

const StyleHeadline = styled(Headline)``;

const PanelHeader = styled(Header)`
&:hover ${StyleHeadline} {
color: ${Colors.ACTION_BLUE_1000};
}
&:hover ${ChevronDown} {
color: ${Colors.ACTION_BLUE_1000};
}
&:hover ${ChevronUp} {
color: ${Colors.ACTION_BLUE_1000};
}
`;

const PanelIcon = ({ isOpen }: { isOpen: boolean }) => (isOpen ? <ChevronUp /> : <ChevronDown />);

export const Compact = ({ heading, description, defaultExpanded = false, children, onExpand, onCollapse }: Props) => {
const [isOpen, setIsOpen] = React.useState<boolean>(defaultExpanded);

return (
<>
<PanelHeader
onClick={() => {
if (isOpen) {
onExpand();
} else {
onCollapse();
}
setIsOpen(!isOpen);
}}
>
<Box display="flex" flexDirection="column" maxWidth="33%">
<Headline as="h4" mr="3">
{heading}
</Headline>
{isOpen && <Description mt="1" description={description} />}
</Box>
{!isOpen && <Description mt="1" description={description} />}
<Box ml="3">
<PanelIcon isOpen={isOpen} />
</Box>
</PanelHeader>
{isOpen && (
<Box mx="2" mb="5">
{children}
</Box>
)}
</>
);
};
116 changes: 116 additions & 0 deletions src/components/Accordion/components/Default.tsx
@@ -0,0 +1,116 @@
import React, { useState, PropsWithChildren } from 'react';
import styled from 'styled-components';

import { Colors } from '../../../essentials';
import { Text } from '../../Text/Text';
import { Box } from '../../Box/Box';
import { Headline } from '../../Headline/Headline';
import { Header } from './Header';
import { ChevronUp } from './ChevronUp';
import { ChevronDown } from './ChevronDown';
import { Description } from './Description';
import { AccordionProps } from '../types';

const ButtonLabel = styled(Text).attrs({ as: 'p' })`
color: ${Colors.ACTION_BLUE_900};
`;

const PanelHeader = styled(Header)`
&:hover {
background-color: ${Colors.ACTION_BLUE_50};
}
&:hover ${ButtonLabel} {
color: ${Colors.ACTION_BLUE_1000};
}
&:hover ${ChevronDown} {
color: ${Colors.ACTION_BLUE_1000};
}
`;

const CardHeader = styled(Header).attrs({ p: '3' })`
background-color: ${Colors.AUTHENTIC_BLUE_50};
border-radius: 0.3125rem 0.3125rem 0 0;
&:hover {
background-color: ${Colors.ACTION_BLUE_50};
}
&:hover ${ButtonLabel} {
color: ${Colors.ACTION_BLUE_1000};
}
&:hover ${ChevronUp} {
color: ${Colors.ACTION_BLUE_1000};
}
`;

const PanelBody = styled(Box).attrs({ my: '3' })`
border: solid 0.0625rem ${Colors.AUTHENTIC_BLUE_200};
border-radius: 0.3125rem;
`;

const PanelIcon = ({ isOpen }: { isOpen: boolean }) =>
isOpen ? <ChevronUp color={Colors.ACTION_BLUE_900} /> : <ChevronDown color={Colors.ACTION_BLUE_900} />;

export const DefaultPanel = ({
heading,
description,
info,
buttonLabel,
defaultExpanded = false,
children,
onExpand,
onCollapse
}: PropsWithChildren<AccordionProps>) => {
const [isOpen, setIsOpen] = useState<boolean>(defaultExpanded);

return (
<>
{isOpen ? (
<PanelBody>
<CardHeader
onClick={() => {
setIsOpen(!isOpen);
onCollapse();
}}
>
<Box display="flex" flexDirection="column" maxWidth="33%">
<Headline as="h4" mr="3">
{heading}
</Headline>
<Description mt="1" description={description} />
</Box>
<Box ml="3" display="flex" flexDirection="row">
<ButtonLabel>{buttonLabel}</ButtonLabel>
<PanelIcon isOpen={isOpen} />
</Box>
</CardHeader>
<Box m="3">{children}</Box>
</PanelBody>
) : (
<PanelHeader
onClick={() => {
setIsOpen(!isOpen);
onExpand();
}}
>
<Headline as="h4" mr="3">
{heading}
</Headline>
<Box>
<Description description={description} />
<Text as="p" style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' }}>
{info}
</Text>
</Box>
<Box ml="3" display="flex" flexDirection="row">
<ButtonLabel>{buttonLabel}</ButtonLabel>
<PanelIcon isOpen={isOpen} />
</Box>
</PanelHeader>
)}
</>
);
};
19 changes: 19 additions & 0 deletions src/components/Accordion/components/Description.tsx
@@ -0,0 +1,19 @@
import React from 'react';
import { MarginProps } from 'styled-system';
import { Text } from '../../Text/Text';

interface Props extends MarginProps {
description?: string;
}

export const Description = ({ description, ...rest }: Props) => (
<Text
as="p"
fontSize="small"
weak
style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' }}
{...rest}
>
{description}
</Text>
);
13 changes: 13 additions & 0 deletions src/components/Accordion/components/Header.tsx
@@ -0,0 +1,13 @@
import styled from 'styled-components';

import { Colors } from '../../../essentials';
import { Box } from '../../Box/Box';

export const Header = styled(Box).attrs({ p: '2', color: Colors.AUTHENTIC_BLUE_900 })`
display: flex;
flex-direction: row;
justify-content: space-between;
cursor: pointer;
min-height: 2.5rem;
`;

0 comments on commit 439b67c

Please sign in to comment.