Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/accordion components #48

Merged
merged 30 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
31c033c
First iteration accordion component
AlbyIsCoding Jun 13, 2022
ec72700
second iteration accordion component
AlbyIsCoding Jun 17, 2022
2e8655a
Merge branch 'main' into feat/accordion-components
AlbyIsCoding Jun 17, 2022
a1fecba
Delete code for symbols
AlbyIsCoding Jun 17, 2022
fee52db
Third iteration Accordion
AlbyIsCoding Jun 20, 2022
e2415d6
code formatting
AlbyIsCoding Jun 20, 2022
2d35f57
Accordion styling first iteration
AlbyIsCoding Jun 21, 2022
5b9ab45
Merge remote-tracking branch 'upstream/main' into feat/accordion-comp…
AlbyIsCoding Jun 21, 2022
65fe1aa
Accordion styling iteration 2
AlbyIsCoding Jun 21, 2022
d9f5316
Code cleanup
AlbyIsCoding Jun 21, 2022
98364e1
code cleanup
AlbyIsCoding Jun 22, 2022
a78d596
code cleanup
AlbyIsCoding Jun 22, 2022
d2894bf
add expand/collapse icon
haakemon Jun 22, 2022
412df2d
css cleanup
AlbyIsCoding Jun 22, 2022
7472524
border styling
AlbyIsCoding Jun 23, 2022
4576958
delete unecessary css
AlbyIsCoding Jun 23, 2022
c3e87f0
Add action prop and change func name
AlbyIsCoding Jun 23, 2022
a24b4fe
Merge remote-tracking branch 'origin/feat/accordion-component-icon' i…
AlbyIsCoding Jun 23, 2022
e09b088
add icon and header title flex
AlbyIsCoding Jun 24, 2022
89ad1c2
Added tests
AlbyIsCoding Jun 24, 2022
1990dc3
PR fixes
AlbyIsCoding Jun 24, 2022
3c4d698
delete test
AlbyIsCoding Jun 24, 2022
fbba7c5
change test description
AlbyIsCoding Jun 24, 2022
493c25b
code cleanup
AlbyIsCoding Jun 27, 2022
0f95680
Fix tests
AlbyIsCoding Jun 27, 2022
f0393a8
remove shadow variabling
AlbyIsCoding Jun 27, 2022
abd3f54
fix aria
AlbyIsCoding Jul 1, 2022
dfcaaf8
fix css
AlbyIsCoding Jul 25, 2022
64da54d
Merge branch 'main' into feat/accordion-components
AlbyIsCoding Jul 25, 2022
df94ac8
implemented usage of useId
AlbyIsCoding Jul 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified .husky/pre-commit
100644 → 100755
Empty file.
7 changes: 7 additions & 0 deletions src/components/Accordion/Accordion.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.accordion {
--component-accordion-color-background: var(--colors-neutral-000);
--component-panel-size-width: 100%;
background-color: var(--component-accordion-color-background);
width: var(--component-panel-size-width);
border-right-style: none;
}
3 changes: 3 additions & 0 deletions src/components/Accordion/Accordion.stories.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.container {
width: 80vw;
}
87 changes: 87 additions & 0 deletions src/components/Accordion/Accordion.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useState } from 'react';
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { config } from 'storybook-addon-designs';
import cn from 'classnames';

import { StoryPage } from '@sb/StoryPage';

import { Button } from '../Button';

import { Accordion } from './Accordion';
import { AccordionHeader } from './AccordionHeader';
import { AccordionContent } from './AccordionContent';
import classes from './Accordion.stories.module.css';

const figmaLink = ''; // TODO: Add figma link

export default {
title: `Components/Accordion`,
component: Accordion,
parameters: {
design: config([
{
type: 'figma',
url: figmaLink,
},
{
type: 'link',
url: figmaLink,
},
]),
docs: {
page: () => (
<StoryPage
description={`TODO: Add a description (supports markdown)`}
/>
),
},
},
} as ComponentMeta<typeof Accordion>;

const Template: ComponentStory<typeof Accordion> = () => {
const [open1, setOpen1] = useState(false);
const [open2, setOpen2] = useState(false);

const handleClick1 = () => {
setOpen1(!open1);
};

const handleClick2 = () => {
setOpen2(!open2);
};

const AccordionExampleContent =
'Accordion content (No preset top spacing, enabling full customization).';

const ActionButton = <Button>Separat funksjonsknapp</Button>;
return (
<div className={cn(classes['container'])}>
<Accordion
onClick={handleClick1}
open={open1}
>
<AccordionHeader actions={ActionButton}>Accordion 1</AccordionHeader>
<AccordionContent>{AccordionExampleContent}</AccordionContent>
</Accordion>
<Accordion
onClick={handleClick2}
open={open2}
>
<AccordionHeader actions={ActionButton}>Accordion 2</AccordionHeader>
<AccordionContent>{AccordionExampleContent}</AccordionContent>
</Accordion>
</div>
);
};

