Notes:

  • The accordion uses progressive enhancement as an approach to build the interactivity of the component.
  • This means that the accordion starts out as a non-interactive component, and then interactivity and appropriate styles are added to it when the JavaScript runs.
  • By default, an accordion is a series of sections, each with a title and some content after it.
  • The heading level I used is H3, assuming the heading level of the slice is H2. Both these heading levels need to be adjusted in the aria-level attribute if needed (for example, if the slice is part of a page where the title of the slice is an H3, not an H2. In that case the accordion headings should go down one level as well.)
  • When the JavaScript runs:
    • Each heading becomes interactive.
    • Clicking on a heading opens the panel that contains the content corresponding to that heading.
    • The interactivity of the heading comes from the fact that the heading becomes a button.
    • Or more precisely: the JS creates a button and uses the heading’s content as content for that button, and then appends that button to the heading itself.
    • An accordion is a set of disclosure widgets: a series of buttons that open and close a series of corresponding panels.
    • Each panel is hidden by default, both from view and from screen readers.
    • When a button is clicked, the corresponding panel is shown.
    • The ARIA attributes on the buttons and their panels tell the screen reader user what the current state is.
    • Each button gets an aria-expanded attribute. This attribute tells the SR that the panel controlled by this button, and which must come after the button in the DOM, is either expanded or collapsed.
    • When a button is clicked/pressed and its panel is open, aria-expanded is set to true. It is set to false when the panel is collapsed.
    • Each panel has aria-hidden set to either true (when the panel is collapsed) or false (when the panel is open).
    • This is mainly enough for the a11y of the accordion.
    • Additionally, each button also has an ID and an aria-controls attribute telling the SR what panel it controls. And each panel has an ID referenced in its button’s aria-controls, and an aria-labelledby attribute referencing the ID of the button, allowing the SR to know that this panel is labelled by the content of that button.
    • Note that aria-controls does not really have any browser support and can therefore be omitted altogether. I just have it included in the script anyway.

Requirements:

  • In order for the script to enhance the series of sections to an accordion, the sections need to be wrapped in a container with a data-accordion attribute set on it.
  • The each heading in each section has data-accordion-heading,
  • and each “panel” is a container that wraps the section’s content and has a data-accordion-panel attribute.
  • The script comes with a few customization options:
    • You can have all panels be open or collapsed by default. Specify that in the allCollapsed property (true or false);
    • You can allow more than one panel to be open at a time. Specify that in the showOneAnswerAtATime attribute (true or false);
    • You can allow the script to add “controls” to the accordion, which are basically two buttons: “Collapse All” and “Expand All”, which do as their names suggest. The withControls property is used to enable or disable that (true or false).
    • Note that since the design does not have the controls as a possible option, the buttons will be added without any styles.
<section class="ps ps-accordion ps-accordion--faq">
    <div class="ps__wrap">
        <div class="ps__head">
            <header class="ps__header">
                <span class="ps__kicker">FAQ</span>
                <h2 class="ps__title" aria-level="">Answers to common questions</h2>
            </header>
            <div class="ps__desc">
                <p>
                    Learn about Prismic by reading questions and answers. It’s almost like talking to a human – and maybe even better.
                </p>
            </div>
        </div>

        <div class="ps__main grid grid--12">
            <div class="span-1-12">
                <div data-accordion class="c-accordion">
                    <h3 data-accordion-heading class="c-accordion__heading" aria-level="">
                        Test tracking
                    </h3>
                    <div data-accordion-panel class="c-accordion__panel">
                        Only use them when the content is appropriate. If the panels are too long, the user will end up scrolling too much (esp. on mobile), which negates the whole purpose of using an accordion. For very long panels, using links to separate pages is preferred.
                        <p>
                            If you use accordions to replace page steps such as checkout form steps, make sure you stop the default browser BACK button behavior so that the BACK button would move up a step in the accordion, because this is the behavior that most users expect, as
                            tests show.
                        </p>
                    </div>

                    <h3 data-accordion-heading class="c-accordion__heading" aria-level="">
                        What's a headless CMS?
                    </h3>
                    <div data-accordion-panel class="c-accordion__panel">
                        <a href="#">LINK</a> The default markup is enhanced using JavaScript so that the buttons are added to the headings. The buttons should NOT be in the headings by default. The markup should always consider what the component or content
                        would look like if no Javascript is enabled and therefore the accordion is not functional. The content is most likely going to be all visible — either as a series of panels with headings or a definition list, depending on the type
                        of content.
                    </div>

                    <h3 data-accordion-heading class="c-accordion__heading" aria-level="">
                        What's special or different about Prismic?
                    </h3>
                    <div data-accordion-panel class="c-accordion__panel">
                        The buttons need to specify which panels they control using <code>aria-controls</code>, and they need to reflect the state of the panel (collapsed/expanded) using <code>aria-expanded="true/false"</code>. The panel itself also should
                        have <code>aria-hidden</code> set to true or false if it is hidden or shown, respectively.
                        <p>
                            The accordion needs to be able to trap keyboard focus when the user is IN the accordion and uses the arrow keys to traverse the items in it. Tabbing out of the accordion must be enabled.
                        </p>
                    </div>

                    <h3 data-accordion-heading class="c-accordion__heading" aria-level="">
                        How is Prismic different from other CMSs?
                    </h3>
                    <div data-accordion-panel class="c-accordion__panel">
                        You can allow one or more panels to be always collapsed or expanded. You can also set a panel to be open by default (e.g. the first one). All this functionality can be tied to the markup using data attributes as flags.
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>
  • Content:
    /*********************************************************\
     * Accordion component-specific styles.
     *********************************************************/
    
    .c-accordion__heading {
      font-size: calc(1rem * var(--text-min-l));
    
      @media screen and (min-width: 40rem) {
        font-size: calc(calc(1rem * var(--text-min-l)) + (var(--text-max-l) - var(--text-min-l)) * (100vw - 40rem) / (80 - 40));
      }
    
      @media screen and (min-width: 80rem) {
        font-size: calc(1rem * var(--text-max-l));
      }
    
      .accordion-js & {
        margin: 0;
      }
    
      >button {
        display: block;
        font: inherit;
        font-size: inherit;
        font-weight: 500;
        width: 100%;
        height: 100%;
        background-color: var(--color--secondary);
        text-align: left;
        line-height: 1.2;
        padding: var(--c-padding);
        padding-right: 80px;
        position: relative;
        border: 1px solid transparent;
        transition: outline 0.1s linear;
        border-radius: 8px;
        margin-bottom: var(--c-padding);
    
        &:focus {
    
          outline: var(--focus-outline);
          z-index: 1; // to ensure the outline isn't cut off where it overlaps with the next item below
        }
    
        &:focus:not(:focus-visible) {
          outline: none;
        }
    
        &.focus:not(.focus-visible) {
          outline: none;
        }
    
    
        &[aria-expanded="true"] {
          margin-bottom: 0;
          border-radius: 8px 8px 0 0;
        }
      }
    }
    
    
    .c-accordion__panel {
      margin-bottom: 4rem; // no-js fallback
    
      .accordion-js & {
        margin-bottom: 0;
        padding: calc(var(--c-padding) / 3) var(--c-padding) var(--c-padding);
        padding-right: 4rem;
        background-color: var(--color--secondary);
    
        &[aria-hidden="true"] {
          margin-bottom: 0;
        }
    
        &[aria-hidden="false"] {
          border-radius: 0 0 8px 8px;
          margin-bottom: var(--c-padding);
        }
      }
    }
    
    /* Styles for the accordion icon */
    .c-accordion .accordion-icon {
      display: block !important; // to override aria-hidden
      position: absolute;
      width: 0.75rem;
      height: 0.5rem;
      top: 50%;
      right: 1em;
      transform: translateY(-50%);
      transform-origin: 50% 50%;
      transition: all 0.1s linear;
    }
    
    .c-accordion [aria-expanded="true"] .accordion-icon {
      -ms-transform: translateY(-50%) rotate(180deg);
      transform: translateY(-50%) rotate(180deg);
    }
    
    .c-accordion [aria-hidden="true"] {
      display: none;
    }
    
    .c-accordion [aria-hidden="false"] {
      display: block !important;
    }
  • URL: /components/raw/accordion/_c-accordion.scss
  • Filesystem Path: src/components/accordion/_c-accordion.scss
  • Size: 2.4 KB
  • Content:
    
    "use strict";
    if (typeof Object.assign != "function") {
      // Must be writable: true, enumerable: false, configurable: true
      Object.defineProperty(Object, "assign", {
        value: function assign(target, varArgs) {
          // .length of function is 2
    
          if (target == null) {
            // TypeError if undefined or null
            throw new TypeError(
              "Cannot convert undefined or null to object"
            );
          }
    
          var to = Object(target);
    
          for (var index = 1; index < arguments.length; index++) {
            var nextSource = arguments[index];
    
            if (nextSource != null) {
              // Skip over if undefined or null
              for (var nextKey in nextSource) {
                // Avoid bugs when hasOwnProperty is shadowed
                if (
                  Object.prototype.hasOwnProperty.call(
                    nextSource,
                    nextKey
                  )
                ) {
                  to[nextKey] = nextSource[nextKey];
                }
              }
            }
          }
          return to;
        },
        writable: true,
        configurable: true
      });
    }
    // add utilities
    var util = {
      keyCodes: {
        UP: 38,
        DOWN: 40,
        LEFT: 37,
        RIGHT: 39,
        HOME: 36,
        END: 35,
        ENTER: 13,
        SPACE: 32,
        DELETE: 46,
        TAB: 9
      },
    
      generateID: function (base) {
        return base + Math.floor(Math.random() * 999);
      },
    
      getDirectChildren: function (elm, selector) {
        return Array.prototype.filter.call(elm.children, function (child) {
          return child.matches(selector);
        });
      }
    };
    
    
    
    
    (function (w, doc, undefined) {
    
      var ARIAaccOptions = {
        showOneAnswerAtATime: true,
        allCollapsed: true,
        withControls: true,
    
        // the following needs to be an SVG icon
        // we will be dynamically inserting this icon into the buttons in this script
        // make sure you add the class `accordion-icon` to it
        icon:
          '<svg class="accordion-icon" width="12" height="8" aria-hidden="true" focusable="false" viewBox="0 0 12 8"><g fill="none"><path fill="#000" d="M1.41.59l4.59 4.58 4.59-4.58 1.41 1.41-6 6-6-6z"/><path d="M-6-8h24v24h-24z"/></g></svg>'
      }
      /**
       * ARIA Accordion
       * Creates a tab list to toggle the visibility of
       * different subsections of a document.
       *
       * Author: Sara Soueidan
       * Version: 0.2.0
       */
    
      var ARIAaccordion = function (inst, options) {
        var _options = Object.assign(ARIAaccOptions, options);
        var el = inst;
        var accordionHeadings = el.querySelectorAll("[data-accordion-heading]");
        var accordionPanels = el.querySelectorAll("[data-accordion-panel]");
        var controlsWrapper;
        var expandButton;
        var collapseButton;
        var accID = util.generateID('c-accordion-');
    
        var init = function () {
          // if you have any functionality in CSS that needs JS to be activated
          // this class added to the accordion container works as a JS hook to announce JS is enabled
          // in the CSS, we have a part that adds borders and paddings to the headings, buttons and panels
          // these borders and padding are only needed if the content turns into an accordion
          el.classList.add("accordion-js");
    
          setupAccordionHeadings(accordionHeadings);
          setupAccordionPanels(accordionPanels);
    
          if (_options.withControls) {
            createControls();
          }
        }
    
        var createControls = function () {
          controlsWrapper = document.createElement("div");
          controlsWrapper.setAttribute("data-accordion-controls", "");
          el.prepend(controlsWrapper);
    
          expandButton = document.createElement("button");
          expandButton.setAttribute("data-accordion-control", "expand");
          expandButton.setAttribute("aria-label", "Expand all panels");
          expandButton.innerText = "Expand All";
          controlsWrapper.appendChild(expandButton);
    
          collapseButton = document.createElement("button");
          collapseButton.setAttribute("data-accordion-control", "collapse");
          collapseButton.setAttribute("aria-label", "Collapse all panels");
          collapseButton.innerText = "collapse All";
          controlsWrapper.appendChild(collapseButton);
    
          // if we start out with an accordion whose panels are collapsed (as opposed to open)
          // disable the Collapse All button
          if (_options.allCollapsed) disableCollapseButton();
    
          setupAccordionControls();
        }
    
        var setupAccordionControls = function () {
          expandButton.addEventListener("click", function () {
            // expand them all
    
            Array.from(accordionHeadings).forEach(function (item, index) {
              item.querySelector("button").setAttribute("aria-expanded", "true");
            });
    
            Array.from(accordionPanels).forEach(function (item, index) {
              item.setAttribute("aria-hidden", "false");
            });
    
            disableExpandButton();
            enableCollapseButton();
          });
    
          collapseButton.addEventListener("click", function () {
            Array.from(accordionHeadings).forEach(function (item, index) {
              item.querySelector("button").setAttribute("aria-expanded", "false");
            });
    
            Array.from(accordionPanels).forEach(function (item, index) {
              item.setAttribute("aria-hidden", "true");
            });
    
            disableCollapseButton();
            enableExpandButton();
          });
        }
    
        var setupAccordionHeadings = function (accordionHeadings) {
          Array.from(accordionHeadings).forEach(function (item, index) {
            var $this = item;
    
    
            let text = $this.innerText;
            let headingButton = document.createElement("button");
            headingButton.setAttribute("aria-expanded", "false");
            headingButton.setAttribute("data-accordion-toggle", "");
            headingButton.setAttribute("id", accID + '__heading-' + index);
            headingButton.setAttribute("aria-controls", accID + '__panel-' + index);
            headingButton.innerText = text;
    
            $this.innerHTML = "";
    
            $this.appendChild(headingButton);
            headingButton.innerHTML += _options.icon;
    
            headingButton.addEventListener("click", function (e) {
              togglePanel(headingButton);
            });
          });
        }
    
        var setupAccordionPanels = function (accordionPanels) {
          Array.from(accordionPanels).forEach(function (item, index) {
            let $this = item;
    
            $this.setAttribute("id", accID + '__panel-' + index);
            $this.setAttribute("aria-labelledby", accID + '__heading-' + index);
            $this.setAttribute("aria-hidden", "true");
          });
        }
    
        var togglePanel = function (toggleButton) {
          var thepanel = toggleButton.parentNode.nextElementSibling;
    
          if (toggleButton.getAttribute("aria-expanded") == "true") {
            toggleButton.setAttribute("aria-expanded", "false");
            thepanel.setAttribute("aria-hidden", "true");
    
            checkToggleCollapseButtonState();
            checkToggleExpandButtonState();
          } else {
            if (_options.showOneAnswerAtATime) {
              // Hide all answers
              Array.from(accordionPanels).forEach((panel) => {
                panel.setAttribute("aria-hidden", "true");
              });
    
              Array.from(accordionHeadings).forEach((heading) => {
                heading.querySelector("button")
                  .setAttribute("aria-expanded", "false");
              })
    
    
              checkToggleCollapseButtonState();
              checkToggleExpandButtonState();
            }
    
            // Show answer
            toggleButton.setAttribute("aria-expanded", "true");
            thepanel.setAttribute("aria-hidden", "false");
    
            checkToggleCollapseButtonState();
            checkToggleExpandButtonState();
          }
        }
    
        var enableCollapseButton = function () {
          if (collapseButton) collapseButton.removeAttribute("disabled");
        }
    
        var disableCollapseButton = function () {
          if (collapseButton) collapseButton.setAttribute("disabled", "disabled");
        }
    
        var enableExpandButton = function () {
          if (expandButton) expandButton.removeAttribute("disabled");
        }
    
        var disableExpandButton = function () {
          if (expandButton) expandButton.setAttribute("disabled", "disabled");
        }
    
        var checkToggleExpandButtonState = function () {
          var closedPanels = el.querySelectorAll(
            'button[aria-expanded="false"]'
          );
    
          if (!closedPanels.length) {
            disableExpandButton();
          } else {
            enableExpandButton();
          }
        }
    
        var checkToggleCollapseButtonState = function () {
          var openPanels = el.querySelectorAll(
            'button[aria-expanded="true"]'
          );
    
          if (openPanels.length === 0) {
            disableCollapseButton();
          } else {
            enableCollapseButton();
          }
        }
    
        init.call(this);
        return this;
      }; // ARIAaccordion()
    
      w.ARIAaccordion = ARIAaccordion;
    })(window, document);
    
    
    var accInstance = "[data-accordion]";
    var els = document.querySelectorAll(accInstance);
    var allAccs = [];
    
    // Generate all accordion instances
    for (var i = 0; i < els.length; i++) {
      // var nAccs = new ARIAaccordion(els[i]);
      var nAccs = new ARIAaccordion(els[i], { withControls: false });
    
      allAccs.push(nAccs);
    }
    
  • URL: /components/raw/accordion/accordion.js
  • Filesystem Path: src/components/accordion/accordion.js
  • Size: 9 KB
  • Content:
    // import the accordion component styles (the actual accordion element)
    @import "_c-accordion.scss";
    
    .ps-accordion__img {
      display: block;
      margin: 0 auto calc(var(--c-padding) * 2);
      background: var(--color--secondary);
    }
  • URL: /components/raw/accordion/accordion.scss
  • Filesystem Path: src/components/accordion/accordion.scss
  • Size: 226 Bytes
  • Content:
    title: Accordion
    # notes: "You can have a note here; it will override the README.md notes"
    context:
      text: 
    
    
    
  • URL: /components/raw/accordion/accordion.yml
  • Filesystem Path: src/components/accordion/accordion.yml
  • Size: 111 Bytes