<div id="cwf-accordion--example-accordion--group" class="cwf-accordion"><button class="cwf-accordion__toggle" aria-controls="cwf-accordion--example-accordion--group" data-action="true">
        Expand All
    </button>
    <div class="cwf-accordion__wrapper">
        <div id="cwf-accordion__panel--example-accordion--group--1" class="cwf-accordion__panel" role="dialog" aria-labelledby="cwf-accordion__title--example-accordion--group--1" aria-describedby="cwf-accordion__body--example-accordion--group--1">
            <div class="cwf-accordion__heading" role="button" tabindex="0" aria-controls="cwf-accordion__overflow--example-accordion--group--1" aria-expanded="true">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="cwf-accordion__chevron" role="presentation">
                    <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
                    <path fill="currentColor" d="M416 352c-8.188 0-16.38-3.125-22.62-9.375L224 173.3l-169.4 169.4c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25C432.4 348.9 424.2 352 416 352z" />
                </svg> <strong id="cwf-accordion__title--example-accordion--group--1" class="cwf-accordion__title">
                    Panel title 1
                </strong>
            </div>
            <div id="cwf-accordion__overflow--example-accordion--group--1" class="cwf-accordion__overflow">
                <div id="cwf-accordion__body--example-accordion--group--1" class="cwf-accordion__body">
                    <p>
                        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed imperdiet
                        risus suscipit sapien congue pretium. In hac habitasse platea dictumst.
                        Donec vel nisi quis dui aliquam porta. Sed nec dolor ullamcorper velit
                        suscipit elementum. Suspendisse et augue vitae tellus congue mollis.
                        Integer sem ex, rhoncus eget cursus non, rhoncus eget ipsum. Praesent
                        ullamcorper facilisis diam, sit amet commodo sem auctor egestas. <a href="https://www.vcu.edu/">
                            Virginia Commonwealth University
                        </a> Vivamus vestibulum, ligula vitae molestie finibus, est lacus ornare
                        nisl, quis ultrices massa mi vitae augue. Pellentesque nulla lectus, placerat
                        id tellus ac, fringilla aliquam velit. Aliquam ullamcorper consectetur urna,
                        vitae maximus nunc malesuada eget. Vestibulum lobortis ut quam a congue.
                        Phasellus facilisis erat at feugiat faucibus. Curabitur cursus dolor in laoreet
                        sodales.
                    </p>
                </div>
            </div>
        </div>
        <div id="cwf-accordion__panel--example-accordion--group--2" class="cwf-accordion__panel" role="dialog" aria-labelledby="cwf-accordion__title--example-accordion--group--2" aria-describedby="cwf-accordion__body--example-accordion--group--2">
            <div class="cwf-accordion__heading" role="button" tabindex="0" aria-controls="cwf-accordion__overflow--example-accordion--group--2" aria-expanded="false">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="cwf-accordion__chevron" role="presentation">
                    <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
                    <path fill="currentColor" d="M416 352c-8.188 0-16.38-3.125-22.62-9.375L224 173.3l-169.4 169.4c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25C432.4 348.9 424.2 352 416 352z" />
                </svg> <strong id="cwf-accordion__title--example-accordion--group--2" class="cwf-accordion__title">
                    Panel title 2
                </strong>
            </div>
            <div id="cwf-accordion__overflow--example-accordion--group--2" class="cwf-accordion__overflow">
                <div id="cwf-accordion__body--example-accordion--group--2" class="cwf-accordion__body">
                    <p>
                        Nulla efficitur dui risus, et venenatis lacus commodo a. Nullam leo
                        odio, posuere vitae erat eu, iaculis vulputate magna. Donec posuere elit
                        eget lectus fermentum euismod. Lorem ipsum dolor sit amet, consectetur
                        adipiscing elit. Aliquam blandit augue ut nulla dictum, placerat
                        vehicula mi pulvinar. Maecenas in quam est. Integer nec ultricies purus,
                        malesuada accumsan neque. Maecenas mauris ligula, lobortis et congue
                        nec, condimentum eget eros. Vivamus pharetra rutrum tellus, sit amet
                        molestie lorem pellentesque et. Class aptent taciti sociosqu ad litora
                        torquent per conubia nostra, per inceptos himenaeos. Nullam pretium nibh
                        ac est dapibus porta eu sollicitudin odio. Nunc sed risus faucibus,
                        commodo eros iaculis, volutpat diam. Mauris mattis velit augue, id
                        tincidunt leo rhoncus non.
                    </p>
                </div>
            </div>
        </div>
        <div id="cwf-accordion__panel--example-accordion--group--3" class="cwf-accordion__panel" role="dialog" aria-labelledby="cwf-accordion__title--example-accordion--group--3" aria-describedby="cwf-accordion__body--example-accordion--group--3">
            <div class="cwf-accordion__heading" role="button" tabindex="0" aria-controls="cwf-accordion__overflow--example-accordion--group--3" aria-expanded="false">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="cwf-accordion__chevron" role="presentation">
                    <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
                    <path fill="currentColor" d="M416 352c-8.188 0-16.38-3.125-22.62-9.375L224 173.3l-169.4 169.4c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25C432.4 348.9 424.2 352 416 352z" />
                </svg> <strong id="cwf-accordion__title--example-accordion--group--3" class="cwf-accordion__title">
                    Panel title 3
                </strong>
            </div>
            <div id="cwf-accordion__overflow--example-accordion--group--3" class="cwf-accordion__overflow">
                <div id="cwf-accordion__body--example-accordion--group--3" class="cwf-accordion__body">
                    <p>
                        Proin ex enim, elementum vitae ipsum vel, congue molestie mauris.
                        Vestibulum consequat scelerisque commodo. In malesuada tincidunt
                        finibus. Donec efficitur, quam et interdum aliquet, nisl sapien sagittis
                        ex, nec luctus est risus eget mauris. Etiam porttitor, magna at
                        sollicitudin facilisis, metus ipsum commodo arcu, vitae tristique lorem
                        quam eget neque. Aliquam rhoncus bibendum est, vel accumsan leo molestie
                        efficitur. Sed nec pretium elit, a vestibulum magna.
                    </p>
                </div>
            </div>
        </div>
    </div>
