diff --git a/src/assets/js/accordion.js b/src/assets/js/accordion.js index 46dd17ab1e..2d71438bf1 100644 --- a/src/assets/js/accordion.js +++ b/src/assets/js/accordion.js @@ -1,165 +1,126 @@ -(function () { - function Accordion(element) { - let thisAccordion = this; - - // Get the accordions, and if the accordion group is multiselectable. - this.accordions = element.getElementsByClassName("accordion__heading"); - this.multiSelectible = element.getAttribute('aria-multiselectable') === 'true' || false; - - // For each of the accordions... - for (let i = 0; i < this.accordions.length; i++) { - - // Get the accordion item's components. - let itemComponents = this.accordionItemComponents(this.accordions[i]); - - // Check if the accordion is currently expanded at moment of click. - let expanded = this.isAccordionOpen(itemComponents.btn); - - // If it is, un-hide its corresponding panel. - itemComponents.panel.hidden = !expanded; - - // When the accordion's button is clicked... - itemComponents.btn.onclick = () => { - - // Toggle the corresponding accordion. - this.toggleAccordion(this.accordions[i]); - } - } - - // Add a listener that listens for when the URL is changed. - window.addEventListener('popstate', function (event) { - - // Activate an accordion based upon the hash parameters in the URL. - thisAccordion.activateAccordionByHash(); +/** + * A class for controlling accordion behavior. + */ +class Accordion { + constructor(element) { + + // Loop through each accordion item and add a listener for when the accordion is toggled. + const accordionItems = element.querySelectorAll('details'); + Array.prototype.forEach.call(accordionItems, (item) => { + // Add a listener for when the details element is toggled. + item.addEventListener('toggle', (event) => { + this.toggleAccordionItem(item, event.newState === 'open'); + }); + + // Add a listener for when the summary element is clicked. + const summary = item.querySelector('summary'); + summary.addEventListener('click', (event) => { + this.accordionItemClick(item); + }); }); - - // Activate any accordion that is defined in the hash parameter if there is one. - this.activateAccordionByHash(); } - // Gets the item components for 'accordion'. - // Returns an object that contains 'btn' and 'panel' elements. - Accordion.prototype.accordionItemComponents = function (accordion) { - let btn = accordion.querySelector('button'); - let panel = accordion.nextElementSibling; - - return { - 'btn': btn, - 'panel': panel - } - } - - // Define whether 'accordion' is open with 'isOpen'. - Accordion.prototype.accordionOpen = function (accordion, isOpen) { - // Get the accordion item's components. - let itemComponents = this.accordionItemComponents(accordion); - - // Set the relevant attributes for 'accordion' based on 'isOpen'. - itemComponents.btn.setAttribute('aria-expanded', isOpen); - itemComponents.btn.setAttribute('aria-selected', isOpen); - itemComponents.panel.hidden = !isOpen; + /** + * Handles the click event for an accordion summary element. + * + * @param accordionItem + * The details element of an accordion item. + */ + accordionItemClick(accordionItem) { + + // Adds a bespoke data attribute to the accordion item + // so that we can determine if it was clicked. This is + // necessary because the 'click' event if fired before + // the 'toggle' event and we don't know if the accordion + // item is being opened or closed yet. + accordionItem.setAttribute('data-accordion-clicked', true); } - // Activate an 'accordion'. - Accordion.prototype.activateAccordion = function (accordion) { - - // Checks if multiple accordions can be open at once. If not, closes other accordions. - if (!this.multiSelectible) { - this.collapseAllAccordions(); + /** + * Handles the toggle event of an accordion details element. + * + * @param accordionItem + * The details element of an accordion item. + * @param isOpening + * A boolean value indicating the new toggle state. + */ + toggleAccordionItem(accordionItem, isOpening) { + + // Set the relevant attributes for 'accordion' based on 'open'. + accordionItem.setAttribute('aria-expanded', isOpening); + accordionItem.setAttribute('aria-selected', isOpening); + + // Check if the accordion was clicked. + const clicked = accordionItem.getAttribute('data-accordion-clicked'); + if (clicked) { + accordionItem.removeAttribute('data-accordion-clicked'); + + // If the accordion is not open (but will be)... + if (isOpening) { + // Define historyString here to be used later. + const historyString = '#' + accordionItem.id; + + // Change window location to add URL params + if (window.history && history.pushState && historyString !== '#') { + // NOTE: doesn't take into account existing params + history.replaceState("", "", historyString); + } + } + // Else if the accordion is closed... + else { + // Empty the history string. + history.replaceState("", "", null); + } } - - // Open the accordion. - this.accordionOpen(accordion, true); } - // Activate any accordion that is defined in the hash parameter if there is one. - Accordion.prototype.activateAccordionByHash = function () { + /** + * Opens an accordion based on the hash in the URL. + */ + static focusAccordionItemByHash() { // Get the hash parameter. - let hash = window.location.hash.substr(1); + const hash = window.location.hash.substr(1); // If the hash parameter is not empty... if (hash !== '') { - // Get the accordion to focus. - let accordionToFocus = document.getElementById(hash); + const hashedAccordionItem = document.getElementById(hash); // If the defined hash parameter finds an element... - if (accordionToFocus !== null) { - - // Get the accordion wrapper of the hash parameter and this accordion wrapper to compare later. - let accordionToFocusAccordionWrapper = accordionToFocus.parentElement - let accordionWrapper = this.accordions[0].parentElement; - - // If the accordion wrapper defined by the hash and this accordion wrapper are the same... - if (accordionToFocusAccordionWrapper === accordionWrapper) { - - // Activate the accordion defined in the hash parameters. - this.activateAccordion(accordionToFocus); + if (hashedAccordionItem !== null) { + // If the summary element is present... + const summary = hashedAccordionItem.querySelector('summary'); + if (summary) { + // Trigger click event for summary. + summary.click(); } } } } +} + +/** + * Initializes the accordion on each of the specified selectors. + * + * @param selector + */ +function applyAccordion(selector) { + const items = document.querySelectorAll(selector); + + Array.prototype.forEach.call(items, (item) => { + new Accordion(item); + }); + + // Add a listener that listens for when the URL is changed. + window.addEventListener('popstate', (event) => { + // Activate an accordion based upon the hash parameters in the URL. + Accordion.focusAccordionItemByHash(); + }); - // Collapse all accordions in this accordion group. - Accordion.prototype.collapseAllAccordions = function () { - - // For each accordion... - for (let i = 0; i < this.accordions.length; i++) { - - // Close it. - this.accordionOpen(this.accordions[i], false); - } - } - - // Check if an accordion is open by inspecting the aria attribute of the 'btn' controlling it. - // Returns a boolean. - Accordion.prototype.isAccordionOpen = function (btn) { - return btn.getAttribute('aria-expanded') === 'true' || false; - } - - // Toggle a specific 'accordion' open or closed. - Accordion.prototype.toggleAccordion = function (accordion) { - // Get the accordion's button element. - let btn = accordion.querySelector('button'); - - // Check if the accordion is currently expanded at moment of click. - let expanded = this.isAccordionOpen(btn); - - // Checks if multiple accordions can be open at once. If not, closes other accordions. - if (!this.multiSelectible && !expanded) { - this.collapseAllAccordions(); - } - - // Toggle the accordion. - this.accordionOpen(accordion, !expanded) - - // If the accordion is not open (but will be)... - if (!expanded) { - - // Define historyString here to be used later. - let historyString = '#' + btn.parentElement.id; - - // Change window location to add URL params - if (window.history && history.pushState && historyString !== '#') { - // NOTE: doesn't take into account existing params - history.replaceState("", "", historyString); - } - } - - // Else if the accordion is closed... - else { - - // Empty the history string. - history.replaceState("", "", " "); - } - } - - // Instantiate accordions on the page. - const accordions = document.getElementsByClassName("accordion"); + // Activate any accordion that is defined in the hash parameter if there is one. + Accordion.focusAccordionItemByHash(); +} - for (let i = 0; i < accordions.length; i++) { - let accordion = new Accordion(accordions[i]); - } -}()); +export { applyAccordion } diff --git a/src/components/accordion/Accordion.stories.js b/src/components/accordion/Accordion.stories.js index 2dcde48897..ba6a7de8f3 100644 --- a/src/components/accordion/Accordion.stories.js +++ b/src/components/accordion/Accordion.stories.js @@ -8,12 +8,6 @@ export default { control: 'boolean', name: 'Multi select' }, - items: { - control: false, - table: { - disable: true, - }, - }, }, }; @@ -27,15 +21,17 @@ const Template = (args) => ({ `, }); +let items = [ + { title: 'Brand Bar', content: '
The brand bar must appear at the top of all core websites to create immediate association with the university.
' }, + { title: 'Brand Footer', content: 'A consistent footer across all web experiences reinforces the connection with Iowa and helps users find important details such as contact information and essential hyperlinks. Footers must appear across all pages of a website.
' }, + { title: 'Logo', content: 'Block IOWA logo tab in black, hyperlinked to uiowa.edu. Use the ALT text "University of Iowa homepage."
' }, + { title: 'Favicon', content: 'Although small, favicons play an important role in visually unifying web pages. Favicons are the symbols used for browser tabs, bookmarks, or shortcuts pinned to the home screen or desktop of a phone, tablet, or computer.
' }, +]; + export const Default = Template.bind({}); Default.args = { multiselectable: false, - items: [ - { title: 'Section 1 title', expanded: true, content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
' }, - { title: 'Section 2 title', content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
' }, - { title: 'Section 3 title', content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
' }, - { title: 'Section 4 title', content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
' }, - ], + items: items, }; export const MultiSelect = Template.bind({}); @@ -43,3 +39,14 @@ MultiSelect.args = { ...Default.args, multiselectable: true, }; + +export const OpenByDefault = Template.bind({}); +OpenByDefault.args = { + ...Default.args, + items: [ + { open: true, ...items[0] }, + { ...items[1] }, + { ...items[2] }, + { ...items[3] }, + ], +}; diff --git a/src/components/accordion/Accordion.vue b/src/components/accordion/Accordion.vue index de8957c721..a05f82017c 100644 --- a/src/components/accordion/Accordion.vue +++ b/src/components/accordion/Accordion.vue @@ -1,7 +1,8 @@ -