-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #134 from oaknational/feat/ENG-693/accordion-compo…
…nent ENG-693: Add `OakAccordion`
- Loading branch information
Showing
11 changed files
with
450 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
src/components/molecules/OakAccordion/OakAccordion.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import React from "react"; | ||
import { Meta, StoryObj } from "@storybook/react"; | ||
|
||
import { OakCheckBox } from "../OakCheckBox"; | ||
|
||
import { OakAccordion } from "./OakAccordion"; | ||
|
||
const meta: Meta<typeof OakAccordion> = { | ||
component: OakAccordion, | ||
tags: ["autodocs"], | ||
title: "components/molecules/OakAccordion", | ||
parameters: { | ||
controls: { | ||
include: ["header", "headerAfterSlot", "children"], | ||
}, | ||
}, | ||
argTypes: { | ||
children: { | ||
control: { | ||
type: "text", | ||
}, | ||
}, | ||
headerAfterSlot: { | ||
control: { | ||
type: "text", | ||
}, | ||
}, | ||
header: { | ||
control: { | ||
type: "text", | ||
}, | ||
}, | ||
}, | ||
args: { | ||
id: "accordion-1", | ||
header: "Embedded content", | ||
children: | ||
"Any cookies required for video or other embedded learning content to work", | ||
}, | ||
render: (args) => <OakAccordion {...args} />, | ||
}; | ||
export default meta; | ||
|
||
type Story = StoryObj<typeof OakAccordion>; | ||
|
||
export const Default: Story = {}; | ||
|
||
export const WithHeaderAfterSlot: Story = { | ||
args: { | ||
headerAfterSlot: ( | ||
<OakCheckBox id="check-me" value="check-me" displayValue="" /> | ||
), | ||
}, | ||
}; | ||
|
||
export const MultipleAccordions: Story = { | ||
parameters: { | ||
controls: { | ||
include: [], | ||
}, | ||
}, | ||
render: () => { | ||
return ( | ||
<> | ||
<OakAccordion | ||
id="necessary-accordion" | ||
header="Strictly necessary" | ||
headerAfterSlot={ | ||
<OakCheckBox | ||
id="necessary" | ||
value="necessary" | ||
displayValue="" | ||
checked | ||
disabled | ||
/> | ||
} | ||
> | ||
Necessary for the website to function | ||
</OakAccordion> | ||
<OakAccordion | ||
id="embedded-accordion" | ||
header="Embedded content" | ||
headerAfterSlot={ | ||
<OakCheckBox | ||
id="embedded" | ||
value="embedded" | ||
displayValue="" | ||
checked={false} | ||
/> | ||
} | ||
> | ||
Any cookies required for video or other embedded learning content to | ||
work | ||
</OakAccordion> | ||
<OakAccordion | ||
id="statistics-accordion" | ||
header="Statistics" | ||
headerAfterSlot={ | ||
<OakCheckBox | ||
id="statistics" | ||
value="statistics" | ||
displayValue="" | ||
checked | ||
/> | ||
} | ||
> | ||
Any cookies that may be used to track website usage | ||
</OakAccordion> | ||
</> | ||
); | ||
}, | ||
}; |
52 changes: 52 additions & 0 deletions
52
src/components/molecules/OakAccordion/OakAccordion.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import React from "react"; | ||
import { create } from "react-test-renderer"; | ||
import { act, fireEvent } from "@testing-library/react"; | ||
import "@testing-library/jest-dom"; | ||
|
||
import { OakAccordion } from "./OakAccordion"; | ||
|
||
import { OakThemeProvider } from "@/components/atoms"; | ||
import { oakDefaultTheme } from "@/styles"; | ||
import renderWithTheme from "@/test-helpers/renderWithTheme"; | ||
|
||
describe(OakAccordion, () => { | ||
it("matches snapshot", () => { | ||
const tree = create( | ||
<OakThemeProvider theme={oakDefaultTheme}> | ||
<OakAccordion | ||
initialOpen | ||
header="See more" | ||
headerAfterSlot="After" | ||
id="see-more" | ||
> | ||
Here it is | ||
</OakAccordion> | ||
</OakThemeProvider>, | ||
).toJSON(); | ||
|
||
expect(tree).toMatchSnapshot(); | ||
}); | ||
|
||
it("toggles open and closed", () => { | ||
const { queryByRole, queryByText, getByText } = renderWithTheme( | ||
<OakAccordion header="See more" id="see-more"> | ||
Here it is | ||
</OakAccordion>, | ||
); | ||
|
||
expect(queryByRole("region")).not.toBeInTheDocument(); | ||
|
||
act(() => { | ||
fireEvent.click(getByText("See more")); | ||
}); | ||
|
||
expect(queryByRole("region")).toBeVisible(); | ||
expect(queryByText("Here it is")).toBeInTheDocument(); | ||
|
||
act(() => { | ||
fireEvent.click(getByText("See more")); | ||
}); | ||
|
||
expect(queryByRole("region")).not.toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import React, { ReactNode, useState } from "react"; | ||
import styled from "styled-components"; | ||
|
||
import { OakBox, OakFlex, OakIcon } from "@/components/atoms"; | ||
import { parseSpacing } from "@/styles/helpers/parseSpacing"; | ||
import { parseDropShadow } from "@/styles/helpers/parseDropShadow"; | ||
|
||
export type OakAccordionProps = { | ||
/** | ||
* The header of the accordion | ||
*/ | ||
header: ReactNode; | ||
/** | ||
* Slot to place content after the header and outside the button | ||
*/ | ||
headerAfterSlot?: ReactNode; | ||
/** | ||
* Whether the accordion should be open initially | ||
*/ | ||
initialOpen?: boolean; | ||
/** | ||
* The content of the accordion | ||
*/ | ||
children: ReactNode; | ||
/** | ||
* The id of the accordion | ||
*/ | ||
id: string; | ||
}; | ||
|
||
const StyledOakFlex = styled(OakFlex)` | ||
font: inherit; | ||
border: none; | ||
background: none; | ||
appearance: none; | ||
margin: -${parseSpacing("inner-padding-m")}; | ||
outline: none; | ||
&:focus-visible { | ||
box-shadow: ${parseDropShadow("drop-shadow-centered-lemon")}, | ||
${parseDropShadow("drop-shadow-centered-grey")}; | ||
} | ||
`; | ||
|
||
/** | ||
* An accordion component that can be used to show/hide content | ||
*/ | ||
export const OakAccordion = ({ | ||
header, | ||
headerAfterSlot, | ||
children, | ||
initialOpen = false, | ||
id, | ||
}: OakAccordionProps) => { | ||
const [isOpen, setOpen] = useState(initialOpen); | ||
|
||
return ( | ||
<OakBox | ||
$borderColor="border-neutral-lighter" | ||
$ba="border-solid-s" | ||
$pa="inner-padding-m" | ||
$background={isOpen ? "bg-neutral" : "bg-primary"} | ||
> | ||
<OakFlex | ||
as="h3" | ||
$font="heading-light-7" | ||
$textDecoration={isOpen ? "underline" : "none"} | ||
> | ||
<StyledOakFlex | ||
as="button" | ||
onClick={() => setOpen(!isOpen)} | ||
$alignItems="center" | ||
$pa="inner-padding-m" | ||
$flexGrow={1} | ||
aria-expanded={isOpen} | ||
id={id} | ||
> | ||
<OakIcon | ||
iconName="chevron-down" | ||
$mr="space-between-s" | ||
$width="all-spacing-6" | ||
$height="all-spacing-6" | ||
alt="" | ||
style={{ transform: isOpen ? "rotate(180deg)" : "none" }} | ||
/> | ||
{header} | ||
</StyledOakFlex> | ||
{headerAfterSlot && ( | ||
<OakFlex $ml="space-between-m">{headerAfterSlot}</OakFlex> | ||
)} | ||
</OakFlex> | ||
<OakBox | ||
$ml="space-between-m" | ||
$pl="inner-padding-m" | ||
$mt="space-between-sssx" | ||
$font="body-3" | ||
hidden={!isOpen} | ||
aria-labelledby={id} | ||
role="region" | ||
> | ||
{children} | ||
</OakBox> | ||
</OakBox> | ||
); | ||
}; |
Oops, something went wrong.