</div>
{% set id = id ?? panels[0].id %}
{% set accordionSuffix = id ? '--' ~ id : '' %}
{% set accordionId = 'cwf-accordion' ~ accordionSuffix %}
<div id="{{ accordionId }}" class="cwf-accordion">
    {%- if panels.length > 1 -%}
        <button class="cwf-accordion__toggle"
            aria-controls="{{ accordionId }}"
            data-action="true">
            Expand All
        </button>
    {%- endif -%}
    <div class="cwf-accordion__wrapper">
        {%- for panel in panels -%}
            {% set panelIndex = panel.id ?? loop.index %}
            {% set panelSuffix = id is defined
                ? id ~ '--' ~ panelIndex
                : panelIndex
            %}
            {% set panelId = 'cwf-accordion__panel--' ~ panelSuffix %}
            {% set panelTitleId = 'cwf-accordion__title--' ~ panelSuffix %}
            {% set panelBodyId = 'cwf-accordion__body--' ~ panelSuffix %}
            {% set panelOverflowId = 'cwf-accordion__overflow--' ~ panelSuffix
            %}
            <div id="{{ panelId }}"
                class="cwf-accordion__panel"
                role="dialog"
                aria-labelledby="{{ panelTitleId }}"
                aria-describedby="{{ panelBodyId }}">
                <div class="cwf-accordion__heading"
                    role="button"
                    tabindex="0"
                    aria-controls="{{ panelOverflowId }}"
                    aria-expanded="{{ panel.open ?? false }}">
                    {% include '../../shared/icons/chevron-up-solid.svg' with {
                        class: 'cwf-accordion__chevron',
                        role: 'presentation'
                    } %}
                    <strong id="{{ panelTitleId }}"
                        class="cwf-accordion__title">
                        {{ panel.title }}
                    </strong>
                </div>
                <div id="{{ panelOverflowId }}" class="cwf-accordion__overflow">
                    <div id="{{ panelBodyId }}" class="cwf-accordion__body">
                        {{ panel.body }}
                    </div>
                </div>
            </div>
        {%- endfor -%}
    </div>
