Skip to main content

Accessible accordion

12th October, 2022

Updated: 12th October, 2022

    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: