Skip to content

Commit

Permalink
Merge pull request #134 from oaknational/feat/ENG-693/accordion-compo…
Browse files Browse the repository at this point in the history
…nent

ENG-693: Add `OakAccordion`
  • Loading branch information
carlmw authored Mar 27, 2024
2 parents 578bae4 + e9bdf3d commit cb857b3
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 2 deletions.
7 changes: 7 additions & 0 deletions src/components/atoms/InternalCheckBox/InternalCheckBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ export type BaseCheckBoxProps = {
id: string;
disabled?: boolean;
value: string;
/**
* Uncontrolled checked state
*/
defaultChecked?: boolean;
/**
* Controlled checked state
*/
checked?: boolean;
onHovered?: (value: string, id: string, duration: number) => void;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
Expand Down
112 changes: 112 additions & 0 deletions src/components/molecules/OakAccordion/OakAccordion.stories.tsx
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 src/components/molecules/OakAccordion/OakAccordion.test.tsx
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();
});
});
106 changes: 106 additions & 0 deletions src/components/molecules/OakAccordion/OakAccordion.tsx
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>
);
};
Loading

0 comments on commit cb857b3

Please sign in to comment.