Skip to content

Commit

Permalink
Merge pull request #2818 from alphagov/kg-i18n-accordion-attributes
Browse files Browse the repository at this point in the history
Add support for localisation via data-* attributes to Accordion component
  • Loading branch information
querkmachine committed Sep 1, 2022
2 parents b8d64b2 + 6ce08cc commit 040db6d
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 11 deletions.
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ This text is announced by screen readers when the character count input is focus

This was added in [pull request #2742: Add ability to customise character count fallback text](https://github.com/alphagov/govuk-frontend/pull/2742).

#### Localise the accordion's toggle buttons

You can now translate the text of the [accordion](https://design-system.service.gov.uk/components/character-count/) component's show and hide toggle buttons.

When using the Nunjucks macro, you can use the new `showSectionHtml` and `hideSectionHtml` parameters to customise the text of the 'show' and 'hide' toggles in each section. You can also use `showAllSectionsHtml` and `hideAllSectionsHtml` parameters to customise the text of the toggle at the top of the accordion.

If you're not using the Nunjucks macro, you can customise these using data-* attributes. Any HTML appearing within the attributes must have quotation marks and brackets converted into their HTML entity equivalents.

- `data-i18n.show-section`
- `data-i18n.hide-section`
- `data-i18n.show-all-sections`
- `data-i18n.hide-all-sections`

This was added in [pull request #2818: Add support for localisation via data-* attributes to Accordion component](https://github.com/alphagov/govuk-frontend/pull/2818).

### Recommended changes

#### Remove `aria-labelledby`, remove `id="error-summary-title"` from title and move `role="alert"` to child container on the error summary component
Expand All @@ -50,7 +65,7 @@ If you're not using the Nunjucks macros, you can improve the experience for scre

This will enable screen reader users to have a better, more coherent experience with the error summary. Most notably it will ensure that users of JAWS 2022 or later will hear the entire contents of the error summary on page load and therefore have further context on why there is an error on the page they're on.

This was added in [pull request #2677: Amend error summary markup to fix page load focus bug in JAWS 2022](https://github.com/alphagov/govuk-frontend/pull/2677)
This was added in [pull request #2677: Amend error summary markup to fix page load focus bug in JAWS 2022](https://github.com/alphagov/govuk-frontend/pull/2677).

### Fixes

Expand Down
109 changes: 106 additions & 3 deletions src/govuk/common.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import './vendor/polyfills/Element/prototype/dataset.mjs'

/**
* TODO: Ideally this would be a NodeList.prototype.forEach polyfill
* This seems to fail in IE8, requires more investigation.
Expand All @@ -12,9 +14,11 @@ export function nodeListForEach (nodes, callback) {
}
}

// Used to generate a unique string, allows multiple instances of the component without
// Them conflicting with each other.
// https://stackoverflow.com/a/8809472
/**
* Used to generate a unique string, allows multiple instances of the component
* without them conflicting with each other.
* https://stackoverflow.com/a/8809472
*/
export function generateUniqueID () {
var d = new Date().getTime()
if (typeof window.performance !== 'undefined' && typeof window.performance.now === 'function') {
Expand All @@ -26,3 +30,102 @@ export function generateUniqueID () {
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
})
}

/**
* Config flattening function. Takes any number of objects, flattens them into
* namespaced key-value pairs, (e.g. {'i18n.showSection': 'Show section'}) and
* combines them together, with greatest priority on the LAST item passed in.
*
* @param {...Object} - Any number of objects to merge together.
* @returns {Object} - A flattened object of key-value pairs.
*/
export function mergeConfigs (/* ...config objects */) {
// Function to take nested objects and flatten them to a dot-separated keyed
// object. Doing this means we don't need to do any deep/recursive merging of
// each of our objects, nor transform our dataset from a flat list into a
// nested object.
var flattenObject = function (configObject) {
// Prepare an empty return object
var flattenedObject = {}

// Our flattening function, this is called recursively for each level of
// depth in the object. At each level we prepend the previous level names to
// the key using `prefix`.
var flattenLoop = function (obj, prefix) {
// Loop through keys...
for (var key in obj) {
// Check to see if this is a prototypical key/value,
// if it is, skip it.
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
continue
}
var value = obj[key]
var prefixedKey = prefix ? prefix + '.' + key : key
if (typeof value === 'object') {
// If the value is a nested object, recurse over that too
flattenLoop(value, prefixedKey)
} else {
// Otherwise, add this value to our return object
flattenedObject[prefixedKey] = value
}
}
}

// Kick off the recursive loop
flattenLoop(configObject)
return flattenedObject
}

// Start with an empty object as our base
var formattedConfigObject = {}

// Loop through each of the remaining passed objects and push their keys
// one-by-one into configObject. Any duplicate keys will override the existing
// key with the new value.
for (var i = 0; i < arguments.length; i++) {
var obj = flattenObject(arguments[i])
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
formattedConfigObject[key] = obj[key]
}
}
}

return formattedConfigObject
}

/**
* Extracts keys starting with a particular namespace from a flattened config
* object, removing the namespace in the process.
*
* @param {Object} configObject - The object to extract key-value pairs from.
* @param {String} namespace - The namespace to filter keys with.
* @returns {Object}
*/
export function extractConfigByNamespace (configObject, namespace) {
// Check we have what we need
if (!configObject || typeof configObject !== 'object') {
throw new Error('Provide a `configObject` of type "object".')
}
if (!namespace || typeof namespace !== 'string') {
throw new Error('Provide a `namespace` of type "string" to filter the `configObject` by.')
}
var newObject = {}
for (var key in configObject) {
// Split the key into parts, using . as our namespace separator
var keyParts = key.split('.')
// Check if the first namespace matches the configured namespace
if (Object.prototype.hasOwnProperty.call(configObject, key) && keyParts[0] === namespace) {
// Remove the first item (the namespace) from the parts array,
// but only if there is more than one part (we don't want blank keys!)
if (keyParts.length > 1) {
keyParts.shift()
}
// Join the remaining parts back together
var newKey = keyParts.join('.')
// Add them to our new object
newObject[newKey] = configObject[key]
}
}
return newObject
}
103 changes: 103 additions & 0 deletions src/govuk/common.unit.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* @jest-environment jsdom
*/
/* eslint-env jest */

import { mergeConfigs, extractConfigByNamespace } from './common.mjs'

// TODO: Write unit tests for `nodeListForEach` and `generateUniqueID`

describe('Common JS utilities', () => {
describe('mergeConfigs', () => {
const config1 = {
a: 'antelope',
c: { a: 'camel' }
}
const config2 = {
a: 'aardvark',
b: 'bee',
c: { a: 'cat', o: 'cobra' }
}
const config3 = {
b: 'bat',
c: { o: 'cow' },
d: 'dog'
}

it('flattens a single object', () => {
const config = mergeConfigs(config1)
expect(config).toEqual({
a: 'antelope',
'c.a': 'camel'
})
})

it('flattens and merges two objects', () => {
const config = mergeConfigs(config1, config2)
expect(config).toEqual({
a: 'aardvark',
b: 'bee',
'c.a': 'cat',
'c.o': 'cobra'
})
})

it('flattens and merges three objects', () => {
const config = mergeConfigs(config1, config2, config3)
expect(config).toEqual({
a: 'aardvark',
b: 'bat',
'c.a': 'cat',
'c.o': 'cow',
d: 'dog'
})
})

it('prioritises the last parameter provided', () => {
const config = mergeConfigs(config1, config2, config3, config1)
expect(config).toEqual({
a: 'antelope',
b: 'bat',
'c.a': 'camel',
'c.o': 'cow',
d: 'dog'
})
})

it('returns an empty object if no parameters are provided', () => {
const config = mergeConfigs()
expect(config).toEqual({})
})
})

describe('extractConfigByNamespace', () => {
const flattenedConfig = {
a: 'aardvark',
'b.a': 'bat',
'b.e': 'bear',
'b.o': 'boar',
'c.a': 'camel',
'c.o': 'cow',
d: 'dog',
e: 'elephant'
}

it('can extract single key-value pairs', () => {
const result = extractConfigByNamespace(flattenedConfig, 'a')
expect(result).toEqual({ a: 'aardvark' })
})

it('can extract multiple key-value pairs', () => {
const result = extractConfigByNamespace(flattenedConfig, 'b')
expect(result).toEqual({ a: 'bat', e: 'bear', o: 'boar' })
})

it('throws an error if no `configObject` is provided', () => {
expect(() => extractConfigByNamespace()).toThrow()
})

it('throws an error if no `namespace` is provided', () => {
expect(() => extractConfigByNamespace(flattenedConfig)).toThrow()
})
})
})
5 changes: 3 additions & 2 deletions src/govuk/components/accordion/accordion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { nodeListForEach } from '../../common.mjs'
import { nodeListForEach, mergeConfigs, extractConfigByNamespace } from '../../common.mjs'
import I18n from '../../i18n.mjs'
import '../../vendor/polyfills/Function/prototype/bind.mjs'
import '../../vendor/polyfills/Element/prototype/classList.mjs'
Expand All @@ -34,7 +34,8 @@ function Accordion ($module) {
showSection: 'Show<span class="govuk-visually-hidden"> this section</span>'
}
}
this.i18n = new I18n(defaultConfig.i18n)
this.config = mergeConfigs(defaultConfig, $module.dataset)
this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'))

this.controlsClass = 'govuk-accordion__controls'
this.showAllClass = 'govuk-accordion__show-all'
Expand Down
41 changes: 41 additions & 0 deletions src/govuk/components/accordion/accordion.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,47 @@ describe('/components/accordion', () => {
expect(ariaLabelledByValue).toEqual(headingTextId)
})
})

