diff --git a/CHANGELOG.md b/CHANGELOG.md index 053d24e531..063a0095e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/app/views/examples/translated/index.njk b/app/views/examples/translated/index.njk new file mode 100644 index 0000000000..2d2954a476 --- /dev/null +++ b/app/views/examples/translated/index.njk @@ -0,0 +1,872 @@ +{% from "accordion/macro.njk" import govukAccordion %} +{% from "back-link/macro.njk" import govukBackLink %} +{% from "breadcrumbs/macro.njk" import govukBreadcrumbs %} +{% from "button/macro.njk" import govukButton %} +{% from "character-count/macro.njk" import govukCharacterCount %} +{% from "checkboxes/macro.njk" import govukCheckboxes %} +{% from "cookie-banner/macro.njk" import govukCookieBanner %} +{% from "date-input/macro.njk" import govukDateInput %} +{% from "details/macro.njk" import govukDetails %} +{% from "error-message/macro.njk" import govukErrorMessage %} +{% from "error-summary/macro.njk" import govukErrorSummary %} +{% from "file-upload/macro.njk" import govukFileUpload %} +{% from "footer/macro.njk" import govukFooter %} +{% from "header/macro.njk" import govukHeader %} +{% from "inset-text/macro.njk" import govukInsetText %} +{% from "notification-banner/macro.njk" import govukNotificationBanner %} +{% from "pagination/macro.njk" import govukPagination %} +{% from "panel/macro.njk" import govukPanel %} +{% from "phase-banner/macro.njk" import govukPhaseBanner %} +{% from "radios/macro.njk" import govukRadios %} +{% from "select/macro.njk" import govukSelect %} +{% from "skip-link/macro.njk" import govukSkipLink %} +{% from "summary-list/macro.njk" import govukSummaryList %} +{% from "table/macro.njk" import govukTable %} +{% from "tabs/macro.njk" import govukTabs %} +{% from "tag/macro.njk" import govukTag %} +{% from "input/macro.njk" import govukInput %} +{% from "textarea/macro.njk" import govukTextarea %} +{% from "warning-text/macro.njk" import govukWarningText %} + +{% extends "layout.njk" %} + +{% block beforeContent %} + {{ govukBackLink({ + "href": "/" + }) }} +{% endblock %} + +{% block content %} +

Translated

