govuk_tech_docs 3.2.1 → 3.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/publish.yaml +1 -1
  3. data/CHANGELOG.md +26 -7
  4. data/README.md +2 -2
  5. data/lib/assets/javascripts/_modules/search.js +2 -2
  6. data/lib/govuk_tech_docs/contribution_banner.rb +1 -1
  7. data/lib/govuk_tech_docs/version.rb +1 -1
  8. data/lib/source/layouts/core.erb +1 -1
  9. data/node_modules/govuk-frontend/govuk/all.js +1548 -311
  10. data/node_modules/govuk-frontend/govuk/common/closest-attribute-value.js +70 -0
  11. data/node_modules/govuk-frontend/govuk/common/index.js +172 -0
  12. data/node_modules/govuk-frontend/govuk/common/normalise-dataset.js +373 -0
  13. data/node_modules/govuk-frontend/govuk/common.js +138 -3
  14. data/node_modules/govuk-frontend/govuk/components/_all.scss +1 -0
  15. data/node_modules/govuk-frontend/govuk/components/accordion/_index.scss +5 -6
  16. data/node_modules/govuk-frontend/govuk/components/accordion/accordion.js +754 -36
  17. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/_index.scss +0 -2
  18. data/node_modules/govuk-frontend/govuk/components/button/_index.scss +29 -21
  19. data/node_modules/govuk-frontend/govuk/components/button/button.js +365 -107
  20. data/node_modules/govuk-frontend/govuk/components/character-count/_index.scss +9 -0
  21. data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +1092 -109
  22. data/node_modules/govuk-frontend/govuk/components/checkboxes/_index.scss +3 -2
  23. data/node_modules/govuk-frontend/govuk/components/checkboxes/checkboxes.js +30 -2
  24. data/node_modules/govuk-frontend/govuk/components/details/details.js +51 -33
  25. data/node_modules/govuk-frontend/govuk/components/error-summary/error-summary.js +289 -6
  26. data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +13 -23
  27. data/node_modules/govuk-frontend/govuk/components/header/_index.scss +30 -24
  28. data/node_modules/govuk-frontend/govuk/components/header/header.js +59 -11
  29. data/node_modules/govuk-frontend/govuk/components/input/_index.scss +13 -23
  30. data/node_modules/govuk-frontend/govuk/components/notification-banner/notification-banner.js +252 -2
  31. data/node_modules/govuk-frontend/govuk/components/pagination/_index.scss +247 -0
  32. data/node_modules/govuk-frontend/govuk/components/pagination/_pagination.scss +2 -0
  33. data/node_modules/govuk-frontend/govuk/components/panel/_index.scss +1 -1
  34. data/node_modules/govuk-frontend/govuk/components/radios/_index.scss +5 -12
  35. data/node_modules/govuk-frontend/govuk/components/radios/radios.js +30 -2
  36. data/node_modules/govuk-frontend/govuk/components/select/_index.scss +11 -0
  37. data/node_modules/govuk-frontend/govuk/components/skip-link/_index.scss +1 -3
  38. data/node_modules/govuk-frontend/govuk/components/skip-link/skip-link.js +10 -4
  39. data/node_modules/govuk-frontend/govuk/components/summary-list/_index.scss +45 -13
  40. data/node_modules/govuk-frontend/govuk/components/table/_index.scss +1 -1
  41. data/node_modules/govuk-frontend/govuk/components/tabs/tabs.js +28 -0
  42. data/node_modules/govuk-frontend/govuk/core/_section-break.scss +1 -1
  43. data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +5 -5
  44. data/node_modules/govuk-frontend/govuk/helpers/_focused.scss +5 -0
  45. data/node_modules/govuk-frontend/govuk/helpers/_links.scss +13 -11
  46. data/node_modules/govuk-frontend/govuk/helpers/_media-queries.scss +2 -2
  47. data/node_modules/govuk-frontend/govuk/helpers/_shape-arrow.scss +1 -1
  48. data/node_modules/govuk-frontend/govuk/helpers/_spacing.scss +3 -3
  49. data/node_modules/govuk-frontend/govuk/helpers/_typography.scss +16 -9
  50. data/node_modules/govuk-frontend/govuk/i18n.js +390 -0
  51. data/node_modules/govuk-frontend/govuk/objects/_button-group.scss +10 -26
  52. data/node_modules/govuk-frontend/govuk/objects/_template.scss +1 -1
  53. data/node_modules/govuk-frontend/govuk/objects/_width-container.scss +0 -4
  54. data/node_modules/govuk-frontend/govuk/overrides/_spacing.scss +56 -12
  55. data/node_modules/govuk-frontend/govuk/settings/_all.scss +1 -0
  56. data/node_modules/govuk-frontend/govuk/settings/_colours-palette.scss +12 -0
  57. data/node_modules/govuk-frontend/govuk/settings/_compatibility.scss +26 -0
  58. data/node_modules/govuk-frontend/govuk/settings/_spacing.scss +4 -8
  59. data/node_modules/govuk-frontend/govuk/settings/_typography-font.scss +23 -0
  60. data/node_modules/govuk-frontend/govuk/settings/_typography-responsive.scss +12 -0
  61. data/node_modules/govuk-frontend/govuk/settings/_warnings.scss +53 -0
  62. data/node_modules/govuk-frontend/govuk/tools/_compatibility.scss +20 -6
  63. data/node_modules/govuk-frontend/govuk/tools/_exports.scss +1 -1
  64. data/node_modules/govuk-frontend/govuk/tools/_font-url.scss +1 -1
  65. data/node_modules/govuk-frontend/govuk/tools/_image-url.scss +1 -1
  66. data/node_modules/govuk-frontend/govuk/tools/_px-to-em.scss +2 -2
  67. data/node_modules/govuk-frontend/govuk/tools/_px-to-rem.scss +1 -1
  68. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Date/now.js +21 -0
  69. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Element/prototype/dataset.js +300 -0
  70. data/node_modules/govuk-frontend/govuk/vendor/polyfills/String/prototype/trim.js +21 -0
  71. data/node_modules/govuk-frontend/govuk-prototype-kit/init.js +7 -0
  72. data/node_modules/govuk-frontend/govuk-prototype-kit/init.scss +12 -0
  73. data/package-lock.json +12 -12
  74. data/package.json +1 -1
  75. metadata +14 -2
