diff --git a/src/components/atoms/InternalCheckBox/InternalCheckBox.tsx b/src/components/atoms/InternalCheckBox/InternalCheckBox.tsx index 89ce3afe..993ebca8 100644 --- a/src/components/atoms/InternalCheckBox/InternalCheckBox.tsx +++ b/src/components/atoms/InternalCheckBox/InternalCheckBox.tsx @@ -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) => void; onFocus?: (event: React.FocusEvent) => void; diff --git a/src/components/molecules/OakAccordion/OakAccordion.stories.tsx b/src/components/molecules/OakAccordion/OakAccordion.stories.tsx new file mode 100644 index 00000000..44b84e79 --- /dev/null +++ b/src/components/molecules/OakAccordion/OakAccordion.stories.tsx @@ -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 = { + 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) => , +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithHeaderAfterSlot: Story = { + args: { + headerAfterSlot: ( + + ), + }, +}; + +export const MultipleAccordions: Story = { + parameters: { + controls: { + include: [], + }, + }, + render: () => { + return ( + <> + + } + > + Necessary for the website to function + + + } + > + Any cookies required for video or other embedded learning content to + work + + + } + > + Any cookies that may be used to track website usage + + + ); + }, +}; diff --git a/src/components/molecules/OakAccordion/OakAccordion.test.tsx b/src/components/molecules/OakAccordion/OakAccordion.test.tsx new file mode 100644 index 00000000..7ef85421 --- /dev/null +++ b/src/components/molecules/OakAccordion/OakAccordion.test.tsx @@ -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( + + + Here it is + + , + ).toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it("toggles open and closed", () => { + const { queryByRole, queryByText, getByText } = renderWithTheme( + + Here it is + , + ); + + 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(); + }); +}); diff --git a/src/components/molecules/OakAccordion/OakAccordion.tsx b/src/components/molecules/OakAccordion/OakAccordion.tsx new file mode 100644 index 00000000..044bdf3f --- /dev/null +++ b/src/components/molecules/OakAccordion/OakAccordion.tsx @@ -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 ( + + + setOpen(!isOpen)} + $alignItems="center" + $pa="inner-padding-m" + $flexGrow={1} + aria-expanded={isOpen} + id={id} + > + + {header} + + {headerAfterSlot && ( + {headerAfterSlot} + )} + + + + ); +}; diff --git a/src/components/molecules/OakAccordion/__snapshots__/OakAccordion.test.tsx.snap b/src/components/molecules/OakAccordion/__snapshots__/OakAccordion.test.tsx.snap new file mode 100644 index 00000000..6f0065a4 --- /dev/null +++ b/src/components/molecules/OakAccordion/__snapshots__/OakAccordion.test.tsx.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OakAccordion matches snapshot 1`] = ` +.c0 { + padding: 1rem; + background: #f2f2f2; + border: 0.063rem solid; + border-color: #cacaca; + font-family: Lexend,sans-serif; +} + +.c1 { + font-family: Lexend,sans-serif; + font-weight: 400; + font-size: 1rem; + line-height: 1.25rem; + -webkit-letter-spacing: 0.0115rem; + -moz-letter-spacing: 0.0115rem; + -ms-letter-spacing: 0.0115rem; + letter-spacing: 0.0115rem; + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c3 { + padding: 1rem; + font-family: Lexend,sans-serif; +} + +.c3:hover { + cursor: pointer; +} + +.c6 { + position: relative; + width: 1.5rem; + min-width: 1.5rem; + height: 1.5rem; + min-height: 1.5rem; + margin-right: 1rem; + font-family: Lexend,sans-serif; +} + +.c8 { + margin-left: 1.5rem; + font-family: Lexend,sans-serif; +} + +.c9 { + padding-left: 1rem; + margin-left: 1.5rem; + margin-top: 0.25rem; + font-family: Lexend,sans-serif; + font-weight: 300; + font-size: 0.875rem; + line-height: 1.25rem; + -webkit-letter-spacing: -0.005rem; + -moz-letter-spacing: -0.005rem; + -ms-letter-spacing: -0.005rem; + letter-spacing: -0.005rem; +} + +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c7 { + object-fit: contain; +} + +.c5 { + font: inherit; + border: none; + background: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + margin: -1rem; + outline: none; +} + +.c5:focus-visible { + box-shadow: 0 0 0 0.125rem rgba(255,229,85,100%), 0 0 0 0.3rem rgba(87,87,87,100%); +} + +
+

+ +
+ After +
+

+ +
+`; diff --git a/src/components/molecules/OakAccordion/index.ts b/src/components/molecules/OakAccordion/index.ts new file mode 100644 index 00000000..9a270de6 --- /dev/null +++ b/src/components/molecules/OakAccordion/index.ts @@ -0,0 +1 @@ +export * from "./OakAccordion"; diff --git a/src/components/molecules/OakCheckBox/OakCheckBox.tsx b/src/components/molecules/OakCheckBox/OakCheckBox.tsx index 23d7d227..b7624088 100644 --- a/src/components/molecules/OakCheckBox/OakCheckBox.tsx +++ b/src/components/molecules/OakCheckBox/OakCheckBox.tsx @@ -60,7 +60,8 @@ export const OakCheckBox = (props: OakCheckBoxProps) => { value, displayValue = value, disabled = false, - defaultChecked = false, + defaultChecked, + checked, onChange, onFocus, onBlur, @@ -121,6 +122,7 @@ export const OakCheckBox = (props: OakCheckBoxProps) => { onFocus={onFocus} onBlur={onBlur} defaultChecked={defaultChecked} + checked={checked} disabled={disabled} /> } diff --git a/src/components/molecules/OakCheckBox/__snapshots__/OakCheckbox.test.tsx.snap b/src/components/molecules/OakCheckBox/__snapshots__/OakCheckbox.test.tsx.snap index 5b0bc8d6..49493c81 100644 --- a/src/components/molecules/OakCheckBox/__snapshots__/OakCheckbox.test.tsx.snap +++ b/src/components/molecules/OakCheckBox/__snapshots__/OakCheckbox.test.tsx.snap @@ -118,7 +118,6 @@ input:checked + .c7 { >