export const Example = Template.bind({});
Example.args = {
// TODO: Add story specific args
};
Example.parameters = {
docs: {
description: {
story: '', // TODO: add story description, supports markdown
},
},
};
99 changes: 99 additions & 0 deletions src/components/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render as renderRtl, screen } from '@testing-library/react';

import type { AccordionProps } from './Accordion';
import { Accordion } from './Accordion';
import { AccordionHeader } from './AccordionHeader';
import { AccordionContent } from './AccordionContent';

const render = (props: Partial<AccordionProps> = {}) => {
const allProps = {
children: (
<>
<AccordionHeader>AccordionHeader</AccordionHeader>
<AccordionContent>AccordionContent</AccordionContent>
</>
),
onClick: jest.fn(),
open: true,
...props,
};
renderRtl(<Accordion {...allProps} />);
};

const user = userEvent.setup();

describe('Accordion', () => {
haakemon marked this conversation as resolved.
Show resolved Hide resolved
it('should call handleClick with open=true when AccordionHeader is clicked', async () => {
haakemon marked this conversation as resolved.
Show resolved Hide resolved
const handleClick = jest.fn();
render({ onClick: handleClick, open: true });

await user.click(screen.getByRole('button', { name: 'AccordionHeader' }));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

describe('Accordion', () => {
it('should call handleClick with open=false when AccordionHeader is clicked', async () => {
const handleClick = jest.fn();
render({ onClick: handleClick, open: false });

await user.click(screen.getByRole('button', { name: 'AccordionHeader' }));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

describe('Accordion', () => {
it('should have aria-expanded=true when clicked and open=true', async () => {
const handleClick = jest.fn();
render({ onClick: handleClick });

await user.click(screen.getByRole('button', { name: 'AccordionHeader' }));
expect(
screen.getByRole('button', { name: 'AccordionHeader', expanded: true }),
).toBeInTheDocument();
});
haakemon marked this conversation as resolved.
Show resolved Hide resolved
});

describe('Accordion', () => {
it('should have aria-expanded=false when clicked and open=false', async () => {
const handleClick = jest.fn();
render({ onClick: handleClick, open: false });

await user.click(screen.getByRole('button', { name: 'AccordionHeader' }));
expect(
screen.getByRole('button', { name: 'AccordionHeader', expanded: false }),
).toBeInTheDocument();
});
});

describe('Accordion', () => {
it('should have aria-expanded=true when accordion is expanded using key press ', async () => {
const handleClick = jest.fn();
render({ onClick: handleClick });

const AccordionHeader = screen.getByRole('button', {
name: 'AccordionHeader',
});
await user.type(AccordionHeader, '{Space}');
expect(
screen.getByRole('button', { name: 'AccordionHeader', expanded: true }),
).toBeInTheDocument();
});
});

describe('Accordion', () => {
it('should have aria-expanded=true when accordion is expanded using key press ', async () => {
const handleClick = jest.fn();
render({ onClick: handleClick, open: false });

const AccordionHeader = screen.getByRole('button', {
name: 'AccordionHeader',
});
await user.type(AccordionHeader, '{Space}');
expect(
screen.getByRole('button', { name: 'AccordionHeader', expanded: false }),
).toBeInTheDocument();
});
});
29 changes: 29 additions & 0 deletions src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import cn from 'classnames';

import type { ClickHandler } from './Context';
import { AccordionContext } from './Context';
import classes from './Accordion.module.css';

export interface AccordionProps {
children: React.ReactNode;
onClick: ClickHandler;
open: boolean;
}

export const Accordion = ({ children, open, onClick }: AccordionProps) => {
return (
<div className={cn(classes['accordion'])}>
<AccordionContext.Provider
value={{
onClick,
open,
}}
>
{children}
</AccordionContext.Provider>
</div>
);
};

export default Accordion;
3 changes: 3 additions & 0 deletions src/components/Accordion/AccordionContent.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.accordion-content {
font-family: inherit;
}
haakemon marked this conversation as resolved.
Show resolved Hide resolved
30 changes: 30 additions & 0 deletions src/components/Accordion/AccordionContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import cn from 'classnames';

import classes from './AccordionContent.module.css';
import { useAccordionContext } from './Context';

export interface AccordionContentProps {
children?: React.ReactNode;
}

export const AccordionContent = ({ children }: AccordionContentProps) => {
const { open } = useAccordionContext();

return (
<div>
{open && (
<div
className={cn(classes['accordion-content'], {
[classes['accordion-content--opened']]: open,
})}
haakemon marked this conversation as resolved.
Show resolved Hide resolved
aria-expanded={open}
>
{children}
</div>
)}
</div>
);
};

export default AccordionContent;
54 changes: 54 additions & 0 deletions src/components/Accordion/AccordionHeader.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.accordion-header {
--component-accordion_header-border_top_style: solid;
--component-accordion_header-border_top_color: var(--colors-neutral-200);
--component-accordion_header-border_top_width: var(--border-width-thin);
--component-accordion_header-color-background: var(--colors-neutral-000);
display: flex;
border-top-width: var(--component-accordion_header-border_top_width);
border-top-style: var(--component-accordion_header-border_top_style);
border-top-color: var(--component-accordion_header-border_top_color);
background-color: var(--component-accordion_header-color-background);
}

.accordion-header-title {
--component-accordion_header_title-spacing-margin_left: 2.5rem;
--component-accordion_header_title-border_top_style: none;
--component-accordion_header_title-border_bottom_style: none;
--component-accordion_header_title-border_right_style: none;
--component-accordion_header_title-border_left_style: none;
--component-accordion_header_title-font_size: var(--font-size-300);
--component-accordion_header_title-font_weight: var(
--component-panel-weight-heading
);
--component-accordion_header_title-color-background: none;
font-family: inherit;
flex: 1 1 auto;
border-top-style: var(--component-accordion_header_title-border_top_style);
border-bottom-style: var(
--component-accordion_header_title-border_bottom_style
);
border-left-style: var(--component-accordion_header_title-border_left_style);
border-right-style: var(
--component-accordion_header_title-border_right_style
);
background-color: var(--component-accordion_header_title-color-background);
text-align: var(--component-accordion_header_title-text-align);
margin-left: var(--component-accordion_header_title-margin-left);
font-size: var(--component-accordion_header_title-font_size);
font-weight: var(--component-accordion_header_title-font_weight);
margin-left: var(--component-accordion_header_title-spacing-margin_left);
}

.accordion-header__actions {
margin-top: 0.3rem;
}

.accordion-header-icon {
padding-top: 1rem;
margin-left: 2.5rem;
}

.accordion-header-icon__opened {
transform: rotate(90deg);
margin-left: 3rem;
}
42 changes: 42 additions & 0 deletions src/components/Accordion/AccordionHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import cn from 'classnames';

import classes from './AccordionHeader.module.css';
import { useAccordionContext } from './Context';
import { ReactComponent as ExpandCollapseArrow } from './expand-collapse.svg';

export interface AccordionHeaderProps {
children?: React.ReactNode;
actions?: React.ReactNode;
}

export const AccordionHeader = ({
children,
actions,
}: AccordionHeaderProps) => {
const { onClick, open } = useAccordionContext();

return (
<div className={cn(classes['accordion-header'])}>
<ExpandCollapseArrow
className={cn(classes['accordion-header-icon'], {
[classes['accordion-header-icon__opened']]: open,
haakemon marked this conversation as resolved.
Show resolved Hide resolved
})}
width='12'
height='18'
onClick={onClick}
/>
<button
className={cn(classes['accordion-header-title'])}
haakemon marked this conversation as resolved.
Show resolved Hide resolved
aria-expanded={open}
type='button'
onClick={onClick}
>
{children}
</button>
<div className={cn(classes['accordion-header__actions'])}>{actions}</div>
</div>
);
};

export default AccordionHeader;
18 changes: 18 additions & 0 deletions src/components/Accordion/Context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createContext, useContext } from 'react';

export type ClickHandler = () => void;

export const AccordionContext = createContext<
{ open: boolean; onClick: ClickHandler } | undefined
>(undefined);

export const useAccordionContext = () => {
const context = useContext(AccordionContext);
if (context === undefined) {
throw new Error(
'useAccordionContext must be used within a AccordionContext',
);
}

return context;
};
3 changes: 3 additions & 0 deletions src/components/Accordion/expand-collapse.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/components/Accordion/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { Accordion } from './Accordion';
export { AccordionHeader } from './AccordionHeader';
export { AccordionContent } from './AccordionContent';
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { Panel, PanelVariant } from './Panel';
export { CircularProgress } from './CircularProgress';
export { AppWrapper } from './AppWrapper';
export { ToggleButton, ToggleButtonGroup } from './ToggleButtonGroup';
export { Accordion, AccordionHeader, AccordionContent } from './Accordion';
export { Button, ButtonVariant } from './Button';