@@ -4,10 +4,24 @@
4
4
  (global.GOVUKFrontend = global.GOVUKFrontend || {}, global.GOVUKFrontend.Accordion = factory());
5
5
  }(this, (function () { 'use strict';
6
6
 
7
+ /**
8
+ * Common helpers which do not require polyfill.
9
+ *
10
+ * IMPORTANT: If a helper require a polyfill, please isolate it in its own module
11
+ * so that the polyfill can be properly tree-shaken and does not burden
12
+ * the components that do not need that helper
13
+ *
14
+ * @module common/index
15
+ */
16
+
7
17
  /**
8
18
  * TODO: Ideally this would be a NodeList.prototype.forEach polyfill
9
19
  * This seems to fail in IE8, requires more investigation.
10
20
  * See: https://github.com/imagitama/nodelist-foreach-polyfill
21
+ *
22
+ * @param {NodeListOf<Element>} nodes - NodeList from querySelectorAll()
23
+ * @param {nodeListIterator} callback - Callback function to run for each node
24
+ * @returns {undefined}
11
25
  */
12
26
  function nodeListForEach (nodes, callback) {
13
27
  if (window.NodeList.prototype.forEach) {
@@ -18,6 +32,500 @@ function nodeListForEach (nodes, callback) {
18
32
  }
19
33
  }
20
34
 
35
+ /**
36
+ * Config flattening function
37
+ *
38
+ * Takes any number of objects, flattens them into namespaced key-value pairs,
39
+ * (e.g. {'i18n.showSection': 'Show section'}) and combines them together, with
40
+ * greatest priority on the LAST item passed in.
41
+ *
42
+ * @returns {object} A flattened object of key-value pairs.
43
+ */
44
+ function mergeConfigs (/* configObject1, configObject2, ...configObjects */) {
45
+ /**
46
+ * Function to take nested objects and flatten them to a dot-separated keyed
47
+ * object. Doing this means we don't need to do any deep/recursive merging of
48
+ * each of our objects, nor transform our dataset from a flat list into a
49
+ * nested object.
50
+ *
51
+ * @param {object} configObject - Deeply nested object
52
+ * @returns {object} Flattened object with dot-separated keys
53
+ */
54
+ var flattenObject = function (configObject) {
55
+ // Prepare an empty return object
56
+ var flattenedObject = {};
57
+
58
+ // Our flattening function, this is called recursively for each level of
59
+ // depth in the object. At each level we prepend the previous level names to
60
+ // the key using `prefix`.
61
+ var flattenLoop = function (obj, prefix) {
62
+ // Loop through keys...
63
+ for (var key in obj) {
64
+ // Check to see if this is a prototypical key/value,
65
+ // if it is, skip it.
66
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
67
+ continue
68
+ }
69
+ var value = obj[key];
70
+ var prefixedKey = prefix ? prefix + '.' + key : key;
71
+ if (typeof value === 'object') {
72
+ // If the value is a nested object, recurse over that too
73
+ flattenLoop(value, prefixedKey);
74
+ } else {
75
+ // Otherwise, add this value to our return object
76
+ flattenedObject[prefixedKey] = value;
77
+ }
78
+ }
79
+ };
80
+
81
+ // Kick off the recursive loop
82
+ flattenLoop(configObject);
83
+ return flattenedObject
84
+ };
85
+
86
+ // Start with an empty object as our base
87
+ var formattedConfigObject = {};
88
+
89
+ // Loop through each of the remaining passed objects and push their keys
90
+ // one-by-one into configObject. Any duplicate keys will override the existing
91
+ // key with the new value.
92
+ for (var i = 0; i < arguments.length; i++) {
93
+ var obj = flattenObject(arguments[i]);
94
+ for (var key in obj) {
95
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
96
+ formattedConfigObject[key] = obj[key];
97
+ }
98
+ }
99
+ }
100
+
101
+ return formattedConfigObject
102
+ }
103
+
104
+ /**
105
+ * Extracts keys starting with a particular namespace from a flattened config
106
+ * object, removing the namespace in the process.
107
+ *
108
+ * @param {object} configObject - The object to extract key-value pairs from.
109
+ * @param {string} namespace - The namespace to filter keys with.
110
+ * @returns {object} Flattened object with dot-separated key namespace removed
111
+ */
112
+ function extractConfigByNamespace (configObject, namespace) {
113
+ // Check we have what we need
114
+ if (!configObject || typeof configObject !== 'object') {
115
+ throw new Error('Provide a `configObject` of type "object".')
116
+ }
117
+ if (!namespace || typeof namespace !== 'string') {
118
+ throw new Error('Provide a `namespace` of type "string" to filter the `configObject` by.')
119
+ }
120
+ var newObject = {};
121
+ for (var key in configObject) {
122
+ // Split the key into parts, using . as our namespace separator
123
+ var keyParts = key.split('.');
124
+ // Check if the first namespace matches the configured namespace
125
+ if (Object.prototype.hasOwnProperty.call(configObject, key) && keyParts[0] === namespace) {
126
+ // Remove the first item (the namespace) from the parts array,
127
+ // but only if there is more than one part (we don't want blank keys!)
128
+ if (keyParts.length > 1) {
129
+ keyParts.shift();
130
+ }
131
+ // Join the remaining parts back together
132
+ var newKey = keyParts.join('.');
133
+ // Add them to our new object
134
+ newObject[newKey] = configObject[key];
135
+ }
136
+ }
137
+ return newObject
138
+ }
139
+
140
+ /**
141
+ * @callback nodeListIterator
142
+ * @param {Element} value - The current node being iterated on
143
+ * @param {number} index - The current index in the iteration
144
+ * @param {NodeListOf<Element>} nodes - NodeList from querySelectorAll()
145
+ * @returns {undefined}
146
+ */
147
+
148
+ /**
149
+ * Internal support for selecting messages to render, with placeholder
150
+ * interpolation and locale-aware number formatting and pluralisation
151
+ *
152
+ * @class
153
+ * @private
154
+ * @param {TranslationsFlattened} translations - Key-value pairs of the translation strings to use.
155
+ * @param {object} [config] - Configuration options for the function.
156
+ * @param {string} config.locale - An overriding locale for the PluralRules functionality.
157
+ */
158
+ function I18n (translations, config) {
159
+ // Make list of translations available throughout function
160
+ this.translations = translations || {};
161
+
162
+ // The locale to use for PluralRules and NumberFormat
163
+ this.locale = (config && config.locale) || document.documentElement.lang || 'en';
164
+ }
165
+
166
+ /**
167
+ * The most used function - takes the key for a given piece of UI text and
168
+ * returns the appropriate string.
169
+ *
170
+ * @param {string} lookupKey - The lookup key of the string to use.
171
+ * @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
172
+ * @returns {string} The appropriate translation string.
173
+ */
174
+ I18n.prototype.t = function (lookupKey, options) {
175
+ if (!lookupKey) {
176
+ // Print a console error if no lookup key has been provided
177
+ throw new Error('i18n: lookup key missing')
178
+ }
179
+
180
+ // If the `count` option is set, determine which plural suffix is needed and
181
+ // change the lookupKey to match. We check to see if it's undefined instead of
182
+ // falsy, as this could legitimately be 0.
183
+ if (options && typeof options.count !== 'undefined') {
184
+ // Get the plural suffix
185
+ lookupKey = lookupKey + '.' + this.getPluralSuffix(lookupKey, options.count);
186
+ }
187
+
188
+ if (lookupKey in this.translations) {
189
+ // Fetch the translation string for that lookup key
190
+ var translationString = this.translations[lookupKey];
191
+
192
+ // Check for ${} placeholders in the translation string
193
+ if (translationString.match(/%{(.\S+)}/)) {
194
+ if (!options) {
195
+ throw new Error('i18n: cannot replace placeholders in string if no option data provided')
196
+ }
197
+
198
+ return this.replacePlaceholders(translationString, options)
199
+ } else {
200
+ return translationString
201
+ }
202
+ } else {
203
+ // If the key wasn't found in our translations object,
204
+ // return the lookup key itself as the fallback
205
+ return lookupKey
206
+ }
207
+ };
208
+
209
+ /**
210
+ * Takes a translation string with placeholders, and replaces the placeholders
211
+ * with the provided data
212
+ *
213
+ * @param {string} translationString - The translation string
214
+ * @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
215
+ * @returns {string} The translation string to output, with ${} placeholders replaced
216
+ */
217
+ I18n.prototype.replacePlaceholders = function (translationString, options) {
218
+ var formatter;
219
+
220
+ if (this.hasIntlNumberFormatSupport()) {
221
+ formatter = new Intl.NumberFormat(this.locale);
222
+ }
223
+
224
+ return translationString.replace(/%{(.\S+)}/g, function (placeholderWithBraces, placeholderKey) {
225
+ if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {
226
+ var placeholderValue = options[placeholderKey];
227
+
228
+ // If a user has passed `false` as the value for the placeholder
229
+ // treat it as though the value should not be displayed
230
+ if (placeholderValue === false) {
231
+ return ''
232
+ }
233
+
234
+ // If the placeholder's value is a number, localise the number formatting
235
+ if (typeof placeholderValue === 'number' && formatter) {
236
+ return formatter.format(placeholderValue)
237
+ }
238
+
239
+ return placeholderValue
240
+ } else {
241
+ throw new Error('i18n: no data found to replace ' + placeholderWithBraces + ' placeholder in string')
242
+ }
243
+ })
244
+ };
245
+
246
+ /**
247
+ * Check to see if the browser supports Intl and Intl.PluralRules.
248
+ *
249
+ * It requires all conditions to be met in order to be supported:
250
+ * - The browser supports the Intl class (true in IE11)
251
+ * - The implementation of Intl supports PluralRules (NOT true in IE11)
252
+ * - The browser/OS has plural rules for the current locale (browser dependent)
253
+ *
254
+ * @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
255
+ */
256
+ I18n.prototype.hasIntlPluralRulesSupport = function () {
257
+ return Boolean(window.Intl && ('PluralRules' in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length))
258
+ };
259
+
260
+ /**
261
+ * Check to see if the browser supports Intl and Intl.NumberFormat.
262
+ *
263
+ * It requires all conditions to be met in order to be supported:
264
+ * - The browser supports the Intl class (true in IE11)
265
+ * - The implementation of Intl supports NumberFormat (also true in IE11)
266
+ * - The browser/OS has number formatting rules for the current locale (browser dependent)
267
+ *
268
+ * @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
269
+ */
270
+ I18n.prototype.hasIntlNumberFormatSupport = function () {
271
+ return Boolean(window.Intl && ('NumberFormat' in window.Intl && Intl.NumberFormat.supportedLocalesOf(this.locale).length))
272
+ };
273
+
274
+ /**
275
+ * Get the appropriate suffix for the plural form.
276
+ *
277
+ * Uses Intl.PluralRules (or our own fallback implementation) to get the
278
+ * 'preferred' form to use for the given count.
279
+ *
280
+ * Checks that a translation has been provided for that plural form – if it
281
+ * hasn't, it'll fall back to the 'other' plural form (unless that doesn't exist
282
+ * either, in which case an error will be thrown)
283
+ *
284
+ * @param {string} lookupKey - The lookup key of the string to use.
285
+ * @param {number} count - Number used to determine which pluralisation to use.
286
+ * @returns {PluralRule} The suffix associated with the correct pluralisation for this locale.
287
+ */
288
+ I18n.prototype.getPluralSuffix = function (lookupKey, count) {
289
+ // Validate that the number is actually a number.
290
+ //
291
+ // Number(count) will turn anything that can't be converted to a Number type
292
+ // into 'NaN'. isFinite filters out NaN, as it isn't a finite number.
293
+ count = Number(count);
294
+ if (!isFinite(count)) { return 'other' }
295
+
296
+ var preferredForm;
297
+
298
+ // Check to verify that all the requirements for Intl.PluralRules are met.
299
+ // If so, we can use that instead of our custom implementation. Otherwise,
300
+ // use the hardcoded fallback.
301
+ if (this.hasIntlPluralRulesSupport()) {
302
+ preferredForm = new Intl.PluralRules(this.locale).select(count);
303
+ } else {
304
+ preferredForm = this.selectPluralFormUsingFallbackRules(count);
305
+ }
306
+
307
+ // Use the correct plural form if provided
308
+ if (lookupKey + '.' + preferredForm in this.translations) {
309
+ return preferredForm
310
+ // Fall back to `other` if the plural form is missing, but log a warning
311
+ // to the console
312
+ } else if (lookupKey + '.other' in this.translations) {
313
+ if (console && 'warn' in console) {
314
+ console.warn('i18n: Missing plural form ".' + preferredForm + '" for "' +
315
+ this.locale + '" locale. Falling back to ".other".');
316
+ }
317
+
318
+ return 'other'
319
+ // If the required `other` plural form is missing, all we can do is error
320
+ } else {
321
+ throw new Error(
322
+ 'i18n: Plural form ".other" is required for "' + this.locale + '" locale'
323
+ )
324
+ }
325
+ };
326
+
327
+ /**
328
+ * Get the plural form using our fallback implementation
329
+ *
330
+ * This is split out into a separate function to make it easier to test the
331
+ * fallback behaviour in an environment where Intl.PluralRules exists.
332
+ *
333
+ * @param {number} count - Number used to determine which pluralisation to use.
334
+ * @returns {PluralRule} The pluralisation form for count in this locale.
335
+ */
336
+ I18n.prototype.selectPluralFormUsingFallbackRules = function (count) {
337
+ // Currently our custom code can only handle positive integers, so let's
338
+ // make sure our number is one of those.
339
+ count = Math.abs(Math.floor(count));
340
+
341
+ var ruleset = this.getPluralRulesForLocale();
342
+
343
+ if (ruleset) {
344
+ return I18n.pluralRules[ruleset](count)
345
+ }
346
+
347
+ return 'other'
348
+ };
349
+
350
+ /**
351
+ * Work out which pluralisation rules to use for the current locale
352
+ *
353
+ * The locale may include a regional indicator (such as en-GB), but we don't
354
+ * usually care about this part, as pluralisation rules are usually the same
355
+ * regardless of region. There are exceptions, however, (e.g. Portuguese) so
356
+ * this searches by both the full and shortened locale codes, just to be sure.
357
+ *
358
+ * @returns {PluralRuleName | undefined} The name of the pluralisation rule to use (a key for one
359
+ * of the functions in this.pluralRules)
360
+ */
361
+ I18n.prototype.getPluralRulesForLocale = function () {
362
+ var locale = this.locale;
363
+ var localeShort = locale.split('-')[0];
364
+
365
+ // Look through the plural rules map to find which `pluralRule` is
366
+ // appropriate for our current `locale`.
367
+ for (var pluralRule in I18n.pluralRulesMap) {
368
+ if (Object.prototype.hasOwnProperty.call(I18n.pluralRulesMap, pluralRule)) {
369
+ var languages = I18n.pluralRulesMap[pluralRule];
370
+ for (var i = 0; i < languages.length; i++) {
371
+ if (languages[i] === locale || languages[i] === localeShort) {
372
+ return pluralRule
373
+ }
374
+ }
375
+ }
376
+ }
377
+ };
378
+
379
+ /**
380
+ * Map of plural rules to languages where those rules apply.
381
+ *
382
+ * Note: These groups are named for the most dominant or recognisable language
383
+ * that uses each system. The groupings do not imply that the languages are
384
+ * related to one another. Many languages have evolved the same systems
385
+ * independently of one another.
386
+ *
387
+ * Code to support more languages can be found in the i18n spike:
388
+ * {@link https://github.com/alphagov/govuk-frontend/blob/spike-i18n-support/src/govuk/i18n.mjs}
389
+ *
390
+ * Languages currently supported:
391
+ *
392
+ * Arabic: Arabic (ar)
393
+ * Chinese: Burmese (my), Chinese (zh), Indonesian (id), Japanese (ja),
394
+ * Javanese (jv), Korean (ko), Malay (ms), Thai (th), Vietnamese (vi)
395
+ * French: Armenian (hy), Bangla (bn), French (fr), Gujarati (gu), Hindi (hi),
396
+ * Persian Farsi (fa), Punjabi (pa), Zulu (zu)
397
+ * German: Afrikaans (af), Albanian (sq), Azerbaijani (az), Basque (eu),
398
+ * Bulgarian (bg), Catalan (ca), Danish (da), Dutch (nl), English (en),
399
+ * Estonian (et), Finnish (fi), Georgian (ka), German (de), Greek (el),
400
+ * Hungarian (hu), Luxembourgish (lb), Norwegian (no), Somali (so),
401
+ * Swahili (sw), Swedish (sv), Tamil (ta), Telugu (te), Turkish (tr),
402
+ * Urdu (ur)
403
+ * Irish: Irish Gaelic (ga)
404
+ * Russian: Russian (ru), Ukrainian (uk)
405
+ * Scottish: Scottish Gaelic (gd)
406
+ * Spanish: European Portuguese (pt-PT), Italian (it), Spanish (es)
407
+ * Welsh: Welsh (cy)
408
+ *
409
+ * @type {Object<PluralRuleName, string[]>}
410
+ */
411
+ I18n.pluralRulesMap = {
412
+ arabic: ['ar'],
413
+ chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],
414
+ french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],
415
+ german: [
416
+ 'af', 'sq', 'az', 'eu', 'bg', 'ca', 'da', 'nl', 'en', 'et', 'fi', 'ka',
417
+ 'de', 'el', 'hu', 'lb', 'no', 'so', 'sw', 'sv', 'ta', 'te', 'tr', 'ur'
418
+ ],
419
+ irish: ['ga'],
420
+ russian: ['ru', 'uk'],
421
+ scottish: ['gd'],
422
+ spanish: ['pt-PT', 'it', 'es'],
423
+ welsh: ['cy']
424
+ };
425
+
426
+ /**
427
+ * Different pluralisation rule sets
428
+ *
429
+ * Returns the appropriate suffix for the plural form associated with `n`.
430
+ * Possible suffixes: 'zero', 'one', 'two', 'few', 'many', 'other' (the actual
431
+ * meaning of each differs per locale). 'other' should always exist, even in
432
+ * languages without plurals, such as Chinese.
433
+ * {@link https://cldr.unicode.org/index/cldr-spec/plural-rules}
434
+ *
435
+ * The count must be a positive integer. Negative numbers and decimals aren't accounted for
436
+ *
437
+ * @type {Object<string, function(number): PluralRule>}
438
+ */
439
+ I18n.pluralRules = {
440
+ arabic: function (n) {
441
+ if (n === 0) { return 'zero' }
442
+ if (n === 1) { return 'one' }
443
+ if (n === 2) { return 'two' }
444
+ if (n % 100 >= 3 && n % 100 <= 10) { return 'few' }
445
+ if (n % 100 >= 11 && n % 100 <= 99) { return 'many' }
446
+ return 'other'
447
+ },
448
+ chinese: function () {
449
+ return 'other'
450
+ },
451
+ french: function (n) {
452
+ return n === 0 || n === 1 ? 'one' : 'other'
453
+ },
454
+ german: function (n) {
455
+ return n === 1 ? 'one' : 'other'
456
+ },
457
+ irish: function (n) {
458
+ if (n === 1) { return 'one' }
459
+ if (n === 2) { return 'two' }
460
+ if (n >= 3 && n <= 6) { return 'few' }
461
+ if (n >= 7 && n <= 10) { return 'many' }
462
+ return 'other'
463
+ },
464
+ russian: function (n) {
465
+ var lastTwo = n % 100;
466
+ var last = lastTwo % 10;
467
+ if (last === 1 && lastTwo !== 11) { return 'one' }
468
+ if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) { return 'few' }
469
+ if (last === 0 || (last >= 5 && last <= 9) || (lastTwo >= 11 && lastTwo <= 14)) { return 'many' }
470
+ // Note: The 'other' suffix is only used by decimal numbers in Russian.
471
+ // We don't anticipate it being used, but it's here for consistency.
472
+ return 'other'
473
+ },
474
+ scottish: function (n) {
475
+ if (n === 1 || n === 11) { return 'one' }
476
+ if (n === 2 || n === 12) { return 'two' }
477
+ if ((n >= 3 && n <= 10) || (n >= 13 && n <= 19)) { return 'few' }
478
+ return 'other'
479
+ },
480
+ spanish: function (n) {
481
+ if (n === 1) { return 'one' }
482
+ if (n % 1000000 === 0 && n !== 0) { return 'many' }
483
+ return 'other'
484
+ },
485
+ welsh: function (n) {
486
+ if (n === 0) { return 'zero' }
487
+ if (n === 1) { return 'one' }
488
+ if (n === 2) { return 'two' }
489
+ if (n === 3) { return 'few' }
490
+ if (n === 6) { return 'many' }
491
+ return 'other'
492
+ }
493
+ };
494
+
495
+ /**
496
+ * Supported languages for plural rules
497
+ *
498
+ * @typedef {'arabic' | 'chinese' | 'french' | 'german' | 'irish' | 'russian' | 'scottish' | 'spanish' | 'welsh'} PluralRuleName
499
+ */
500
+
501
+ /**
502
+ * Plural rule category mnemonic tags
503
+ *
504
+ * @typedef {'zero' | 'one' | 'two' | 'few' | 'many' | 'other'} PluralRule
505
+ */
506
+
507
+ /**
508
+ * Translated message by plural rule they correspond to.
509
+ *
510
+ * Allows to group pluralised messages under a single key when passing
511
+ * translations to a component's constructor
512
+ *
513
+ * @typedef {object} TranslationPluralForms
514
+ * @property {string} [other] - General plural form
515
+ * @property {string} [zero] - Plural form used with 0
516
+ * @property {string} [one] - Plural form used with 1
517
+ * @property {string} [two] - Plural form used with 2
518
+ * @property {string} [few] - Plural form used for a few
519
+ * @property {string} [many] - Plural form used for many
520
+ */
521
+
522
+ /**
523
+ * Translated messages (flattened)
524
+ *
525
+ * @private
526
+ * @typedef {Object<string, string> | {}} TranslationsFlattened
527
+ */
528
+
21
529
  (function(undefined) {
22
530
 
23
531
  // Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Object/defineProperty/detect.js
@@ -758,13 +1266,188 @@ if (detect) return
758
1266
 
759
1267
  }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
760
1268
 
761
- function Accordion ($module) {
1269
+ (function(undefined) {
1270
+
1271
+ // Detection from https://github.com/mdn/content/blob/cf607d68522cd35ee7670782d3ee3a361eaef2e4/files/en-us/web/javascript/reference/global_objects/string/trim/index.md#polyfill
1272
+ var detect = ('trim' in String.prototype);
1273
+
1274
+ if (detect) return
1275
+
1276
+ // Polyfill from https://github.com/mdn/content/blob/cf607d68522cd35ee7670782d3ee3a361eaef2e4/files/en-us/web/javascript/reference/global_objects/string/trim/index.md#polyfill
1277
+ String.prototype.trim = function () {
1278
+ return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
1279
+ };
1280
+
1281
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
1282
+
1283
+ (function(undefined) {
1284
+
1285
+ // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/detect.js
1286
+ var detect = (function(){
1287
+ if (!document.documentElement.dataset) {
1288
+ return false;
1289
+ }
1290
+ var el = document.createElement('div');
1291
+ el.setAttribute("data-a-b", "c");
1292
+ return el.dataset && el.dataset.aB == "c";
1293
+ }());
1294
+
1295
+ if (detect) return
1296
+
1297
+ // Polyfill derived from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/polyfill.js
1298
+ Object.defineProperty(Element.prototype, 'dataset', {
1299
+ get: function() {
1300
+ var element = this;
1301
+ var attributes = this.attributes;
1302
+ var map = {};
1303
+
1304
+ for (var i = 0; i < attributes.length; i++) {
1305
+ var attribute = attributes[i];
1306
+
1307
+ // This regex has been edited from the original polyfill, to add
1308
+ // support for period (.) separators in data-* attribute names. These
1309
+ // are allowed in the HTML spec, but were not covered by the original
1310
+ // polyfill's regex. We use periods in our i18n implementation.
1311
+ if (attribute && attribute.name && (/^data-\w[.\w-]*$/).test(attribute.name)) {
1312
+ var name = attribute.name;
1313
+ var value = attribute.value;
1314
+
1315
+ var propName = name.substr(5).replace(/-./g, function (prop) {
1316
+ return prop.charAt(1).toUpperCase();
1317
+ });
1318
+
1319
+ // If this browser supports __defineGetter__ and __defineSetter__,
1320
+ // continue using defineProperty. If not (like IE 8 and below), we use
1321
+ // a hacky fallback which at least gives an object in the right format
1322
+ if ('__defineGetter__' in Object.prototype && '__defineSetter__' in Object.prototype) {
1323
+ Object.defineProperty(map, propName, {
1324
+ enumerable: true,
1325
+ get: function() {
1326
+ return this.value;
1327
+ }.bind({value: value || ''}),
1328
+ set: function setter(name, value) {
1329
+ if (typeof value !== 'undefined') {
1330
+ this.setAttribute(name, value);
1331
+ } else {
1332
+ this.removeAttribute(name);
1333
+ }
1334
+ }.bind(element, name)
1335
+ });
1336
+ } else {
1337
+ map[propName] = value;
1338
+ }
1339
+
1340
+ }
1341
+ }
1342
+
1343
+ return map;
1344
+ }
1345
+ });
1346
+
1347
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
1348
+
1349
+ /**
1350
+ * Normalise string
1351
+ *
1352
+ * 'If it looks like a duck, and it quacks like a duck…' 🦆
1353
+ *
1354
+ * If the passed value looks like a boolean or a number, convert it to a boolean
1355
+ * or number.
1356
+ *
1357
+ * Designed to be used to convert config passed via data attributes (which are
1358
+ * always strings) into something sensible.
1359
+ *
1360
+ * @param {string} value - The value to normalise
1361
+ * @returns {string | boolean | number | undefined} Normalised data
1362
+ */
1363
+ function normaliseString (value) {
1364
+ if (typeof value !== 'string') {
1365
+ return value
1366
+ }
1367
+
1368
+ var trimmedValue = value.trim();
1369
+
1370
+ if (trimmedValue === 'true') {
1371
+ return true
1372
+ }
1373
+
1374
+ if (trimmedValue === 'false') {
1375
+ return false
1376
+ }
1377
+
1378
+ // Empty / whitespace-only strings are considered finite so we need to check
1379
+ // the length of the trimmed string as well
1380
+ if (trimmedValue.length > 0 && isFinite(trimmedValue)) {
1381
+ return Number(trimmedValue)
1382
+ }
1383
+
1384
+ return value
1385
+ }
1386
+
1387
+ /**
1388
+ * Normalise dataset
1389
+ *
1390
+ * Loop over an object and normalise each value using normaliseData function
1391
+ *
1392
+ * @param {DOMStringMap} dataset - HTML element dataset
1393
+ * @returns {Object<string, string | boolean | number | undefined>} Normalised dataset
1394
+ */
1395
+ function normaliseDataset (dataset) {
1396
+ var out = {};
1397
+
1398
+ for (var key in dataset) {
1399
+ out[key] = normaliseString(dataset[key]);
1400
+ }
1401
+
1402
+ return out
1403
+ }
1404
+
1405
+ /**
1406
+ * @constant
1407
+ * @type {AccordionTranslations}
1408
+ * @see Default value for {@link AccordionConfig.i18n}
1409
+ * @default
1410
+ */
1411
+ var ACCORDION_TRANSLATIONS = {
1412
+ hideAllSections: 'Hide all sections',
1413
+ hideSection: 'Hide',
1414
+ hideSectionAriaLabel: 'Hide this section',
1415
+ showAllSections: 'Show all sections',
1416
+ showSection: 'Show',
1417
+ showSectionAriaLabel: 'Show this section'
1418
+ };
1419
+
1420
+ /**
1421
+ * Accordion component
1422
+ *
1423
+ * This allows a collection of sections to be collapsed by default, showing only
1424
+ * their headers. Sections can be expanded or collapsed individually by clicking
1425
+ * their headers. A "Show all sections" button is also added to the top of the
1426
+ * accordion, which switches to "Hide all sections" when all the sections are
1427
+ * expanded.
1428
+ *
1429
+ * The state of each section is saved to the DOM via the `aria-expanded`
1430
+ * attribute, which also provides accessibility.
1431
+ *
1432
+ * @class
1433
+ * @param {HTMLElement} $module - HTML element to use for accordion
1434
+ * @param {AccordionConfig} [config] - Accordion config
1435
+ */
1436
+ function Accordion ($module, config) {
762
1437
  this.$module = $module;
763
- this.moduleId = $module.getAttribute('id');
764
1438
  this.$sections = $module.querySelectorAll('.govuk-accordion__section');
765
- this.$showAllButton = '';
766
1439
  this.browserSupportsSessionStorage = helper.checkForSessionStorage();
767
1440
 
1441
+ var defaultConfig = {
1442
+ i18n: ACCORDION_TRANSLATIONS
1443
+ };
1444
+ this.config = mergeConfigs(
1445
+ defaultConfig,
1446
+ config || {},
1447
+ normaliseDataset($module.dataset)
1448
+ );
1449
+ this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
1450
+
768
1451
  this.controlsClass = 'govuk-accordion__controls';
769
1452
  this.showAllClass = 'govuk-accordion__show-all';
770
1453
  this.showAllTextClass = 'govuk-accordion__show-all-text';
@@ -855,7 +1538,7 @@ Accordion.prototype.constructHeaderMarkup = function ($headerWrapper, index) {
855
1538
  // Create a button element that will replace the '.govuk-accordion__section-button' span
856
1539
  var $button = document.createElement('button');
857
1540
  $button.setAttribute('type', 'button');
858
- $button.setAttribute('aria-controls', this.moduleId + '-content-' + (index + 1));
1541
+ $button.setAttribute('aria-controls', this.$module.id + '-content-' + (index + 1));
859
1542
 
860
1543
  // Copy all attributes (https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes) from $span to $button
861
1544
  for (var i = 0; i < $span.attributes.length; i++) {
@@ -972,17 +1655,34 @@ Accordion.prototype.setExpanded = function (expanded, $section) {
972
1655
  var $icon = $section.querySelector('.' + this.upChevronIconClass);
973
1656
  var $showHideText = $section.querySelector('.' + this.sectionShowHideTextClass);
974
1657
  var $button = $section.querySelector('.' + this.sectionButtonClass);
975
- var $newButtonText = expanded ? 'Hide' : 'Show';
976
-
977
- // Build additional copy of "this section" for assistive technology and place inside toggle link
978
- var $visuallyHiddenText = document.createElement('span');
979
- $visuallyHiddenText.classList.add('govuk-visually-hidden');
980
- $visuallyHiddenText.innerHTML = ' this section';
1658
+ var newButtonText = expanded
1659
+ ? this.i18n.t('hideSection')
1660
+ : this.i18n.t('showSection');
981
1661
 
982
- $showHideText.innerHTML = $newButtonText;
983
- $showHideText.appendChild($visuallyHiddenText);
1662
+ $showHideText.innerText = newButtonText;
984
1663
  $button.setAttribute('aria-expanded', expanded);
985
1664
 
1665
+ // Update aria-label combining
1666
+ var $header = $section.querySelector('.' + this.sectionHeadingTextClass);
1667
+ var ariaLabelParts = [$header.innerText.trim()];
1668
+
1669
+ var $summary = $section.querySelector('.' + this.sectionSummaryClass);
1670
+ if ($summary) {
1671
+ ariaLabelParts.push($summary.innerText.trim());
1672
+ }
1673
+
1674
+ var ariaLabelMessage = expanded
1675
+ ? this.i18n.t('hideSectionAriaLabel')
1676
+ : this.i18n.t('showSectionAriaLabel');
1677
+ ariaLabelParts.push(ariaLabelMessage);
1678
+
1679
+ /*
1680
+ * Join with a comma to add pause for assistive technology.
1681
+ * Example: [heading]Section A ,[pause] Show this section.
1682
+ * https://accessibility.blog.gov.uk/2017/12/18/what-working-on-gov-uk-navigation-taught-us-about-accessibility/
1683
+ */
1684
+ $button.setAttribute('aria-label', ariaLabelParts.join(' , '));
1685
+
986
1686
  // Swap icon, change class
987
1687
  if (expanded) {
988
1688
  $section.classList.add(this.sectionExpandedClass);
@@ -1017,9 +1717,11 @@ Accordion.prototype.checkIfAllSectionsOpen = function () {
1017
1717
  Accordion.prototype.updateShowAllButton = function (expanded) {
1018
1718
  var $showAllIcon = this.$showAllButton.querySelector('.' + this.upChevronIconClass);
1019
1719
  var $showAllText = this.$showAllButton.querySelector('.' + this.showAllTextClass);
1020
- var newButtonText = expanded ? 'Hide all sections' : 'Show all sections';
1720
+ var newButtonText = expanded
1721
+ ? this.i18n.t('hideAllSections')
1722
+ : this.i18n.t('showAllSections');
1021
1723
  this.$showAllButton.setAttribute('aria-expanded', expanded);
1022
- $showAllText.innerHTML = newButtonText;
1724
+ $showAllText.innerText = newButtonText;
1023
1725
 
1024
1726
  // Swap icon, toggle class
1025
1727
  if (expanded) {
@@ -1040,9 +1742,7 @@ var helper = {
1040
1742
  window.sessionStorage.removeItem(testString);
1041
1743
  return result
1042
1744
  } catch (exception) {
1043
- if ((typeof console === 'undefined' || typeof console.log === 'undefined')) {
1044
- console.log('Notice: sessionStorage not available.');
1045
- }
1745
+ return false
1046
1746
  }
1047
1747
  }
1048
1748
  };
@@ -1059,14 +1759,6 @@ Accordion.prototype.storeState = function ($section) {
1059
1759
  var contentId = $button.getAttribute('aria-controls');
1060
1760
  var contentState = $button.getAttribute('aria-expanded');
1061
1761
 
1062
- if (typeof contentId === 'undefined' && (typeof console === 'undefined' || typeof console.log === 'undefined')) {
1063
- console.error(new Error('No aria controls present in accordion section heading.'));
1064
- }
1065
-
1066
- if (typeof contentState === 'undefined' && (typeof console === 'undefined' || typeof console.log === 'undefined')) {
1067
- console.error(new Error('No aria expanded present in accordion section heading.'));
1068
- }
1069
-
1070
1762
  // Only set the state when both `contentId` and `contentState` are taken from the DOM.
1071
1763
  if (contentId && contentState) {
1072
1764
  window.sessionStorage.setItem(contentId, contentState);
@@ -1092,17 +1784,14 @@ Accordion.prototype.setInitialState = function ($section) {
1092
1784
  };
1093
1785
 
1094
1786
  /**
1095
- * Create an element to improve semantics of the section button with punctuation
1096
- * @return {object} DOM element
1097
- *
1098
- * Used to add pause (with a comma) for assistive technology.
1099
- * Example: [heading]Section A ,[pause] Show this section.
1100
- * https://accessibility.blog.gov.uk/2017/12/18/what-working-on-gov-uk-navigation-taught-us-about-accessibility/
1101
- *
1102
- * Adding punctuation to the button can also improve its general semantics by dividing its contents
1103
- * into thematic chunks.
1104
- * See https://github.com/alphagov/govuk-frontend/issues/2327#issuecomment-922957442
1105
- */
1787
+ * Create an element to improve semantics of the section button with punctuation
1788
+ *
1789
+ * @returns {HTMLSpanElement} DOM element
1790
+ *
1791
+ * Adding punctuation to the button can also improve its general semantics by dividing its contents
1792
+ * into thematic chunks.
1793
+ * See https://github.com/alphagov/govuk-frontend/issues/2327#issuecomment-922957442
1794
+ */
1106
1795
  Accordion.prototype.getButtonPunctuationEl = function () {
1107
1796
  var $punctuationEl = document.createElement('span');
1108
1797
  $punctuationEl.classList.add('govuk-visually-hidden', 'govuk-accordion__section-heading-divider');
@@ -1110,6 +1799,35 @@ Accordion.prototype.getButtonPunctuationEl = function () {
1110
1799
  return $punctuationEl
1111
1800
  };
1112
1801
 
1802
+ /**
1803
+ * Accordion config
1804
+ *
1805
+ * @typedef {object} AccordionConfig
1806
+ * @property {AccordionTranslations} [i18n = ACCORDION_TRANSLATIONS] - See constant {@link ACCORDION_TRANSLATIONS}
1807
+ */
1808
+
1809
+ /**
1810
+ * Accordion translations
1811
+ *
1812
+ * @typedef {object} AccordionTranslations
1813
+ *
1814
+ * Messages used by the component for the labels of its buttons. This includes
1815
+ * the visible text shown on screen, and text to help assistive technology users
1816
+ * for the buttons toggling each section.
1817
+ * @property {string} [hideAllSections] - The text content for the 'Hide all
1818
+ * sections' button, used when at least one section is expanded.
1819
+ * @property {string} [hideSection] - The text content for the 'Hide'
1820
+ * button, used when a section is expanded.
1821
+ * @property {string} [hideSectionAriaLabel] - The text content appended to the
1822
+ * 'Hide' button's accessible name when a section is expanded.
1823
+ * @property {string} [showAllSections] - The text content for the 'Show all
1824
+ * sections' button, used when all sections are collapsed.
1825
+ * @property {string} [showSection] - The text content for the 'Show'
1826
+ * button, used when a section is collapsed.
1827
+ * @property {string} [showSectionAriaLabel] - The text content appended to the
1828
+ * 'Show' button's accessible name when a section is expanded.
1829
+ */
1830
+
1113
1831
  return Accordion;
1114
1832
 
1115
1833
  })));