Skip to content

Commit

Permalink
Update Accordion design
Browse files Browse the repository at this point in the history
- Update GOVUK Frontend 4.0 version of Accordion

- Maintain nodeListForEach, still required

- Remove custom CSS for condensed layout, this is not in use on GOVK and goes against Design system Guidance. Additionally the accessibility impact has not been reviewed in depth.

- Layering on GOVUK features on-top of Design System Accordion

- Maintain existing GemAccoridon features of manuals-frontend that allow a user to navigation through this section. [1]

[1]#1937
  • Loading branch information
Chris Yoong committed Feb 3, 2022
1 parent 4f454d4 commit 4c370b2
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 784 deletions.
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
/* global nodeListForEach */
// = require ../vendor/polyfills/closest.js
// = require ../vendor/polyfills/indexOf.js
// = require ../vendor/polyfills/common.js

// This component relies on JavaScript from GOV.UK Frontend
// = require govuk/components/accordion/accordion.js
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};
window.GOVUK.Modules = window.GOVUK.Modules || {}
window.GOVUK.Modules.GovukAccordion = window.GOVUKFrontend.Accordion;

(function (Modules) {
function GemAccordion ($module) {
this.$module = $module
this.sectionClass = 'gem-c-accordion__section'
this.moduleId = this.$module.getAttribute('id')
this.sections = this.$module.querySelectorAll('.' + this.sectionClass)
this.openAllButton = ''
this.controlsClass = 'gem-c-accordion__controls'
this.openAllClass = 'gem-c-accordion__open-all'
this.openAllTextClass = 'gem-c-accordion__open-all-text'
this.sectionHeaderClass = 'gem-c-accordion__section-header'
this.sectionHeadingClass = 'gem-c-accordion__section-heading'
this.sectionSummaryClass = 'gem-c-accordion__section-summary'
this.sectionButtonClass = 'gem-c-accordion__section-button'
this.sectionExpandedClass = 'gem-c-accordion__section--expanded'
this.sectionInnerContent = 'gem-c-accordion__section-content'
this.toggleLinkClass = 'js-toggle-link'
this.sectionShowHideIconClass = 'gem-c-accordion__toggle-link'
this.sectionShowHideTextClass = 'gem-c-accordion__toggle-text'
this.upChevonIconClass = 'gem-c-accordion-nav__chevron'
this.downChevonIconClass = 'gem-c-accordion-nav__chevron--down'
this.sectionClass = 'govuk-accordion__section'
this.sectionHeaderClass = 'govuk-accordion__section-header'
this.sectionInnerContent = 'govuk-accordion__section-content'

// Translated component content and language attribute pulled from data attributes
this.$module.actions = {}
Expand All @@ -39,279 +24,14 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
}

GemAccordion.prototype.init = function () {
this.browserSupportsSessionStorage = helper.checkForSessionStorage()

// Indicate that JavaScript has worked
this.$module.classList.add('gem-c-accordion--active')

this.initControls()
this.initSectionHeaders()

// Feature flag for anchor tag navigation used on manuals
if (this.$module.getAttribute('data-anchor-navigation') === 'true') {
this.openByAnchorOnLoad()
this.addEventListenersForAnchors()
}

// See if "Show all sections" button text should be updated
var areAllSectionsOpen = this.checkIfAllSectionsOpen()
this.updateOpenAllButton(areAllSectionsOpen)
}

// Initialise controls and set attributes
GemAccordion.prototype.initControls = function () {
// Create "Show all" button and set attributes
this.openAllButton = document.createElement('button')
this.openAllButton.setAttribute('class', this.openAllClass)
this.openAllButton.setAttribute('aria-expanded', 'false')

// Create icon, add to element
var icon = document.createElement('span')
icon.classList.add(this.upChevonIconClass)
this.openAllButton.appendChild(icon)

// Create control wrapper and add controls to it
var accordionControls = document.createElement('div')
accordionControls.setAttribute('class', this.controlsClass)
accordionControls.appendChild(this.openAllButton)
this.$module.insertBefore(accordionControls, this.$module.firstChild)

// Build additional wrapper for open all toggle text, place icon after wrapped text.
var wrapperOpenAllText = document.createElement('span')
wrapperOpenAllText.classList.add(this.openAllTextClass)
this.openAllButton.insertBefore(wrapperOpenAllText, this.openAllButton.childNodes[0] || null)

// Handle events for the controls
this.openAllButton.addEventListener('click', this.onOpenOrCloseAllToggle.bind(this))
}

// Initialise section headers
GemAccordion.prototype.initSectionHeaders = function () {
// Loop through section headers
nodeListForEach(this.sections, function (section, i) {
// Set header attributes
var header = section.querySelector('.' + this.sectionHeaderClass)

this.initHeaderAttributes(header, i)
this.setExpanded(this.isExpanded(section), section)

// Handle events
header.addEventListener('click', this.onSectionToggle.bind(this, section))

// See if there is any state stored in sessionStorage and set the sections to
// open or closed.
this.setInitialState(section)
}.bind(this))
}

// Set individual header attributes
GemAccordion.prototype.initHeaderAttributes = function (headerWrapper, index) {
var span = headerWrapper.querySelector('.' + this.sectionButtonClass)
var heading = headerWrapper.querySelector('.' + this.sectionHeadingClass)
var summary = headerWrapper.querySelector('.' + this.sectionSummaryClass)

// Copy existing span element to an actual button element, for improved accessibility.
var button = document.createElement('button')
button.setAttribute('id', this.moduleId + '-heading-' + (index + 1))
button.setAttribute('aria-controls', this.moduleId + '-content-' + (index + 1))

// Create show / hide arrow icons with text.
var showIcons = document.createElement('span')
showIcons.classList.add(this.sectionShowHideIconClass, this.toggleLinkClass)

// Add pause after heading for assistive technology.
var srPause = document.createElement('span')
srPause.classList.add('govuk-visually-hidden')
srPause.innerHTML = ', '

// Build additional copy for assistive technology
var srAdditionalCopy = document.createElement('span')
srAdditionalCopy.classList.add('govuk-visually-hidden')
srAdditionalCopy.innerHTML = this.$module.actions.thisSectionVisuallyHidden

if (this.$module.actions.locale) {
srAdditionalCopy.lang = this.filterLocale('this_section_visually_hidden')
}

// Build additional wrapper for toggle text, place icon after wrapped text.
var wrapperShowHideIcon = document.createElement('span')
var icon = document.createElement('span')
icon.classList.add(this.upChevonIconClass)
showIcons.appendChild(icon)
wrapperShowHideIcon.classList.add(this.sectionShowHideTextClass)
showIcons.insertBefore(wrapperShowHideIcon, showIcons.childNodes[0] || null)

// Copy all attributes (https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes) from span to button
for (var i = 0; i < span.attributes.length; i++) {
var attr = span.attributes.item(i)
button.setAttribute(attr.nodeName, attr.nodeValue)
}

// span could contain HTML elements (see https://www.w3.org/TR/2011/WD-html5-20110525/content-models.html#phrasing-content)
button.innerHTML = span.innerHTML
heading.removeChild(span)
heading.appendChild(button)
button.appendChild(srPause)

// If summary content exists add to DOM in correct order
if (typeof (summary) !== 'undefined' && summary !== null) {
button.setAttribute('aria-describedby', this.moduleId + '-summary-' + (index + 1))
button.appendChild(summary)
}

button.appendChild(showIcons)
button.appendChild(srAdditionalCopy)
}

// When section toggled, set and store state
GemAccordion.prototype.onSectionToggle = function (section) {
var expanded = this.isExpanded(section)
this.setExpanded(!expanded, section)

// Store the state in sessionStorage when a change is triggered
this.storeState(section)
}

// When Open/Close All toggled, set and store state
GemAccordion.prototype.onOpenOrCloseAllToggle = function () {
var module = this
var sections = this.sections
var nowExpanded = !this.checkIfAllSectionsOpen()

nodeListForEach(sections, function (section) {
module.setExpanded(nowExpanded, section)
// Store the state in sessionStorage when a change is triggered
module.storeState(section)
})

module.updateOpenAllButton(nowExpanded)
}

// Set section attributes when opened/closed
GemAccordion.prototype.setExpanded = function (expanded, section) {
var icon = section.querySelector('.' + this.upChevonIconClass)
var showHideText = section.querySelector('.' + this.sectionShowHideTextClass)
var button = section.querySelector('.' + this.sectionButtonClass)
var newButtonText = expanded ? this.$module.actions.hideText : this.$module.actions.showText

showHideText.innerHTML = newButtonText
button.setAttribute('aria-expanded', expanded)
button.classList.add(this.toggleLinkClass)

if (this.$module.actions.locale) {
showHideText.lang = this.filterLocale(expanded ? 'hide_text' : 'show_text')
}

// Swap icon, change class
if (expanded) {
section.classList.add(this.sectionExpandedClass)
icon.classList.remove(this.downChevonIconClass)
} else {
section.classList.remove(this.sectionExpandedClass)
icon.classList.add(this.downChevonIconClass)
}

// See if "Show all sections" button text should be updated
var areAllSectionsOpen = this.checkIfAllSectionsOpen()
this.updateOpenAllButton(areAllSectionsOpen)
}

// Get state of section
GemAccordion.prototype.isExpanded = function (section) {
return section.classList.contains(this.sectionExpandedClass)
}

// Check if all sections are open
GemAccordion.prototype.checkIfAllSectionsOpen = function () {
// Get a count of all the Accordion sections
var sectionsCount = this.sections.length
// Get a count of all Accordion sections that are expanded
var expandedSectionCount = this.$module.querySelectorAll('.' + this.sectionExpandedClass).length
var areAllSectionsOpen = sectionsCount === expandedSectionCount

return areAllSectionsOpen
}

// Update "Show all sections" button
GemAccordion.prototype.updateOpenAllButton = function (expanded) {
var icon = this.openAllButton.querySelector('.' + this.upChevonIconClass)
var openAllCopy = this.openAllButton.querySelector('.' + this.openAllTextClass)
var newButtonText = expanded ? this.$module.actions.hideAllText : this.$module.actions.showAllText

this.openAllButton.setAttribute('aria-expanded', expanded)
openAllCopy.innerHTML = newButtonText

if (this.$module.actions.locale) {
openAllCopy.lang = this.filterLocale(expanded ? 'hide_all_text' : 'show_all_text')
}

// Swap icon, toggle class
if (expanded) {
icon.classList.remove(this.downChevonIconClass)
} else {
icon.classList.add(this.downChevonIconClass)
}
}

var helper = {
checkForSessionStorage: function () {
var testString = 'this is the test string'
var result
try {
window.sessionStorage.setItem(testString, testString)
result = window.sessionStorage.getItem(testString) === testString.toString()
window.sessionStorage.removeItem(testString)
return result
} catch (exception) {
if ((typeof console === 'undefined' || typeof console.log === 'undefined')) {
console.log('Notice: sessionStorage not available.')
}
}
}
}

// Set the state of the accordions in sessionStorage
GemAccordion.prototype.storeState = function (section) {
if (this.browserSupportsSessionStorage) {
// We need a unique way of identifying each content in the GemAccordion. Since
// an `#id` should be unique and an `id` is required for `aria-` attributes
// `id` can be safely used.
var button = section.querySelector('.' + this.sectionButtonClass)

if (button) {
var contentId = button.getAttribute('aria-controls')
var contentState = button.getAttribute('aria-expanded')

if (typeof contentId === 'undefined' && (typeof console === 'undefined' || typeof console.log === 'undefined')) {
console.error(new Error('No aria controls present in accordion section heading.'))
}

if (typeof contentState === 'undefined' && (typeof console === 'undefined' || typeof console.log === 'undefined')) {
console.error(new Error('No aria expanded present in accordion section heading.'))
}

// Only set the state when both `contentId` and `contentState` are taken from the DOM.
if (contentId && contentState) {
window.sessionStorage.setItem(contentId, contentState)
}
}
}
}

// Read the state of the accordions from sessionStorage
GemAccordion.prototype.setInitialState = function (section) {
if (this.browserSupportsSessionStorage) {
var button = section.querySelector('.' + this.sectionButtonClass)

if (button) {
var contentId = button.getAttribute('aria-controls')
var contentState = contentId ? window.sessionStorage.getItem(contentId) : null

if (contentState !== null) {
this.setExpanded(contentState === 'true', section)
}
}
}
}

// Navigate to and open accordions with anchored content on page load if a hash is present
Expand All @@ -337,17 +57,17 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
// Find the parent accordion section for the given id and open it
GemAccordion.prototype.openForAnchor = function (hash) {
var target = document.getElementById(hash)
var section = this.getContainingSection(target)

this.setExpanded(true, section)
var $section = this.getContainingSection(target)
var $header = $section.querySelector('.' + this.sectionHeaderClass)
// Pass the selected section to "Accordion.prototype.onSectionToggle" to open
$header.click()
}

// Loop through the given id's ancestors until the parent section class is found
GemAccordion.prototype.getContainingSection = function (target) {
while (!target.classList.contains(this.sectionClass)) {
target = target.parentElement
}

return target
}

Expand Down
Loading

0 comments on commit 4c370b2

Please sign in to comment.