</div>
{
  "id": "example-accordion--group",
  "panels": [
    {
      "open": true,
      "title": "Panel title 1",
      "body": "<p>\n    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed imperdiet\n    risus suscipit sapien congue pretium. In hac habitasse platea dictumst.\n    Donec vel nisi quis dui aliquam porta. Sed nec dolor ullamcorper velit\n    suscipit elementum. Suspendisse et augue vitae tellus congue mollis.\n    Integer sem ex, rhoncus eget cursus non, rhoncus eget ipsum. Praesent\n    ullamcorper facilisis diam, sit amet commodo sem auctor egestas. <a href=\"https://www.vcu.edu/\">\n        Virginia Commonwealth University\n    </a> Vivamus vestibulum, ligula vitae molestie finibus, est lacus ornare\n    nisl, quis ultrices massa mi vitae augue. Pellentesque nulla lectus, placerat\n    id tellus ac, fringilla aliquam velit. Aliquam ullamcorper consectetur urna,\n    vitae maximus nunc malesuada eget. Vestibulum lobortis ut quam a congue.\n    Phasellus facilisis erat at feugiat faucibus. Curabitur cursus dolor in laoreet\n    sodales.\n</p>"
    },
    {
      "title": "Panel title 2",
      "body": "<p>\n    Nulla efficitur dui risus, et venenatis lacus commodo a. Nullam leo\n    odio, posuere vitae erat eu, iaculis vulputate magna. Donec posuere elit\n    eget lectus fermentum euismod. Lorem ipsum dolor sit amet, consectetur\n    adipiscing elit. Aliquam blandit augue ut nulla dictum, placerat\n    vehicula mi pulvinar. Maecenas in quam est. Integer nec ultricies purus,\n    malesuada accumsan neque. Maecenas mauris ligula, lobortis et congue\n    nec, condimentum eget eros. Vivamus pharetra rutrum tellus, sit amet\n    molestie lorem pellentesque et. Class aptent taciti sociosqu ad litora\n    torquent per conubia nostra, per inceptos himenaeos. Nullam pretium nibh\n    ac est dapibus porta eu sollicitudin odio. Nunc sed risus faucibus,\n    commodo eros iaculis, volutpat diam. Mauris mattis velit augue, id\n    tincidunt leo rhoncus non.\n</p>"
    },
    {
      "title": "Panel title 3",
      "body": "<p>\n    Proin ex enim, elementum vitae ipsum vel, congue molestie mauris.\n    Vestibulum consequat scelerisque commodo. In malesuada tincidunt\n    finibus. Donec efficitur, quam et interdum aliquet, nisl sapien sagittis\n    ex, nec luctus est risus eget mauris. Etiam porttitor, magna at\n    sollicitudin facilisis, metus ipsum commodo arcu, vitae tristique lorem\n    quam eget neque. Aliquam rhoncus bibendum est, vel accumsan leo molestie\n    efficitur. Sed nec pretium elit, a vestibulum magna.\n</p>"
    }
  ]
}
  • Content:
    // Accordion component styles
    
    @use "../../shared/animation";
    @use "../../shared/style";
    @use "../../shared/theme";
    
    // Selector prefix
    $prefix: "cwf" !default;
    
    // Accordion foreground (text) colors
    $color--light: style.color("gray-light") !default;
    $color--medium: style.color("gray-dark") !default;
    $color--dark: style.color("black") !default;
    
    // Accordion background colors
    $background-color--light: style.color("white-dark") !default;
    $background-color--medium: style.darken("white-dark", 5.25%) !default;
    $background-color--dark: style.lighten("white-darkest", 33%) !default;
    
    .#{$prefix}-accordion {
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        @include style.spacing;
    
        --cwf-accordion--foreground-light-color: #{$color--light};
        --cwf-accordion--foreground-medium-color: #{$color--medium};
        --cwf-accordion--foreground-dark-color: #{$color--dark};
        --cwf-accordion--background-light-color: #{$background-color--light};
        --cwf-accordion--background-medium-color: #{$background-color--medium};
        --cwf-accordion--background-dark-color: #{$background-color--dark};
    }
    
    .#{$prefix}-accordion__toggle {
        margin-bottom: 0.5rem;
        padding: 0;
        border: none;
        background-color: transparent;
        font-size: 1rem;
        text-decoration: underline;
        font-family: theme.font--sans-serif();
        color: var(--cwf-accordion--foreground-light-color);
        @include animation.transition(color);
    
        @include style.cursor;
    
        &:hover,
        &:focus {
            color: var(--cwf-accordion--foreground-medium-color);
        }
    }
    
    .#{$prefix}-accordion__wrapper {
        width: 100%;
        margin-bottom: -1px;
        overflow: hidden;
        border: 1px solid var(--cwf-accordion--background-dark-color);
    }
    
    .#{$prefix}-accordion__panel {
        &:not(:first-child) {
            border-top: 1px solid var(--cwf-accordion--background-dark-color);
        }
        &:last-child {
            margin-bottom: 0;
        }
    }
    
    .#{$prefix}-accordion__heading {
        display: flex;
        align-items: center;
        margin-bottom: -1px;
        padding: 0.75rem 1.5rem;
        border-bottom: 1px solid var(--cwf-accordion--background-dark-color);
        background-color: var(--cwf-accordion--background-light-color);
        font-family: theme.font--sans-serif();
        color: var(--cwf-accordion--foreground-medium-color);
        @include animation.transition(background-color, color);
    
        @include style.cursor;
    
        &:hover,
        &:focus {
            background-color: var(--cwf-accordion--background-medium-color);
            color: var(--cwf-accordion--foreground-dark-color);
        }
    }
    
    .#{$prefix}-accordion__heading[aria-expanded="false"] {
        color: var(--cwf-accordion--foreground-light-color);
    
        &:hover,
        &:focus {
            color: var(--cwf-accordion--foreground-medium-color);
        }
    }
    
    .#{$prefix}-accordion__chevron {
        min-width: 1rem;
        width: 1rem;
        margin-right: 1.5rem;
        @include animation.transition(color, transform);
    }
    
    .#{$prefix}-accordion__heading[aria-expanded="false"],
    .#{$prefix}-accordion__heading--closing {
        & .#{$prefix}-accordion__chevron {
            @include animation.flip;
        }
    }
    
    .#{$prefix}-accordion__title {
        font-size: 1rem;
        @include animation.transition(color);
    }
    
    .#{$prefix}-accordion__overflow {
        display: block;
        overflow: hidden;
        @include animation.transition(height);
    }
    
    .#{$prefix}-accordion__heading[aria-expanded="false"]
        ~ .#{$prefix}-accordion__overflow {
        display: none;
    }
    
    .#{$prefix}-accordion__body {
        padding: 1.5rem;
    
        @include style.children;
    }
    
  • URL: /components/raw/accordion/_index.scss
  • Filesystem Path: components/accordion/_index.scss
  • Size: 3.5 KB
  • Content:
    // The default component class
    import { Component } from '../../shared/component.js';
    
    // Check whether reduced motion is enabled locally or globally
    import { reducedMotion } from '../../shared/media/index.js';
    
    // Provide functionality to all accordions
    export class Accordion extends Component {
        constructor({
            prefix = 'cwf',
            accordion = 'accordion',
            toggle = 'accordion__toggle',
            panel = 'accordion__panel',
            heading = 'accordion__heading',
            closing = 'accordion__heading--closing',
            overflow = 'accordion__overflow',
            body = 'accordion__body'
        } = {}) {
            super({
                prefix,
                classes: {
                    accordion,
                    toggle,
                    panel,
                    heading,
                    closing,
                    overflow,
                    body
                },
                references: {}
            });
    
            // Initialize an object for the current event
            this.current = {};
    
            // Bind "this" to the necessary methods
            this.onClick = this.onClick.bind(this);
            this.onKeyDown = this.onKeyDown.bind(this);
            this.onHashChange = this.onHashChange.bind(this);
        }
    
        // Get references to all accordion elements
        getReferences() {
            // Attempt to grab all toggles,
            this.references.toggles = Array.from(
                document.querySelectorAll(this.selectors.toggle)
            );
            // ... accordions,...
            this.references.accordions = Array.from(
                document.querySelectorAll(this.selectors.accordion)
            );
            // .. and accordion panels from the page
            this.references.panels = [];
            this.references.accordions.forEach((accordion) => {
                const panels = Array.from(
                    accordion.querySelectorAll(this.selectors.panel)
                );
                panels.forEach((panel) => {
                    const heading = panel.querySelector(this.selectors.heading);
                    const overflow = panel.querySelector(this.selectors.overflow);
                    const body = panel.querySelector(this.selectors.body);
                    this.references.panels.push({
                        accordion,
                        panel,
                        heading,
                        overflow,
                        body
                    });
                });
            });
        }
    
        // Calculate an element's height (including padding, margin, and border)
        calcElHeight(element) {
            // Get the computed styles and top/bottom margins of the element...
            const styles = window.getComputedStyle(element),
                margins =
                    parseFloat(styles.marginTop) + parseFloat(styles.marginBottom);
            // ... and return their sum rounded up in pixels
            return Math.ceil(element.offsetHeight + margins) + 'px';
        }
    
        // Open an accordion panel
        openPanel({ heading, overflow, body }) {
            // Focus the heading of the panel if instructed to do so
            if (this.current.focus) heading.focus();
    
            // If the panel is already opened, do nothing else
            if (heading.getAttribute('aria-expanded') === 'true') return;
    
            // If the current type is "hash", or if reduced motion is enabled locally or globally,...
            if (this.current.type === 'hash' || reducedMotion())
                // ... simply open the panel with no animation
                return heading.setAttribute('aria-expanded', true);
    
            // Set the overflow's height to zero
            overflow.style.height = 0;
    
            // End the height transition
            function transitionEnd({ propertyName }) {
                // If the transition property isn't height, do nothing
                if (propertyName !== 'height') return;
    
                // Unbind from the overflow's transitionend event...
                overflow.removeEventListener('transitionend', transitionEnd);
                // ... and remove its height property
                overflow.style.removeProperty('height');
            }
    
            // Start the height transition
            function transitionStart() {
                // Bind to the overflow's transitionend event...
                overflow.addEventListener('transitionend', transitionEnd);
                // ... and set its height to that of the body's
                window.requestAnimationFrame(
                    () => (overflow.style.height = this.calcElHeight(body))
                );
            }
    
            // Set the heading's aria-expanded attribute to true...
            heading.setAttribute('aria-expanded', true);
            // ... and start the height transition
            window.requestAnimationFrame(transitionStart.bind(this));
        }
    
        // Close an accordion panel
        closePanel({ heading, overflow, body }) {
            // If the panel is already closed, do nothing else
            if (heading.getAttribute('aria-expanded') === 'false') return;
    
            // If the current type is "hash", or if reduced motion is enabled locally or globally,...
            if (this.current.type === 'hash' || reducedMotion())
                // ... simply close the panel with no animation
                return heading.setAttribute('aria-expanded', false);
    
            // Grab the closing class from the global selectors
            const { closing } = this.classes;
    
            // Set the overflow's height to that of the body's
            overflow.style.height = this.calcElHeight(body);
    
            // End the height transition
            function transitionEnd({ propertyName }) {
                // If the transition property isn't height, do nothing
                if (propertyName !== 'height') return;
    
                // Unbind from the overflow's transitionend event,...
                overflow.removeEventListener('transitionend', transitionEnd);
                // ... set the heading's aria-expanded to false,...
                heading.setAttribute('aria-expanded', false);
                // ... remove the closing class from the heading,...
                heading.classList.remove(closing);
                // ... and remove the overflow's height property
                overflow.style.removeProperty('height');
            }
    
            // Start the height transition
            function transitionStart() {
                // Add a class to the heading signifying the panel is closing,...
                heading.classList.add(closing);
                // ... bind to the overflow's transitionend event,...
                overflow.addEventListener('transitionend', transitionEnd);
                // ... and set its height to zero
                window.requestAnimationFrame(() => (overflow.style.height = 0));
            }
    
            // Start the height transition
            window.requestAnimationFrame(transitionStart.bind(this));
        }
    
        // Set the the toggle button's action (expand or collapse)
        setToggle(action) {
            // Attempt to get the toggle to update, either from the current state or accordion...
            const toggle =
                this.current.toggle ||
                this.references.toggles.find(
                    (toggle) =>
                        toggle.getAttribute('aria-controls') ===
                        this.current.accordion.id
                );
            // ... and if it wasn't found, do nothing else
            if (!toggle) return;
    
            // Finally, set the toggle's text...
            toggle.textContent = action ? 'Expand All' : 'Collapse All';
            // ... and action data attribute
            toggle.setAttribute('data-action', action);
        }
    
        // Toggle the panels
        togglePanels() {
            // For each panel of the current accordion,...
            this.current.panels.forEach(({ panel, heading, overflow, body }) => {
                // Ensure the heading does not have a closing class...
                heading.classList.remove(this.classes.closing);
                // ... and the overflow does not have a height set,...
                overflow.style.removeProperty('height');
                // ... and store all relevant panel elements
                const target = { heading, overflow, body };
    
                // If the type of action was not from a toggle and the panel does not equal the current panel, close it
                if (this.current.type !== 'toggle' && panel !== this.current.panel)
                    return this.closePanel(target);
    
                // If the current action is to open, open the panel
                if (this.current.action) return this.openPanel(target);
    
                // Otherwise, close the panel
                return this.closePanel(target);
            });
    
            // Set the toggle (flip if the action was from a toggle, otherwise always open)
            this.setToggle(
                this.current.type === 'toggle' ? !this.current.action : true
            );
    
            // Finally, reset the current accordion
            this.current = {};
        }
    
        // Find an element by type
        findElementByType(type, element, target) {
            // Define ways of finding an element based on type...
            const instructions = {
                toggle: {
                    accordion: document.getElementById(
                        target.getAttribute('aria-controls')
                    )
                },
                heading: {
                    accordion: target.parentNode.parentNode.parentNode,
                    panel: target.parentNode
                },
                hash: {
                    accordion: target.parentNode.parentNode,
                    panel: target
                }
            };
            // ... and return the proper element
            return instructions[type][element];
        }
    
        // Set action by type and target
        setActionByTypeAndTarget(type, target) {
            // If the type is "toggle", return the target's action dataset attribute...
            if (type === 'toggle') return target.dataset.action === 'true';
            // ... otherwise, return the opposite of the target's aria-expanded attribute
            return target.getAttribute('aria-expanded') !== 'true';
        }
    
        // Set the current accordion
        setCurrentAccordion(type, target, focus = false) {
            // Grab all necessary references and data...
            const accordion = this.findElementByType(type, 'accordion', target),
                panel =
                    type !== 'toggle'
                        ? this.findElementByType(type, 'panel', target)
                        : undefined,
                toggle = type === 'toggle' ? target : undefined,
                panels = this.references.panels.filter(
                    (panel) => panel.accordion === accordion
                ),
                action =
                    type !== 'hash'
                        ? this.setActionByTypeAndTarget(type, target)
                        : true;
            // ... and set the current accordion
            this.current = {
                type,
                accordion,
                panel,
                panels,
                toggle,
                action,
                focus
            };
        }
    
        // Handle click events
        onClick(event) {
            // Grab the current target of the event
            const { currentTarget } = event;
    
            // If the current target is a toggle, find its associated accordion accordingly...
            if (currentTarget.classList.contains(this.classes.toggle))
                this.setCurrentAccordion('toggle', currentTarget);
            // ... and if the current target is a panel heading, find its associated accordion accordingly
            if (currentTarget.classList.contains(this.classes.heading))
                this.setCurrentAccordion('heading', currentTarget);
    
            // Finally, prevent the default behavior...
            event.preventDefault();
            // ... and toggle the panels
            return this.togglePanels(event);
        }
    
        // Focus a panel heading
        focusHeading({ key, currentTarget }) {
            // Grab the first/last panel as well as the max index
            const firstPanel = this.current.panels[0],
                maxIndex = this.current.panels.length - 1,
                lastPanel = this.current.panels[maxIndex];
    
            // If the "Home" key was pressed, focus the first panel heading...
            if (key === 'Home') return firstPanel.heading.focus();
            // ... and if the "End" key was pressed, focus the last panel heading
            if (key === 'End') return lastPanel.heading.focus();
    
            // Get the currently focused panel and its index
            const currentPanel = this.current.panels.find(
                    (panel) => panel.heading === currentTarget
                ),
                currentIndex = this.current.panels.indexOf(currentPanel);
    
            // If the up arrow key was pressed,...
            if (key === 'ArrowUp') {
                // ... focus the last panel heading if the first panel heading is focused,...
                if (!currentIndex) return lastPanel.heading.focus();
                // ... otherwise focus the previous panel's heading
                return this.current.panels[currentIndex - 1].heading.focus();
            }
    
            // If the down arrow key was pressed,...
            if (key === 'ArrowDown') {
                // ... focus the first panel heading if the last panel is focused,...
                if (currentIndex === maxIndex) return firstPanel.heading.focus();
                // ... otherwise focus the next panel's heading
                return this.current.panels[currentIndex + 1].heading.focus();
            }
        }
    
        // Handle keydown events
        onKeyDown(event) {
            // Grab the target and key of the event and create a list of valid keys
            const { key, currentTarget } = event;
    
            // If a valid key has not been pressed, do nothing else
            if (
                !['Enter', ' ', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key)
            )
                return;
    
            // Otherwise, find the heading's associated accordion,...
            this.setCurrentAccordion('heading', currentTarget);
            // ... prevent the default behavior
            event.preventDefault();
    
            switch (key) {
                // When the "Enter" or space key is pressed...
                case 'Enter':
                case ' ':
                    // ... toggle the panels
                    return this.togglePanels(event);
                // When the up or down arrow keys are pressed...
                case 'ArrowUp':
                case 'ArrowDown':
                    // ... focus a panel heading relative to the current target
                    return this.focusHeading({ key, currentTarget });
                // When the "Home" or "End" keys are pressed...
                case 'Home':
                case 'End':
                    // ... focus their respective panel heading
                    return this.focusHeading({ key });
            }
        }
    
        // Handle hash change events
        onHashChange(event) {
            // Grab the location hash...
            const { hash } = window.location,
                id = hash.substr(1),
                // ... and see if there's a valid target with that ID
                target = this.references.panels.find(
                    (element) => element.panel.id === id
                );
    
            // If no target was matched, do nothing else
            if (!target) return;
    
            // Set the current accordion...
            this.setCurrentAccordion('hash', target.panel, true);
            // ... and toggle the panels
            return this.togglePanels(event);
        }
    
        // Mount/unmount accordion functionality
        mount(run) {
            super.mount(run);
    
            // If mounting, get/store the references
            if (run) this.getReferences();
    
            // If no accordions were found on the page,...
            if (!this.references.accordions.length) {
                // ... and unmounting,...
                if (!run) {
                    // ... delete the accordions references,...
                    delete this.references.accordions;
                    // ... otherwise, do nothing else
                } else return;
            }
    
            // If accordion toggles were found on the page,...
            if (this.references.toggles.length) {
                // ... listen to their click events
                this.references.toggles.forEach((toggle) =>
                    toggle[this.listener]('click', this.onClick)
                );
            }
    
            // If unmounting, delete all accordion toggle references
            if (!run) delete this.references.toggles;
    
            // If accordion panels were found on the page,...
            if (this.references.panels.length) {
                // .. for each panel heading,...
                this.references.panels.forEach(({ heading }) => {
                    // ... bind to its click/keydown events
                    heading[this.listener]('click', this.onClick);
                    heading[this.listener]('keydown', this.onKeyDown);
                });
            }
    
            // If unmounting, delete all accordion panel references
            if (!run) delete this.references.panels;
    
            // Handle location hash panel toggling on page load (if mounting)...
            if (run) this.onHashChange();
            // ... and on every hash change after that
            window[this.listener]('hashchange', this.onHashChange);
        }
    }
    
  • URL: /components/raw/accordion/index.js
  • Filesystem Path: components/accordion/index.js
  • Size: 16.7 KB