+ + {{ govukAccordion({ + id: "accordion-default", + items: [ + { + heading: { + text: "Ysgrifennu'n dda ar gyfer y we" + }, + content: { + html: "

Dyma'r cynnwys ar gyfer Ysgrifennu'n Dda ar gyfer y we.

" + } + }, + { + heading: { + text: "Ysgrifennu'n dda ar gyfer arbenigwyr" + }, + content: { + html: "

Dyma'r cynnwys ar gyfer Ysgrifennu'n Dda ar gyfer arbenigwyr.

" + } + }, + { + heading: { + text: "Adnabod eich cynulleidfa" + }, + content: { + html: "

Dyma'r cynnwys ar gyfer Adnabod eich cynulleidfa.

" + } + }, + { + heading: { + text: "Sut mae pobl yn darllen" + }, + content: { + html: "

Dyma'r cynnwys ar gyfer Sut mae pobl yn darllen.

" + } + } + ] + }) }} + + {{ govukBackLink({ + text: "Ol", + href: "#" + }) }} + + {{ govukBreadcrumbs({ + items: [ + { + text: "Cartref", + href: "#" + }, + { + text: "Pasbortau, teithio a byw dramor", + href: "#" + }, + { + text: "Teithio dramor", + href: "#" + } + ] + }) }} + + {{ govukButton({ + text: "Cadw a pharhau", + classes: "govuk-!-margin-top-4" + }) }} + + {{ govukCharacterCount({ + name: "with-hint", + id: "with-hint", + maxlength: 200, + fallbackHintText: "Gallwch ddefnyddio hyd at %{count} nod", + label: { + text: "Allwch chi roi mwy o fanylion?", + classes: "govuk-label--l", + isPageHeading: true + }, + hint: { + text: "Peidiwch â chynnwys gwybodaeth bersonol neu ariannol fel eich rhif Yswiriant Gwladol neu fanylion eich cerdyn credyd." + } + }) }} + + {{ govukCheckboxes({ + idPrefix: "waste", + name: "waste", + fieldset: { + legend: { + text: "Pa fathau o wastraff ydych chi'n eu cludo?", + isPageHeading: true, + classes: "govuk-fieldset__legend--l" + } + }, + hint: { + text: "Dewiswch bob un sy'n berthnasol." + }, + items: [ + { + value: "carcasses", + text: "Gwastraff o garcasau anifeiliaid" + }, + { + value: "mines", + text: "Gwastraff o fwyngloddiau neu chwareli" + }, + { + value: "farm", + text: "Gwastraff fferm neu amaethyddol" + } + ] + }) }} + + {% set cookieBannerHtml %} +

Rydym yn defnyddio rhai cwcis hanfodol i wneud i'r gwasanaeth hwn weithio.

+

Hoffem hefyd ddefnyddio cwcis dadansoddol fel y gallwn ddeall sut rydych yn defnyddio'r gwasanaeth a gwneud gwelliannau.

+ {% endset %} + + {{ govukCookieBanner({ + classes: "govuk-!-margin-bottom-4", + ariaLabel: "Cwcis ar y gwasanaeth hwn", + messages: [ + { + headingText: "Cwcis ar y gwasanaeth hwn", + html: cookieBannerHtml, + actions: [ + { + text: "Derbyn cwcis dadansoddeg", + type: "button", + name: "cookies", + value: "accept" + }, + { + text: "Gwrthod cwcis dadansoddeg", + type: "button", + name: "cookies", + value: "reject" + }, + { + text: "Gweld cwcis", + href: "#" + } + ] + } + ] + }) }} + + {% set acceptHtml %} +

Rydych chi wedi derbyn cwcis ychwanegol. Gallwch newid eich gosodiadau cwci ar unrhyw adeg.

+ {% endset %} + + {{ govukCookieBanner({ + ariaLabel: "Cwcis ar y gwasanaeth hwn", + classes: "govuk-!-margin-bottom-4", + messages: [ + { + html: acceptHtml, + actions: [ + { + text: "Cuddio neges cwci", + href: "#", + type: "button" + } + ] + } + ] + }) }} + + {{ govukDateInput({ + id: "passport-issued", + namePrefix: "passport-issued", + fieldset: { + legend: { + text: "Pryd gafodd eich pasbort ei gyhoeddi?", + isPageHeading: true, + classes: "govuk-fieldset__legend--l" + } + }, + hint: { + text: "Er enghraifft, 27 3 2007" + }, + items: [ + { + name: "day", + label: "Dydd", + classes: "govuk-input--width-2", + autocomplete: "bday-day" + }, + { + name: "month", + label: "Mis", + classes: "govuk-input--width-2", + autocomplete: "bday-month" + }, + { + name: "year", + label: "Blwyddyn", + classes: "govuk-input--width-4", + autocomplete: "bday-year" + } + ] + }) }} + + {{ govukDetails({ + summaryText: "Help gyda chenedligrwydd", + text: "Mae angen i ni wybod eich cenedligrwydd fel y gallwn weithio allan pa etholiadau y mae gennych hawl i bleidleisio ynddynt. Os na allwch nodi eich cenedligrwydd, bydd yn rhaid i chi anfon copïau o ddogfennau adnabod drwy'r post." + }) }} + + {{ govukErrorSummary({ + titleText: "Mae yna broblem", + disableAutoFocus: true, + errorList: [ + { + text: "Rhaid i'r dyddiad y cyhoeddwyd eich pasbort fod yn y gorffennol", + href: "#passport-issued-day" + }, + { + text: "Rhowch god post, fel AA1 1AA", + href: "#postcode-input" + } + ] + }) }} + + {{ govukFileUpload({ + id: "file-upload-1", + name: "file-upload-1", + label: { + text: "Llwythwch ffeil i fyny" + } + }) }} + + {{ govukFooter({ + classes: "govuk-!-margin-bottom-4", + navigation: [ + { + title: "Gwasanaethau a gwybodaeth", + width: "two-thirds", + columns: 2, + items: [ + { + href: "#", + text: "Budd-daliadau" + }, + { + href: "#", + text: "Genedigaethau, marwolaethau, priodasau a gofal" + }, + { + href: "#", + text: "Busnes a hunan-gyflogedig" + } + ] + }, + { + title: "Adrannau a pholisi", + width: "one-third", + items: [ + { + href: "#", + text: "Sut mae llywodraeth yn gweithio" + }, + { + href: "#", + text: "Adrannau" + }, + { + href: "#", + text: "Ar draws y byd" + } + ] + } + ], + meta: { + visuallyHiddenTitle: "Dolenni cymorth", + items: [ + { + href: "#", + text: "Cwcis" + }, + { + href: "#", + text: "Cysylltwch" + }, + { + href: "#", + text: "Telerau ac Amodau" + } + ], + html: 'Adeiladwyd gan y Government Digital Service' + }, + contentLicence: { + html: "Mae’r holl gynnwys ar gael o dan y Drwydded Llywodraeth Agored v3.0, ac eithrio lle nodir yn wahanol" + }, + copyright: { + html: "Hawlfraint y Goron" + } + }) }} + + {{ govukHeader({ + homepageUrl: "#", + serviceName: "Enw gwasanaeth", + serviceUrl: "#", + menuButtonLabel: "Dangos neu guddio'r ddewislen", + menuButtonText: "Dewislen", + navigation: [ + { + href: "#1", + text: "Eitem llywio 1", + active: true + }, + { + href: "#2", + text: "Eitem llywio 2" + }, + { + href: "#3", + text: "Eitem llywio 3" + }, + { + href: "#4", + text: "Eitem llywio 4" + } + ] + }) }} + + {{ govukInsetText({ + text: "Gall gymryd hyd at 8 wythnos i gofrestru atwrneiaeth arhosol os nad oes unrhyw gamgymeriadau yn y cais." + }) }} + + {% set notificationBannerHtml %} +

+ Mae gennych 7 diwrnod ar ôl i anfon eich cais. + Gweld cais. +

+ {% endset %} + + {{ govukNotificationBanner({ + html: notificationBannerHtml, + titleText: "Pwysig" + }) }} + + {{ govukPagination({ + previous: { + text: "Blaenorol", + href: "#" + }, + next: { + text: "Nesaf", + href: "#" + }, + items: [ + { + number: 1, + href: "#", + visuallyHiddenText: "Tudalen 1" + }, + { + number: 2, + current: true, + href: "#", + visuallyHiddenText: "Tudalen 2" + }, + { + number: 3, + href: "#", + visuallyHiddenText: "Tudalen 3" + } + ] + }) }} + + {{ govukPanel({ + titleText: "Cais wedi'i gwblhau", + html: "Eich rhif cyfeirnod
HDJ2123F" + }) }} + + {{ govukPhaseBanner({ + tag: { + text: "alpha" + }, + html: "Mae hwn yn wasanaeth newydd – bydd eich feedback yn ein helpu i'w wella.", + classes: "govuk-!-margin-bottom-4" + }) }} + + {{ govukRadios({ + idPrefix: "where-do-you-live", + name: "where-do-you-live", + fieldset: { + legend: { + text: "Ble rydych chi'n byw?", + isPageHeading: true, + classes: "govuk-fieldset__legend--l" + } + }, + items: [ + { + value: "england", + text: "Lloegr" + }, + { + value: "scotland", + text: "Alban" + }, + { + value: "wales", + text: "Cymru" + }, + { + value: "northern-ireland", + text: "Gogledd Iwerddon" + } + ] + }) }} + + {{ govukSelect({ + id: "sort", + name: "sort", + label: { + text: "Trefnu yn ôl" + }, + items: [ + { + value: "published", + text: "Cyhoeddwyd yn ddiweddar" + }, + { + value: "updated", + text: "Wedi'i ddiweddaru'n ddiweddar", + selected: true + }, + { + value: "popular", + text: "Mwyaf poblogaidd" + }, + { + value: "comments", + text: "Y rhan fwyaf o sylwadau" + } + ] + }) }} + + {{ govukSkipLink({ + text: "Neidio i'r prif gynnwys", + href: "#content" + }) }} + + {{ govukSummaryList({ + rows: [ + { + key: { + text: "Enw" + }, + value: { + text: "Sarah Philips" + }, + actions: { + items: [ + { + href: "#", + text: "Newid", + visuallyHiddenText: "enw" + } + ] + } + }, + { + key: { + text: "Dyddiad Geni" + }, + value: { + text: "5 January 1978" + }, + actions: { + items: [ + { + href: "#", + text: "Newid", + visuallyHiddenText: "dyddiad geni" + } + ] + } + }, + { + key: { + text: "Cyfeiriad" + }, + value: { + html: "72 Guild Street
London
SE23 6FH" + }, + actions: { + items: [ + { + href: "#", + text: "Newid", + visuallyHiddenText: "cyfeiriad" + } + ] + } + }, + { + key: { + text: "Manylion cyswllt" + }, + value: { + html: '

07700 900457

sarah.phillips@example.com

' + }, + actions: { + items: [ + { + href: "#", + text: "Newid", + visuallyHiddenText: "manylion cyswllt" + } + ] + } + } + ] + }) }} + + + + {{ govukTable({ + caption: "Dyddiadau a symiau", + captionClasses: "govuk-table__caption--m", + firstCellIsHeader: true, + head: [ + { + text: "Dyddiad" + }, + { + text: "Swm" + } + ], + rows: [ + [ + { + text: "Y 6 wythnos gyntaf" + }, + { + text: "£109.80 yr wythnos" + } + ], + [ + { + text: "33 wythnos nesaf" + }, + { + text: "£109.80 yr wythnos" + } + ], + [ + { + text: "Amcangyfrif o gyfanswm yr incwm" + }, + { + text: "£4,282.20" + } + ] + ] + }) }} + + {% set pastDayHtml %} +

Ddoe

+ {{ govukTable({ + head: [ + { + text: "Rheolwr achos" + }, + { + text: "Agorwyd achosion" + }, + { + text: "Caewyd achosion" + } + ], + rows: [ + [ + { + text: "David Francis" + }, + { + text: "3" + }, + { + text: "0" + } + ], + [ + { + text: "Paul Farmer" + }, + { + text: "1" + }, + { + text: "0" + } + ], + [ + { + text: "Rita Patel" + }, + { + text: "2" + }, + { + text: "0" + } + ] + ] + }) }} + {% endset -%} + + {% set pastWeekHtml %} +

Wythnos diwethaf

+ {{ govukTable({ + head: [ + { + text: "Rheolwr achos" + }, + { + text: "Agorwyd achosion" + }, + { + text: "Caewyd achosion" + } + ], + rows: [ + [ + { + text: "David Francis" + }, + { + text: "24" + }, + { + text: "18" + } + ], + [ + { + text: "Paul Farmer" + }, + { + text: "16" + }, + { + text: "20" + } + ], + [ + { + text: "Rita Patel" + }, + { + text: "24" + }, + { + text: "27" + } + ] + ] + }) }} + {% endset -%} + + {% set pastMonthHtml %} +

Mis diwethaf

+ {{ govukTable({ + head: [ + { + text: "Rheolwr achos" + }, + { + text: "Agorwyd achosion" + }, + { + text: "Caewyd achosion" + } + ], + rows: [ + [ + { + text: "David Francis" + }, + { + text: "98" + }, + { + text: "95" + } + ], + [ + { + text: "Paul Farmer" + }, + { + text: "122" + }, + { + text: "131" + } + ], + [ + { + text: "Rita Patel" + }, + { + text: "126" + }, + { + text: "142" + } + ] + ] + }) }} + {% endset -%} + + {% set pastYearHtml %} +

Blwyddyn diwethaf

+ {{ govukTable({ + head: [ + { + text: "Rheolwr achos" + }, + { + text: "Agorwyd achosion" + }, + { + text: "Caewyd achosion" + } + ], + rows: [ + [ + { + text: "David Francis" + }, + { + text: "1380" + }, + { + text: "1472" + } + ], + [ + { + text: "Paul Farmer" + }, + { + text: "1129" + }, + { + text: "1083" + } + ], + [ + { + text: "Rita Patel" + }, + { + text: "1539" + }, + { + text: "1265" + } + ] + ] + }) }} + {% endset -%} + + {{ govukTabs({ + title: "Cynnwys", + items: [ + { + label: "Ddoe", + id: "past-day", + panel: { + html: pastDayHtml + } + }, + { + label: "Wythnos diwethaf", + id: "past-week", + panel: { + html: pastWeekHtml + } + }, + { + label: "Mis diwethaf", + id: "past-month", + panel: { + html: pastMonthHtml + } + }, + { + label: "Blwyddyn diwethaf", + id: "past-year", + panel: { + html: pastYearHtml + } + } + ] + }) }} + + {{ govukTag({ + text: "cyflawn", + classes: "govuk-!-margin-bottom-4" + }) }} + + {{ govukInput({ + label: { + text: "Beth yw enw'r digwyddiad?", + classes: "govuk-label--l", + isPageHeading: true + }, + id: "event-name", + name: "event-name" + }) }} + + {{ govukTextarea({ + name: "more-detail", + id: "more-detail", + label: { + text: "Allwch chi roi mwy o fanylion?", + classes: "govuk-label--l", + isPageHeading: true + }, + hint: { + text: "Peidiwch â chynnwys gwybodaeth bersonol neu ariannol, fel eich rhif Yswiriant Gwladol neu fanylion cerdyn credyd." + } + }) }} + + {{ govukWarningText({ + text: "Gallwch gael dirwy o hyd at £5,000 os na fyddwch yn cofrestru.", + iconFallbackText: "Rhybudd" + }) }} +{% endblock %} diff --git a/src/govuk/common.mjs b/src/govuk/common.mjs index 7d2835ebea..d16e108c0f 100644 --- a/src/govuk/common.mjs +++ b/src/govuk/common.mjs @@ -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. @@ -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') { @@ -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 +} diff --git a/src/govuk/common.unit.test.mjs b/src/govuk/common.unit.test.mjs new file mode 100644 index 0000000000..d401a298d6 --- /dev/null +++ b/src/govuk/common.unit.test.mjs @@ -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() + }) + }) +}) diff --git a/src/govuk/components/accordion/accordion.mjs b/src/govuk/components/accordion/accordion.mjs index e63b659150..4621151bb7 100644 --- a/src/govuk/components/accordion/accordion.mjs +++ b/src/govuk/components/accordion/accordion.mjs @@ -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' @@ -34,7 +34,8 @@ function Accordion ($module) { showSection: 'Show this section' } } - 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' diff --git a/src/govuk/components/accordion/accordion.test.js b/src/govuk/components/accordion/accordion.test.js index e131887a9a..5d216a89ec 100644 --- a/src/govuk/components/accordion/accordion.test.js +++ b/src/govuk/components/accordion/accordion.test.js @@ -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) + }) + }) }) }) }) diff --git a/src/govuk/components/accordion/accordion.yaml b/src/govuk/components/accordion/accordion.yaml index 1241089944..7154c60ebc 100644 --- a/src/govuk/components/accordion/accordion.yaml +++ b/src/govuk/components/accordion/accordion.yaml @@ -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 @@ -169,6 +185,29 @@ examples: content: html: Link B +- name: with translations + data: + id: with-translations + hideAllSectionsHtml: Collapse all sections + showAllSectionsHtml: Expand all sections + hideSectionHtml: |- + Collapse this section + showSectionHtml: |- + Expand this section + 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: | + + + # Hidden examples are not shown in the review app, but are used for tests and HTML fixtures - name: classes hidden: true diff --git a/src/govuk/components/accordion/template.njk b/src/govuk/components/accordion/template.njk index b9339fe71e..fd79045abe 100644 --- a/src/govuk/components/accordion/template.njk +++ b/src/govuk/components/accordion/template.njk @@ -2,7 +2,11 @@ {% set headingLevel = params.headingLevel if params.headingLevel else 2 %}
+ {%- 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 %}
diff --git a/src/govuk/components/accordion/template.test.js b/src/govuk/components/accordion/template.test.js index ffc1338a4f..48c4033854 100644 --- a/src/govuk/components/accordion/template.test.js +++ b/src/govuk/components/accordion/template.test.js @@ -98,5 +98,15 @@ describe('Accordion', () => { expect($items.length).toEqual(2) }) + + it('renders with localisation data attributes', () => { + const $ = render('accordion', examples['with translations']) + const $component = $('.govuk-accordion') + + expect($component.attr('data-i18n.hide-all-sections')).toEqual('Collapse all sections') + expect($component.attr('data-i18n.show-all-sections')).toEqual('Expand all sections') + expect($component.attr('data-i18n.hide-section')).toEqual('Collapse this section') + expect($component.attr('data-i18n.show-section')).toEqual('Expand this section') + }) }) }) diff --git a/src/govuk/i18n.unit.test.mjs b/src/govuk/i18n.unit.test.mjs index 0d4100ff0c..0de2304e90 100644 --- a/src/govuk/i18n.unit.test.mjs +++ b/src/govuk/i18n.unit.test.mjs @@ -16,25 +16,25 @@ describe('I18n', () => { } }) - test('returns the text for a given lookup key', () => { + it('returns the text for a given lookup key', () => { const i18n = new I18n(config) const returnString = i18n.t('textString') expect(returnString).toBe('Hello world') }) - test('returns the HTML for a given lookup key', () => { + it('returns the HTML for a given lookup key', () => { const i18n = new I18n(config) const returnString = i18n.t('htmlString') expect(returnString).toBe('Hello world') }) - test('returns the lookup key if no translation is defined', () => { + it('returns the lookup key if no translation is defined', () => { const i18n = new I18n(config) const returnString = i18n.t('missingString') expect(returnString).toBe('missingString') }) - test('throws an error if no lookup key is provided', () => { + it('throws an error if no lookup key is provided', () => { const i18n = new I18n(config) expect(() => i18n.t()).toThrow('i18n lookup key missing.') }) diff --git a/src/govuk/vendor/polyfills/Element/prototype/dataset.mjs b/src/govuk/vendor/polyfills/Element/prototype/dataset.mjs new file mode 100644 index 0000000000..94847c5790 --- /dev/null +++ b/src/govuk/vendor/polyfills/Element/prototype/dataset.mjs @@ -0,0 +1,68 @@ +import '../../Object/defineProperty.mjs' +import '../../Element.mjs' + +(function(undefined) { + + // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/detect.js + var detect = (function(){ + if (!document.documentElement.dataset) { + return false; + } + var el = document.createElement('div'); + el.setAttribute("data-a-b", "c"); + return el.dataset && el.dataset.aB == "c"; + }()) + + if (detect) return + + // Polyfill derived from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/polyfill.js + Object.defineProperty(Element.prototype, 'dataset', { + get: function() { + var element = this; + var attributes = this.attributes; + var map = {}; + + for (var i = 0; i < attributes.length; i++) { + var attribute = attributes[i]; + + // This regex has been edited from the original polyfill, to add + // support for period (.) separators in data-* attribute names. These + // are allowed in the HTML spec, but were not covered by the original + // polyfill's regex. We use periods in our i18n implementation. + if (attribute && attribute.name && (/^data-\w[.\w-]*$/).test(attribute.name)) { + var name = attribute.name; + var value = attribute.value; + + var propName = name.substr(5).replace(/-./g, function (prop) { + return prop.charAt(1).toUpperCase(); + }); + + // If this browser supports __defineGetter__ and __defineSetter__, + // continue using defineProperty. If not (like IE 8 and below), we use + // a hacky fallback which at least gives an object in the right format + if ('__defineGetter__' in Object.prototype && '__defineSetter__' in Object.prototype) { + Object.defineProperty(map, propName, { + enumerable: true, + get: function() { + return this.value; + }.bind({value: value || ''}), + set: function setter(name, value) { + if (typeof value !== 'undefined') { + this.setAttribute(name, value); + } else { + this.removeAttribute(name); + } + }.bind(element, name) + }); + } else { + map[propName] = value + } + + } + } + + return map; + } + }); + +}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); \ No newline at end of file