describe('localisation', () => {
it('should localise "Show all sections" based on data attribute', async () => {
await page.goto(baseUrl + '/components/accordion/with-translations/preview', { waitUntil: 'load' })

const showAllSectionsDataAttribute = await page.evaluate(() => document.body.querySelector('.govuk-accordion').getAttribute('data-i18n.show-all-sections'))
const allSectionsToggleText = await page.evaluate(() => document.body.querySelector('.govuk-accordion__show-all-text').innerHTML)

expect(allSectionsToggleText).toEqual(showAllSectionsDataAttribute)
})

it('should localise "Hide all sections" based on data attribute', async () => {
await page.goto(baseUrl + '/components/accordion/with-translations/preview', { waitUntil: 'load' })
await page.click('.govuk-accordion .govuk-accordion__section:nth-of-type(2) .govuk-accordion__section-header')
await page.click('.govuk-accordion .govuk-accordion__section:nth-of-type(3) .govuk-accordion__section-header')

const hideAllSectionsDataAttribute = await page.evaluate(() => document.body.querySelector('.govuk-accordion').getAttribute('data-i18n.hide-all-sections'))
const allSectionsToggleText = await page.evaluate(() => document.body.querySelector('.govuk-accordion__show-all-text').innerHTML)

expect(allSectionsToggleText).toEqual(hideAllSectionsDataAttribute)
})

it('should localise "Show section" based on data attribute', async () => {
await page.goto(baseUrl + '/components/accordion/with-translations/preview', { waitUntil: 'load' })

const showSectionDataAttribute = await page.evaluate(() => document.body.querySelector('.govuk-accordion').getAttribute('data-i18n.show-section'))
const firstSectionToggleText = await page.evaluate(() => document.body.querySelector('.govuk-accordion__section-toggle-text').innerHTML)

expect(firstSectionToggleText).toEqual(showSectionDataAttribute)
})

it('should localise "Hide section" based on data attribute', async () => {
await page.goto(baseUrl + '/components/accordion/with-translations/preview', { waitUntil: 'load' })
await page.click('.govuk-accordion .govuk-accordion__section:nth-of-type(2) .govuk-accordion__section-header')

const hideSectionDataAttribute = await page.evaluate(() => document.body.querySelector('.govuk-accordion').getAttribute('data-i18n.hide-section'))
const firstSectionToggleText = await page.evaluate(() => document.body.querySelector('.govuk-accordion__section-toggle-text').innerHTML)

expect(firstSectionToggleText).toEqual(hideSectionDataAttribute)
})
})
})
})
})
39 changes: 39 additions & 0 deletions src/govuk/components/accordion/accordion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ params:
type: object
required: false
description: HTML attributes (for example data attributes) to add to the accordion.
- name: hideAllSectionsHtml
type: string
required: false
description: The HTML content of the toggle at the top of the accordion when all sections are expanded. Defaults to 'Hide all sections'.
- name: hideSectionHtml
type: string
required: false
description: The HTML content of the toggle within each section of the accordion, visible when the section is expanded. Defaults to 'Hide this section', with the 'this section' text only exposed to assistive technologies.
- name: showAllSectionsHtml
type: string
required: false
description: The HTML content of the toggle at the top of the accordion when some or all sections are collapsed. Defaults to 'Show all sections'.
- name: showSectionHtml
type: string
required: false
description: The HTML content of the toggle within each section of the accordion, visible when the section is collapsed. Defaults to 'Show this section', with the 'this section' text only exposed to assistive technologies.
- name: items
type: array
required: true
Expand Down Expand Up @@ -169,6 +185,29 @@ examples:
content:
html: <a class="govuk-link" href="#">Link B</a>

- name: with translations
data:
id: with-translations
hideAllSectionsHtml: Collapse all sections
showAllSectionsHtml: Expand all sections
hideSectionHtml: |-
Collapse <span class="govuk-visually-hidden">this section</span>
showSectionHtml: |-
Expand <span class="govuk-visually-hidden">this section</span>
items:
- heading:
text: Section A
content:
text: We need to know your nationality so we can work out which elections you’re entitled to vote in. If you cannot provide your nationality, you’ll have to send copies of identity documents through the post.
- heading:
text: Section B
content:
html: |
<ul class="govuk-list govuk-list--bullet">
<li>Example item 2</li>
</ul>

# Hidden examples are not shown in the review app, but are used for tests and HTML fixtures
- name: classes
hidden: true
Expand Down
6 changes: 5 additions & 1 deletion src/govuk/components/accordion/template.njk
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
{% set headingLevel = params.headingLevel if params.headingLevel else 2 %}

<div class="govuk-accordion {%- if params.classes %} {{ params.classes }}{% endif -%}" data-module="govuk-accordion" id="{{ id }}"
{%- for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}>
{%- if params.hideAllSectionsHtml %} data-i18n.hide-all-sections="{{ params.hideAllSectionsHtml | escape }}"{% endif %}
{%- if params.hideSectionHtml %} data-i18n.hide-section="{{ params.hideSectionHtml | escape }}"{% endif %}
{%- if params.showAllSectionsHtml %} data-i18n.show-all-sections="{{ params.showAllSectionsHtml | escape }}"{% endif %}
{%- if params.showSectionHtml %} data-i18n.show-section="{{ params.showSectionHtml | escape }}"{% endif %}
{%- for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}>
{% for item in params.items %}
{% if item %}
<div class="govuk-accordion__section {% if item.expanded %}govuk-accordion__section--expanded{% endif %}">
Expand Down
Loading

0 comments on commit 040db6d

Please sign in to comment.