Accordion

The accordion component is a group of collapsable panels that hide or reveal their content when its header is clicked. When an accordion panel heading is clicked, all other accordion panels that are expanded will collapse. The accordion can be used with a single collapsible panel or multiple. When more than one collapsible panel is used within an accordion, a toggle button is added, allowing users to expand/collapse all accordion panels at once.

Markup

Whether you are using an accordion with a single panel or multiple, ensure that all panels are wrapped in a div.cwf-accordion__wrapper element. Each individual panel will have a div.cwf-accordion__panel element.

If you are using this component with a group of accordions ensure you are rendering the expand/collapse all toggle button with a class of .cwf-accordion__toggle. This is automatically done with the core Twig macro and CMS implementation.

Javascript

Each accordion panel heading is listening for click events or keydown events when focused.

The following events will trigger the panel to expand or collapse:

  • A click
  • When focused, pressing the Space key
  • When focused, pressing the Enter key

The following keydown events will change panel heading focus:

  • Up arrow - Moves focus to the previous panel’s heading, unless on the first panel which will focus the last panel’s heading
  • Down arrow - Moves focus to the next panel’s heading, unless on the last panel which will focus the first panel’s heading
  • Home - Focuses the first panel’s heading
  • End - Focuses the last panel’s heading

T4 implementation

