Accessible accordion
I made this into a repo
import React, { useState, setState } from "react";
import { AccordionProps } from "./Accordion.types";
import * as Styles from "./Accordion.styles";
// import './Accordion.css';
import AccordionSection from "./AccordionSection";
const Accordion: React.FC<AccordionProps> = ({ sections }) => {
const [openAll, setOpenAll] = useState(true);
const [accordionStates, setAccordionStates] = useState(sections);
const updateAccordionState = (title, value) => {
const newStatus = accordionStates.map(accordionState => {
if(accordionState.title === title) {
accordionState.isExpanded = value;
}
return accordionState;
});
setAccordionStates(newStatus);
};
const toggleAll = () => {
setOpenAll(!openAll);
const newStatus = accordionStates.map(accordionState => {
accordionState.isExpanded = openAll;
return accordionState;
});
setAccordionStates(newStatus);
}
return (
<Styles.Container data-testid="Accordion">
<Styles.AccordionControls>
<Styles.OpenAllButton onClick={toggleAll} type="button" aria-expanded={!openAll}>
{openAll ? "Open all" : "Close all"}
<Styles.VisuallyHidden> sections</Styles.VisuallyHidden>
</Styles.OpenAllButton>
</Styles.AccordionControls>
{accordionStates.map((section, i) =>
<AccordionSection {...section} key={i} onToggle={updateAccordionState} />
)}
</Styles.Container>
);
};
export default Accordion;
import React from "react";
import parse from "html-react-parser"
import { AccordionSectionProps } from "./Accordion.types";
import * as Styles from "./Accordion.styles";
const AccordionSection: React.FC<AccordionSectionProps> = ({ title, content, summary, isExpanded, onToggle }) => {
const key = title.slice(0, 15).trim().toLowerCase().replace(/[^a-zA-Z0-9]+/g, "-");
const onSectionToggle = () => (isExpanded === true) ? onToggle(title, false) : onToggle(title, true);
return (
<Styles.Section className={isExpanded && "accordion__section--expanded"}>
<Styles.SectionHeader onClick={onSectionToggle}>
<Styles.SectionHeading>
<Styles.SectionButton type="button" id={`${key}-heading`} aria-controls={`${key}-content`} aria-expanded={isExpanded ? "true" : "false"}>
{title}
<Styles.AccordionIcon aria-hidden="true"></Styles.AccordionIcon>
</Styles.SectionButton>
</Styles.SectionHeading>
{summary && <Styles.SectionSummary id={`${key}-summary`}>
{summary}
</Styles.SectionSummary>
}
</Styles.SectionHeader>
<Styles.SectionContent id={`${key}-content`} aria-labelledby={`${key}-heading`}>
{parse(content, { htmlparser2: { decodeEntities: true } })}
</Styles.SectionContent>
</Styles.Section>
);
};
export default AccordionSection;
import styled from "styled-components";
// Accordion
export const Container = styled.div`
${props => props.theme.fontStyles}
margin-bottom: 20px;
border-bottom: 1px solid ${props => props.theme.theme_vars.colours.grey};
@media (min-width: 40.0625em) {
margin-bottom: 30px
}
`
export const AccordionControls = styled.div`
text-align: right;
`
export const OpenAllButton = styled.button`
font-size: ${props => props.theme.theme_vars.fontSizes.extra_small};
position: relative;
z-index: 1;
margin: 0;
padding: 0;
border-width: 0;
color: ${props => props.theme.theme_vars.colours.action};
background: none;
cursor: pointer;
&:focus {
outline: 3px solid transparent;
color: ${props => props.theme.theme_vars.colours.black};
background-color: ${props => props.theme.theme_vars.colours.focus};
-webkit-box-shadow: 0 -2px ${props => props.theme.theme_vars.colours.focus}, 0 4px ${props => props.theme.theme_vars.colours.black};
box-shadow: 0 -2px ${props => props.theme.theme_vars.colours.focus}, 0 4px ${props => props.theme.theme_vars.colours.black};
text-decoration: none;
color: ${props => props.theme.theme_vars.colours.black};
&:hover {
color: #003078;
}
}
`
export const VisuallyHidden = styled.span`
${props => props.theme.visuallyHidden}
`
// AccordionSection
export const Section = styled.div`
padding-top: 15px;
padding-top: 0;
`
export const SectionHeader = styled.div`
${props => props.theme.headingStyles}
position: relative;
padding-right: 40px;
border-top: 1px solid ${props => props.theme.theme_vars.colours.grey};
color: ${props => props.theme.theme_vars.colours.action};
cursor: pointer;
padding-top: 15px;
padding-bottom: 15px;
&:hover {
border-top-color: ${props => props.theme.theme_vars.colours.action};
-webkit-box-shadow: inset 0 3px 0 0 ${props => props.theme.theme_vars.colours.action};
box-shadow: inset 0 3px 0 0 ${props => props.theme.theme_vars.colours.action}
}
`
export const SectionHeading = styled.h2`
margin-top: 0;
margin-bottom: 0;
`
export const SectionButton = styled.button`
${props => props.theme.theme_vars.h3}
display: inline-block;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
padding: 0;
border-width: 0;
color: inherit;
background: none;
text-align: left;
cursor: pointer;
-webkit-appearance: none;
&:focus {
outline: 3px solid transparent;
color: ${props => props.theme.theme_vars.colours.blacl};
background-color: ${props => props.theme.theme_vars.colours.focus};
-webkit-box-shadow: 0 -2px ${props => props.theme.theme_vars.colours.focus}, 0 4px ${props => props.theme.theme_vars.colours.black};
box-shadow: 0 -2px ${props => props.theme.theme_vars.colours.focus}, 0 4px ${props => props.theme.theme_vars.colours.black};
text-decoration: none
}
&::-moz-focus-inner {
padding: 0;
border: 0
}
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0
}
&:hover:not(:focus) {
text-decoration: underline;
}
&:hover {
text-decoration: none;
}
`
export const AccordionIcon = styled.span`
position: absolute;
top: 50%;
right: 15px;
width: 16px;
height: 16px;
margin-top: -8px;
&:after,
&:before {
content: "";
-webkit-box-sizing: border-box;
box-sizing: border-box;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 25%;
height: 25%;
margin: auto;
border: 2px solid transparent;
background-color: ${props => props.theme.theme_vars.colours.black}
}
&:before {
width: 100%;
}
&:after {
height: 100%;
.accordion__section--expanded & {
content: " ";
display: none;
}
}
`
export const SectionSummary = styled.div`
margin-top: 10px;
margin-bottom: 0;
`
export const SectionContent = styled.div`
display: none;
padding-top: 15px;
padding-bottom: 15px;
@media (min-width: 40.0625em) {
padding-top: 15px
}
@media (min-width: 40.0625em) {
padding-bottom: 15px
}
>:last-child {
margin-bottom: 0
}
.accordion__section--expanded & {
display: block;
}
`
export interface AccordionProps {
/**
* accepts multiple sections
*/
sections: Array<AccordionSectionProps>;
}
// export interface AccordionSectionArray {
// /**
// * Section title
// */
// title: string,
// /**
// * Section content
// */
// content: string
// /**
// * Section summary
// */
// summary: string
// }
export interface AccordionSectionProps {
/**
* Section title
*/
title: string,
/**
* Section content
*/
content: string
/**
* Section summary
*/
summary?: string,
/**
* Section summary
*/
isExpanded?: boolean,
}
0d5a395e-94db-42a4-b203-07c3425156a5
Created on: 12th October, 2022
Last updated: 12th October, 2022
Tagged With: