govuk_publishing_components 32.0.0 → 33.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/component_guide/accessibility-test.js +0 -1
  3. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-core.js +175 -0
  4. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-ecommerce-tracker.js +1 -1
  5. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js +5 -13
  6. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.js +80 -309
  7. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.js +2 -2
  8. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-specialist-link-tracker.js +140 -0
  9. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js +3 -0
  10. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js +1 -0
  11. data/app/assets/javascripts/govuk_publishing_components/components/accordion.js +12 -1
  12. data/app/assets/javascripts/govuk_publishing_components/components/layout-super-navigation-header.js +13 -4
  13. data/app/assets/javascripts/govuk_publishing_components/components/single-page-notification-button.js +24 -8
  14. data/app/assets/javascripts/govuk_publishing_components/vendor/lux/lux-reporter.js +83 -86
  15. data/app/assets/stylesheets/govuk_publishing_components/components/_big-number.scss +2 -5
  16. data/app/assets/stylesheets/govuk_publishing_components/components/_image-card.scss +1 -5
  17. data/app/assets/stylesheets/govuk_publishing_components/components/_input.scss +3 -5
  18. data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +10 -30
  19. data/app/assets/stylesheets/govuk_publishing_components/components/_search.scss +0 -7
  20. data/app/assets/stylesheets/govuk_publishing_components/components/_share-links.scss +0 -6
  21. data/app/views/govuk_publishing_components/components/_accordion.html.erb +14 -1
  22. data/app/views/govuk_publishing_components/components/_error_summary.html.erb +27 -26
  23. data/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb +2 -2
  24. data/app/views/govuk_publishing_components/components/_phase_banner.html.erb +1 -1
  25. data/app/views/govuk_publishing_components/components/_share_links.html.erb +18 -15
  26. data/app/views/govuk_publishing_components/components/_single_page_notification_button.html.erb +1 -1
  27. data/app/views/govuk_publishing_components/components/docs/accordion.yml +15 -3
  28. data/app/views/govuk_publishing_components/components/docs/button.yml +10 -0
  29. data/app/views/govuk_publishing_components/components/docs/share_links.yml +59 -30
  30. data/app/views/govuk_publishing_components/components/docs/single_page_notification_button.yml +10 -1
  31. data/app/views/govuk_publishing_components/components/feedback/_yes_no_banner.html.erb +3 -3
  32. data/config/locales/ar.yml +4 -1
  33. data/config/locales/az.yml +4 -1
  34. data/config/locales/be.yml +4 -1
  35. data/config/locales/bg.yml +4 -1
  36. data/config/locales/bn.yml +4 -1
  37. data/config/locales/cs.yml +4 -1
  38. data/config/locales/cy.yml +4 -1
  39. data/config/locales/da.yml +4 -1
  40. data/config/locales/de.yml +4 -1
  41. data/config/locales/dr.yml +4 -1
  42. data/config/locales/el.yml +4 -1
  43. data/config/locales/en.yml +20 -17
  44. data/config/locales/es-419.yml +4 -1
  45. data/config/locales/es.yml +4 -1
  46. data/config/locales/et.yml +4 -1
  47. data/config/locales/fa.yml +4 -1
  48. data/config/locales/fi.yml +4 -1
  49. data/config/locales/fr.yml +4 -1
  50. data/config/locales/gd.yml +4 -1
  51. data/config/locales/gu.yml +4 -1
  52. data/config/locales/he.yml +4 -1
  53. data/config/locales/hi.yml +4 -1
  54. data/config/locales/hr.yml +4 -1
  55. data/config/locales/hu.yml +4 -1
  56. data/config/locales/hy.yml +4 -1
  57. data/config/locales/id.yml +4 -1
  58. data/config/locales/is.yml +4 -1
  59. data/config/locales/it.yml +4 -1
  60. data/config/locales/ja.yml +4 -1
  61. data/config/locales/ka.yml +4 -1
  62. data/config/locales/kk.yml +4 -1
  63. data/config/locales/ko.yml +4 -1
  64. data/config/locales/lt.yml +4 -1
  65. data/config/locales/lv.yml +4 -1
  66. data/config/locales/ms.yml +4 -1
  67. data/config/locales/mt.yml +4 -1
  68. data/config/locales/nl.yml +4 -1
  69. data/config/locales/no.yml +4 -1
  70. data/config/locales/pa-pk.yml +4 -1
  71. data/config/locales/pa.yml +4 -1
  72. data/config/locales/pl.yml +4 -1
  73. data/config/locales/ps.yml +4 -1
  74. data/config/locales/pt.yml +4 -1
  75. data/config/locales/ro.yml +4 -1
  76. data/config/locales/ru.yml +4 -1
  77. data/config/locales/si.yml +4 -1
  78. data/config/locales/sk.yml +4 -1
  79. data/config/locales/sl.yml +4 -1
  80. data/config/locales/so.yml +4 -1
  81. data/config/locales/sq.yml +4 -1
  82. data/config/locales/sr.yml +4 -1
  83. data/config/locales/sv.yml +4 -1
  84. data/config/locales/sw.yml +4 -1
  85. data/config/locales/ta.yml +4 -1
  86. data/config/locales/th.yml +4 -1
  87. data/config/locales/tk.yml +4 -1
  88. data/config/locales/tr.yml +4 -1
  89. data/config/locales/uk.yml +4 -1
  90. data/config/locales/ur.yml +4 -1
  91. data/config/locales/uz.yml +4 -1
  92. data/config/locales/vi.yml +4 -1
  93. data/config/locales/zh-hk.yml +4 -1
  94. data/config/locales/zh-tw.yml +4 -1
  95. data/config/locales/zh.yml +4 -1
  96. data/lib/govuk_publishing_components/presenters/button_helper.rb +7 -1
  97. data/lib/govuk_publishing_components/presenters/single_page_notification_button_helper.rb +25 -1
  98. data/lib/govuk_publishing_components/version.rb +1 -1
  99. data/node_modules/axe-core/axe.js +4567 -4678
  100. data/node_modules/axe-core/axe.min.js +2 -2
  101. data/node_modules/axe-core/package.json +2 -2
  102. data/node_modules/axe-core/sri-history.json +8 -0
  103. data/node_modules/govuk-frontend/README.md +1 -2
  104. data/node_modules/govuk-frontend/govuk/all.js +1398 -273
  105. data/node_modules/govuk-frontend/govuk/common/closest-attribute-value.js +70 -0
  106. data/node_modules/govuk-frontend/govuk/common/index.js +172 -0
  107. data/node_modules/govuk-frontend/govuk/common/normalise-dataset.js +373 -0
  108. data/node_modules/govuk-frontend/govuk/common.js +138 -3
  109. data/node_modules/govuk-frontend/govuk/components/accordion/accordion.js +753 -25
  110. data/node_modules/govuk-frontend/govuk/components/accordion/fixtures.json +54 -22
  111. data/node_modules/govuk-frontend/govuk/components/accordion/macro-options.json +36 -0
  112. data/node_modules/govuk-frontend/govuk/components/accordion/template.njk +7 -1
  113. data/node_modules/govuk-frontend/govuk/components/back-link/fixtures.json +12 -12
  114. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/fixtures.json +22 -22
  115. data/node_modules/govuk-frontend/govuk/components/button/_index.scss +23 -5
  116. data/node_modules/govuk-frontend/govuk/components/button/button.js +365 -107
  117. data/node_modules/govuk-frontend/govuk/components/button/fixtures.json +85 -66
  118. data/node_modules/govuk-frontend/govuk/components/button/template.njk +1 -1
  119. data/node_modules/govuk-frontend/govuk/components/character-count/_index.scss +9 -0
  120. data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +1033 -121
  121. data/node_modules/govuk-frontend/govuk/components/character-count/fixtures.json +112 -36
  122. data/node_modules/govuk-frontend/govuk/components/character-count/macro-options.json +42 -0
  123. data/node_modules/govuk-frontend/govuk/components/character-count/template.njk +27 -3
  124. data/node_modules/govuk-frontend/govuk/components/checkboxes/checkboxes.js +30 -2
  125. data/node_modules/govuk-frontend/govuk/components/checkboxes/fixtures.json +96 -93
  126. data/node_modules/govuk-frontend/govuk/components/cookie-banner/fixtures.json +46 -46
  127. data/node_modules/govuk-frontend/govuk/components/date-input/fixtures.json +50 -50
  128. data/node_modules/govuk-frontend/govuk/components/details/details.js +43 -13
  129. data/node_modules/govuk-frontend/govuk/components/details/fixtures.json +20 -20
  130. data/node_modules/govuk-frontend/govuk/components/error-message/fixtures.json +20 -20
  131. data/node_modules/govuk-frontend/govuk/components/error-summary/error-summary.js +268 -6
  132. data/node_modules/govuk-frontend/govuk/components/error-summary/fixtures.json +44 -35
  133. data/node_modules/govuk-frontend/govuk/components/error-summary/template.njk +25 -21
  134. data/node_modules/govuk-frontend/govuk/components/fieldset/fixtures.json +51 -39
  135. data/node_modules/govuk-frontend/govuk/components/file-upload/fixtures.json +26 -26
  136. data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +1 -1
  137. data/node_modules/govuk-frontend/govuk/components/footer/fixtures.json +46 -46
  138. data/node_modules/govuk-frontend/govuk/components/footer/macro-options.json +2 -2
  139. data/node_modules/govuk-frontend/govuk/components/header/fixtures.json +93 -38
  140. data/node_modules/govuk-frontend/govuk/components/header/header.js +6 -0
  141. data/node_modules/govuk-frontend/govuk/components/header/macro-options.json +8 -2
  142. data/node_modules/govuk-frontend/govuk/components/header/template.njk +4 -2
  143. data/node_modules/govuk-frontend/govuk/components/hint/fixtures.json +12 -12
  144. data/node_modules/govuk-frontend/govuk/components/input/fixtures.json +80 -80
  145. data/node_modules/govuk-frontend/govuk/components/inset-text/fixtures.json +12 -12
  146. data/node_modules/govuk-frontend/govuk/components/label/fixtures.json +34 -34
  147. data/node_modules/govuk-frontend/govuk/components/notification-banner/fixtures.json +56 -46
  148. data/node_modules/govuk-frontend/govuk/components/notification-banner/notification-banner.js +252 -2
  149. data/node_modules/govuk-frontend/govuk/components/notification-banner/template.njk +1 -1
  150. data/node_modules/govuk-frontend/govuk/components/pagination/_index.scss +10 -7
  151. data/node_modules/govuk-frontend/govuk/components/pagination/fixtures.json +33 -26
  152. data/node_modules/govuk-frontend/govuk/components/panel/fixtures.json +18 -18
  153. data/node_modules/govuk-frontend/govuk/components/phase-banner/fixtures.json +14 -14
  154. data/node_modules/govuk-frontend/govuk/components/radios/fixtures.json +94 -91
  155. data/node_modules/govuk-frontend/govuk/components/radios/radios.js +30 -2
  156. data/node_modules/govuk-frontend/govuk/components/select/fixtures.json +32 -32
  157. data/node_modules/govuk-frontend/govuk/components/skip-link/fixtures.json +22 -20
  158. data/node_modules/govuk-frontend/govuk/components/skip-link/skip-link.js +10 -4
  159. data/node_modules/govuk-frontend/govuk/components/summary-list/fixtures.json +50 -50
  160. data/node_modules/govuk-frontend/govuk/components/table/_index.scss +1 -1
  161. data/node_modules/govuk-frontend/govuk/components/table/fixtures.json +40 -40
  162. data/node_modules/govuk-frontend/govuk/components/tabs/fixtures.json +29 -29
  163. data/node_modules/govuk-frontend/govuk/components/tabs/tabs.js +28 -0
  164. data/node_modules/govuk-frontend/govuk/components/tag/fixtures.json +28 -28
  165. data/node_modules/govuk-frontend/govuk/components/textarea/fixtures.json +34 -34
  166. data/node_modules/govuk-frontend/govuk/components/warning-text/fixtures.json +14 -14
  167. data/node_modules/govuk-frontend/govuk/core/_section-break.scss +1 -1
  168. data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +2 -2
  169. data/node_modules/govuk-frontend/govuk/helpers/_links.scss +6 -6
  170. data/node_modules/govuk-frontend/govuk/i18n.js +390 -0
  171. data/node_modules/govuk-frontend/govuk/macros/i18n.njk +15 -0
  172. data/node_modules/govuk-frontend/govuk/settings/_all.scss +1 -0
  173. data/node_modules/govuk-frontend/govuk/settings/_colours-palette.scss +12 -0
  174. data/node_modules/govuk-frontend/govuk/settings/_compatibility.scss +26 -0
  175. data/node_modules/govuk-frontend/govuk/settings/_typography-font.scss +23 -0
  176. data/node_modules/govuk-frontend/govuk/settings/_typography-responsive.scss +12 -0
  177. data/node_modules/govuk-frontend/govuk/settings/_warnings.scss +53 -0
  178. data/node_modules/govuk-frontend/govuk/tools/_compatibility.scss +20 -6
  179. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Date/now.js +21 -0
  180. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Element/prototype/dataset.js +300 -0
  181. data/node_modules/govuk-frontend/govuk/vendor/polyfills/String/prototype/trim.js +21 -0
  182. data/node_modules/govuk-frontend/govuk-esm/all.mjs +50 -27
  183. data/node_modules/govuk-frontend/govuk-esm/common/closest-attribute-value.mjs +15 -0
  184. data/node_modules/govuk-frontend/govuk-esm/common/index.mjs +159 -0
  185. data/node_modules/govuk-frontend/govuk-esm/common/normalise-dataset.mjs +58 -0
  186. data/node_modules/govuk-frontend/govuk-esm/common.mjs +6 -28
  187. data/node_modules/govuk-frontend/govuk-esm/components/accordion/accordion.mjs +113 -43
  188. data/node_modules/govuk-frontend/govuk-esm/components/button/button.mjs +67 -30
  189. data/node_modules/govuk-frontend/govuk-esm/components/character-count/character-count.mjs +325 -123
  190. data/node_modules/govuk-frontend/govuk-esm/components/checkboxes/checkboxes.mjs +9 -3
  191. data/node_modules/govuk-frontend/govuk-esm/components/details/details.mjs +22 -8
  192. data/node_modules/govuk-frontend/govuk-esm/components/error-summary/error-summary.mjs +48 -6
  193. data/node_modules/govuk-frontend/govuk-esm/components/header/header.mjs +6 -0
  194. data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs +32 -2
  195. data/node_modules/govuk-frontend/govuk-esm/components/radios/radios.mjs +9 -3
  196. data/node_modules/govuk-frontend/govuk-esm/components/skip-link/skip-link.mjs +10 -4
  197. data/node_modules/govuk-frontend/govuk-esm/components/tabs/tabs.mjs +8 -2
  198. data/node_modules/govuk-frontend/govuk-esm/i18n.mjs +380 -0
  199. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Date/now.mjs +13 -0
  200. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/dataset.mjs +68 -0
  201. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/String/prototype/trim.mjs +13 -0
  202. data/node_modules/govuk-frontend/govuk-prototype-kit/init.js +7 -0
  203. data/node_modules/govuk-frontend/govuk-prototype-kit/init.scss +12 -0
  204. data/node_modules/govuk-frontend/govuk-prototype-kit.config.json +138 -7
  205. data/node_modules/govuk-frontend/package.json +1 -1
  206. metadata +22 -3
@@ -4,10 +4,24 @@
4
4
  (factory((global.GOVUKFrontend = {})));
5
5
  }(this, (function (exports) { '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,9 +32,13 @@ function nodeListForEach (nodes, callback) {
18
32
  }
19
33
  }
20
34
 
21
- // Used to generate a unique string, allows multiple instances of the component without
22
- // Them conflicting with each other.
23
- // https://stackoverflow.com/a/8809472
35
+ /**
36
+ * Used to generate a unique string, allows multiple instances of the component
37
+ * without them conflicting with each other.
38
+ * https://stackoverflow.com/a/8809472
39
+ *
40
+ * @returns {string} Unique ID
41
+ */
24
42
  function generateUniqueID () {
25
43
  var d = new Date().getTime();
26
44
  if (typeof window.performance !== 'undefined' && typeof window.performance.now === 'function') {
@@ -33,6 +51,500 @@ function generateUniqueID () {
33
51
  })
34
52
  }
35
53
 
54
+ /**
55
+ * Config flattening function
56
+ *
57
+ * Takes any number of objects, flattens them into namespaced key-value pairs,
58
+ * (e.g. {'i18n.showSection': 'Show section'}) and combines them together, with
59
+ * greatest priority on the LAST item passed in.
60
+ *
61
+ * @returns {object} A flattened object of key-value pairs.
62
+ */
63
+ function mergeConfigs (/* configObject1, configObject2, ...configObjects */) {
64
+ /**
65
+ * Function to take nested objects and flatten them to a dot-separated keyed
66
+ * object. Doing this means we don't need to do any deep/recursive merging of
67
+ * each of our objects, nor transform our dataset from a flat list into a
68
+ * nested object.
69
+ *
70
+ * @param {object} configObject - Deeply nested object
71
+ * @returns {object} Flattened object with dot-separated keys
72
+ */
73
+ var flattenObject = function (configObject) {
74
+ // Prepare an empty return object
75
+ var flattenedObject = {};
76
+
77
+ // Our flattening function, this is called recursively for each level of
78
+ // depth in the object. At each level we prepend the previous level names to
79
+ // the key using `prefix`.
80
+ var flattenLoop = function (obj, prefix) {
81
+ // Loop through keys...
82
+ for (var key in obj) {
83
+ // Check to see if this is a prototypical key/value,
84
+ // if it is, skip it.
85
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
86
+ continue
87
+ }
88
+ var value = obj[key];
89
+ var prefixedKey = prefix ? prefix + '.' + key : key;
90
+ if (typeof value === 'object') {
91
+ // If the value is a nested object, recurse over that too
92
+ flattenLoop(value, prefixedKey);
93
+ } else {
94
+ // Otherwise, add this value to our return object
95
+ flattenedObject[prefixedKey] = value;
96
+ }
97
+ }
98
+ };
99
+
100
+ // Kick off the recursive loop
101
+ flattenLoop(configObject);
102
+ return flattenedObject
103
+ };
104
+
105
+ // Start with an empty object as our base
106
+ var formattedConfigObject = {};
107
+
108
+ // Loop through each of the remaining passed objects and push their keys
109
+ // one-by-one into configObject. Any duplicate keys will override the existing
110
+ // key with the new value.
111
+ for (var i = 0; i < arguments.length; i++) {
112
+ var obj = flattenObject(arguments[i]);
113
+ for (var key in obj) {
114
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
115
+ formattedConfigObject[key] = obj[key];
116
+ }
117
+ }
118
+ }
119
+
120
+ return formattedConfigObject
121
+ }
122
+
123
+ /**
124
+ * Extracts keys starting with a particular namespace from a flattened config
125
+ * object, removing the namespace in the process.
126
+ *
127
+ * @param {object} configObject - The object to extract key-value pairs from.
128
+ * @param {string} namespace - The namespace to filter keys with.
129
+ * @returns {object} Flattened object with dot-separated key namespace removed
130
+ */
131
+ function extractConfigByNamespace (configObject, namespace) {
132
+ // Check we have what we need
133
+ if (!configObject || typeof configObject !== 'object') {
134
+ throw new Error('Provide a `configObject` of type "object".')
135
+ }
136
+ if (!namespace || typeof namespace !== 'string') {
137
+ throw new Error('Provide a `namespace` of type "string" to filter the `configObject` by.')
138
+ }
139
+ var newObject = {};
140
+ for (var key in configObject) {
141
+ // Split the key into parts, using . as our namespace separator
142
+ var keyParts = key.split('.');
143
+ // Check if the first namespace matches the configured namespace
144
+ if (Object.prototype.hasOwnProperty.call(configObject, key) && keyParts[0] === namespace) {
145
+ // Remove the first item (the namespace) from the parts array,
146
+ // but only if there is more than one part (we don't want blank keys!)
147
+ if (keyParts.length > 1) {
148
+ keyParts.shift();
149
+ }
150
+ // Join the remaining parts back together
151
+ var newKey = keyParts.join('.');
152
+ // Add them to our new object
153
+ newObject[newKey] = configObject[key];
154
+ }
155
+ }
156
+ return newObject
157
+ }
158
+
159
+ /**
160
+ * @callback nodeListIterator
161
+ * @param {Element} value - The current node being iterated on
162
+ * @param {number} index - The current index in the iteration
163
+ * @param {NodeListOf<Element>} nodes - NodeList from querySelectorAll()
164
+ * @returns {undefined}
165
+ */
166
+
167
+ /**
168
+ * Internal support for selecting messages to render, with placeholder
169
+ * interpolation and locale-aware number formatting and pluralisation
170
+ *
171
+ * @class
172
+ * @private
173
+ * @param {TranslationsFlattened} translations - Key-value pairs of the translation strings to use.
174
+ * @param {object} [config] - Configuration options for the function.
175
+ * @param {string} config.locale - An overriding locale for the PluralRules functionality.
176
+ */
177
+ function I18n (translations, config) {
178
+ // Make list of translations available throughout function
179
+ this.translations = translations || {};
180
+
181
+ // The locale to use for PluralRules and NumberFormat
182
+ this.locale = (config && config.locale) || document.documentElement.lang || 'en';
183
+ }
184
+
185
+ /**
186
+ * The most used function - takes the key for a given piece of UI text and
187
+ * returns the appropriate string.
188
+ *
189
+ * @param {string} lookupKey - The lookup key of the string to use.
190
+ * @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
191
+ * @returns {string} The appropriate translation string.
192
+ */
193
+ I18n.prototype.t = function (lookupKey, options) {
194
+ if (!lookupKey) {
195
+ // Print a console error if no lookup key has been provided
196
+ throw new Error('i18n: lookup key missing')
197
+ }
198
+
199
+ // If the `count` option is set, determine which plural suffix is needed and
200
+ // change the lookupKey to match. We check to see if it's undefined instead of
201
+ // falsy, as this could legitimately be 0.
202
+ if (options && typeof options.count !== 'undefined') {
203
+ // Get the plural suffix
204
+ lookupKey = lookupKey + '.' + this.getPluralSuffix(lookupKey, options.count);
205
+ }
206
+
207
+ if (lookupKey in this.translations) {
208
+ // Fetch the translation string for that lookup key
209
+ var translationString = this.translations[lookupKey];
210
+
211
+ // Check for ${} placeholders in the translation string
212
+ if (translationString.match(/%{(.\S+)}/)) {
213
+ if (!options) {
214
+ throw new Error('i18n: cannot replace placeholders in string if no option data provided')
215
+ }
216
+
217
+ return this.replacePlaceholders(translationString, options)
218
+ } else {
219
+ return translationString
220
+ }
221
+ } else {
222
+ // If the key wasn't found in our translations object,
223
+ // return the lookup key itself as the fallback
224
+ return lookupKey
225
+ }
226
+ };
227
+
228
+ /**
229
+ * Takes a translation string with placeholders, and replaces the placeholders
230
+ * with the provided data
231
+ *
232
+ * @param {string} translationString - The translation string
233
+ * @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
234
+ * @returns {string} The translation string to output, with ${} placeholders replaced
235
+ */
236
+ I18n.prototype.replacePlaceholders = function (translationString, options) {
237
+ var formatter;
238
+
239
+ if (this.hasIntlNumberFormatSupport()) {
240
+ formatter = new Intl.NumberFormat(this.locale);
241
+ }
242
+
243
+ return translationString.replace(/%{(.\S+)}/g, function (placeholderWithBraces, placeholderKey) {
244
+ if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {
245
+ var placeholderValue = options[placeholderKey];
246
+
247
+ // If a user has passed `false` as the value for the placeholder
248
+ // treat it as though the value should not be displayed
249
+ if (placeholderValue === false) {
250
+ return ''
251
+ }
252
+
253
+ // If the placeholder's value is a number, localise the number formatting
254
+ if (typeof placeholderValue === 'number' && formatter) {
255
+ return formatter.format(placeholderValue)
256
+ }
257
+
258
+ return placeholderValue
259
+ } else {
260
+ throw new Error('i18n: no data found to replace ' + placeholderWithBraces + ' placeholder in string')
261
+ }
262
+ })
263
+ };
264
+
265
+ /**
266
+ * Check to see if the browser supports Intl and Intl.PluralRules.
267
+ *
268
+ * It requires all conditions to be met in order to be supported:
269
+ * - The browser supports the Intl class (true in IE11)
270
+ * - The implementation of Intl supports PluralRules (NOT true in IE11)
271
+ * - The browser/OS has plural rules for the current locale (browser dependent)
272
+ *
273
+ * @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
274
+ */
275
+ I18n.prototype.hasIntlPluralRulesSupport = function () {
276
+ return Boolean(window.Intl && ('PluralRules' in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length))
277
+ };
278
+
279
+ /**
280
+ * Check to see if the browser supports Intl and Intl.NumberFormat.
281
+ *
282
+ * It requires all conditions to be met in order to be supported:
283
+ * - The browser supports the Intl class (true in IE11)
284
+ * - The implementation of Intl supports NumberFormat (also true in IE11)
285
+ * - The browser/OS has number formatting rules for the current locale (browser dependent)
286
+ *
287
+ * @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
288
+ */
289
+ I18n.prototype.hasIntlNumberFormatSupport = function () {
290
+ return Boolean(window.Intl && ('NumberFormat' in window.Intl && Intl.NumberFormat.supportedLocalesOf(this.locale).length))
291
+ };
292
+
293
+ /**
294
+ * Get the appropriate suffix for the plural form.
295
+ *
296
+ * Uses Intl.PluralRules (or our own fallback implementation) to get the
297
+ * 'preferred' form to use for the given count.
298
+ *
299
+ * Checks that a translation has been provided for that plural form – if it
300
+ * hasn't, it'll fall back to the 'other' plural form (unless that doesn't exist
301
+ * either, in which case an error will be thrown)
302
+ *
303
+ * @param {string} lookupKey - The lookup key of the string to use.
304
+ * @param {number} count - Number used to determine which pluralisation to use.
305
+ * @returns {PluralRule} The suffix associated with the correct pluralisation for this locale.
306
+ */
307
+ I18n.prototype.getPluralSuffix = function (lookupKey, count) {
308
+ // Validate that the number is actually a number.
309
+ //
310
+ // Number(count) will turn anything that can't be converted to a Number type
311
+ // into 'NaN'. isFinite filters out NaN, as it isn't a finite number.
312
+ count = Number(count);
313
+ if (!isFinite(count)) { return 'other' }
314
+
315
+ var preferredForm;
316
+
317
+ // Check to verify that all the requirements for Intl.PluralRules are met.
318
+ // If so, we can use that instead of our custom implementation. Otherwise,
319
+ // use the hardcoded fallback.
320
+ if (this.hasIntlPluralRulesSupport()) {
321
+ preferredForm = new Intl.PluralRules(this.locale).select(count);
322
+ } else {
323
+ preferredForm = this.selectPluralFormUsingFallbackRules(count);
324
+ }
325
+
326
+ // Use the correct plural form if provided
327
+ if (lookupKey + '.' + preferredForm in this.translations) {
328
+ return preferredForm
329
+ // Fall back to `other` if the plural form is missing, but log a warning
330
+ // to the console
331
+ } else if (lookupKey + '.other' in this.translations) {
332
+ if (console && 'warn' in console) {
333
+ console.warn('i18n: Missing plural form ".' + preferredForm + '" for "' +
334
+ this.locale + '" locale. Falling back to ".other".');
335
+ }
336
+
337
+ return 'other'
338
+ // If the required `other` plural form is missing, all we can do is error
339
+ } else {
340
+ throw new Error(
341
+ 'i18n: Plural form ".other" is required for "' + this.locale + '" locale'
342
+ )
343
+ }
344
+ };
345
+
346
+ /**
347
+ * Get the plural form using our fallback implementation
348
+ *
349
+ * This is split out into a separate function to make it easier to test the
350
+ * fallback behaviour in an environment where Intl.PluralRules exists.
351
+ *
352
+ * @param {number} count - Number used to determine which pluralisation to use.
353
+ * @returns {PluralRule} The pluralisation form for count in this locale.
354
+ */
355
+ I18n.prototype.selectPluralFormUsingFallbackRules = function (count) {
356
+ // Currently our custom code can only handle positive integers, so let's
357
+ // make sure our number is one of those.
358
+ count = Math.abs(Math.floor(count));
359
+
360
+ var ruleset = this.getPluralRulesForLocale();
361
+
362
+ if (ruleset) {
363
+ return I18n.pluralRules[ruleset](count)
364
+ }
365
+
366
+ return 'other'
367
+ };
368
+
369
+ /**
370
+ * Work out which pluralisation rules to use for the current locale
371
+ *
372
+ * The locale may include a regional indicator (such as en-GB), but we don't
373
+ * usually care about this part, as pluralisation rules are usually the same
374
+ * regardless of region. There are exceptions, however, (e.g. Portuguese) so
375
+ * this searches by both the full and shortened locale codes, just to be sure.
376
+ *
377
+ * @returns {PluralRuleName | undefined} The name of the pluralisation rule to use (a key for one
378
+ * of the functions in this.pluralRules)
379
+ */
380
+ I18n.prototype.getPluralRulesForLocale = function () {
381
+ var locale = this.locale;
382
+ var localeShort = locale.split('-')[0];
383
+
384
+ // Look through the plural rules map to find which `pluralRule` is
385
+ // appropriate for our current `locale`.
386
+ for (var pluralRule in I18n.pluralRulesMap) {
387
+ if (Object.prototype.hasOwnProperty.call(I18n.pluralRulesMap, pluralRule)) {
388
+ var languages = I18n.pluralRulesMap[pluralRule];
389
+ for (var i = 0; i < languages.length; i++) {
390
+ if (languages[i] === locale || languages[i] === localeShort) {
391
+ return pluralRule
392
+ }
393
+ }
394
+ }
395
+ }
396
+ };
397
+
398
+ /**
399
+ * Map of plural rules to languages where those rules apply.
400
+ *
401
+ * Note: These groups are named for the most dominant or recognisable language
402
+ * that uses each system. The groupings do not imply that the languages are
403
+ * related to one another. Many languages have evolved the same systems
404
+ * independently of one another.
405
+ *
406
+ * Code to support more languages can be found in the i18n spike:
407
+ * {@link https://github.com/alphagov/govuk-frontend/blob/spike-i18n-support/src/govuk/i18n.mjs}
408
+ *
409
+ * Languages currently supported:
410
+ *
411
+ * Arabic: Arabic (ar)
412
+ * Chinese: Burmese (my), Chinese (zh), Indonesian (id), Japanese (ja),
413
+ * Javanese (jv), Korean (ko), Malay (ms), Thai (th), Vietnamese (vi)
414
+ * French: Armenian (hy), Bangla (bn), French (fr), Gujarati (gu), Hindi (hi),
415
+ * Persian Farsi (fa), Punjabi (pa), Zulu (zu)
416
+ * German: Afrikaans (af), Albanian (sq), Azerbaijani (az), Basque (eu),
417
+ * Bulgarian (bg), Catalan (ca), Danish (da), Dutch (nl), English (en),
418
+ * Estonian (et), Finnish (fi), Georgian (ka), German (de), Greek (el),
419
+ * Hungarian (hu), Luxembourgish (lb), Norwegian (no), Somali (so),
420
+ * Swahili (sw), Swedish (sv), Tamil (ta), Telugu (te), Turkish (tr),
421
+ * Urdu (ur)
422
+ * Irish: Irish Gaelic (ga)
423
+ * Russian: Russian (ru), Ukrainian (uk)
424
+ * Scottish: Scottish Gaelic (gd)
425
+ * Spanish: European Portuguese (pt-PT), Italian (it), Spanish (es)
426
+ * Welsh: Welsh (cy)
427
+ *
428
+ * @type {Object<PluralRuleName, string[]>}
429
+ */
430
+ I18n.pluralRulesMap = {
431
+ arabic: ['ar'],
432
+ chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],
433
+ french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],
434
+ german: [
435
+ 'af', 'sq', 'az', 'eu', 'bg', 'ca', 'da', 'nl', 'en', 'et', 'fi', 'ka',
436
+ 'de', 'el', 'hu', 'lb', 'no', 'so', 'sw', 'sv', 'ta', 'te', 'tr', 'ur'
437
+ ],
438
+ irish: ['ga'],
439
+ russian: ['ru', 'uk'],
440
+ scottish: ['gd'],
441
+ spanish: ['pt-PT', 'it', 'es'],
442
+ welsh: ['cy']
443
+ };
444
+
445
+ /**
446
+ * Different pluralisation rule sets
447
+ *
448
+ * Returns the appropriate suffix for the plural form associated with `n`.
449
+ * Possible suffixes: 'zero', 'one', 'two', 'few', 'many', 'other' (the actual
450
+ * meaning of each differs per locale). 'other' should always exist, even in
451
+ * languages without plurals, such as Chinese.
452
+ * {@link https://cldr.unicode.org/index/cldr-spec/plural-rules}
453
+ *
454
+ * The count must be a positive integer. Negative numbers and decimals aren't accounted for
455
+ *
456
+ * @type {Object<string, function(number): PluralRule>}
457
+ */
458
+ I18n.pluralRules = {
459
+ arabic: function (n) {
460
+ if (n === 0) { return 'zero' }
461
+ if (n === 1) { return 'one' }
462
+ if (n === 2) { return 'two' }
463
+ if (n % 100 >= 3 && n % 100 <= 10) { return 'few' }
464
+ if (n % 100 >= 11 && n % 100 <= 99) { return 'many' }
465
+ return 'other'
466
+ },
467
+ chinese: function () {
468
+ return 'other'
469
+ },
470
+ french: function (n) {
471
+ return n === 0 || n === 1 ? 'one' : 'other'
472
+ },
473
+ german: function (n) {
474
+ return n === 1 ? 'one' : 'other'
475
+ },
476
+ irish: function (n) {
477
+ if (n === 1) { return 'one' }
478
+ if (n === 2) { return 'two' }
479
+ if (n >= 3 && n <= 6) { return 'few' }
480
+ if (n >= 7 && n <= 10) { return 'many' }
481
+ return 'other'
482
+ },
483
+ russian: function (n) {
484
+ var lastTwo = n % 100;
485
+ var last = lastTwo % 10;
486
+ if (last === 1 && lastTwo !== 11) { return 'one' }
487
+ if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) { return 'few' }
488
+ if (last === 0 || (last >= 5 && last <= 9) || (lastTwo >= 11 && lastTwo <= 14)) { return 'many' }
489
+ // Note: The 'other' suffix is only used by decimal numbers in Russian.
490
+ // We don't anticipate it being used, but it's here for consistency.
491
+ return 'other'
492
+ },
493
+ scottish: function (n) {
494
+ if (n === 1 || n === 11) { return 'one' }
495
+ if (n === 2 || n === 12) { return 'two' }
496
+ if ((n >= 3 && n <= 10) || (n >= 13 && n <= 19)) { return 'few' }
497
+ return 'other'
498
+ },
499
+ spanish: function (n) {
500
+ if (n === 1) { return 'one' }
501
+ if (n % 1000000 === 0 && n !== 0) { return 'many' }
502
+ return 'other'
503
+ },
504
+ welsh: function (n) {
505
+ if (n === 0) { return 'zero' }
506
+ if (n === 1) { return 'one' }
507
+ if (n === 2) { return 'two' }
508
+ if (n === 3) { return 'few' }
509
+ if (n === 6) { return 'many' }
510
+ return 'other'
511
+ }
512
+ };
513
+
514
+ /**
515
+ * Supported languages for plural rules
516
+ *
517
+ * @typedef {'arabic' | 'chinese' | 'french' | 'german' | 'irish' | 'russian' | 'scottish' | 'spanish' | 'welsh'} PluralRuleName
518
+ */
519
+
520
+ /**
521
+ * Plural rule category mnemonic tags
522
+ *
523
+ * @typedef {'zero' | 'one' | 'two' | 'few' | 'many' | 'other'} PluralRule
524
+ */
525
+
526
+ /**
527
+ * Translated message by plural rule they correspond to.
528
+ *
529
+ * Allows to group pluralised messages under a single key when passing
530
+ * translations to a component's constructor
531
+ *
532
+ * @typedef {object} TranslationPluralForms
533
+ * @property {string} [other] - General plural form
534
+ * @property {string} [zero] - Plural form used with 0
535
+ * @property {string} [one] - Plural form used with 1
536
+ * @property {string} [two] - Plural form used with 2
537
+ * @property {string} [few] - Plural form used for a few
538
+ * @property {string} [many] - Plural form used for many
539
+ */
540
+
541
+ /**
542
+ * Translated messages (flattened)
543
+ *
544
+ * @private
545
+ * @typedef {Object<string, string> | {}} TranslationsFlattened
546
+ */
547
+
36
548
  (function(undefined) {
37
549
 
38
550
  // Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Object/defineProperty/detect.js
@@ -773,13 +1285,188 @@ if (detect) return
773
1285
 
774
1286
  }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
775
1287
 
776
- function Accordion ($module) {
1288
+ (function(undefined) {
1289
+
1290
+ // Detection from https://github.com/mdn/content/blob/cf607d68522cd35ee7670782d3ee3a361eaef2e4/files/en-us/web/javascript/reference/global_objects/string/trim/index.md#polyfill
1291
+ var detect = ('trim' in String.prototype);
1292
+
1293
+ if (detect) return
1294
+
1295
+ // Polyfill from https://github.com/mdn/content/blob/cf607d68522cd35ee7670782d3ee3a361eaef2e4/files/en-us/web/javascript/reference/global_objects/string/trim/index.md#polyfill
1296
+ String.prototype.trim = function () {
1297
+ return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
1298
+ };
1299
+
1300
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
1301
+
1302
+ (function(undefined) {
1303
+
1304
+ // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/detect.js
1305
+ var detect = (function(){
1306
+ if (!document.documentElement.dataset) {
1307
+ return false;
1308
+ }
1309
+ var el = document.createElement('div');
1310
+ el.setAttribute("data-a-b", "c");
1311
+ return el.dataset && el.dataset.aB == "c";
1312
+ }());
1313
+
1314
+ if (detect) return
1315
+
1316
+ // Polyfill derived from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/polyfill.js
1317
+ Object.defineProperty(Element.prototype, 'dataset', {
1318
+ get: function() {
1319
+ var element = this;
1320
+ var attributes = this.attributes;
1321
+ var map = {};
1322
+
1323
+ for (var i = 0; i < attributes.length; i++) {
1324
+ var attribute = attributes[i];
1325
+
1326
+ // This regex has been edited from the original polyfill, to add
1327
+ // support for period (.) separators in data-* attribute names. These
1328
+ // are allowed in the HTML spec, but were not covered by the original
1329
+ // polyfill's regex. We use periods in our i18n implementation.
1330
+ if (attribute && attribute.name && (/^data-\w[.\w-]*$/).test(attribute.name)) {
1331
+ var name = attribute.name;
1332
+ var value = attribute.value;
1333
+
1334
+ var propName = name.substr(5).replace(/-./g, function (prop) {
1335
+ return prop.charAt(1).toUpperCase();
1336
+ });
1337
+
1338
+ // If this browser supports __defineGetter__ and __defineSetter__,
1339
+ // continue using defineProperty. If not (like IE 8 and below), we use
1340
+ // a hacky fallback which at least gives an object in the right format
1341
+ if ('__defineGetter__' in Object.prototype && '__defineSetter__' in Object.prototype) {
1342
+ Object.defineProperty(map, propName, {
1343
+ enumerable: true,
1344
+ get: function() {
1345
+ return this.value;
1346
+ }.bind({value: value || ''}),
1347
+ set: function setter(name, value) {
1348
+ if (typeof value !== 'undefined') {
1349
+ this.setAttribute(name, value);
1350
+ } else {
1351
+ this.removeAttribute(name);
1352
+ }
1353
+ }.bind(element, name)
1354
+ });
1355
+ } else {
1356
+ map[propName] = value;
1357
+ }
1358
+
1359
+ }
1360
+ }
1361
+
1362
+ return map;
1363
+ }
1364
+ });
1365
+
1366
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
1367
+
1368
+ /**
1369
+ * Normalise string
1370
+ *
1371
+ * 'If it looks like a duck, and it quacks like a duck…' 🦆
1372
+ *
1373
+ * If the passed value looks like a boolean or a number, convert it to a boolean
1374
+ * or number.
1375
+ *
1376
+ * Designed to be used to convert config passed via data attributes (which are
1377
+ * always strings) into something sensible.
1378
+ *
1379
+ * @param {string} value - The value to normalise
1380
+ * @returns {string | boolean | number | undefined} Normalised data
1381
+ */
1382
+ function normaliseString (value) {
1383
+ if (typeof value !== 'string') {
1384
+ return value
1385
+ }
1386
+
1387
+ var trimmedValue = value.trim();
1388
+
1389
+ if (trimmedValue === 'true') {
1390
+ return true
1391
+ }
1392
+
1393
+ if (trimmedValue === 'false') {
1394
+ return false
1395
+ }
1396
+
1397
+ // Empty / whitespace-only strings are considered finite so we need to check
1398
+ // the length of the trimmed string as well
1399
+ if (trimmedValue.length > 0 && isFinite(trimmedValue)) {
1400
+ return Number(trimmedValue)
1401
+ }
1402
+
1403
+ return value
1404
+ }
1405
+
1406
+ /**
1407
+ * Normalise dataset
1408
+ *
1409
+ * Loop over an object and normalise each value using normaliseData function
1410
+ *
1411
+ * @param {DOMStringMap} dataset - HTML element dataset
1412
+ * @returns {Object<string, string | boolean | number | undefined>} Normalised dataset
1413
+ */
1414
+ function normaliseDataset (dataset) {
1415
+ var out = {};
1416
+
1417
+ for (var key in dataset) {
1418
+ out[key] = normaliseString(dataset[key]);
1419
+ }
1420
+
1421
+ return out
1422
+ }
1423
+
1424
+ /**
1425
+ * @constant
1426
+ * @type {AccordionTranslations}
1427
+ * @see Default value for {@link AccordionConfig.i18n}
1428
+ * @default
1429
+ */
1430
+ var ACCORDION_TRANSLATIONS = {
1431
+ hideAllSections: 'Hide all sections',
1432
+ hideSection: 'Hide',
1433
+ hideSectionAriaLabel: 'Hide this section',
1434
+ showAllSections: 'Show all sections',
1435
+ showSection: 'Show',
1436
+ showSectionAriaLabel: 'Show this section'
1437
+ };
1438
+
1439
+ /**
1440
+ * Accordion component
1441
+ *
1442
+ * This allows a collection of sections to be collapsed by default, showing only
1443
+ * their headers. Sections can be expanded or collapsed individually by clicking
1444
+ * their headers. A "Show all sections" button is also added to the top of the
1445
+ * accordion, which switches to "Hide all sections" when all the sections are
1446
+ * expanded.
1447
+ *
1448
+ * The state of each section is saved to the DOM via the `aria-expanded`
1449
+ * attribute, which also provides accessibility.
1450
+ *
1451
+ * @class
1452
+ * @param {HTMLElement} $module - HTML element to use for accordion
1453
+ * @param {AccordionConfig} [config] - Accordion config
1454
+ */
1455
+ function Accordion ($module, config) {
777
1456
  this.$module = $module;
778
- this.moduleId = $module.getAttribute('id');
779
1457
  this.$sections = $module.querySelectorAll('.govuk-accordion__section');
780
- this.$showAllButton = '';
781
1458
  this.browserSupportsSessionStorage = helper.checkForSessionStorage();
782
1459
 
1460
+ var defaultConfig = {
1461
+ i18n: ACCORDION_TRANSLATIONS
1462
+ };
1463
+ this.config = mergeConfigs(
1464
+ defaultConfig,
1465
+ config || {},
1466
+ normaliseDataset($module.dataset)
1467
+ );
1468
+ this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
1469
+
783
1470
  this.controlsClass = 'govuk-accordion__controls';
784
1471
  this.showAllClass = 'govuk-accordion__show-all';
785
1472
  this.showAllTextClass = 'govuk-accordion__show-all-text';
@@ -870,7 +1557,7 @@ Accordion.prototype.constructHeaderMarkup = function ($headerWrapper, index) {
870
1557
  // Create a button element that will replace the '.govuk-accordion__section-button' span
871
1558
  var $button = document.createElement('button');
872
1559
  $button.setAttribute('type', 'button');
873
- $button.setAttribute('aria-controls', this.moduleId + '-content-' + (index + 1));
1560
+ $button.setAttribute('aria-controls', this.$module.id + '-content-' + (index + 1));
874
1561
 
875
1562
  // Copy all attributes (https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes) from $span to $button
876
1563
  for (var i = 0; i < $span.attributes.length; i++) {
@@ -987,17 +1674,34 @@ Accordion.prototype.setExpanded = function (expanded, $section) {
987
1674
  var $icon = $section.querySelector('.' + this.upChevronIconClass);
988
1675
  var $showHideText = $section.querySelector('.' + this.sectionShowHideTextClass);
989
1676
  var $button = $section.querySelector('.' + this.sectionButtonClass);
990
- var newButtonText = expanded ? 'Hide' : 'Show';
991
-
992
- // Build additional copy of "this section" for assistive technology and place inside toggle link
993
- var $visuallyHiddenText = document.createElement('span');
994
- $visuallyHiddenText.classList.add('govuk-visually-hidden');
995
- $visuallyHiddenText.innerHTML = ' this section';
1677
+ var newButtonText = expanded
1678
+ ? this.i18n.t('hideSection')
1679
+ : this.i18n.t('showSection');
996
1680
 
997
- $showHideText.innerHTML = newButtonText;
998
- $showHideText.appendChild($visuallyHiddenText);
1681
+ $showHideText.innerText = newButtonText;
999
1682
  $button.setAttribute('aria-expanded', expanded);
1000
1683
 
1684
+ // Update aria-label combining
1685
+ var $header = $section.querySelector('.' + this.sectionHeadingTextClass);
1686
+ var ariaLabelParts = [$header.innerText.trim()];
1687
+
1688
+ var $summary = $section.querySelector('.' + this.sectionSummaryClass);
1689
+ if ($summary) {
1690
+ ariaLabelParts.push($summary.innerText.trim());
1691
+ }
1692
+
1693
+ var ariaLabelMessage = expanded
1694
+ ? this.i18n.t('hideSectionAriaLabel')
1695
+ : this.i18n.t('showSectionAriaLabel');
1696
+ ariaLabelParts.push(ariaLabelMessage);
1697
+
1698
+ /*
1699
+ * Join with a comma to add pause for assistive technology.
1700
+ * Example: [heading]Section A ,[pause] Show this section.
1701
+ * https://accessibility.blog.gov.uk/2017/12/18/what-working-on-gov-uk-navigation-taught-us-about-accessibility/
1702
+ */
1703
+ $button.setAttribute('aria-label', ariaLabelParts.join(' , '));
1704
+
1001
1705
  // Swap icon, change class
1002
1706
  if (expanded) {
1003
1707
  $section.classList.add(this.sectionExpandedClass);
@@ -1032,9 +1736,11 @@ Accordion.prototype.checkIfAllSectionsOpen = function () {
1032
1736
  Accordion.prototype.updateShowAllButton = function (expanded) {
1033
1737
  var $showAllIcon = this.$showAllButton.querySelector('.' + this.upChevronIconClass);
1034
1738
  var $showAllText = this.$showAllButton.querySelector('.' + this.showAllTextClass);
1035
- var newButtonText = expanded ? 'Hide all sections' : 'Show all sections';
1739
+ var newButtonText = expanded
1740
+ ? this.i18n.t('hideAllSections')
1741
+ : this.i18n.t('showAllSections');
1036
1742
  this.$showAllButton.setAttribute('aria-expanded', expanded);
1037
- $showAllText.innerHTML = newButtonText;
1743
+ $showAllText.innerText = newButtonText;
1038
1744
 
1039
1745
  // Swap icon, toggle class
1040
1746
  if (expanded) {
@@ -1097,17 +1803,14 @@ Accordion.prototype.setInitialState = function ($section) {
1097
1803
  };
1098
1804
 
1099
1805
  /**
1100
- * Create an element to improve semantics of the section button with punctuation
1101
- * @return {object} DOM element
1102
- *
1103
- * Used to add pause (with a comma) for assistive technology.
1104
- * Example: [heading]Section A ,[pause] Show this section.
1105
- * https://accessibility.blog.gov.uk/2017/12/18/what-working-on-gov-uk-navigation-taught-us-about-accessibility/
1106
- *
1107
- * Adding punctuation to the button can also improve its general semantics by dividing its contents
1108
- * into thematic chunks.
1109
- * See https://github.com/alphagov/govuk-frontend/issues/2327#issuecomment-922957442
1110
- */
1806
+ * Create an element to improve semantics of the section button with punctuation
1807
+ *
1808
+ * @returns {HTMLSpanElement} DOM element
1809
+ *
1810
+ * Adding punctuation to the button can also improve its general semantics by dividing its contents
1811
+ * into thematic chunks.
1812
+ * See https://github.com/alphagov/govuk-frontend/issues/2327#issuecomment-922957442
1813
+ */
1111
1814
  Accordion.prototype.getButtonPunctuationEl = function () {
1112
1815
  var $punctuationEl = document.createElement('span');
1113
1816
  $punctuationEl.classList.add('govuk-visually-hidden', 'govuk-accordion__section-heading-divider');
@@ -1115,6 +1818,35 @@ Accordion.prototype.getButtonPunctuationEl = function () {
1115
1818
  return $punctuationEl
1116
1819
  };
1117
1820
 
1821
+ /**
1822
+ * Accordion config
1823
+ *
1824
+ * @typedef {object} AccordionConfig
1825
+ * @property {AccordionTranslations} [i18n = ACCORDION_TRANSLATIONS] - See constant {@link ACCORDION_TRANSLATIONS}
1826
+ */
1827
+
1828
+ /**
1829
+ * Accordion translations
1830
+ *
1831
+ * @typedef {object} AccordionTranslations
1832
+ *
1833
+ * Messages used by the component for the labels of its buttons. This includes
1834
+ * the visible text shown on screen, and text to help assistive technology users
1835
+ * for the buttons toggling each section.
1836
+ * @property {string} [hideAllSections] - The text content for the 'Hide all
1837
+ * sections' button, used when at least one section is expanded.
1838
+ * @property {string} [hideSection] - The text content for the 'Hide'
1839
+ * button, used when a section is expanded.
1840
+ * @property {string} [hideSectionAriaLabel] - The text content appended to the
1841
+ * 'Hide' button's accessible name when a section is expanded.
1842
+ * @property {string} [showAllSections] - The text content for the 'Show all
1843
+ * sections' button, used when all sections are collapsed.
1844
+ * @property {string} [showSection] - The text content for the 'Show'
1845
+ * button, used when a section is collapsed.
1846
+ * @property {string} [showSectionAriaLabel] - The text content appended to the
1847
+ * 'Show' button's accessible name when a section is expanded.
1848
+ */
1849
+
1118
1850
  (function(undefined) {
1119
1851
 
1120
1852
  // Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Window/detect.js
@@ -1388,44 +2120,79 @@ if (detect) return
1388
2120
  var KEY_SPACE = 32;
1389
2121
  var DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
1390
2122
 
1391
- function Button ($module) {
2123
+ /**
2124
+ * JavaScript enhancements for the Button component
2125
+ *
2126
+ * @class
2127
+ * @param {HTMLElement} $module - The element this component controls
2128
+ * @param {ButtonConfig} config - Button config
2129
+ */
2130
+ function Button ($module, config) {
2131
+ if (!$module) {
2132
+ return this
2133
+ }
2134
+
1392
2135
  this.$module = $module;
1393
2136
  this.debounceFormSubmitTimer = null;
2137
+
2138
+ var defaultConfig = {
2139
+ preventDoubleClick: false
2140
+ };
2141
+ this.config = mergeConfigs(
2142
+ defaultConfig,
2143
+ config || {},
2144
+ normaliseDataset($module.dataset)
2145
+ );
1394
2146
  }
1395
2147
 
1396
2148
  /**
1397
- * JavaScript 'shim' to trigger the click event of element(s) when the space key is pressed.
1398
- *
1399
- * Created since some Assistive Technologies (for example some Screenreaders)
1400
- * will tell a user to press space on a 'button', so this functionality needs to be shimmed
1401
- * See https://github.com/alphagov/govuk_elements/pull/272#issuecomment-233028270
1402
- *
1403
- * @param {object} event event
1404
- */
1405
- Button.prototype.handleKeyDown = function (event) {
1406
- // get the target element
1407
- var target = event.target;
1408
- // if the element has a role='button' and the pressed key is a space, we'll simulate a click
1409
- if (target.getAttribute('role') === 'button' && event.keyCode === KEY_SPACE) {
1410
- event.preventDefault();
1411
- // trigger the target's click event
1412
- target.click();
2149
+ * Initialise component
2150
+ */
2151
+ Button.prototype.init = function () {
2152
+ if (!this.$module) {
2153
+ return
1413
2154
  }
1414
- };
2155
+
2156
+ this.$module.addEventListener('keydown', this.handleKeyDown);
2157
+ this.$module.addEventListener('click', this.debounce.bind(this));
2158
+ };
1415
2159
 
1416
2160
  /**
1417
- * If the click quickly succeeds a previous click then nothing will happen.
1418
- * This stops people accidentally causing multiple form submissions by
1419
- * double clicking buttons.
1420
- */
1421
- Button.prototype.debounce = function (event) {
2161
+ * Trigger a click event when the space key is pressed
2162
+ *
2163
+ * Some screen readers tell users they can activate things with the 'button'
2164
+ * role, so we need to match the functionality of native HTML buttons
2165
+ *
2166
+ * See https://github.com/alphagov/govuk_elements/pull/272#issuecomment-233028270
2167
+ *
2168
+ * @param {KeyboardEvent} event
2169
+ */
2170
+ Button.prototype.handleKeyDown = function (event) {
1422
2171
  var target = event.target;
1423
- // Check the button that is clicked on has the preventDoubleClick feature enabled
1424
- if (target.getAttribute('data-prevent-double-click') !== 'true') {
2172
+
2173
+ if (target.getAttribute('role') === 'button' && event.keyCode === KEY_SPACE) {
2174
+ event.preventDefault(); // prevent the page from scrolling
2175
+ target.click();
2176
+ }
2177
+ };
2178
+
2179
+ /**
2180
+ * Debounce double-clicks
2181
+ *
2182
+ * If the click quickly succeeds a previous click then nothing will happen. This
2183
+ * stops people accidentally causing multiple form submissions by double
2184
+ * clicking buttons.
2185
+ *
2186
+ * @param {MouseEvent} event
2187
+ * @returns {undefined | false} - Returns undefined, or false when debounced
2188
+ */
2189
+ Button.prototype.debounce = function (event) {
2190
+ // Check the button that was clicked has preventDoubleClick enabled
2191
+ if (!this.config.preventDoubleClick) {
1425
2192
  return
1426
2193
  }
1427
2194
 
1428
- // If the timer is still running then we want to prevent the click from submitting the form
2195
+ // If the timer is still running, prevent the click from submitting the form
1429
2196
  if (this.debounceFormSubmitTimer) {
1430
2197
  event.preventDefault();
1431
2198
  return false
@@ -1437,13 +2204,13 @@ Button.prototype.debounce = function (event) {
1437
2204
  };
1438
2205
 
1439
2206
  /**
1440
- * Initialise an event listener for keydown at document level
1441
- * this will help listening for later inserted elements with a role="button"
1442
- */
1443
- Button.prototype.init = function () {
1444
- this.$module.addEventListener('keydown', this.handleKeyDown);
1445
- this.$module.addEventListener('click', this.debounce);
1446
- };
2207
+ * Button config
2208
+ *
2209
+ * @typedef {object} ButtonConfig
2210
+ * @property {boolean} [preventDoubleClick = false] -
2211
+ * Prevent accidental double clicks on submit buttons from submitting forms
2212
+ * multiple times.
2213
+ */
1447
2214
 
1448
2215
  /**
1449
2216
  * JavaScript 'polyfill' for HTML5's <details> and <summary> elements
@@ -1455,6 +2222,12 @@ Button.prototype.init = function () {
1455
2222
  var KEY_ENTER = 13;
1456
2223
  var KEY_SPACE$1 = 32;
1457
2224
 
2225
+ /**
2226
+ * Details component
2227
+ *
2228
+ * @class
2229
+ * @param {HTMLElement} $module - HTML element to use for details
2230
+ */
1458
2231
  function Details ($module) {
1459
2232
  this.$module = $module;
1460
2233
  }
@@ -1521,9 +2294,10 @@ Details.prototype.polyfillDetails = function () {
1521
2294
  };
1522
2295
 
1523
2296
  /**
1524
- * Define a statechange function that updates aria-expanded and style.display
1525
- * @param {object} summary element
1526
- */
2297
+ * Define a statechange function that updates aria-expanded and style.display
2298
+ *
2299
+ * @returns {boolean} Returns true
2300
+ */
1527
2301
  Details.prototype.polyfillSetAttributes = function () {
1528
2302
  if (this.$module.hasAttribute('open')) {
1529
2303
  this.$module.removeAttribute('open');
@@ -1539,10 +2313,11 @@ Details.prototype.polyfillSetAttributes = function () {
1539
2313
  };
1540
2314
 
1541
2315
  /**
1542
- * Handle cross-modal click events
1543
- * @param {object} node element
1544
- * @param {function} callback function
1545
- */
2316
+ * Handle cross-modal click events
2317
+ *
2318
+ * @param {object} node - element
2319
+ * @param {polyfillHandleInputsCallback} callback - function
2320
+ */
1546
2321
  Details.prototype.polyfillHandleInputs = function (node, callback) {
1547
2322
  node.addEventListener('keypress', function (event) {
1548
2323
  var target = event.target;
@@ -1576,7 +2351,181 @@ Details.prototype.polyfillHandleInputs = function (node, callback) {
1576
2351
  node.addEventListener('click', callback);
1577
2352
  };
1578
2353
 
1579
- function CharacterCount ($module) {
2354
+ /**
2355
+ * @callback polyfillHandleInputsCallback
2356
+ * @param {KeyboardEvent} event - Keyboard event
2357
+ * @returns {undefined}
2358
+ */
2359
+
2360
+ (function(undefined) {
2361
+
2362
+ // Detection from https://github.com/Financial-Times/polyfill-library/blob/v3.111.0/polyfills/Date/now/detect.js
2363
+ var detect = ('Date' in self && 'now' in self.Date && 'getTime' in self.Date.prototype);
2364
+
2365
+ if (detect) return
2366
+
2367
+ // Polyfill from https://polyfill.io/v3/polyfill.js?version=3.111.0&features=Date.now&flags=always
2368
+ Date.now = function () {
2369
+ return new Date().getTime();
2370
+ };
2371
+
2372
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
2373
+
2374
+ (function(undefined) {
2375
+
2376
+ // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/detect.js
2377
+ var detect = (
2378
+ 'document' in this && "matches" in document.documentElement
2379
+ );
2380
+
2381
+ if (detect) return
2382
+
2383
+ // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/polyfill.js
2384
+ Element.prototype.matches = Element.prototype.webkitMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || function matches(selector) {
2385
+ var element = this;
2386
+ var elements = (element.document || element.ownerDocument).querySelectorAll(selector);
2387
+ var index = 0;
2388
+
2389
+ while (elements[index] && elements[index] !== element) {
2390
+ ++index;
2391
+ }
2392
+
2393
+ return !!elements[index];
2394
+ };
2395
+
2396
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
2397
+
2398
+ (function(undefined) {
2399
+
2400
+ // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/detect.js
2401
+ var detect = (
2402
+ 'document' in this && "closest" in document.documentElement
2403
+ );
2404
+
2405
+ if (detect) return
2406
+
2407
+ // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/polyfill.js
2408
+ Element.prototype.closest = function closest(selector) {
2409
+ var node = this;
2410
+
2411
+ while (node) {
2412
+ if (node.matches(selector)) return node;
2413
+ else node = 'SVGElement' in window && node instanceof SVGElement ? node.parentNode : node.parentElement;
2414
+ }
2415
+
2416
+ return null;
2417
+ };
2418
+
2419
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
2420
+
2421
+ /**
2422
+ * Returns the value of the given attribute closest to the given element (including itself)
2423
+ *
2424
+ * @param {HTMLElement} $element - The element to start walking the DOM tree up
2425
+ * @param {string} attributeName - The name of the attribute
2426
+ * @returns {string | undefined} Attribute value
2427
+ */
2428
+ function closestAttributeValue ($element, attributeName) {
2429
+ var closestElementWithAttribute = $element.closest('[' + attributeName + ']');
2430
+ if (closestElementWithAttribute) {
2431
+ return closestElementWithAttribute.getAttribute(attributeName)
2432
+ }
2433
+ }
2434
+
2435
+ /**
2436
+ * @constant
2437
+ * @type {CharacterCountTranslations}
2438
+ * @see Default value for {@link CharacterCountConfig.i18n}
2439
+ * @default
2440
+ */
2441
+ var CHARACTER_COUNT_TRANSLATIONS = {
2442
+ // Characters
2443
+ charactersUnderLimit: {
2444
+ one: 'You have %{count} character remaining',
2445
+ other: 'You have %{count} characters remaining'
2446
+ },
2447
+ charactersAtLimit: 'You have 0 characters remaining',
2448
+ charactersOverLimit: {
2449
+ one: 'You have %{count} character too many',
2450
+ other: 'You have %{count} characters too many'
2451
+ },
2452
+ // Words
2453
+ wordsUnderLimit: {
2454
+ one: 'You have %{count} word remaining',
2455
+ other: 'You have %{count} words remaining'
2456
+ },
2457
+ wordsAtLimit: 'You have 0 words remaining',
2458
+ wordsOverLimit: {
2459
+ one: 'You have %{count} word too many',
2460
+ other: 'You have %{count} words too many'
2461
+ },
2462
+ textareaDescription: {
2463
+ other: ''
2464
+ }
2465
+ };
2466
+
2467
+ /**
2468
+ * JavaScript enhancements for the CharacterCount component
2469
+ *
2470
+ * Tracks the number of characters or words in the `.govuk-js-character-count`
2471
+ * `<textarea>` inside the element. Displays a message with the remaining number
2472
+ * of characters/words available, or the number of characters/words in excess.
2473
+ *
2474
+ * You can configure the message to only appear after a certain percentage
2475
+ * of the available characters/words has been entered.
2476
+ *
2477
+ * @class
2478
+ * @param {HTMLElement} $module - The element this component controls
2479
+ * @param {CharacterCountConfig} [config] - Character count config
2480
+ */
2481
+ function CharacterCount ($module, config) {
2482
+ if (!$module) {
2483
+ return this
2484
+ }
2485
+
2486
+ var defaultConfig = {
2487
+ threshold: 0,
2488
+ i18n: CHARACTER_COUNT_TRANSLATIONS
2489
+ };
2490
+
2491
+ // Read config set using dataset ('data-' values)
2492
+ var datasetConfig = normaliseDataset($module.dataset);
2493
+
2494
+ // To ensure data-attributes take complete precedence, even if they change the
2495
+ // type of count, we need to reset the `maxlength` and `maxwords` from the
2496
+ // JavaScript config.
2497
+ //
2498
+ // We can't mutate `config`, though, as it may be shared across multiple
2499
+ // components inside `initAll`.
2500
+ var configOverrides = {};
2501
+ if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
2502
+ configOverrides = {
2503
+ maxlength: false,
2504
+ maxwords: false
2505
+ };
2506
+ }
2507
+
2508
+ this.config = mergeConfigs(
2509
+ defaultConfig,
2510
+ config || {},
2511
+ configOverrides,
2512
+ datasetConfig
2513
+ );
2514
+
2515
+ this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), {
2516
+ // Read the fallback if necessary rather than have it set in the defaults
2517
+ locale: closestAttributeValue($module, 'lang')
2518
+ });
2519
+
2520
+ // Determine the limit attribute (characters or words)
2521
+ if (this.config.maxwords) {
2522
+ this.maxLength = this.config.maxwords;
2523
+ } else if (this.config.maxlength) {
2524
+ this.maxLength = this.config.maxlength;
2525
+ } else {
2526
+ return
2527
+ }
2528
+
1580
2529
  this.$module = $module;
1581
2530
  this.$textarea = $module.querySelector('.govuk-js-character-count');
1582
2531
  this.$visibleCountMessage = null;
@@ -1584,26 +2533,28 @@ function CharacterCount ($module) {
1584
2533
  this.lastInputTimestamp = null;
1585
2534
  }
1586
2535
 
1587
- CharacterCount.prototype.defaults = {
1588
- characterCountAttribute: 'data-maxlength',
1589
- wordCountAttribute: 'data-maxwords'
1590
- };
1591
-
1592
- // Initialize component
2536
+ /**
2537
+ * Initialise component
2538
+ */
1593
2539
  CharacterCount.prototype.init = function () {
1594
2540
  // Check that required elements are present
1595
2541
  if (!this.$textarea) {
1596
2542
  return
1597
2543
  }
1598
2544
 
1599
- // Check for module
1600
- var $module = this.$module;
1601
2545
  var $textarea = this.$textarea;
1602
- var $fallbackLimitMessage = document.getElementById($textarea.id + '-info');
2546
+ var $textareaDescription = document.getElementById($textarea.id + '-info');
1603
2547
 
1604
- // Move the fallback count message to be immediately after the textarea
2548
+ // Inject a decription for the textarea if none is present already
2549
+ // for when the component was rendered with no maxlength, maxwords
2550
+ // nor custom textareaDescriptionText
2551
+ if ($textareaDescription.innerText.match(/^\s*$/)) {
2552
+ $textareaDescription.innerText = this.i18n.t('textareaDescription', { count: this.maxLength });
2553
+ }
2554
+
2555
+ // Move the textarea description to be immediately after the textarea
1605
2556
  // Kept for backwards compatibility
1606
- $textarea.insertAdjacentElement('afterend', $fallbackLimitMessage);
2557
+ $textarea.insertAdjacentElement('afterend', $textareaDescription);
1607
2558
 
1608
2559
  // Create the *screen reader* specific live-updating counter
1609
2560
  // This doesn't need any styling classes, as it is never visible
@@ -1611,36 +2562,20 @@ CharacterCount.prototype.init = function () {
1611
2562
  $screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden';
1612
2563
  $screenReaderCountMessage.setAttribute('aria-live', 'polite');
1613
2564
  this.$screenReaderCountMessage = $screenReaderCountMessage;
1614
- $fallbackLimitMessage.insertAdjacentElement('afterend', $screenReaderCountMessage);
2565
+ $textareaDescription.insertAdjacentElement('afterend', $screenReaderCountMessage);
1615
2566
 
1616
2567
  // Create our live-updating counter element, copying the classes from the
1617
- // fallback element for backwards compatibility as these may have been configured
2568
+ // textarea description for backwards compatibility as these may have been
2569
+ // configured
1618
2570
  var $visibleCountMessage = document.createElement('div');
1619
- $visibleCountMessage.className = $fallbackLimitMessage.className;
2571
+ $visibleCountMessage.className = $textareaDescription.className;
1620
2572
  $visibleCountMessage.classList.add('govuk-character-count__status');
1621
2573
  $visibleCountMessage.setAttribute('aria-hidden', 'true');
1622
2574
  this.$visibleCountMessage = $visibleCountMessage;
1623
- $fallbackLimitMessage.insertAdjacentElement('afterend', $visibleCountMessage);
1624
-
1625
- // Hide the fallback limit message
1626
- $fallbackLimitMessage.classList.add('govuk-visually-hidden');
2575
+ $textareaDescription.insertAdjacentElement('afterend', $visibleCountMessage);
1627
2576
 
1628
- // Read options set using dataset ('data-' values)
1629
- this.options = this.getDataset($module);
1630
-
1631
- // Determine the limit attribute (characters or words)
1632
- var countAttribute = this.defaults.characterCountAttribute;
1633
- if (this.options.maxwords) {
1634
- countAttribute = this.defaults.wordCountAttribute;
1635
- }
1636
-
1637
- // Save the element limit
1638
- this.maxLength = $module.getAttribute(countAttribute);
1639
-
1640
- // Check for limit
1641
- if (!this.maxLength) {
1642
- return
1643
- }
2577
+ // Hide the textarea description
2578
+ $textareaDescription.classList.add('govuk-visually-hidden');
1644
2579
 
1645
2580
  // Remove hard limit if set
1646
2581
  $textarea.removeAttribute('maxlength');
@@ -1648,9 +2583,9 @@ CharacterCount.prototype.init = function () {
1648
2583
  this.bindChangeEvents();
1649
2584
 
1650
2585
  // When the page is restored after navigating 'back' in some browsers the
1651
- // state of the character count is not restored until *after* the DOMContentLoaded
1652
- // event is fired, so we need to manually update it after the pageshow event
1653
- // in browsers that support it.
2586
+ // state of the character count is not restored until *after* the
2587
+ // DOMContentLoaded event is fired, so we need to manually update it after the
2588
+ // pageshow event in browsers that support it.
1654
2589
  if ('onpageshow' in window) {
1655
2590
  window.addEventListener('pageshow', this.updateCountMessage.bind(this));
1656
2591
  } else {
@@ -1659,35 +2594,12 @@ CharacterCount.prototype.init = function () {
1659
2594
  this.updateCountMessage();
1660
2595
  };
1661
2596
 
1662
- // Read data attributes
1663
- CharacterCount.prototype.getDataset = function (element) {
1664
- var dataset = {};
1665
- var attributes = element.attributes;
1666
- if (attributes) {
1667
- for (var i = 0; i < attributes.length; i++) {
1668
- var attribute = attributes[i];
1669
- var match = attribute.name.match(/^data-(.+)/);
1670
- if (match) {
1671
- dataset[match[1]] = attribute.value;
1672
- }
1673
- }
1674
- }
1675
- return dataset
1676
- };
1677
-
1678
- // Counts characters or words in text
1679
- CharacterCount.prototype.count = function (text) {
1680
- var length;
1681
- if (this.options.maxwords) {
1682
- var tokens = text.match(/\S+/g) || []; // Matches consecutive non-whitespace chars
1683
- length = tokens.length;
1684
- } else {
1685
- length = text.length;
1686
- }
1687
- return length
1688
- };
1689
-
1690
- // Bind input propertychange to the elements and update based on the change
2597
+ /**
2598
+ * Bind change events
2599
+ *
2600
+ * Set up event listeners on the $textarea so that the count messages update
2601
+ * when the user types.
2602
+ */
1691
2603
  CharacterCount.prototype.bindChangeEvents = function () {
1692
2604
  var $textarea = this.$textarea;
1693
2605
  $textarea.addEventListener('keyup', this.handleKeyUp.bind(this));
@@ -1697,10 +2609,52 @@ CharacterCount.prototype.bindChangeEvents = function () {
1697
2609
  $textarea.addEventListener('blur', this.handleBlur.bind(this));
1698
2610
  };
1699
2611
 
1700
- // Speech recognition software such as Dragon NaturallySpeaking will modify the
1701
- // fields by directly changing its `value`. These changes don't trigger events
1702
- // in JavaScript, so we need to poll to handle when and if they occur.
1703
- CharacterCount.prototype.checkIfValueChanged = function () {
2612
+ /**
2613
+ * Handle key up event
2614
+ *
2615
+ * Update the visible character counter and keep track of when the last update
2616
+ * happened for each keypress
2617
+ */
2618
+ CharacterCount.prototype.handleKeyUp = function () {
2619
+ this.updateVisibleCountMessage();
2620
+ this.lastInputTimestamp = Date.now();
2621
+ };
2622
+
2623
+ /**
2624
+ * Handle focus event
2625
+ *
2626
+ * Speech recognition software such as Dragon NaturallySpeaking will modify the
2627
+ * fields by directly changing its `value`. These changes don't trigger events
2628
+ * in JavaScript, so we need to poll to handle when and if they occur.
2629
+ *
2630
+ * Once the keyup event hasn't been detected for at least 1000 ms (1s), check if
2631
+ * the textarea value has changed and update the count message if it has.
2632
+ *
2633
+ * This is so that the update triggered by the manual comparison doesn't
2634
+ * conflict with debounced KeyboardEvent updates.
2635
+ */
2636
+ CharacterCount.prototype.handleFocus = function () {
2637
+ this.valueChecker = setInterval(function () {
2638
+ if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
2639
+ this.updateIfValueChanged();
2640
+ }
2641
+ }.bind(this), 1000);
2642
+ };
2643
+
2644
+ /**
2645
+ * Handle blur event
2646
+ *
2647
+ * Stop checking the textarea value once the textarea no longer has focus
2648
+ */
2649
+ CharacterCount.prototype.handleBlur = function () {
2650
+ // Cancel value checking on blur
2651
+ clearInterval(this.valueChecker);
2652
+ };
2653
+
2654
+ /**
2655
+ * Update count message if textarea value has changed
2656
+ */
2657
+ CharacterCount.prototype.updateIfValueChanged = function () {
1704
2658
  if (!this.$textarea.oldValue) this.$textarea.oldValue = '';
1705
2659
  if (this.$textarea.value !== this.$textarea.oldValue) {
1706
2660
  this.$textarea.oldValue = this.$textarea.value;
@@ -1708,14 +2662,20 @@ CharacterCount.prototype.checkIfValueChanged = function () {
1708
2662
  }
1709
2663
  };
1710
2664
 
1711
- // Helper function to update both the visible and screen reader-specific
1712
- // counters simultaneously (e.g. on init)
2665
+ /**
2666
+ * Update count message
2667
+ *
2668
+ * Helper function to update both the visible and screen reader-specific
2669
+ * counters simultaneously (e.g. on init)
2670
+ */
1713
2671
  CharacterCount.prototype.updateCountMessage = function () {
1714
2672
  this.updateVisibleCountMessage();
1715
2673
  this.updateScreenReaderCountMessage();
1716
2674
  };
1717
2675
 
1718
- // Update visible counter
2676
+ /**
2677
+ * Update visible count message
2678
+ */
1719
2679
  CharacterCount.prototype.updateVisibleCountMessage = function () {
1720
2680
  var $textarea = this.$textarea;
1721
2681
  var $visibleCountMessage = this.$visibleCountMessage;
@@ -1741,10 +2701,12 @@ CharacterCount.prototype.updateVisibleCountMessage = function () {
1741
2701
  }
1742
2702
 
1743
2703
  // Update message
1744
- $visibleCountMessage.innerHTML = this.formattedUpdateMessage();
2704
+ $visibleCountMessage.innerText = this.getCountMessage();
1745
2705
  };
1746
2706
 
1747
- // Update screen reader-specific counter
2707
+ /**
2708
+ * Update screen reader count message
2709
+ */
1748
2710
  CharacterCount.prototype.updateScreenReaderCountMessage = function () {
1749
2711
  var $screenReaderCountMessage = this.$screenReaderCountMessage;
1750
2712
 
@@ -1757,71 +2719,168 @@ CharacterCount.prototype.updateScreenReaderCountMessage = function () {
1757
2719
  }
1758
2720
 
1759
2721
  // Update message
1760
- $screenReaderCountMessage.innerHTML = this.formattedUpdateMessage();
2722
+ $screenReaderCountMessage.innerText = this.getCountMessage();
1761
2723
  };
1762
2724
 
1763
- // Format update message
1764
- CharacterCount.prototype.formattedUpdateMessage = function () {
1765
- var $textarea = this.$textarea;
1766
- var options = this.options;
1767
- var remainingNumber = this.maxLength - this.count($textarea.value);
2725
+ /**
2726
+ * Count the number of characters (or words, if `config.maxwords` is set)
2727
+ * in the given text
2728
+ *
2729
+ * @param {string} text - The text to count the characters of
2730
+ * @returns {number} the number of characters (or words) in the text
2731
+ */
2732
+ CharacterCount.prototype.count = function (text) {
2733
+ if (this.config.maxwords) {
2734
+ var tokens = text.match(/\S+/g) || []; // Matches consecutive non-whitespace chars
2735
+ return tokens.length
2736
+ } else {
2737
+ return text.length
2738
+ }
2739
+ };
2740
+
2741
+ /**
2742
+ * Get count message
2743
+ *
2744
+ * @returns {string} Status message
2745
+ */
2746
+ CharacterCount.prototype.getCountMessage = function () {
2747
+ var remainingNumber = this.maxLength - this.count(this.$textarea.value);
1768
2748
 
1769
- var charVerb = 'remaining';
1770
- var charNoun = 'character';
1771
- var displayNumber = remainingNumber;
1772
- if (options.maxwords) {
1773
- charNoun = 'word';
2749
+ var countType = this.config.maxwords ? 'words' : 'characters';
2750
+ return this.formatCountMessage(remainingNumber, countType)
2751
+ };
2752
+
2753
+ /**
2754
+ * Formats the message shown to users according to what's counted
2755
+ * and how many remain
2756
+ *
2757
+ * @param {number} remainingNumber - The number of words/characaters remaining
2758
+ * @param {string} countType - "words" or "characters"
2759
+ * @returns {string} Status message
2760
+ */
2761
+ CharacterCount.prototype.formatCountMessage = function (remainingNumber, countType) {
2762
+ if (remainingNumber === 0) {
2763
+ return this.i18n.t(countType + 'AtLimit')
1774
2764
  }
1775
- charNoun = charNoun + ((remainingNumber === -1 || remainingNumber === 1) ? '' : 's');
1776
2765
 
1777
- charVerb = (remainingNumber < 0) ? 'too many' : 'remaining';
1778
- displayNumber = Math.abs(remainingNumber);
2766
+ var translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit';
1779
2767
 
1780
- return 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb
2768
+ return this.i18n.t(countType + translationKeySuffix, { count: Math.abs(remainingNumber) })
1781
2769
  };
1782
2770
 
1783
- // Checks whether the value is over the configured threshold for the input.
1784
- // If there is no configured threshold, it is set to 0 and this function will
1785
- // always return true.
2771
+ /**
2772
+ * Check if count is over threshold
2773
+ *
2774
+ * Checks whether the value is over the configured threshold for the input.
2775
+ * If there is no configured threshold, it is set to 0 and this function will
2776
+ * always return true.
2777
+ *
2778
+ * @returns {boolean} true if the current count is over the config.threshold
2779
+ * (or no threshold is set)
2780
+ */
1786
2781
  CharacterCount.prototype.isOverThreshold = function () {
2782
+ // No threshold means we're always above threshold so save some computation
2783
+ if (!this.config.threshold) {
2784
+ return true
2785
+ }
2786
+
1787
2787
  var $textarea = this.$textarea;
1788
- var options = this.options;
1789
2788
 
1790
2789
  // Determine the remaining number of characters/words
1791
2790
  var currentLength = this.count($textarea.value);
1792
2791
  var maxLength = this.maxLength;
1793
2792
 
1794
- // Set threshold if presented in options
1795
- var thresholdPercent = options.threshold ? options.threshold : 0;
1796
- var thresholdValue = maxLength * thresholdPercent / 100;
2793
+ var thresholdValue = maxLength * this.config.threshold / 100;
1797
2794
 
1798
2795
  return (thresholdValue <= currentLength)
1799
2796
  };
1800
2797
 
1801
- // Update the visible character counter and keep track of when the last update
1802
- // happened for each keypress
1803
- CharacterCount.prototype.handleKeyUp = function () {
1804
- this.updateVisibleCountMessage();
1805
- this.lastInputTimestamp = Date.now();
1806
- };
2798
+ /**
2799
+ * Character count config
2800
+ *
2801
+ * @typedef {CharacterCountConfigWithMaxLength | CharacterCountConfigWithMaxWords} CharacterCountConfig
2802
+ */
1807
2803
 
1808
- CharacterCount.prototype.handleFocus = function () {
1809
- // If the field is focused, and a keyup event hasn't been detected for at
1810
- // least 1000 ms (1 second), then run the manual change check.
1811
- // This is so that the update triggered by the manual comparison doesn't
1812
- // conflict with debounced KeyboardEvent updates.
1813
- this.valueChecker = setInterval(function () {
1814
- if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
1815
- this.checkIfValueChanged();
1816
- }
1817
- }.bind(this), 1000);
1818
- };
2804
+ /**
2805
+ * Character count config (with maximum number of characters)
2806
+ *
2807
+ * @typedef {object} CharacterCountConfigWithMaxLength
2808
+ * @property {number} [maxlength] - The maximum number of characters.
2809
+ * If maxwords is provided, the maxlength option will be ignored.
2810
+ * @property {number} [threshold = 0] - The percentage value of the limit at
2811
+ * which point the count message is displayed. If this attribute is set, the
2812
+ * count message will be hidden by default.
2813
+ * @property {CharacterCountTranslations} [i18n = CHARACTER_COUNT_TRANSLATIONS] - See constant {@link CHARACTER_COUNT_TRANSLATIONS}
2814
+ */
1819
2815
 
1820
- CharacterCount.prototype.handleBlur = function () {
1821
- // Cancel value checking on blur
1822
- clearInterval(this.valueChecker);
1823
- };
2816
+ /**
2817
+ * Character count config (with maximum number of words)
2818
+ *
2819
+ * @typedef {object} CharacterCountConfigWithMaxWords
2820
+ * @property {number} [maxwords] - The maximum number of words. If maxwords is
2821
+ * provided, the maxlength option will be ignored.
2822
+ * @property {number} [threshold = 0] - The percentage value of the limit at
2823
+ * which point the count message is displayed. If this attribute is set, the
2824
+ * count message will be hidden by default.
2825
+ * @property {CharacterCountTranslations} [i18n = CHARACTER_COUNT_TRANSLATIONS] - See constant {@link CHARACTER_COUNT_TRANSLATIONS}
2826
+ */
2827
+
2828
+ /**
2829
+ * Character count translations
2830
+ *
2831
+ * @typedef {object} CharacterCountTranslations
2832
+ *
2833
+ * Messages shown to users as they type. It provides feedback on how many words
2834
+ * or characters they have remaining or if they are over the limit. This also
2835
+ * includes a message used as an accessible description for the textarea.
2836
+ * @property {TranslationPluralForms} [charactersUnderLimit] - Message displayed
2837
+ * when the number of characters is under the configured maximum, `maxlength`.
2838
+ * This message is displayed visually and through assistive technologies. The
2839
+ * component will replace the `%{count}` placeholder with the number of
2840
+ * remaining characters. This is a [pluralised list of
2841
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
2842
+ * @property {string} [charactersAtLimit] - Message displayed when the number of
2843
+ * characters reaches the configured maximum, `maxlength`. This message is
2844
+ * displayed visually and through assistive technologies.
2845
+ * @property {TranslationPluralForms} [charactersOverLimit] - Message displayed
2846
+ * when the number of characters is over the configured maximum, `maxlength`.
2847
+ * This message is displayed visually and through assistive technologies. The
2848
+ * component will replace the `%{count}` placeholder with the number of
2849
+ * remaining characters. This is a [pluralised list of
2850
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
2851
+ * @property {TranslationPluralForms} [wordsUnderLimit] - Message displayed when
2852
+ * the number of words is under the configured maximum, `maxlength`. This
2853
+ * message is displayed visually and through assistive technologies. The
2854
+ * component will replace the `%{count}` placeholder with the number of
2855
+ * remaining words. This is a [pluralised list of
2856
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
2857
+ * @property {string} [wordsAtLimit] - Message displayed when the number of
2858
+ * words reaches the configured maximum, `maxlength`. This message is
2859
+ * displayed visually and through assistive technologies.
2860
+ * @property {TranslationPluralForms} [wordsOverLimit] - Message displayed when
2861
+ * the number of words is over the configured maximum, `maxlength`. This
2862
+ * message is displayed visually and through assistive technologies. The
2863
+ * component will replace the `%{count}` placeholder with the number of
2864
+ * remaining words. This is a [pluralised list of
2865
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
2866
+ * @property {TranslationPluralForms} [textareaDescription] - Message made
2867
+ * available to assistive technologies, if none is already present in the
2868
+ * HTML, to describe that the component accepts only a limited amount of
2869
+ * content. It is visible on the page when JavaScript is unavailable. The
2870
+ * component will replace the `%{count}` placeholder with the value of the
2871
+ * `maxlength` or `maxwords` parameter.
2872
+ */
2873
+
2874
+ /**
2875
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
2876
+ */
1824
2877
 
2878
+ /**
2879
+ * Checkboxes component
2880
+ *
2881
+ * @class
2882
+ * @param {HTMLElement} $module - HTML element to use for checkboxes
2883
+ */
1825
2884
  function Checkboxes ($module) {
1826
2885
  this.$module = $module;
1827
2886
  this.$inputs = $module.querySelectorAll('input[type="checkbox"]');
@@ -1891,7 +2950,7 @@ Checkboxes.prototype.syncAllConditionalReveals = function () {
1891
2950
  * Synchronise the visibility of the conditional reveal, and its accessible
1892
2951
  * state, with the input's checked state.
1893
2952
  *
1894
- * @param {HTMLInputElement} $input Checkbox input
2953
+ * @param {HTMLInputElement} $input - Checkbox input
1895
2954
  */
1896
2955
  Checkboxes.prototype.syncConditionalRevealWithInputState = function ($input) {
1897
2956
  var $target = document.getElementById($input.getAttribute('aria-controls'));
@@ -1949,7 +3008,7 @@ Checkboxes.prototype.unCheckExclusiveInputs = function ($input) {
1949
3008
  * Handle a click within the $module – if the click occurred on a checkbox, sync
1950
3009
  * the state of any associated conditional reveal with the checkbox state.
1951
3010
  *
1952
- * @param {MouseEvent} event Click event
3011
+ * @param {MouseEvent} event - Click event
1953
3012
  */
1954
3013
  Checkboxes.prototype.handleClick = function (event) {
1955
3014
  var $target = event.target;
@@ -1979,55 +3038,39 @@ Checkboxes.prototype.handleClick = function (event) {
1979
3038
  }
1980
3039
  };
1981
3040
 
1982
- (function(undefined) {
1983
-
1984
- // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/detect.js
1985
- var detect = (
1986
- 'document' in this && "matches" in document.documentElement
1987
- );
1988
-
1989
- if (detect) return
1990
-
1991
- // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/polyfill.js
1992
- Element.prototype.matches = Element.prototype.webkitMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || function matches(selector) {
1993
- var element = this;
1994
- var elements = (element.document || element.ownerDocument).querySelectorAll(selector);
1995
- var index = 0;
3041
+ /**
3042
+ * JavaScript enhancements for the ErrorSummary
3043
+ *
3044
+ * Takes focus on initialisation for accessible announcement, unless disabled in configuration.
3045
+ *
3046
+ * @class
3047
+ * @param {HTMLElement} $module - The element this component controls
3048
+ * @param {ErrorSummaryConfig} config - Error summary config
3049
+ */
3050
+ function ErrorSummary ($module, config) {
3051
+ // Some consuming code may not be passing a module,
3052
+ // for example if they initialise the component
3053
+ // on their own by directly passing the result
3054
+ // of `document.querySelector`.
3055
+ // To avoid breaking further JavaScript initialisation
3056
+ // we need to safeguard against this so things keep
3057
+ // working the same now we read the elements data attributes
3058
+ if (!$module) {
3059
+ // Little safety in case code gets ported as-is
3060
+ // into and ES6 class constructor, where the return value matters
3061
+ return this
3062
+ }
1996
3063
 
1997
- while (elements[index] && elements[index] !== element) {
1998
- ++index;
1999
- }
3064
+ this.$module = $module;
2000
3065
 
2001
- return !!elements[index];
3066
+ var defaultConfig = {
3067
+ disableAutoFocus: false
2002
3068
  };
2003
-
2004
- }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
2005
-
2006
- (function(undefined) {
2007
-
2008
- // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/detect.js
2009
- var detect = (
2010
- 'document' in this && "closest" in document.documentElement
3069
+ this.config = mergeConfigs(
3070
+ defaultConfig,
3071
+ config || {},
3072
+ normaliseDataset($module.dataset)
2011
3073
  );
2012
-
2013
- if (detect) return
2014
-
2015
- // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/polyfill.js
2016
- Element.prototype.closest = function closest(selector) {
2017
- var node = this;
2018
-
2019
- while (node) {
2020
- if (node.matches(selector)) return node;
2021
- else node = 'SVGElement' in window && node instanceof SVGElement ? node.parentNode : node.parentElement;
2022
- }
2023
-
2024
- return null;
2025
- };
2026
-
2027
- }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
2028
-
2029
- function ErrorSummary ($module) {
2030
- this.$module = $module;
2031
3074
  }
2032
3075
 
2033
3076
  ErrorSummary.prototype.init = function () {
@@ -2046,7 +3089,7 @@ ErrorSummary.prototype.init = function () {
2046
3089
  ErrorSummary.prototype.setFocus = function () {
2047
3090
  var $module = this.$module;
2048
3091
 
2049
- if ($module.getAttribute('data-disable-auto-focus') === 'true') {
3092
+ if (this.config.disableAutoFocus) {
2050
3093
  return
2051
3094
  }
2052
3095
 
@@ -2062,10 +3105,10 @@ ErrorSummary.prototype.setFocus = function () {
2062
3105
  };
2063
3106
 
2064
3107
  /**
2065
- * Click event handler
2066
- *
2067
- * @param {MouseEvent} event - Click event
2068
- */
3108
+ * Click event handler
3109
+ *
3110
+ * @param {MouseEvent} event - Click event
3111
+ */
2069
3112
  ErrorSummary.prototype.handleClick = function (event) {
2070
3113
  var target = event.target;
2071
3114
  if (this.focusTarget(target)) {
@@ -2189,8 +3232,32 @@ ErrorSummary.prototype.getAssociatedLegendOrLabel = function ($input) {
2189
3232
  $input.closest('label')
2190
3233
  };
2191
3234
 
2192
- function NotificationBanner ($module) {
3235
+ /**
3236
+ * Error summary config
3237
+ *
3238
+ * @typedef {object} ErrorSummaryConfig
3239
+ * @property {boolean} [disableAutoFocus = false] -
3240
+ * If set to `true` the error summary will not be focussed when the page loads.
3241
+ */
3242
+
3243
+ /**
3244
+ * Notification Banner component
3245
+ *
3246
+ * @class
3247
+ * @param {HTMLElement} $module - HTML element to use for notification banner
3248
+ * @param {NotificationBannerConfig} config - Notification banner config
3249
+ */
3250
+ function NotificationBanner ($module, config) {
2193
3251
  this.$module = $module;
3252
+
3253
+ var defaultConfig = {
3254
+ disableAutoFocus: false
3255
+ };
3256
+ this.config = mergeConfigs(
3257
+ defaultConfig,
3258
+ config || {},
3259
+ normaliseDataset($module.dataset)
3260
+ );
2194
3261
  }
2195
3262
 
2196
3263
  /**
@@ -2219,7 +3286,7 @@ NotificationBanner.prototype.init = function () {
2219
3286
  NotificationBanner.prototype.setFocus = function () {
2220
3287
  var $module = this.$module;
2221
3288
 
2222
- if ($module.getAttribute('data-disable-auto-focus') === 'true') {
3289
+ if (this.config.disableAutoFocus) {
2223
3290
  return
2224
3291
  }
2225
3292
 
@@ -2241,6 +3308,23 @@ NotificationBanner.prototype.setFocus = function () {
2241
3308
  $module.focus();
2242
3309
  };
2243
3310
 
3311
+ /**
3312
+ * Notification banner config
3313
+ *
3314
+ * @typedef {object} NotificationBannerConfig
3315
+ * @property {boolean} [disableAutoFocus = false] -
3316
+ * If set to `true` the notification banner will not be focussed when the page
3317
+ * loads. This only applies if the component has a `role` of `alert` – in
3318
+ * other cases the component will not be focused on page load, regardless of
3319
+ * this option.
3320
+ */
3321
+
3322
+ /**
3323
+ * Header component
3324
+ *
3325
+ * @class
3326
+ * @param {HTMLElement} $module - HTML element to use for header
3327
+ */
2244
3328
  function Header ($module) {
2245
3329
  this.$module = $module;
2246
3330
  this.$menuButton = $module && $module.querySelector('.govuk-js-header-toggle');
@@ -2330,6 +3414,12 @@ Header.prototype.handleMenuButtonClick = function () {
2330
3414
  this.syncState();
2331
3415
  };
2332
3416
 
3417
+ /**
3418
+ * Radios component
3419
+ *
3420
+ * @class
3421
+ * @param {HTMLElement} $module - HTML element to use for radios
3422
+ */
2333
3423
  function Radios ($module) {
2334
3424
  this.$module = $module;
2335
3425
  this.$inputs = $module.querySelectorAll('input[type="radio"]');
@@ -2400,7 +3490,7 @@ Radios.prototype.syncAllConditionalReveals = function () {
2400
3490
  * Synchronise the visibility of the conditional reveal, and its accessible
2401
3491
  * state, with the input's checked state.
2402
3492
  *
2403
- * @param {HTMLInputElement} $input Radio input
3493
+ * @param {HTMLInputElement} $input - Radio input
2404
3494
  */
2405
3495
  Radios.prototype.syncConditionalRevealWithInputState = function ($input) {
2406
3496
  var $target = document.getElementById($input.getAttribute('aria-controls'));
@@ -2421,7 +3511,7 @@ Radios.prototype.syncConditionalRevealWithInputState = function ($input) {
2421
3511
  * with the same name (because checking one radio could have un-checked a radio
2422
3512
  * in another $module)
2423
3513
  *
2424
- * @param {MouseEvent} event Click event
3514
+ * @param {MouseEvent} event - Click event
2425
3515
  */
2426
3516
  Radios.prototype.handleClick = function (event) {
2427
3517
  var $clickedInput = event.target;
@@ -2445,6 +3535,12 @@ Radios.prototype.handleClick = function (event) {
2445
3535
  }.bind(this));
2446
3536
  };
2447
3537
 
3538
+ /**
3539
+ * Skip link component
3540
+ *
3541
+ * @class
3542
+ * @param {HTMLElement} $module - HTML element to use for skip link
3543
+ */
2448
3544
  function SkipLink ($module) {
2449
3545
  this.$module = $module;
2450
3546
  this.$linkedElement = null;
@@ -2470,10 +3566,10 @@ SkipLink.prototype.init = function () {
2470
3566
  };
2471
3567
 
2472
3568
  /**
2473
- * Get linked element
2474
- *
2475
- * @returns {HTMLElement} $linkedElement - DOM element linked to from the skip link
2476
- */
3569
+ * Get linked element
3570
+ *
3571
+ * @returns {HTMLElement} $linkedElement - DOM element linked to from the skip link
3572
+ */
2477
3573
  SkipLink.prototype.getLinkedElement = function () {
2478
3574
  var linkedElementId = this.getFragmentFromUrl();
2479
3575
 
@@ -2574,6 +3670,12 @@ SkipLink.prototype.getFragmentFromUrl = function () {
2574
3670
 
2575
3671
  }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
2576
3672
 
3673
+ /**
3674
+ * Tabs component
3675
+ *
3676
+ * @class
3677
+ * @param {HTMLElement} $module - HTML element to use for tabs
3678
+ */
2577
3679
  function Tabs ($module) {
2578
3680
  this.$module = $module;
2579
3681
  this.$tabs = $module.querySelectorAll('.govuk-tabs__tab');
@@ -2848,67 +3950,90 @@ Tabs.prototype.getHref = function ($tab) {
2848
3950
  return hash
2849
3951
  };
2850
3952
 
2851
- function initAll (options) {
2852
- // Set the options to an empty object by default if no options are passed.
2853
- options = typeof options !== 'undefined' ? options : {};
3953
+ /**
3954
+ * Initialise all components
3955
+ *
3956
+ * Use the `data-module` attributes to find, instantiate and init all of the
3957
+ * components provided as part of GOV.UK Frontend.
3958
+ *
3959
+ * @param {Config} [config] - Config for all components
3960
+ */
3961
+ function initAll (config) {
3962
+ config = typeof config !== 'undefined' ? config : {};
2854
3963
 
2855
3964
  // Allow the user to initialise GOV.UK Frontend in only certain sections of the page
2856
3965
  // Defaults to the entire document if nothing is set.
2857
- var scope = typeof options.scope !== 'undefined' ? options.scope : document;
3966
+ var $scope = typeof config.scope !== 'undefined' ? config.scope : document;
2858
3967
 
2859
- var $buttons = scope.querySelectorAll('[data-module="govuk-button"]');
2860
- nodeListForEach($buttons, function ($button) {
2861
- new Button($button).init();
2862
- });
2863
-
2864
- var $accordions = scope.querySelectorAll('[data-module="govuk-accordion"]');
3968
+ var $accordions = $scope.querySelectorAll('[data-module="govuk-accordion"]');
2865
3969
  nodeListForEach($accordions, function ($accordion) {
2866
- new Accordion($accordion).init();
3970
+ new Accordion($accordion, config.accordion).init();
2867
3971
  });
2868
3972
 
2869
- var $details = scope.querySelectorAll('[data-module="govuk-details"]');
2870
- nodeListForEach($details, function ($detail) {
2871
- new Details($detail).init();
3973
+ var $buttons = $scope.querySelectorAll('[data-module="govuk-button"]');
3974
+ nodeListForEach($buttons, function ($button) {
3975
+ new Button($button, config.button).init();
2872
3976
  });
2873
3977
 
2874
- var $characterCounts = scope.querySelectorAll('[data-module="govuk-character-count"]');
3978
+ var $characterCounts = $scope.querySelectorAll('[data-module="govuk-character-count"]');
2875
3979
  nodeListForEach($characterCounts, function ($characterCount) {
2876
- new CharacterCount($characterCount).init();
3980
+ new CharacterCount($characterCount, config.characterCount).init();
2877
3981
  });
2878
3982
 
2879
- var $checkboxes = scope.querySelectorAll('[data-module="govuk-checkboxes"]');
3983
+ var $checkboxes = $scope.querySelectorAll('[data-module="govuk-checkboxes"]');
2880
3984
  nodeListForEach($checkboxes, function ($checkbox) {
2881
3985
  new Checkboxes($checkbox).init();
2882
3986
  });
2883
3987
 
3988
+ var $details = $scope.querySelectorAll('[data-module="govuk-details"]');
3989
+ nodeListForEach($details, function ($detail) {
3990
+ new Details($detail).init();
3991
+ });
3992
+
2884
3993
  // Find first error summary module to enhance.
2885
- var $errorSummary = scope.querySelector('[data-module="govuk-error-summary"]');
2886
- new ErrorSummary($errorSummary).init();
3994
+ var $errorSummary = $scope.querySelector('[data-module="govuk-error-summary"]');
3995
+ if ($errorSummary) {
3996
+ new ErrorSummary($errorSummary, config.errorSummary).init();
3997
+ }
2887
3998
 
2888
3999
  // Find first header module to enhance.
2889
- var $toggleButton = scope.querySelector('[data-module="govuk-header"]');
2890
- new Header($toggleButton).init();
4000
+ var $header = $scope.querySelector('[data-module="govuk-header"]');
4001
+ if ($header) {
4002
+ new Header($header).init();
4003
+ }
2891
4004
 
2892
- var $notificationBanners = scope.querySelectorAll('[data-module="govuk-notification-banner"]');
4005
+ var $notificationBanners = $scope.querySelectorAll('[data-module="govuk-notification-banner"]');
2893
4006
  nodeListForEach($notificationBanners, function ($notificationBanner) {
2894
- new NotificationBanner($notificationBanner).init();
4007
+ new NotificationBanner($notificationBanner, config.notificationBanner).init();
2895
4008
  });
2896
4009
 
2897
- var $radios = scope.querySelectorAll('[data-module="govuk-radios"]');
4010
+ var $radios = $scope.querySelectorAll('[data-module="govuk-radios"]');
2898
4011
  nodeListForEach($radios, function ($radio) {
2899
4012
  new Radios($radio).init();
2900
4013
  });
2901
4014
 
2902
4015
  // Find first skip link module to enhance.
2903
- var $skipLink = scope.querySelector('[data-module="govuk-skip-link"]');
4016
+ var $skipLink = $scope.querySelector('[data-module="govuk-skip-link"]');
2904
4017
  new SkipLink($skipLink).init();
2905
4018
 
2906
- var $tabs = scope.querySelectorAll('[data-module="govuk-tabs"]');
4019
+ var $tabs = $scope.querySelectorAll('[data-module="govuk-tabs"]');
2907
4020
  nodeListForEach($tabs, function ($tabs) {
2908
4021
  new Tabs($tabs).init();
2909
4022
  });
2910
4023
  }
2911
4024
 
4025
+ /**
4026
+ * Config for all components
4027
+ *
4028
+ * @typedef {object} Config
4029
+ * @property {HTMLElement} [scope=document] - Scope to query for components
4030
+ * @property {import('./components/accordion/accordion.mjs').AccordionConfig} [accordion] - Accordion config
4031
+ * @property {import('./components/button/button.mjs').ButtonConfig} [button] - Button config
4032
+ * @property {import('./components/character-count/character-count.mjs').CharacterCountConfig} [characterCount] - Character Count config
4033
+ * @property {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} [errorSummary] - Error Summary config
4034
+ * @property {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} [notificationBanner] - Notification Banner config
4035
+ */
4036
+
2912
4037
  exports.initAll = initAll;
2913
4038
  exports.Accordion = Accordion;
2914
4039
  exports.Button = Button;