Accordions are implemented in T4 as the “Accordion Panel” plugin, meaning its classes are .plugin- prefixed instead of .cwf- prefixed.

The plugin outputs a single panel, and using 1 or more sequential plugins group them together as an entire accordion. The expand/collapse all toggle button is automatically included when more than one panel is used within the accordion group.

Area keywords

In the “Name” field of the “Accordion Panel” plugin, the following case-insensitive keywords can be used to change the area in which the accordion panel appears:

  • Feature - Moves the accordion panel to the feature area of the page (below the main navigation, above the sidebars and main content).
  • Sidebar - Moves the accordion panel to the sidebar area of the page (right of the main content).
  • Footer - Moves the accordion panel to the footer area of the page (above the footer, below the sidebars and main content).

This plugin can also be used within the global “Site-Feature”, “Site-Sidebar”, and “Site-Footer” sections to have it displayed globally within the feature, sidebar, and footer areas respectively.

Injectors

In the “Injectors”* or “Name” field of the “Accordion Panel” plugin, the following injectors can be used:

  • id:{custom_id} - Overrides the default, T4 ID of the accordion panel with a custom ID.
  • class:{custom_classes} - Adds custom classes to the accordion panel.
  • style:{custom_styles} - Adds custom styles to a style attribute of the accordion panel.
  • before:{custom_html} - Adds custom HTML before the accordion panel.
  • after:{custom_html} - Adds custom HTML after the accordion panel.
  • area:{feature|sidebar|footer}* - Moves the card to the feature (below the main navigation), sidebar (right of the main column), or footer area (above the footer).

* These features are only supported on T41.