defra_ruby_template 5.4.0 → 5.11.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 (428) hide show
  1. checksums.yaml +4 -4
  2. data/app/views/layouts/defra_ruby_template.html.erb +43 -23
  3. data/lib/defra_ruby_template/version.rb +1 -1
  4. data/lib/defra_ruby_template.rb +3 -8
  5. data/node_modules/govuk-frontend/dist/govuk/all.bundle.js +882 -492
  6. data/node_modules/govuk-frontend/dist/govuk/all.bundle.js.map +1 -1
  7. data/node_modules/govuk-frontend/dist/govuk/all.bundle.mjs +877 -492
  8. data/node_modules/govuk-frontend/dist/govuk/all.bundle.mjs.map +1 -1
  9. data/node_modules/govuk-frontend/dist/govuk/all.mjs +5 -0
  10. data/node_modules/govuk-frontend/dist/govuk/all.mjs.map +1 -1
  11. data/node_modules/govuk-frontend/dist/govuk/all.scss +6 -0
  12. data/node_modules/govuk-frontend/dist/govuk/all.scss.map +1 -1
  13. data/node_modules/govuk-frontend/dist/govuk/assets/images/govuk-crest.svg +1 -0
  14. data/node_modules/govuk-frontend/dist/govuk/assets/rebrand/images/favicon.ico +0 -0
  15. data/node_modules/govuk-frontend/dist/govuk/assets/rebrand/images/favicon.svg +1 -0
  16. data/node_modules/govuk-frontend/dist/govuk/assets/rebrand/images/govuk-crest.svg +1 -0
  17. data/node_modules/govuk-frontend/dist/govuk/assets/rebrand/images/govuk-icon-180.png +0 -0
  18. data/node_modules/govuk-frontend/dist/govuk/assets/rebrand/images/govuk-icon-192.png +0 -0
  19. data/node_modules/govuk-frontend/dist/govuk/assets/rebrand/images/govuk-icon-512.png +0 -0
  20. data/node_modules/govuk-frontend/dist/govuk/assets/rebrand/images/govuk-icon-mask.svg +1 -0
  21. data/node_modules/govuk-frontend/dist/govuk/assets/rebrand/images/govuk-opengraph-image.png +0 -0
  22. data/node_modules/govuk-frontend/dist/govuk/assets/rebrand/manifest.json +39 -0
  23. data/node_modules/govuk-frontend/dist/govuk/common/closest-attribute-value.mjs.map +1 -1
  24. data/node_modules/govuk-frontend/dist/govuk/common/configuration.mjs +169 -0
  25. data/node_modules/govuk-frontend/dist/govuk/common/configuration.mjs.map +1 -0
  26. data/node_modules/govuk-frontend/dist/govuk/common/govuk-frontend-version.mjs +1 -1
  27. data/node_modules/govuk-frontend/dist/govuk/common/index.mjs +20 -83
  28. data/node_modules/govuk-frontend/dist/govuk/common/index.mjs.map +1 -1
  29. data/node_modules/govuk-frontend/dist/govuk/component.mjs +60 -0
  30. data/node_modules/govuk-frontend/dist/govuk/component.mjs.map +1 -0
  31. data/node_modules/govuk-frontend/dist/govuk/components/_index.scss +1 -0
  32. data/node_modules/govuk-frontend/dist/govuk/components/_index.scss.map +1 -1
  33. data/node_modules/govuk-frontend/dist/govuk/components/accordion/_index.scss +35 -31
  34. data/node_modules/govuk-frontend/dist/govuk/components/accordion/_index.scss.map +1 -1
  35. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js +296 -195
  36. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js.map +1 -1
  37. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs +295 -194
  38. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs.map +1 -1
  39. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.mjs +88 -110
  40. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.mjs.map +1 -1
  41. data/node_modules/govuk-frontend/dist/govuk/components/accordion/fixtures.json +16 -3
  42. data/node_modules/govuk-frontend/dist/govuk/components/back-link/fixtures.json +25 -15
  43. data/node_modules/govuk-frontend/dist/govuk/components/back-link/template-with-custom-link.html +1 -0
  44. data/node_modules/govuk-frontend/dist/govuk/components/back-link/template.njk +2 -2
  45. data/node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/fixtures.json +53 -15
  46. data/node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/macro-options.json +6 -0
  47. data/node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/template-default.html +2 -2
  48. data/node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/template-inverse.html +2 -2
  49. data/node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/template-with-collapse-on-mobile.html +2 -2
  50. data/node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/template-with-last-breadcrumb-as-current-page.html +2 -2
  51. data/node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/template-with-multiple-levels.html +2 -2
  52. data/node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/template-with-one-level.html +2 -2
  53. data/node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/template-without-the-home-section.html +2 -2
  54. data/node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/template.njk +2 -2
  55. data/node_modules/govuk-frontend/dist/govuk/components/button/button.bundle.js +217 -106
  56. data/node_modules/govuk-frontend/dist/govuk/components/button/button.bundle.js.map +1 -1
  57. data/node_modules/govuk-frontend/dist/govuk/components/button/button.bundle.mjs +216 -105
  58. data/node_modules/govuk-frontend/dist/govuk/components/button/button.bundle.mjs.map +1 -1
  59. data/node_modules/govuk-frontend/dist/govuk/components/button/button.mjs +9 -22
  60. data/node_modules/govuk-frontend/dist/govuk/components/button/button.mjs.map +1 -1
  61. data/node_modules/govuk-frontend/dist/govuk/components/button/fixtures.json +38 -0
  62. data/node_modules/govuk-frontend/dist/govuk/components/button/macro-options.json +2 -1
  63. data/node_modules/govuk-frontend/dist/govuk/components/character-count/_index.scss +12 -4
  64. data/node_modules/govuk-frontend/dist/govuk/components/character-count/_index.scss.map +1 -1
  65. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js +256 -145
  66. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js.map +1 -1
  67. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs +255 -144
  68. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs.map +1 -1
  69. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.mjs +29 -34
  70. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.mjs.map +1 -1
  71. data/node_modules/govuk-frontend/dist/govuk/components/character-count/fixtures.json +80 -1
  72. data/node_modules/govuk-frontend/dist/govuk/components/character-count/macro-options.json +6 -6
  73. data/node_modules/govuk-frontend/dist/govuk/components/character-count/template-with-error.html +12 -0
  74. data/node_modules/govuk-frontend/dist/govuk/components/character-count/template-with-hint-and-error.html +15 -0
  75. data/node_modules/govuk-frontend/dist/govuk/components/character-count/template.njk +5 -4
  76. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/_index.scss +6 -1
  77. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/_index.scss.map +1 -1
  78. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js +96 -50
  79. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js.map +1 -1
  80. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs +95 -49
  81. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs.map +1 -1
  82. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs +9 -18
  83. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs.map +1 -1
  84. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/fixtures.json +88 -1
  85. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/template-small-with-divider-and-none.html +34 -0
  86. data/node_modules/govuk-frontend/dist/govuk/components/cookie-banner/_index.scss +23 -14
  87. data/node_modules/govuk-frontend/dist/govuk/components/cookie-banner/_index.scss.map +1 -1
  88. data/node_modules/govuk-frontend/dist/govuk/components/cookie-banner/fixtures.json +23 -0
  89. data/node_modules/govuk-frontend/dist/govuk/components/date-input/_index.scss +2 -2
  90. data/node_modules/govuk-frontend/dist/govuk/components/date-input/_index.scss.map +1 -1
  91. data/node_modules/govuk-frontend/dist/govuk/components/date-input/fixtures.json +27 -0
  92. data/node_modules/govuk-frontend/dist/govuk/components/date-input/macro-options.json +2 -2
  93. data/node_modules/govuk-frontend/dist/govuk/components/details/_index.scss +7 -2
  94. data/node_modules/govuk-frontend/dist/govuk/components/details/_index.scss.map +1 -1
  95. data/node_modules/govuk-frontend/dist/govuk/components/details/fixtures.json +10 -0
  96. data/node_modules/govuk-frontend/dist/govuk/components/error-message/fixtures.json +9 -0
  97. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/_index.scss +4 -0
  98. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/_index.scss.map +1 -1
  99. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js +238 -127
  100. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js.map +1 -1
  101. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs +237 -126
  102. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs.map +1 -1
  103. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs +10 -22
  104. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs.map +1 -1
  105. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/fixtures.json +19 -0
  106. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js +218 -107
  107. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js.map +1 -1
  108. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs +217 -106
  109. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs.map +1 -1
  110. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs +10 -22
  111. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs.map +1 -1
  112. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/fixtures.json +4 -0
  113. data/node_modules/govuk-frontend/dist/govuk/components/fieldset/_index.scss +3 -5
  114. data/node_modules/govuk-frontend/dist/govuk/components/fieldset/_index.scss.map +1 -1
  115. data/node_modules/govuk-frontend/dist/govuk/components/fieldset/fixtures.json +18 -0
  116. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/_index.scss +175 -9
  117. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/_index.scss.map +1 -1
  118. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js +744 -0
  119. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js.map +1 -0
  120. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs +736 -0
  121. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs.map +1 -0
  122. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs +257 -0
  123. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs.map +1 -0
  124. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/fixtures.json +220 -16
  125. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/macro-options.json +52 -3
  126. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/template-allows-direct-media-capture.html +6 -0
  127. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/template-allows-image-files-only.html +6 -0
  128. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/template-allows-multiple-files.html +6 -0
  129. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/template-disabled.html +6 -0
  130. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/template-enhanced-disabled.html +13 -0
  131. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/template-enhanced-with-error-message-and-hint.html +16 -0
  132. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/template-enhanced.html +10 -0
  133. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/template-translated.html +10 -0
  134. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/template.njk +42 -5
  135. data/node_modules/govuk-frontend/dist/govuk/components/footer/_index.scss +61 -19
  136. data/node_modules/govuk-frontend/dist/govuk/components/footer/_index.scss.map +1 -1
  137. data/node_modules/govuk-frontend/dist/govuk/components/footer/fixtures.json +34 -0
  138. data/node_modules/govuk-frontend/dist/govuk/components/footer/macro-options.json +6 -0
  139. data/node_modules/govuk-frontend/dist/govuk/components/footer/template.njk +10 -0
  140. data/node_modules/govuk-frontend/dist/govuk/components/header/_index.scss +223 -21
  141. data/node_modules/govuk-frontend/dist/govuk/components/header/_index.scss.map +1 -1
  142. data/node_modules/govuk-frontend/dist/govuk/components/header/fixtures.json +78 -32
  143. data/node_modules/govuk-frontend/dist/govuk/components/header/header.bundle.js +90 -43
  144. data/node_modules/govuk-frontend/dist/govuk/components/header/header.bundle.js.map +1 -1
  145. data/node_modules/govuk-frontend/dist/govuk/components/header/header.bundle.mjs +89 -42
  146. data/node_modules/govuk-frontend/dist/govuk/components/header/header.bundle.mjs.map +1 -1
  147. data/node_modules/govuk-frontend/dist/govuk/components/header/header.mjs +10 -18
  148. data/node_modules/govuk-frontend/dist/govuk/components/header/header.mjs.map +1 -1
  149. data/node_modules/govuk-frontend/dist/govuk/components/header/macro-options.json +31 -12
  150. data/node_modules/govuk-frontend/dist/govuk/components/header/template-default.html +20 -12
  151. data/node_modules/govuk-frontend/dist/govuk/components/header/template-full-width-with-navigation.html +21 -15
  152. data/node_modules/govuk-frontend/dist/govuk/components/header/template-full-width.html +21 -15
  153. data/node_modules/govuk-frontend/dist/govuk/components/header/template-navigation-item-with-html.html +20 -12
  154. data/node_modules/govuk-frontend/dist/govuk/components/header/template-navigation-item-with-text-without-link.html +20 -12
  155. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-custom-menu-button-label.html +20 -12
  156. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-custom-menu-button-text.html +20 -12
  157. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-custom-navigation-label.html +20 -12
  158. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-full-width-border.html +30 -0
  159. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-large-navigation.html +20 -12
  160. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-navigation.html +20 -12
  161. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-product-name.html +21 -15
  162. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-service-name-and-navigation.html +20 -12
  163. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-service-name-but-no-service-url.html +20 -12
  164. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-service-name.html +20 -12
  165. data/node_modules/govuk-frontend/dist/govuk/components/header/template-with-st-edwards-crown.html +10 -12
  166. data/node_modules/govuk-frontend/dist/govuk/components/header/template.njk +11 -41
  167. data/node_modules/govuk-frontend/dist/govuk/components/hint/fixtures.json +6 -0
  168. data/node_modules/govuk-frontend/dist/govuk/components/input/_index.scss +7 -4
  169. data/node_modules/govuk-frontend/dist/govuk/components/input/_index.scss.map +1 -1
  170. data/node_modules/govuk-frontend/dist/govuk/components/input/fixtures.json +97 -27
  171. data/node_modules/govuk-frontend/dist/govuk/components/input/macro-options.json +6 -6
  172. data/node_modules/govuk-frontend/dist/govuk/components/input/template-default.html +2 -2
  173. data/node_modules/govuk-frontend/dist/govuk/components/input/template-disabled.html +6 -0
  174. data/node_modules/govuk-frontend/dist/govuk/components/input/template-with-error-and-hint.html +12 -0
  175. data/node_modules/govuk-frontend/dist/govuk/components/input/template-with-error-message.html +2 -5
  176. data/node_modules/govuk-frontend/dist/govuk/components/input/template.njk +5 -4
  177. data/node_modules/govuk-frontend/dist/govuk/components/inset-text/fixtures.json +6 -0
  178. data/node_modules/govuk-frontend/dist/govuk/components/label/fixtures.json +17 -0
  179. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/_index.scss +3 -2
  180. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/_index.scss.map +1 -1
  181. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/fixtures.json +24 -0
  182. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js +238 -127
  183. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js.map +1 -1
  184. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs +237 -126
  185. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs.map +1 -1
  186. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs +10 -22
  187. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs.map +1 -1
  188. data/node_modules/govuk-frontend/dist/govuk/components/pagination/_index.scss +24 -37
  189. data/node_modules/govuk-frontend/dist/govuk/components/pagination/_index.scss.map +1 -1
  190. data/node_modules/govuk-frontend/dist/govuk/components/pagination/fixtures.json +15 -0
  191. data/node_modules/govuk-frontend/dist/govuk/components/pagination/macro-options.json +4 -4
  192. data/node_modules/govuk-frontend/dist/govuk/components/panel/_index.scss +13 -9
  193. data/node_modules/govuk-frontend/dist/govuk/components/panel/_index.scss.map +1 -1
  194. data/node_modules/govuk-frontend/dist/govuk/components/panel/fixtures.json +9 -0
  195. data/node_modules/govuk-frontend/dist/govuk/components/password-input/_index.scss +12 -9
  196. data/node_modules/govuk-frontend/dist/govuk/components/password-input/_index.scss.map +1 -1
  197. data/node_modules/govuk-frontend/dist/govuk/components/password-input/fixtures.json +27 -2
  198. data/node_modules/govuk-frontend/dist/govuk/components/password-input/macro-options.json +3 -3
  199. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js +220 -110
  200. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js.map +1 -1
  201. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs +219 -109
  202. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs.map +1 -1
  203. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.mjs +12 -25
  204. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.mjs.map +1 -1
  205. data/node_modules/govuk-frontend/dist/govuk/components/password-input/template-default.html +3 -3
  206. data/node_modules/govuk-frontend/dist/govuk/components/password-input/template.njk +4 -2
  207. data/node_modules/govuk-frontend/dist/govuk/components/phase-banner/_index.scss +0 -8
  208. data/node_modules/govuk-frontend/dist/govuk/components/phase-banner/_index.scss.map +1 -1
  209. data/node_modules/govuk-frontend/dist/govuk/components/phase-banner/fixtures.json +7 -0
  210. data/node_modules/govuk-frontend/dist/govuk/components/radios/_index.scss +7 -5
  211. data/node_modules/govuk-frontend/dist/govuk/components/radios/_index.scss.map +1 -1
  212. data/node_modules/govuk-frontend/dist/govuk/components/radios/fixtures.json +51 -5
  213. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.bundle.js +96 -50
  214. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.bundle.js.map +1 -1
  215. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs +95 -49
  216. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs.map +1 -1
  217. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.mjs +9 -18
  218. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.mjs.map +1 -1
  219. data/node_modules/govuk-frontend/dist/govuk/components/radios/template-small-with-a-divider.html +1 -1
  220. data/node_modules/govuk-frontend/dist/govuk/components/radios/template-with-a-divider.html +1 -1
  221. data/node_modules/govuk-frontend/dist/govuk/components/select/_index.scss +5 -5
  222. data/node_modules/govuk-frontend/dist/govuk/components/select/_index.scss.map +1 -1
  223. data/node_modules/govuk-frontend/dist/govuk/components/select/fixtures.json +43 -9
  224. data/node_modules/govuk-frontend/dist/govuk/components/select/macro-options.json +2 -2
  225. data/node_modules/govuk-frontend/dist/govuk/components/select/template-id.html +7 -0
  226. data/node_modules/govuk-frontend/dist/govuk/components/select/template.njk +6 -4
  227. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/README.md +15 -0
  228. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/_index.scss +187 -0
  229. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/_index.scss.map +1 -0
  230. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/_service-navigation.scss +4 -0
  231. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/_service-navigation.scss.map +1 -0
  232. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/fixtures.json +515 -0
  233. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/macro-options.json +138 -0
  234. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/macro.njk +3 -0
  235. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js +229 -0
  236. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js.map +1 -0
  237. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs +221 -0
  238. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs.map +1 -0
  239. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.mjs +85 -0
  240. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.mjs.map +1 -0
  241. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template-default.html +57 -0
  242. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template-with-html-navigation-items.html +49 -0
  243. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template-with-large-navigation.html +153 -0
  244. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template-with-long-service-name.html +20 -0
  245. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template-with-navigation-with-a-current-item.html +58 -0
  246. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template-with-navigation-with-an-active-item.html +58 -0
  247. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template-with-non-link-navigation-items.html +49 -0
  248. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template-with-service-link.html +20 -0
  249. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template-with-service-name-and-navigation.html +63 -0
  250. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template-with-service-name.html +18 -0
  251. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/template.njk +103 -0
  252. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/_index.scss +6 -4
  253. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/_index.scss.map +1 -1
  254. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/fixtures.json +9 -0
  255. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js +95 -48
  256. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js.map +1 -1
  257. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs +94 -47
  258. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs.map +1 -1
  259. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs +15 -23
  260. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs.map +1 -1
  261. data/node_modules/govuk-frontend/dist/govuk/components/summary-list/_index.scss +14 -22
  262. data/node_modules/govuk-frontend/dist/govuk/components/summary-list/_index.scss.map +1 -1
  263. data/node_modules/govuk-frontend/dist/govuk/components/summary-list/fixtures.json +174 -1
  264. data/node_modules/govuk-frontend/dist/govuk/components/summary-list/template-as-a-summary-card-extreme.html +106 -0
  265. data/node_modules/govuk-frontend/dist/govuk/components/table/fixtures.json +22 -1
  266. data/node_modules/govuk-frontend/dist/govuk/components/tabs/fixtures.json +15 -1
  267. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js +95 -53
  268. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js.map +1 -1
  269. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs +94 -52
  270. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs.map +1 -1
  271. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.mjs +15 -28
  272. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.mjs.map +1 -1
  273. data/node_modules/govuk-frontend/dist/govuk/components/tag/_index.scss +11 -8
  274. data/node_modules/govuk-frontend/dist/govuk/components/tag/_index.scss.map +1 -1
  275. data/node_modules/govuk-frontend/dist/govuk/components/tag/fixtures.json +14 -0
  276. data/node_modules/govuk-frontend/dist/govuk/components/task-list/_index.scss +12 -10
  277. data/node_modules/govuk-frontend/dist/govuk/components/task-list/_index.scss.map +1 -1
  278. data/node_modules/govuk-frontend/dist/govuk/components/task-list/fixtures.json +44 -0
  279. data/node_modules/govuk-frontend/dist/govuk/components/task-list/template-with-empty-values.html +27 -0
  280. data/node_modules/govuk-frontend/dist/govuk/components/task-list/template.njk +1 -1
  281. data/node_modules/govuk-frontend/dist/govuk/components/textarea/_index.scss +3 -3
  282. data/node_modules/govuk-frontend/dist/govuk/components/textarea/_index.scss.map +1 -1
  283. data/node_modules/govuk-frontend/dist/govuk/components/textarea/fixtures.json +32 -1
  284. data/node_modules/govuk-frontend/dist/govuk/components/textarea/macro-options.json +3 -3
  285. data/node_modules/govuk-frontend/dist/govuk/components/textarea/template.njk +6 -4
  286. data/node_modules/govuk-frontend/dist/govuk/components/warning-text/_index.scss +6 -5
  287. data/node_modules/govuk-frontend/dist/govuk/components/warning-text/_index.scss.map +1 -1
  288. data/node_modules/govuk-frontend/dist/govuk/components/warning-text/fixtures.json +8 -0
  289. data/node_modules/govuk-frontend/dist/govuk/core/_govuk-frontend-properties.scss +1 -1
  290. data/node_modules/govuk-frontend/dist/govuk/core/_govuk-frontend-properties.scss.map +1 -1
  291. data/node_modules/govuk-frontend/dist/govuk/errors/index.mjs +16 -3
  292. data/node_modules/govuk-frontend/dist/govuk/errors/index.mjs.map +1 -1
  293. data/node_modules/govuk-frontend/dist/govuk/govuk-frontend.min.css +2 -2
  294. data/node_modules/govuk-frontend/dist/govuk/govuk-frontend.min.css.map +1 -1
  295. data/node_modules/govuk-frontend/dist/govuk/govuk-frontend.min.js +1 -1
  296. data/node_modules/govuk-frontend/dist/govuk/govuk-frontend.min.js.map +1 -1
  297. data/node_modules/govuk-frontend/dist/govuk/helpers/_colour.scss +55 -8
  298. data/node_modules/govuk-frontend/dist/govuk/helpers/_colour.scss.map +1 -1
  299. data/node_modules/govuk-frontend/dist/govuk/helpers/_focused.scss +14 -4
  300. data/node_modules/govuk-frontend/dist/govuk/helpers/_focused.scss.map +1 -1
  301. data/node_modules/govuk-frontend/dist/govuk/helpers/_grid.scss +1 -1
  302. data/node_modules/govuk-frontend/dist/govuk/helpers/_grid.scss.map +1 -1
  303. data/node_modules/govuk-frontend/dist/govuk/helpers/_links.scss +2 -2
  304. data/node_modules/govuk-frontend/dist/govuk/helpers/_links.scss.map +1 -1
  305. data/node_modules/govuk-frontend/dist/govuk/helpers/_shape-arrow.scss +1 -1
  306. data/node_modules/govuk-frontend/dist/govuk/helpers/_shape-arrow.scss.map +1 -1
  307. data/node_modules/govuk-frontend/dist/govuk/helpers/_typography.scss +18 -1
  308. data/node_modules/govuk-frontend/dist/govuk/helpers/_typography.scss.map +1 -1
  309. data/node_modules/govuk-frontend/dist/govuk/helpers/_visually-hidden.scss +30 -62
  310. data/node_modules/govuk-frontend/dist/govuk/helpers/_visually-hidden.scss.map +1 -1
  311. data/node_modules/govuk-frontend/dist/govuk/i18n.mjs.map +1 -1
  312. data/node_modules/govuk-frontend/dist/govuk/init.mjs +90 -24
  313. data/node_modules/govuk-frontend/dist/govuk/init.mjs.map +1 -1
  314. data/node_modules/govuk-frontend/dist/govuk/macros/logo.njk +78 -0
  315. data/node_modules/govuk-frontend/dist/govuk/objects/_template.scss +5 -1
  316. data/node_modules/govuk-frontend/dist/govuk/objects/_template.scss.map +1 -1
  317. data/node_modules/govuk-frontend/dist/govuk/overrides/_typography.scss +5 -1
  318. data/node_modules/govuk-frontend/dist/govuk/overrides/_typography.scss.map +1 -1
  319. data/node_modules/govuk-frontend/dist/govuk/settings/_colours-applied.scss +39 -1
  320. data/node_modules/govuk-frontend/dist/govuk/settings/_colours-applied.scss.map +1 -1
  321. data/node_modules/govuk-frontend/dist/govuk/settings/_colours-organisations.scss +244 -10
  322. data/node_modules/govuk-frontend/dist/govuk/settings/_colours-organisations.scss.map +1 -1
  323. data/node_modules/govuk-frontend/dist/govuk/settings/_typography-responsive.scss +5 -10
  324. data/node_modules/govuk-frontend/dist/govuk/settings/_typography-responsive.scss.map +1 -1
  325. data/node_modules/govuk-frontend/dist/govuk/template.njk +20 -9
  326. data/node_modules/govuk-frontend/dist/govuk/tools/_index.scss +1 -0
  327. data/node_modules/govuk-frontend/dist/govuk/tools/_index.scss.map +1 -1
  328. data/node_modules/govuk-frontend/dist/govuk/tools/_rebrand.scss +65 -0
  329. data/node_modules/govuk-frontend/dist/govuk/tools/_rebrand.scss.map +1 -0
  330. data/node_modules/govuk-frontend/dist/govuk-prototype-kit/functions.js +25 -0
  331. data/node_modules/govuk-frontend/dist/govuk-prototype-kit/init.scss +1 -1
  332. data/node_modules/govuk-frontend/dist/govuk-prototype-kit/init.scss.map +1 -1
  333. data/node_modules/govuk-frontend/govuk-prototype-kit.config.json +8 -1
  334. data/node_modules/govuk-frontend/package.json +16 -16
  335. data/spec/spec_helper.rb +98 -0
  336. data/spec/tasks/assets_spec.rb +53 -0
  337. data/vendor/assets/assets/fonts/bold-affa96571d-v2.woff +0 -0
  338. data/vendor/assets/assets/fonts/bold-b542beb274-v2.woff2 +0 -0
  339. data/vendor/assets/assets/fonts/light-94a07e06a1-v2.woff2 +0 -0
  340. data/vendor/assets/assets/fonts/light-f591b13f7d-v2.woff +0 -0
  341. data/vendor/assets/assets/images/favicon.ico +0 -0
  342. data/vendor/assets/assets/images/favicon.svg +1 -0
  343. data/vendor/assets/assets/images/govuk-crest.svg +1 -0
  344. data/vendor/assets/assets/images/govuk-icon-180.png +0 -0
  345. data/vendor/assets/assets/images/govuk-icon-192.png +0 -0
  346. data/vendor/assets/assets/images/govuk-icon-512.png +0 -0
  347. data/vendor/assets/assets/images/govuk-icon-mask.svg +1 -0
  348. data/vendor/assets/assets/images/govuk-opengraph-image.png +0 -0
  349. data/vendor/assets/assets/manifest.json +39 -0
  350. data/vendor/assets/assets/rebrand/images/favicon.ico +0 -0
  351. data/vendor/assets/assets/rebrand/images/favicon.svg +1 -0
  352. data/vendor/assets/assets/rebrand/images/govuk-crest.svg +1 -0
  353. data/vendor/assets/assets/rebrand/images/govuk-icon-180.png +0 -0
  354. data/vendor/assets/assets/rebrand/images/govuk-icon-192.png +0 -0
  355. data/vendor/assets/assets/rebrand/images/govuk-icon-512.png +0 -0
  356. data/vendor/assets/assets/rebrand/images/govuk-icon-mask.svg +1 -0
  357. data/vendor/assets/assets/rebrand/images/govuk-opengraph-image.png +0 -0
  358. data/vendor/assets/assets/rebrand/manifest.json +39 -0
  359. data/vendor/assets/images/govuk-crest.svg +1 -0
  360. data/vendor/assets/javascripts/defra_ruby_template.js +882 -492
  361. data/vendor/assets/stylesheets/all.scss +6 -0
  362. data/vendor/assets/stylesheets/components/_index.scss +1 -0
  363. data/vendor/assets/stylesheets/components/accordion/_index.scss +35 -31
  364. data/vendor/assets/stylesheets/components/character-count/_index.scss +12 -4
  365. data/vendor/assets/stylesheets/components/checkboxes/_index.scss +6 -1
  366. data/vendor/assets/stylesheets/components/cookie-banner/_index.scss +23 -14
  367. data/vendor/assets/stylesheets/components/date-input/_index.scss +2 -2
  368. data/vendor/assets/stylesheets/components/details/_index.scss +7 -2
  369. data/vendor/assets/stylesheets/components/error-summary/_index.scss +4 -0
  370. data/vendor/assets/stylesheets/components/fieldset/_index.scss +3 -5
  371. data/vendor/assets/stylesheets/components/file-upload/_index.scss +175 -9
  372. data/vendor/assets/stylesheets/components/footer/_index.scss +61 -19
  373. data/vendor/assets/stylesheets/components/header/_index.scss +223 -21
  374. data/vendor/assets/stylesheets/components/input/_index.scss +7 -4
  375. data/vendor/assets/stylesheets/components/notification-banner/_index.scss +3 -2
  376. data/vendor/assets/stylesheets/components/pagination/_index.scss +24 -37
  377. data/vendor/assets/stylesheets/components/panel/_index.scss +13 -9
  378. data/vendor/assets/stylesheets/components/password-input/_index.scss +12 -9
  379. data/vendor/assets/stylesheets/components/phase-banner/_index.scss +0 -8
  380. data/vendor/assets/stylesheets/components/radios/_index.scss +7 -5
  381. data/vendor/assets/stylesheets/components/select/_index.scss +5 -5
  382. data/vendor/assets/stylesheets/components/service-navigation/_index.scss +187 -0
  383. data/vendor/assets/stylesheets/components/service-navigation/_service-navigation.scss +4 -0
  384. data/vendor/assets/stylesheets/components/skip-link/_index.scss +6 -4
  385. data/vendor/assets/stylesheets/components/summary-list/_index.scss +14 -22
  386. data/vendor/assets/stylesheets/components/tag/_index.scss +11 -8
  387. data/vendor/assets/stylesheets/components/task-list/_index.scss +12 -10
  388. data/vendor/assets/stylesheets/components/textarea/_index.scss +3 -3
  389. data/vendor/assets/stylesheets/components/warning-text/_index.scss +6 -5
  390. data/vendor/assets/stylesheets/core/_govuk-frontend-properties.scss +1 -1
  391. data/vendor/assets/stylesheets/defra_ruby_template.scss +6 -0
  392. data/vendor/assets/stylesheets/govuk-frontend.min.css +2 -2
  393. data/vendor/assets/stylesheets/helpers/_colour.scss +55 -8
  394. data/vendor/assets/stylesheets/helpers/_focused.scss +14 -4
  395. data/vendor/assets/stylesheets/helpers/_grid.scss +1 -1
  396. data/vendor/assets/stylesheets/helpers/_links.scss +2 -2
  397. data/vendor/assets/stylesheets/helpers/_shape-arrow.scss +1 -1
  398. data/vendor/assets/stylesheets/helpers/_typography.scss +18 -1
  399. data/vendor/assets/stylesheets/helpers/_visually-hidden.scss +30 -62
  400. data/vendor/assets/stylesheets/objects/_template.scss +5 -1
  401. data/vendor/assets/stylesheets/overrides/_typography.scss +5 -1
  402. data/vendor/assets/stylesheets/settings/_colours-applied.scss +39 -1
  403. data/vendor/assets/stylesheets/settings/_colours-organisations.scss +244 -10
  404. data/vendor/assets/stylesheets/settings/_typography-responsive.scss +5 -10
  405. data/vendor/assets/stylesheets/tools/_index.scss +1 -0
  406. data/vendor/assets/stylesheets/tools/_rebrand.scss +65 -0
  407. metadata +100 -24
  408. data/.github/dependabot.yml +0 -14
  409. data/.github/workflows/ci.yml +0 -31
  410. data/.gitignore +0 -8
  411. data/.rspec +0 -1
  412. data/.rubocop.yml +0 -3
  413. data/.ruby-version +0 -1
  414. data/Gemfile +0 -9
  415. data/Gemfile.lock +0 -58
  416. data/defra_ruby_template.gemspec +0 -35
  417. data/node_modules/.package-lock.json +0 -16
  418. data/node_modules/govuk-frontend/dist/govuk/assets/images/govuk-crest-2x.png +0 -0
  419. data/node_modules/govuk-frontend/dist/govuk/assets/images/govuk-crest.png +0 -0
  420. data/node_modules/govuk-frontend/dist/govuk/common/normalise-dataset.mjs +0 -18
  421. data/node_modules/govuk-frontend/dist/govuk/common/normalise-dataset.mjs.map +0 -1
  422. data/node_modules/govuk-frontend/dist/govuk/common/normalise-string.mjs +0 -31
  423. data/node_modules/govuk-frontend/dist/govuk/common/normalise-string.mjs.map +0 -1
  424. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/template-with-value.html +0 -6
  425. data/node_modules/govuk-frontend/dist/govuk/govuk-frontend-component.mjs +0 -16
  426. data/node_modules/govuk-frontend/dist/govuk/govuk-frontend-component.mjs.map +0 -1
  427. data/package-lock.json +0 -24
  428. data/package.json +0 -29
@@ -1,81 +1,11 @@
1
1
  (function (global, factory) {
2
2
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
3
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = {}));
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = global.GOVUKFrontend || {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- const version = '5.4.0';
7
+ const version = '5.10.2';
8
8
 
9
- function normaliseString(value, property) {
10
- const trimmedValue = value ? value.trim() : '';
11
- let output;
12
- let outputType = property == null ? void 0 : property.type;
13
- if (!outputType) {
14
- if (['true', 'false'].includes(trimmedValue)) {
15
- outputType = 'boolean';
16
- }
17
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
18
- outputType = 'number';
19
- }
20
- }
21
- switch (outputType) {
22
- case 'boolean':
23
- output = trimmedValue === 'true';
24
- break;
25
- case 'number':
26
- output = Number(trimmedValue);
27
- break;
28
- default:
29
- output = value;
30
- }
31
- return output;
32
- }
33
-
34
- /**
35
- * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
36
- */
37
-
38
- function mergeConfigs(...configObjects) {
39
- const formattedConfigObject = {};
40
- for (const configObject of configObjects) {
41
- for (const key of Object.keys(configObject)) {
42
- const option = formattedConfigObject[key];
43
- const override = configObject[key];
44
- if (isObject(option) && isObject(override)) {
45
- formattedConfigObject[key] = mergeConfigs(option, override);
46
- } else {
47
- formattedConfigObject[key] = override;
48
- }
49
- }
50
- }
51
- return formattedConfigObject;
52
- }
53
- function extractConfigByNamespace(Component, dataset, namespace) {
54
- const property = Component.schema.properties[namespace];
55
- if ((property == null ? void 0 : property.type) !== 'object') {
56
- return;
57
- }
58
- const newObject = {
59
- [namespace]: ({})
60
- };
61
- for (const [key, value] of Object.entries(dataset)) {
62
- let current = newObject;
63
- const keyParts = key.split('.');
64
- for (const [index, name] of keyParts.entries()) {
65
- if (typeof current === 'object') {
66
- if (index < keyParts.length - 1) {
67
- if (!isObject(current[name])) {
68
- current[name] = {};
69
- }
70
- current = current[name];
71
- } else if (key !== namespace) {
72
- current[name] = normaliseString(value);
73
- }
74
- }
75
- }
76
- }
77
- return newObject[namespace];
78
- }
79
9
  function getFragmentFromUrl(url) {
80
10
  if (!url.includes('#')) {
81
11
  return undefined;
@@ -114,75 +44,42 @@
114
44
  (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
115
45
  $element.focus();
116
46
  }
47
+ function isInitialised($root, moduleName) {
48
+ return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
49
+ }
50
+
51
+ /**
52
+ * Checks if GOV.UK Frontend is supported on this page
53
+ *
54
+ * Some browsers will load and run our JavaScript but GOV.UK Frontend
55
+ * won't be supported.
56
+ *
57
+ * @param {HTMLElement | null} [$scope] - (internal) `<body>` HTML element checked for browser support
58
+ * @returns {boolean} Whether GOV.UK Frontend is supported on this page
59
+ */
117
60
  function isSupported($scope = document.body) {
118
61
  if (!$scope) {
119
62
  return false;
120
63
  }
121
64
  return $scope.classList.contains('govuk-frontend-supported');
122
65
  }
123
- function validateConfig(schema, config) {
124
- const validationErrors = [];
125
- for (const [name, conditions] of Object.entries(schema)) {
126
- const errors = [];
127
- if (Array.isArray(conditions)) {
128
- for (const {
129
- required,
130
- errorMessage
131
- } of conditions) {
132
- if (!required.every(key => !!config[key])) {
133
- errors.push(errorMessage);
134
- }
135
- }
136
- if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
137
- validationErrors.push(...errors);
138
- }
139
- }
140
- }
141
- return validationErrors;
142
- }
143
66
  function isArray(option) {
144
67
  return Array.isArray(option);
145
68
  }
146
69
  function isObject(option) {
147
70
  return !!option && typeof option === 'object' && !isArray(option);
148
71
  }
149
-
150
- /**
151
- * Schema for component config
152
- *
153
- * @typedef {object} Schema
154
- * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
155
- * @property {SchemaCondition[]} [anyOf] - List of schema conditions
156
- */
157
-
72
+ function formatErrorMessage(Component, message) {
73
+ return `${Component.moduleName}: ${message}`;
74
+ }
158
75
  /**
159
- * Schema property for component config
160
- *
161
- * @typedef {object} SchemaProperty
162
- * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
76
+ * @typedef ComponentWithModuleName
77
+ * @property {string} moduleName - Name of the component
163
78
  */
164
-
165
79
  /**
166
- * Schema condition for component config
167
- *
168
- * @typedef {object} SchemaCondition
169
- * @property {string[]} required - List of required config fields
170
- * @property {string} errorMessage - Error message when required config fields not provided
80
+ * @import { ObjectNested } from './configuration.mjs'
171
81
  */
172
82
 
173
- function normaliseDataset(Component, dataset) {
174
- const out = {};
175
- for (const [field, property] of Object.entries(Component.schema.properties)) {
176
- if (field in dataset) {
177
- out[field] = normaliseString(dataset[field], property);
178
- }
179
- if ((property == null ? void 0 : property.type) === 'object') {
180
- out[field] = extractConfigByNamespace(Component, dataset, field);
181
- }
182
- }
183
- return out;
184
- }
185
-
186
83
  class GOVUKFrontendError extends Error {
187
84
  constructor(...args) {
188
85
  super(...args);
@@ -212,30 +109,248 @@
212
109
  let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
213
110
  if (typeof messageOrOptions === 'object') {
214
111
  const {
215
- componentName,
112
+ component,
216
113
  identifier,
217
114
  element,
218
115
  expectedType
219
116
  } = messageOrOptions;
220
- message = `${componentName}: ${identifier}`;
117
+ message = identifier;
221
118
  message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';
119
+ message = formatErrorMessage(component, message);
222
120
  }
223
121
  super(message);
224
122
  this.name = 'ElementError';
225
123
  }
226
124
  }
125
+ class InitError extends GOVUKFrontendError {
126
+ constructor(componentOrMessage) {
127
+ const message = typeof componentOrMessage === 'string' ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`);
128
+ super(message);
129
+ this.name = 'InitError';
130
+ }
131
+ }
132
+ /**
133
+ * @import { ComponentWithModuleName } from '../common/index.mjs'
134
+ */
227
135
 
228
- class GOVUKFrontendComponent {
229
- constructor() {
230
- this.checkSupport();
136
+ class Component {
137
+ /**
138
+ * Returns the root element of the component
139
+ *
140
+ * @protected
141
+ * @returns {RootElementType} - the root element of component
142
+ */
143
+ get $root() {
144
+ return this._$root;
145
+ }
146
+ constructor($root) {
147
+ this._$root = void 0;
148
+ const childConstructor = this.constructor;
149
+ if (typeof childConstructor.moduleName !== 'string') {
150
+ throw new InitError(`\`moduleName\` not defined in component`);
151
+ }
152
+ if (!($root instanceof childConstructor.elementType)) {
153
+ throw new ElementError({
154
+ element: $root,
155
+ component: childConstructor,
156
+ identifier: 'Root element (`$root`)',
157
+ expectedType: childConstructor.elementType.name
158
+ });
159
+ } else {
160
+ this._$root = $root;
161
+ }
162
+ childConstructor.checkSupport();
163
+ this.checkInitialised();
164
+ const moduleName = childConstructor.moduleName;
165
+ this.$root.setAttribute(`data-${moduleName}-init`, '');
166
+ }
167
+ checkInitialised() {
168
+ const constructor = this.constructor;
169
+ const moduleName = constructor.moduleName;
170
+ if (moduleName && isInitialised(this.$root, moduleName)) {
171
+ throw new InitError(constructor);
172
+ }
231
173
  }
232
- checkSupport() {
174
+ static checkSupport() {
233
175
  if (!isSupported()) {
234
176
  throw new SupportError();
235
177
  }
236
178
  }
237
179
  }
238
180
 
181
+ /**
182
+ * @typedef ChildClass
183
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
184
+ */
185
+
186
+ /**
187
+ * @typedef {typeof Component & ChildClass} ChildClassConstructor
188
+ */
189
+ Component.elementType = HTMLElement;
190
+
191
+ const configOverride = Symbol.for('configOverride');
192
+ class ConfigurableComponent extends Component {
193
+ [configOverride](param) {
194
+ return {};
195
+ }
196
+
197
+ /**
198
+ * Returns the root element of the component
199
+ *
200
+ * @protected
201
+ * @returns {ConfigurationType} - the root element of component
202
+ */
203
+ get config() {
204
+ return this._config;
205
+ }
206
+ constructor($root, config) {
207
+ super($root);
208
+ this._config = void 0;
209
+ const childConstructor = this.constructor;
210
+ if (!isObject(childConstructor.defaults)) {
211
+ throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
212
+ }
213
+ const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
214
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
215
+ }
216
+ }
217
+ function normaliseString(value, property) {
218
+ const trimmedValue = value ? value.trim() : '';
219
+ let output;
220
+ let outputType = property == null ? void 0 : property.type;
221
+ if (!outputType) {
222
+ if (['true', 'false'].includes(trimmedValue)) {
223
+ outputType = 'boolean';
224
+ }
225
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
226
+ outputType = 'number';
227
+ }
228
+ }
229
+ switch (outputType) {
230
+ case 'boolean':
231
+ output = trimmedValue === 'true';
232
+ break;
233
+ case 'number':
234
+ output = Number(trimmedValue);
235
+ break;
236
+ default:
237
+ output = value;
238
+ }
239
+ return output;
240
+ }
241
+ function normaliseDataset(Component, dataset) {
242
+ if (!isObject(Component.schema)) {
243
+ throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));
244
+ }
245
+ const out = {};
246
+ const entries = Object.entries(Component.schema.properties);
247
+ for (const entry of entries) {
248
+ const [namespace, property] = entry;
249
+ const field = namespace.toString();
250
+ if (field in dataset) {
251
+ out[field] = normaliseString(dataset[field], property);
252
+ }
253
+ if ((property == null ? void 0 : property.type) === 'object') {
254
+ out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);
255
+ }
256
+ }
257
+ return out;
258
+ }
259
+ function mergeConfigs(...configObjects) {
260
+ const formattedConfigObject = {};
261
+ for (const configObject of configObjects) {
262
+ for (const key of Object.keys(configObject)) {
263
+ const option = formattedConfigObject[key];
264
+ const override = configObject[key];
265
+ if (isObject(option) && isObject(override)) {
266
+ formattedConfigObject[key] = mergeConfigs(option, override);
267
+ } else {
268
+ formattedConfigObject[key] = override;
269
+ }
270
+ }
271
+ }
272
+ return formattedConfigObject;
273
+ }
274
+ function validateConfig(schema, config) {
275
+ const validationErrors = [];
276
+ for (const [name, conditions] of Object.entries(schema)) {
277
+ const errors = [];
278
+ if (Array.isArray(conditions)) {
279
+ for (const {
280
+ required,
281
+ errorMessage
282
+ } of conditions) {
283
+ if (!required.every(key => !!config[key])) {
284
+ errors.push(errorMessage);
285
+ }
286
+ }
287
+ if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
288
+ validationErrors.push(...errors);
289
+ }
290
+ }
291
+ }
292
+ return validationErrors;
293
+ }
294
+ function extractConfigByNamespace(schema, dataset, namespace) {
295
+ const property = schema.properties[namespace];
296
+ if ((property == null ? void 0 : property.type) !== 'object') {
297
+ return;
298
+ }
299
+ const newObject = {
300
+ [namespace]: {}
301
+ };
302
+ for (const [key, value] of Object.entries(dataset)) {
303
+ let current = newObject;
304
+ const keyParts = key.split('.');
305
+ for (const [index, name] of keyParts.entries()) {
306
+ if (isObject(current)) {
307
+ if (index < keyParts.length - 1) {
308
+ if (!isObject(current[name])) {
309
+ current[name] = {};
310
+ }
311
+ current = current[name];
312
+ } else if (key !== namespace) {
313
+ current[name] = normaliseString(value);
314
+ }
315
+ }
316
+ }
317
+ }
318
+ return newObject[namespace];
319
+ }
320
+ /**
321
+ * Schema for component config
322
+ *
323
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
324
+ * @typedef {object} Schema
325
+ * @property {Record<keyof ConfigurationType, SchemaProperty | undefined>} properties - Schema properties
326
+ * @property {SchemaCondition<ConfigurationType>[]} [anyOf] - List of schema conditions
327
+ */
328
+ /**
329
+ * Schema property for component config
330
+ *
331
+ * @typedef {object} SchemaProperty
332
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
333
+ */
334
+ /**
335
+ * Schema condition for component config
336
+ *
337
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
338
+ * @typedef {object} SchemaCondition
339
+ * @property {(keyof ConfigurationType)[]} required - List of required config fields
340
+ * @property {string} errorMessage - Error message when required config fields not provided
341
+ */
342
+ /**
343
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
344
+ * @typedef ChildClass
345
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
346
+ * @property {Schema<ConfigurationType>} [schema] - The schema of the component configuration
347
+ * @property {ConfigurationType} [defaults] - The default values of the configuration of the component
348
+ */
349
+ /**
350
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
351
+ * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
352
+ */
353
+
239
354
  class I18n {
240
355
  constructor(translations = {}, config = {}) {
241
356
  var _config$locale;
@@ -442,16 +557,15 @@
442
557
  * attribute, which also provides accessibility.
443
558
  *
444
559
  * @preserve
560
+ * @augments ConfigurableComponent<AccordionConfig>
445
561
  */
446
- class Accordion extends GOVUKFrontendComponent {
562
+ class Accordion extends ConfigurableComponent {
447
563
  /**
448
- * @param {Element | null} $module - HTML element to use for accordion
564
+ * @param {Element | null} $root - HTML element to use for accordion
449
565
  * @param {AccordionConfig} [config] - Accordion config
450
566
  */
451
- constructor($module, config = {}) {
452
- super();
453
- this.$module = void 0;
454
- this.config = void 0;
567
+ constructor($root, config = {}) {
568
+ super($root, config);
455
569
  this.i18n = void 0;
456
570
  this.controlsClass = 'govuk-accordion__controls';
457
571
  this.showAllClass = 'govuk-accordion__show-all';
@@ -473,33 +587,21 @@
473
587
  this.sectionSummaryFocusClass = 'govuk-accordion__section-summary-focus';
474
588
  this.sectionContentClass = 'govuk-accordion__section-content';
475
589
  this.$sections = void 0;
476
- this.browserSupportsSessionStorage = false;
477
590
  this.$showAllButton = null;
478
591
  this.$showAllIcon = null;
479
592
  this.$showAllText = null;
480
- if (!($module instanceof HTMLElement)) {
481
- throw new ElementError({
482
- componentName: 'Accordion',
483
- element: $module,
484
- identifier: 'Root element (`$module`)'
485
- });
486
- }
487
- this.$module = $module;
488
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $module.dataset));
489
593
  this.i18n = new I18n(this.config.i18n);
490
- const $sections = this.$module.querySelectorAll(`.${this.sectionClass}`);
594
+ const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
491
595
  if (!$sections.length) {
492
596
  throw new ElementError({
493
- componentName: 'Accordion',
597
+ component: Accordion,
494
598
  identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
495
599
  });
496
600
  }
497
601
  this.$sections = $sections;
498
- this.browserSupportsSessionStorage = helper.checkForSessionStorage();
499
602
  this.initControls();
500
603
  this.initSectionHeaders();
501
- const areAllSectionsOpen = this.checkIfAllSectionsOpen();
502
- this.updateShowAllButton(areAllSectionsOpen);
604
+ this.updateShowAllButton(this.areAllSectionsOpen());
503
605
  }
504
606
  initControls() {
505
607
  this.$showAllButton = document.createElement('button');
@@ -512,7 +614,7 @@
512
614
  const $accordionControls = document.createElement('div');
513
615
  $accordionControls.setAttribute('class', this.controlsClass);
514
616
  $accordionControls.appendChild(this.$showAllButton);
515
- this.$module.insertBefore($accordionControls, this.$module.firstChild);
617
+ this.$root.insertBefore($accordionControls, this.$root.firstChild);
516
618
  this.$showAllText = document.createElement('span');
517
619
  this.$showAllText.classList.add(this.showAllTextClass);
518
620
  this.$showAllButton.appendChild(this.$showAllText);
@@ -526,7 +628,7 @@
526
628
  const $header = $section.querySelector(`.${this.sectionHeaderClass}`);
527
629
  if (!$header) {
528
630
  throw new ElementError({
529
- componentName: 'Accordion',
631
+ component: Accordion,
530
632
  identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
531
633
  });
532
634
  }
@@ -542,22 +644,22 @@
542
644
  const $summary = $header.querySelector(`.${this.sectionSummaryClass}`);
543
645
  if (!$heading) {
544
646
  throw new ElementError({
545
- componentName: 'Accordion',
647
+ component: Accordion,
546
648
  identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
547
649
  });
548
650
  }
549
651
  if (!$span) {
550
652
  throw new ElementError({
551
- componentName: 'Accordion',
653
+ component: Accordion,
552
654
  identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
553
655
  });
554
656
  }
555
657
  const $button = document.createElement('button');
556
658
  $button.setAttribute('type', 'button');
557
- $button.setAttribute('aria-controls', `${this.$module.id}-content-${index + 1}`);
659
+ $button.setAttribute('aria-controls', `${this.$root.id}-content-${index + 1}`);
558
660
  for (const attr of Array.from($span.attributes)) {
559
- if (attr.nodeName !== 'id') {
560
- $button.setAttribute(attr.nodeName, `${attr.nodeValue}`);
661
+ if (attr.name !== 'id') {
662
+ $button.setAttribute(attr.name, attr.value);
561
663
  }
562
664
  }
563
665
  const $headingText = document.createElement('span');
@@ -566,7 +668,7 @@
566
668
  const $headingTextFocus = document.createElement('span');
567
669
  $headingTextFocus.classList.add(this.sectionHeadingTextFocusClass);
568
670
  $headingText.appendChild($headingTextFocus);
569
- $headingTextFocus.innerHTML = $span.innerHTML;
671
+ Array.from($span.childNodes).forEach($child => $headingTextFocus.appendChild($child));
570
672
  const $showHideToggle = document.createElement('span');
571
673
  $showHideToggle.classList.add(this.sectionShowHideToggleClass);
572
674
  $showHideToggle.setAttribute('data-nosnippet', '');
@@ -581,16 +683,16 @@
581
683
  $showHideToggleFocus.appendChild($showHideText);
582
684
  $button.appendChild($headingText);
583
685
  $button.appendChild(this.getButtonPunctuationEl());
584
- if ($summary != null && $summary.parentNode) {
686
+ if ($summary) {
585
687
  const $summarySpan = document.createElement('span');
586
688
  const $summarySpanFocus = document.createElement('span');
587
689
  $summarySpanFocus.classList.add(this.sectionSummaryFocusClass);
588
690
  $summarySpan.appendChild($summarySpanFocus);
589
691
  for (const attr of Array.from($summary.attributes)) {
590
- $summarySpan.setAttribute(attr.nodeName, `${attr.nodeValue}`);
692
+ $summarySpan.setAttribute(attr.name, attr.value);
591
693
  }
592
- $summarySpanFocus.innerHTML = $summary.innerHTML;
593
- $summary.parentNode.replaceChild($summarySpan, $summary);
694
+ Array.from($summary.childNodes).forEach($child => $summarySpanFocus.appendChild($child));
695
+ $summary.remove();
594
696
  $button.appendChild($summarySpan);
595
697
  $button.appendChild(this.getButtonPunctuationEl());
596
698
  }
@@ -609,15 +711,15 @@
609
711
  }
610
712
  }
611
713
  onSectionToggle($section) {
612
- const expanded = this.isExpanded($section);
613
- this.setExpanded(!expanded, $section);
614
- this.storeState($section);
714
+ const nowExpanded = !this.isExpanded($section);
715
+ this.setExpanded(nowExpanded, $section);
716
+ this.storeState($section, nowExpanded);
615
717
  }
616
718
  onShowOrHideAllToggle() {
617
- const nowExpanded = !this.checkIfAllSectionsOpen();
719
+ const nowExpanded = !this.areAllSectionsOpen();
618
720
  this.$sections.forEach($section => {
619
721
  this.setExpanded(nowExpanded, $section);
620
- this.storeState($section);
722
+ this.storeState($section, nowExpanded);
621
723
  });
622
724
  this.updateShowAllButton(nowExpanded);
623
725
  }
@@ -628,7 +730,7 @@
628
730
  const $content = $section.querySelector(`.${this.sectionContentClass}`);
629
731
  if (!$content) {
630
732
  throw new ElementError({
631
- componentName: 'Accordion',
733
+ component: Accordion,
632
734
  identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
633
735
  });
634
736
  }
@@ -659,17 +761,13 @@
659
761
  $section.classList.remove(this.sectionExpandedClass);
660
762
  $showHideIcon.classList.add(this.downChevronIconClass);
661
763
  }
662
- const areAllSectionsOpen = this.checkIfAllSectionsOpen();
663
- this.updateShowAllButton(areAllSectionsOpen);
764
+ this.updateShowAllButton(this.areAllSectionsOpen());
664
765
  }
665
766
  isExpanded($section) {
666
767
  return $section.classList.contains(this.sectionExpandedClass);
667
768
  }
668
- checkIfAllSectionsOpen() {
669
- const sectionsCount = this.$sections.length;
670
- const expandedSectionCount = this.$module.querySelectorAll(`.${this.sectionExpandedClass}`).length;
671
- const areAllSectionsOpen = sectionsCount === expandedSectionCount;
672
- return areAllSectionsOpen;
769
+ areAllSectionsOpen() {
770
+ return Array.from(this.$sections).every($section => this.isExpanded($section));
673
771
  }
674
772
  updateShowAllButton(expanded) {
675
773
  if (!this.$showAllButton || !this.$showAllText || !this.$showAllIcon) {
@@ -679,78 +777,53 @@
679
777
  this.$showAllText.textContent = expanded ? this.i18n.t('hideAllSections') : this.i18n.t('showAllSections');
680
778
  this.$showAllIcon.classList.toggle(this.downChevronIconClass, !expanded);
681
779
  }
682
- storeState($section) {
683
- if (this.browserSupportsSessionStorage && this.config.rememberExpanded) {
684
- const $button = $section.querySelector(`.${this.sectionButtonClass}`);
685
- if ($button) {
686
- const contentId = $button.getAttribute('aria-controls');
687
- const contentState = $button.getAttribute('aria-expanded');
688
- if (contentId && contentState) {
689
- window.sessionStorage.setItem(contentId, contentState);
690
- }
691
- }
692
- }
780
+
781
+ /**
782
+ * Get the identifier for a section
783
+ *
784
+ * We need a unique way of identifying each content in the Accordion.
785
+ * Since an `#id` should be unique and an `id` is required for `aria-`
786
+ * attributes `id` can be safely used.
787
+ *
788
+ * @param {Element} $section - Section element
789
+ * @returns {string | undefined | null} Identifier for section
790
+ */
791
+ getIdentifier($section) {
792
+ const $button = $section.querySelector(`.${this.sectionButtonClass}`);
793
+ return $button == null ? void 0 : $button.getAttribute('aria-controls');
693
794
  }
694
- setInitialState($section) {
695
- if (this.browserSupportsSessionStorage && this.config.rememberExpanded) {
696
- const $button = $section.querySelector(`.${this.sectionButtonClass}`);
697
- if ($button) {
698
- const contentId = $button.getAttribute('aria-controls');
699
- const contentState = contentId ? window.sessionStorage.getItem(contentId) : null;
700
- if (contentState !== null) {
701
- this.setExpanded(contentState === 'true', $section);
702
- }
703
- }
795
+ storeState($section, isExpanded) {
796
+ if (!this.config.rememberExpanded) {
797
+ return;
704
798
  }
705
- }
706
- getButtonPunctuationEl() {
707
- const $punctuationEl = document.createElement('span');
708
- $punctuationEl.classList.add('govuk-visually-hidden', this.sectionHeadingDividerClass);
709
- $punctuationEl.innerHTML = ', ';
710
- return $punctuationEl;
711
- }
712
- }
713
- Accordion.moduleName = 'govuk-accordion';
714
- Accordion.defaults = Object.freeze({
715
- i18n: {
716
- hideAllSections: 'Hide all sections',
717
- hideSection: 'Hide',
718
- hideSectionAriaLabel: 'Hide this section',
719
- showAllSections: 'Show all sections',
720
- showSection: 'Show',
721
- showSectionAriaLabel: 'Show this section'
722
- },
723
- rememberExpanded: true
724
- });
725
- Accordion.schema = Object.freeze({
726
- properties: {
727
- i18n: {
728
- type: 'object'
729
- },
730
- rememberExpanded: {
731
- type: 'boolean'
799
+ const id = this.getIdentifier($section);
800
+ if (id) {
801
+ try {
802
+ window.sessionStorage.setItem(id, isExpanded.toString());
803
+ } catch (exception) {}
732
804
  }
733
805
  }
734
- });
735
- const helper = {
736
- /**
737
- * Check for `window.sessionStorage`, and that it actually works.
738
- *
739
- * @returns {boolean} True if session storage is available
740
- */
741
- checkForSessionStorage: function () {
742
- const testString = 'this is the test string';
743
- let result;
744
- try {
745
- window.sessionStorage.setItem(testString, testString);
746
- result = window.sessionStorage.getItem(testString) === testString.toString();
747
- window.sessionStorage.removeItem(testString);
748
- return result;
749
- } catch (exception) {
750
- return false;
806
+ setInitialState($section) {
807
+ if (!this.config.rememberExpanded) {
808
+ return;
809
+ }
810
+ const id = this.getIdentifier($section);
811
+ if (id) {
812
+ try {
813
+ const state = window.sessionStorage.getItem(id);
814
+ if (state !== null) {
815
+ this.setExpanded(state === 'true', $section);
816
+ }
817
+ } catch (exception) {}
751
818
  }
752
819
  }
753
- };
820
+ getButtonPunctuationEl() {
821
+ const $punctuationEl = document.createElement('span');
822
+ $punctuationEl.classList.add('govuk-visually-hidden', this.sectionHeadingDividerClass);
823
+ $punctuationEl.textContent = ', ';
824
+ return $punctuationEl;
825
+ }
826
+ }
754
827
 
755
828
  /**
756
829
  * Accordion config
@@ -786,8 +859,30 @@
786
859
  */
787
860
 
788
861
  /**
789
- * @typedef {import('../../common/index.mjs').Schema} Schema
862
+ * @import { Schema } from '../../common/configuration.mjs'
790
863
  */
864
+ Accordion.moduleName = 'govuk-accordion';
865
+ Accordion.defaults = Object.freeze({
866
+ i18n: {
867
+ hideAllSections: 'Hide all sections',
868
+ hideSection: 'Hide',
869
+ hideSectionAriaLabel: 'Hide this section',
870
+ showAllSections: 'Show all sections',
871
+ showSection: 'Show',
872
+ showSectionAriaLabel: 'Show this section'
873
+ },
874
+ rememberExpanded: true
875
+ });
876
+ Accordion.schema = Object.freeze({
877
+ properties: {
878
+ i18n: {
879
+ type: 'object'
880
+ },
881
+ rememberExpanded: {
882
+ type: 'boolean'
883
+ }
884
+ }
885
+ });
791
886
 
792
887
  const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
793
888
 
@@ -795,28 +890,18 @@
795
890
  * JavaScript enhancements for the Button component
796
891
  *
797
892
  * @preserve
893
+ * @augments ConfigurableComponent<ButtonConfig>
798
894
  */
799
- class Button extends GOVUKFrontendComponent {
895
+ class Button extends ConfigurableComponent {
800
896
  /**
801
- * @param {Element | null} $module - HTML element to use for button
897
+ * @param {Element | null} $root - HTML element to use for button
802
898
  * @param {ButtonConfig} [config] - Button config
803
899
  */
804
- constructor($module, config = {}) {
805
- super();
806
- this.$module = void 0;
807
- this.config = void 0;
900
+ constructor($root, config = {}) {
901
+ super($root, config);
808
902
  this.debounceFormSubmitTimer = null;
809
- if (!($module instanceof HTMLElement)) {
810
- throw new ElementError({
811
- componentName: 'Button',
812
- element: $module,
813
- identifier: 'Root element (`$module`)'
814
- });
815
- }
816
- this.$module = $module;
817
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $module.dataset));
818
- this.$module.addEventListener('keydown', event => this.handleKeyDown(event));
819
- this.$module.addEventListener('click', event => this.debounce(event));
903
+ this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
904
+ this.$root.addEventListener('click', event => this.debounce(event));
820
905
  }
821
906
  handleKeyDown(event) {
822
907
  const $target = event.target;
@@ -851,7 +936,7 @@
851
936
  */
852
937
 
853
938
  /**
854
- * @typedef {import('../../common/index.mjs').Schema} Schema
939
+ * @import { Schema } from '../../common/configuration.mjs'
855
940
  */
856
941
  Button.moduleName = 'govuk-button';
857
942
  Button.defaults = Object.freeze({
@@ -881,69 +966,63 @@
881
966
  * of the available characters/words has been entered.
882
967
  *
883
968
  * @preserve
969
+ * @augments ConfigurableComponent<CharacterCountConfig>
884
970
  */
885
- class CharacterCount extends GOVUKFrontendComponent {
971
+ class CharacterCount extends ConfigurableComponent {
972
+ [configOverride](datasetConfig) {
973
+ let configOverrides = {};
974
+ if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
975
+ configOverrides = {
976
+ maxlength: undefined,
977
+ maxwords: undefined
978
+ };
979
+ }
980
+ return configOverrides;
981
+ }
982
+
886
983
  /**
887
- * @param {Element | null} $module - HTML element to use for character count
984
+ * @param {Element | null} $root - HTML element to use for character count
888
985
  * @param {CharacterCountConfig} [config] - Character count config
889
986
  */
890
- constructor($module, config = {}) {
987
+ constructor($root, config = {}) {
891
988
  var _ref, _this$config$maxwords;
892
- super();
893
- this.$module = void 0;
989
+ super($root, config);
894
990
  this.$textarea = void 0;
895
991
  this.$visibleCountMessage = void 0;
896
992
  this.$screenReaderCountMessage = void 0;
897
993
  this.lastInputTimestamp = null;
898
994
  this.lastInputValue = '';
899
995
  this.valueChecker = null;
900
- this.config = void 0;
901
996
  this.i18n = void 0;
902
997
  this.maxLength = void 0;
903
- if (!($module instanceof HTMLElement)) {
904
- throw new ElementError({
905
- componentName: 'Character count',
906
- element: $module,
907
- identifier: 'Root element (`$module`)'
908
- });
909
- }
910
- const $textarea = $module.querySelector('.govuk-js-character-count');
998
+ const $textarea = this.$root.querySelector('.govuk-js-character-count');
911
999
  if (!($textarea instanceof HTMLTextAreaElement || $textarea instanceof HTMLInputElement)) {
912
1000
  throw new ElementError({
913
- componentName: 'Character count',
1001
+ component: CharacterCount,
914
1002
  element: $textarea,
915
1003
  expectedType: 'HTMLTextareaElement or HTMLInputElement',
916
1004
  identifier: 'Form field (`.govuk-js-character-count`)'
917
1005
  });
918
1006
  }
919
- const datasetConfig = normaliseDataset(CharacterCount, $module.dataset);
920
- let configOverrides = {};
921
- if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
922
- configOverrides = {
923
- maxlength: undefined,
924
- maxwords: undefined
925
- };
926
- }
927
- this.config = mergeConfigs(CharacterCount.defaults, config, configOverrides, datasetConfig);
928
1007
  const errors = validateConfig(CharacterCount.schema, this.config);
929
1008
  if (errors[0]) {
930
- throw new ConfigError(`Character count: ${errors[0]}`);
1009
+ throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
931
1010
  }
932
1011
  this.i18n = new I18n(this.config.i18n, {
933
- locale: closestAttributeValue($module, 'lang')
1012
+ locale: closestAttributeValue(this.$root, 'lang')
934
1013
  });
935
1014
  this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
936
- this.$module = $module;
937
1015
  this.$textarea = $textarea;
938
1016
  const textareaDescriptionId = `${this.$textarea.id}-info`;
939
1017
  const $textareaDescription = document.getElementById(textareaDescriptionId);
940
1018
  if (!$textareaDescription) {
941
1019
  throw new ElementError({
942
- componentName: 'Character count',
1020
+ component: CharacterCount,
943
1021
  element: $textareaDescription,
944
1022
  identifier: `Count message (\`id="${textareaDescriptionId}"\`)`
945
1023
  });
946
1024
  }
1025
+ this.$errorMessage = this.$root.querySelector('.govuk-error-message');
947
1026
  if (`${$textareaDescription.textContent}`.match(/^\s*$/)) {
948
1027
  $textareaDescription.textContent = this.i18n.t('textareaDescription', {
949
1028
  count: this.maxLength
@@ -1002,7 +1081,9 @@
1002
1081
  const remainingNumber = this.maxLength - this.count(this.$textarea.value);
1003
1082
  const isError = remainingNumber < 0;
1004
1083
  this.$visibleCountMessage.classList.toggle('govuk-character-count__message--disabled', !this.isOverThreshold());
1005
- this.$textarea.classList.toggle('govuk-textarea--error', isError);
1084
+ if (!this.$errorMessage) {
1085
+ this.$textarea.classList.toggle('govuk-textarea--error', isError);
1086
+ }
1006
1087
  this.$visibleCountMessage.classList.toggle('govuk-error-message', isError);
1007
1088
  this.$visibleCountMessage.classList.toggle('govuk-hint', !isError);
1008
1089
  this.$visibleCountMessage.textContent = this.getCountMessage();
@@ -1111,8 +1192,8 @@
1111
1192
  */
1112
1193
 
1113
1194
  /**
1114
- * @typedef {import('../../common/index.mjs').Schema} Schema
1115
- * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
1195
+ * @import { Schema } from '../../common/configuration.mjs'
1196
+ * @import { TranslationPluralForms } from '../../i18n.mjs'
1116
1197
  */
1117
1198
  CharacterCount.moduleName = 'govuk-character-count';
1118
1199
  CharacterCount.defaults = Object.freeze({
@@ -1170,7 +1251,7 @@
1170
1251
  *
1171
1252
  * @preserve
1172
1253
  */
1173
- class Checkboxes extends GOVUKFrontendComponent {
1254
+ class Checkboxes extends Component {
1174
1255
  /**
1175
1256
  * Checkboxes can be associated with a 'conditionally revealed' content block
1176
1257
  * – for example, a checkbox for 'Phone' could reveal an additional form field
@@ -1183,27 +1264,18 @@
1183
1264
  * (for example if the user has navigated back), and set up event handlers to
1184
1265
  * keep the reveal in sync with the checkbox state.
1185
1266
  *
1186
- * @param {Element | null} $module - HTML element to use for checkboxes
1267
+ * @param {Element | null} $root - HTML element to use for checkboxes
1187
1268
  */
1188
- constructor($module) {
1189
- super();
1190
- this.$module = void 0;
1269
+ constructor($root) {
1270
+ super($root);
1191
1271
  this.$inputs = void 0;
1192
- if (!($module instanceof HTMLElement)) {
1193
- throw new ElementError({
1194
- componentName: 'Checkboxes',
1195
- element: $module,
1196
- identifier: 'Root element (`$module`)'
1197
- });
1198
- }
1199
- const $inputs = $module.querySelectorAll('input[type="checkbox"]');
1272
+ const $inputs = this.$root.querySelectorAll('input[type="checkbox"]');
1200
1273
  if (!$inputs.length) {
1201
1274
  throw new ElementError({
1202
- componentName: 'Checkboxes',
1275
+ component: Checkboxes,
1203
1276
  identifier: 'Form inputs (`<input type="checkbox">`)'
1204
1277
  });
1205
1278
  }
1206
- this.$module = $module;
1207
1279
  this.$inputs = $inputs;
1208
1280
  this.$inputs.forEach($input => {
1209
1281
  const targetId = $input.getAttribute('data-aria-controls');
@@ -1212,7 +1284,7 @@
1212
1284
  }
1213
1285
  if (!document.getElementById(targetId)) {
1214
1286
  throw new ElementError({
1215
- componentName: 'Checkboxes',
1287
+ component: Checkboxes,
1216
1288
  identifier: `Conditional reveal (\`id="${targetId}"\`)`
1217
1289
  });
1218
1290
  }
@@ -1221,7 +1293,7 @@
1221
1293
  });
1222
1294
  window.addEventListener('pageshow', () => this.syncAllConditionalReveals());
1223
1295
  this.syncAllConditionalReveals();
1224
- this.$module.addEventListener('click', event => this.handleClick(event));
1296
+ this.$root.addEventListener('click', event => this.handleClick(event));
1225
1297
  }
1226
1298
  syncAllConditionalReveals() {
1227
1299
  this.$inputs.forEach($input => this.syncConditionalRevealWithInputState($input));
@@ -1287,29 +1359,19 @@
1287
1359
  * configuration.
1288
1360
  *
1289
1361
  * @preserve
1362
+ * @augments ConfigurableComponent<ErrorSummaryConfig>
1290
1363
  */
1291
- class ErrorSummary extends GOVUKFrontendComponent {
1364
+ class ErrorSummary extends ConfigurableComponent {
1292
1365
  /**
1293
- * @param {Element | null} $module - HTML element to use for error summary
1366
+ * @param {Element | null} $root - HTML element to use for error summary
1294
1367
  * @param {ErrorSummaryConfig} [config] - Error summary config
1295
1368
  */
1296
- constructor($module, config = {}) {
1297
- super();
1298
- this.$module = void 0;
1299
- this.config = void 0;
1300
- if (!($module instanceof HTMLElement)) {
1301
- throw new ElementError({
1302
- componentName: 'Error summary',
1303
- element: $module,
1304
- identifier: 'Root element (`$module`)'
1305
- });
1306
- }
1307
- this.$module = $module;
1308
- this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $module.dataset));
1369
+ constructor($root, config = {}) {
1370
+ super($root, config);
1309
1371
  if (!this.config.disableAutoFocus) {
1310
- setFocus(this.$module);
1372
+ setFocus(this.$root);
1311
1373
  }
1312
- this.$module.addEventListener('click', event => this.handleClick(event));
1374
+ this.$root.addEventListener('click', event => this.handleClick(event));
1313
1375
  }
1314
1376
  handleClick(event) {
1315
1377
  const $target = event.target;
@@ -1372,7 +1434,7 @@
1372
1434
  */
1373
1435
 
1374
1436
  /**
1375
- * @typedef {import('../../common/index.mjs').Schema} Schema
1437
+ * @import { Schema } from '../../common/configuration.mjs'
1376
1438
  */
1377
1439
  ErrorSummary.moduleName = 'govuk-error-summary';
1378
1440
  ErrorSummary.defaults = Object.freeze({
@@ -1390,16 +1452,15 @@
1390
1452
  * Exit this page component
1391
1453
  *
1392
1454
  * @preserve
1455
+ * @augments ConfigurableComponent<ExitThisPageConfig>
1393
1456
  */
1394
- class ExitThisPage extends GOVUKFrontendComponent {
1457
+ class ExitThisPage extends ConfigurableComponent {
1395
1458
  /**
1396
- * @param {Element | null} $module - HTML element that wraps the Exit This Page button
1459
+ * @param {Element | null} $root - HTML element that wraps the Exit This Page button
1397
1460
  * @param {ExitThisPageConfig} [config] - Exit This Page config
1398
1461
  */
1399
- constructor($module, config = {}) {
1400
- super();
1401
- this.$module = void 0;
1402
- this.config = void 0;
1462
+ constructor($root, config = {}) {
1463
+ super($root, config);
1403
1464
  this.i18n = void 0;
1404
1465
  this.$button = void 0;
1405
1466
  this.$skiplinkButton = null;
@@ -1411,25 +1472,16 @@
1411
1472
  this.timeoutTime = 5000;
1412
1473
  this.keypressTimeoutId = null;
1413
1474
  this.timeoutMessageId = null;
1414
- if (!($module instanceof HTMLElement)) {
1415
- throw new ElementError({
1416
- componentName: 'Exit this page',
1417
- element: $module,
1418
- identifier: 'Root element (`$module`)'
1419
- });
1420
- }
1421
- const $button = $module.querySelector('.govuk-exit-this-page__button');
1475
+ const $button = this.$root.querySelector('.govuk-exit-this-page__button');
1422
1476
  if (!($button instanceof HTMLAnchorElement)) {
1423
1477
  throw new ElementError({
1424
- componentName: 'Exit this page',
1478
+ component: ExitThisPage,
1425
1479
  element: $button,
1426
1480
  expectedType: 'HTMLAnchorElement',
1427
1481
  identifier: 'Button (`.govuk-exit-this-page__button`)'
1428
1482
  });
1429
1483
  }
1430
- this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $module.dataset));
1431
1484
  this.i18n = new I18n(this.config.i18n);
1432
- this.$module = $module;
1433
1485
  this.$button = $button;
1434
1486
  const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
1435
1487
  if ($skiplinkButton instanceof HTMLAnchorElement) {
@@ -1448,7 +1500,7 @@
1448
1500
  this.$updateSpan = document.createElement('span');
1449
1501
  this.$updateSpan.setAttribute('role', 'status');
1450
1502
  this.$updateSpan.className = 'govuk-visually-hidden';
1451
- this.$module.appendChild(this.$updateSpan);
1503
+ this.$root.appendChild(this.$updateSpan);
1452
1504
  }
1453
1505
  initButtonClickHandler() {
1454
1506
  this.$button.addEventListener('click', this.handleClick.bind(this));
@@ -1594,7 +1646,7 @@
1594
1646
  */
1595
1647
 
1596
1648
  /**
1597
- * @typedef {import('../../common/index.mjs').Schema} Schema
1649
+ * @import { Schema } from '../../common/configuration.mjs'
1598
1650
  */
1599
1651
  ExitThisPage.moduleName = 'govuk-exit-this-page';
1600
1652
  ExitThisPage.defaults = Object.freeze({
@@ -1613,48 +1665,289 @@
1613
1665
  }
1614
1666
  });
1615
1667
 
1668
+ /**
1669
+ * File upload component
1670
+ *
1671
+ * @preserve
1672
+ * @augments ConfigurableComponent<FileUploadConfig>
1673
+ */
1674
+ class FileUpload extends ConfigurableComponent {
1675
+ /**
1676
+ * @param {Element | null} $root - File input element
1677
+ * @param {FileUploadConfig} [config] - File Upload config
1678
+ */
1679
+ constructor($root, config = {}) {
1680
+ super($root, config);
1681
+ this.$input = void 0;
1682
+ this.$button = void 0;
1683
+ this.$status = void 0;
1684
+ this.i18n = void 0;
1685
+ this.id = void 0;
1686
+ this.$announcements = void 0;
1687
+ this.enteredAnotherElement = void 0;
1688
+ const $input = this.$root.querySelector('input');
1689
+ if ($input === null) {
1690
+ throw new ElementError({
1691
+ component: FileUpload,
1692
+ identifier: 'File inputs (`<input type="file">`)'
1693
+ });
1694
+ }
1695
+ if ($input.type !== 'file') {
1696
+ throw new ElementError(formatErrorMessage(FileUpload, 'File input (`<input type="file">`) attribute (`type`) is not `file`'));
1697
+ }
1698
+ this.$input = $input;
1699
+ this.$input.setAttribute('hidden', 'true');
1700
+ if (!this.$input.id) {
1701
+ throw new ElementError({
1702
+ component: FileUpload,
1703
+ identifier: 'File input (`<input type="file">`) attribute (`id`)'
1704
+ });
1705
+ }
1706
+ this.id = this.$input.id;
1707
+ this.i18n = new I18n(this.config.i18n, {
1708
+ locale: closestAttributeValue(this.$root, 'lang')
1709
+ });
1710
+ const $label = this.findLabel();
1711
+ if (!$label.id) {
1712
+ $label.id = `${this.id}-label`;
1713
+ }
1714
+ this.$input.id = `${this.id}-input`;
1715
+ const $button = document.createElement('button');
1716
+ $button.classList.add('govuk-file-upload-button');
1717
+ $button.type = 'button';
1718
+ $button.id = this.id;
1719
+ $button.classList.add('govuk-file-upload-button--empty');
1720
+ const ariaDescribedBy = this.$input.getAttribute('aria-describedby');
1721
+ if (ariaDescribedBy) {
1722
+ $button.setAttribute('aria-describedby', ariaDescribedBy);
1723
+ }
1724
+ const $status = document.createElement('span');
1725
+ $status.className = 'govuk-body govuk-file-upload-button__status';
1726
+ $status.setAttribute('aria-live', 'polite');
1727
+ $status.innerText = this.i18n.t('noFileChosen');
1728
+ $button.appendChild($status);
1729
+ const commaSpan = document.createElement('span');
1730
+ commaSpan.className = 'govuk-visually-hidden';
1731
+ commaSpan.innerText = ', ';
1732
+ commaSpan.id = `${this.id}-comma`;
1733
+ $button.appendChild(commaSpan);
1734
+ const containerSpan = document.createElement('span');
1735
+ containerSpan.className = 'govuk-file-upload-button__pseudo-button-container';
1736
+ const buttonSpan = document.createElement('span');
1737
+ buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload-button__pseudo-button';
1738
+ buttonSpan.innerText = this.i18n.t('chooseFilesButton');
1739
+ containerSpan.appendChild(buttonSpan);
1740
+ containerSpan.insertAdjacentText('beforeend', ' ');
1741
+ const instructionSpan = document.createElement('span');
1742
+ instructionSpan.className = 'govuk-body govuk-file-upload-button__instruction';
1743
+ instructionSpan.innerText = this.i18n.t('dropInstruction');
1744
+ containerSpan.appendChild(instructionSpan);
1745
+ $button.appendChild(containerSpan);
1746
+ $button.setAttribute('aria-labelledby', `${$label.id} ${commaSpan.id} ${$button.id}`);
1747
+ $button.addEventListener('click', this.onClick.bind(this));
1748
+ $button.addEventListener('dragover', event => {
1749
+ event.preventDefault();
1750
+ });
1751
+ this.$root.insertAdjacentElement('afterbegin', $button);
1752
+ this.$input.setAttribute('tabindex', '-1');
1753
+ this.$input.setAttribute('aria-hidden', 'true');
1754
+ this.$button = $button;
1755
+ this.$status = $status;
1756
+ this.$input.addEventListener('change', this.onChange.bind(this));
1757
+ this.updateDisabledState();
1758
+ this.observeDisabledState();
1759
+ this.$announcements = document.createElement('span');
1760
+ this.$announcements.classList.add('govuk-file-upload-announcements');
1761
+ this.$announcements.classList.add('govuk-visually-hidden');
1762
+ this.$announcements.setAttribute('aria-live', 'assertive');
1763
+ this.$root.insertAdjacentElement('afterend', this.$announcements);
1764
+ this.$button.addEventListener('drop', this.onDrop.bind(this));
1765
+ document.addEventListener('dragenter', this.updateDropzoneVisibility.bind(this));
1766
+ document.addEventListener('dragenter', () => {
1767
+ this.enteredAnotherElement = true;
1768
+ });
1769
+ document.addEventListener('dragleave', () => {
1770
+ if (!this.enteredAnotherElement && !this.$button.disabled) {
1771
+ this.hideDraggingState();
1772
+ this.$announcements.innerText = this.i18n.t('leftDropZone');
1773
+ }
1774
+ this.enteredAnotherElement = false;
1775
+ });
1776
+ }
1777
+ updateDropzoneVisibility(event) {
1778
+ if (this.$button.disabled) return;
1779
+ if (event.target instanceof Node) {
1780
+ if (this.$root.contains(event.target)) {
1781
+ if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
1782
+ if (!this.$button.classList.contains('govuk-file-upload-button--dragging')) {
1783
+ this.showDraggingState();
1784
+ this.$announcements.innerText = this.i18n.t('enteredDropZone');
1785
+ }
1786
+ }
1787
+ } else {
1788
+ if (this.$button.classList.contains('govuk-file-upload-button--dragging')) {
1789
+ this.hideDraggingState();
1790
+ this.$announcements.innerText = this.i18n.t('leftDropZone');
1791
+ }
1792
+ }
1793
+ }
1794
+ }
1795
+ showDraggingState() {
1796
+ this.$button.classList.add('govuk-file-upload-button--dragging');
1797
+ }
1798
+ hideDraggingState() {
1799
+ this.$button.classList.remove('govuk-file-upload-button--dragging');
1800
+ }
1801
+ onDrop(event) {
1802
+ event.preventDefault();
1803
+ if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
1804
+ this.$input.files = event.dataTransfer.files;
1805
+ this.$input.dispatchEvent(new CustomEvent('change'));
1806
+ this.hideDraggingState();
1807
+ }
1808
+ }
1809
+ onChange() {
1810
+ const fileCount = this.$input.files.length;
1811
+ if (fileCount === 0) {
1812
+ this.$status.innerText = this.i18n.t('noFileChosen');
1813
+ this.$button.classList.add('govuk-file-upload-button--empty');
1814
+ } else {
1815
+ if (fileCount === 1) {
1816
+ this.$status.innerText = this.$input.files[0].name;
1817
+ } else {
1818
+ this.$status.innerText = this.i18n.t('multipleFilesChosen', {
1819
+ count: fileCount
1820
+ });
1821
+ }
1822
+ this.$button.classList.remove('govuk-file-upload-button--empty');
1823
+ }
1824
+ }
1825
+ findLabel() {
1826
+ const $label = document.querySelector(`label[for="${this.$input.id}"]`);
1827
+ if (!$label) {
1828
+ throw new ElementError({
1829
+ component: FileUpload,
1830
+ identifier: `Field label (\`<label for=${this.$input.id}>\`)`
1831
+ });
1832
+ }
1833
+ return $label;
1834
+ }
1835
+ onClick() {
1836
+ this.$input.click();
1837
+ }
1838
+ observeDisabledState() {
1839
+ const observer = new MutationObserver(mutationList => {
1840
+ for (const mutation of mutationList) {
1841
+ if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
1842
+ this.updateDisabledState();
1843
+ }
1844
+ }
1845
+ });
1846
+ observer.observe(this.$input, {
1847
+ attributes: true
1848
+ });
1849
+ }
1850
+ updateDisabledState() {
1851
+ this.$button.disabled = this.$input.disabled;
1852
+ this.$root.classList.toggle('govuk-drop-zone--disabled', this.$button.disabled);
1853
+ }
1854
+ }
1855
+ FileUpload.moduleName = 'govuk-file-upload';
1856
+ FileUpload.defaults = Object.freeze({
1857
+ i18n: {
1858
+ chooseFilesButton: 'Choose file',
1859
+ dropInstruction: 'or drop file',
1860
+ noFileChosen: 'No file chosen',
1861
+ multipleFilesChosen: {
1862
+ one: '%{count} file chosen',
1863
+ other: '%{count} files chosen'
1864
+ },
1865
+ enteredDropZone: 'Entered drop zone',
1866
+ leftDropZone: 'Left drop zone'
1867
+ }
1868
+ });
1869
+ FileUpload.schema = Object.freeze({
1870
+ properties: {
1871
+ i18n: {
1872
+ type: 'object'
1873
+ }
1874
+ }
1875
+ });
1876
+ function isContainingFiles(dataTransfer) {
1877
+ const hasNoTypesInfo = dataTransfer.types.length === 0;
1878
+ const isDraggingFiles = dataTransfer.types.some(type => type === 'Files');
1879
+ return hasNoTypesInfo || isDraggingFiles;
1880
+ }
1881
+
1882
+ /**
1883
+ * @typedef {HTMLInputElement & {files: FileList}} HTMLFileInputElement
1884
+ */
1885
+
1886
+ /**
1887
+ * File upload config
1888
+ *
1889
+ * @see {@link FileUpload.defaults}
1890
+ * @typedef {object} FileUploadConfig
1891
+ * @property {FileUploadTranslations} [i18n=FileUpload.defaults.i18n] - File upload translations
1892
+ */
1893
+
1894
+ /**
1895
+ * File upload translations
1896
+ *
1897
+ * @see {@link FileUpload.defaults.i18n}
1898
+ * @typedef {object} FileUploadTranslations
1899
+ *
1900
+ * Messages used by the component
1901
+ * @property {string} [chooseFile] - The text of the button that opens the file picker
1902
+ * @property {string} [dropInstruction] - The text informing users they can drop files
1903
+ * @property {TranslationPluralForms} [multipleFilesChosen] - The text displayed when multiple files
1904
+ * have been chosen by the user
1905
+ * @property {string} [noFileChosen] - The text to displayed when no file has been chosen by the user
1906
+ * @property {string} [enteredDropZone] - The text announced by assistive technology
1907
+ * when user drags files and enters the drop zone
1908
+ * @property {string} [leftDropZone] - The text announced by assistive technology
1909
+ * when user drags files and leaves the drop zone without dropping
1910
+ */
1911
+
1912
+ /**
1913
+ * @import { Schema } from '../../common/configuration.mjs'
1914
+ * @import { TranslationPluralForms } from '../../i18n.mjs'
1915
+ */
1916
+
1616
1917
  /**
1617
1918
  * Header component
1618
1919
  *
1619
1920
  * @preserve
1620
1921
  */
1621
- class Header extends GOVUKFrontendComponent {
1922
+ class Header extends Component {
1622
1923
  /**
1623
1924
  * Apply a matchMedia for desktop which will trigger a state sync if the
1624
1925
  * browser viewport moves between states.
1625
1926
  *
1626
- * @param {Element | null} $module - HTML element to use for header
1927
+ * @param {Element | null} $root - HTML element to use for header
1627
1928
  */
1628
- constructor($module) {
1629
- super();
1630
- this.$module = void 0;
1929
+ constructor($root) {
1930
+ super($root);
1631
1931
  this.$menuButton = void 0;
1632
1932
  this.$menu = void 0;
1633
1933
  this.menuIsOpen = false;
1634
1934
  this.mql = null;
1635
- if (!$module) {
1636
- throw new ElementError({
1637
- componentName: 'Header',
1638
- element: $module,
1639
- identifier: 'Root element (`$module`)'
1640
- });
1641
- }
1642
- this.$module = $module;
1643
- const $menuButton = $module.querySelector('.govuk-js-header-toggle');
1935
+ const $menuButton = this.$root.querySelector('.govuk-js-header-toggle');
1644
1936
  if (!$menuButton) {
1645
1937
  return this;
1646
1938
  }
1939
+ this.$root.classList.add('govuk-header--with-js-navigation');
1647
1940
  const menuId = $menuButton.getAttribute('aria-controls');
1648
1941
  if (!menuId) {
1649
1942
  throw new ElementError({
1650
- componentName: 'Header',
1943
+ component: Header,
1651
1944
  identifier: 'Navigation button (`<button class="govuk-js-header-toggle">`) attribute (`aria-controls`)'
1652
1945
  });
1653
1946
  }
1654
1947
  const $menu = document.getElementById(menuId);
1655
1948
  if (!$menu) {
1656
1949
  throw new ElementError({
1657
- componentName: 'Header',
1950
+ component: Header,
1658
1951
  element: $menu,
1659
1952
  identifier: `Navigation (\`<ul id="${menuId}">\`)`
1660
1953
  });
@@ -1668,7 +1961,7 @@
1668
1961
  const breakpoint = getBreakpoint('desktop');
1669
1962
  if (!breakpoint.value) {
1670
1963
  throw new ElementError({
1671
- componentName: 'Header',
1964
+ component: Header,
1672
1965
  identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
1673
1966
  });
1674
1967
  }
@@ -1708,27 +2001,17 @@
1708
2001
  * Notification Banner component
1709
2002
  *
1710
2003
  * @preserve
2004
+ * @augments ConfigurableComponent<NotificationBannerConfig>
1711
2005
  */
1712
- class NotificationBanner extends GOVUKFrontendComponent {
2006
+ class NotificationBanner extends ConfigurableComponent {
1713
2007
  /**
1714
- * @param {Element | null} $module - HTML element to use for notification banner
2008
+ * @param {Element | null} $root - HTML element to use for notification banner
1715
2009
  * @param {NotificationBannerConfig} [config] - Notification banner config
1716
2010
  */
1717
- constructor($module, config = {}) {
1718
- super();
1719
- this.$module = void 0;
1720
- this.config = void 0;
1721
- if (!($module instanceof HTMLElement)) {
1722
- throw new ElementError({
1723
- componentName: 'Notification banner',
1724
- element: $module,
1725
- identifier: 'Root element (`$module`)'
1726
- });
1727
- }
1728
- this.$module = $module;
1729
- this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $module.dataset));
1730
- if (this.$module.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
1731
- setFocus(this.$module);
2011
+ constructor($root, config = {}) {
2012
+ super($root, config);
2013
+ if (this.$root.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
2014
+ setFocus(this.$root);
1732
2015
  }
1733
2016
  }
1734
2017
  }
@@ -1744,7 +2027,7 @@
1744
2027
  */
1745
2028
 
1746
2029
  /**
1747
- * @typedef {import('../../common/index.mjs').Schema} Schema
2030
+ * @import { Schema } from '../../common/configuration.mjs'
1748
2031
  */
1749
2032
  NotificationBanner.moduleName = 'govuk-notification-banner';
1750
2033
  NotificationBanner.defaults = Object.freeze({
@@ -1762,31 +2045,23 @@
1762
2045
  * Password input component
1763
2046
  *
1764
2047
  * @preserve
2048
+ * @augments ConfigurableComponent<PasswordInputConfig>
1765
2049
  */
1766
- class PasswordInput extends GOVUKFrontendComponent {
2050
+ class PasswordInput extends ConfigurableComponent {
1767
2051
  /**
1768
- * @param {Element | null} $module - HTML element to use for password input
2052
+ * @param {Element | null} $root - HTML element to use for password input
1769
2053
  * @param {PasswordInputConfig} [config] - Password input config
1770
2054
  */
1771
- constructor($module, config = {}) {
1772
- super();
1773
- this.$module = void 0;
1774
- this.config = void 0;
2055
+ constructor($root, config = {}) {
2056
+ super($root, config);
1775
2057
  this.i18n = void 0;
1776
2058
  this.$input = void 0;
1777
2059
  this.$showHideButton = void 0;
1778
2060
  this.$screenReaderStatusMessage = void 0;
1779
- if (!($module instanceof HTMLElement)) {
1780
- throw new ElementError({
1781
- componentName: 'Password input',
1782
- element: $module,
1783
- identifier: 'Root element (`$module`)'
1784
- });
1785
- }
1786
- const $input = $module.querySelector('.govuk-js-password-input-input');
2061
+ const $input = this.$root.querySelector('.govuk-js-password-input-input');
1787
2062
  if (!($input instanceof HTMLInputElement)) {
1788
2063
  throw new ElementError({
1789
- componentName: 'Password input',
2064
+ component: PasswordInput,
1790
2065
  element: $input,
1791
2066
  expectedType: 'HTMLInputElement',
1792
2067
  identifier: 'Form field (`.govuk-js-password-input-input`)'
@@ -1795,10 +2070,10 @@
1795
2070
  if ($input.type !== 'password') {
1796
2071
  throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
1797
2072
  }
1798
- const $showHideButton = $module.querySelector('.govuk-js-password-input-toggle');
2073
+ const $showHideButton = this.$root.querySelector('.govuk-js-password-input-toggle');
1799
2074
  if (!($showHideButton instanceof HTMLButtonElement)) {
1800
2075
  throw new ElementError({
1801
- componentName: 'Password input',
2076
+ component: PasswordInput,
1802
2077
  element: $showHideButton,
1803
2078
  expectedType: 'HTMLButtonElement',
1804
2079
  identifier: 'Button (`.govuk-js-password-input-toggle`)'
@@ -1807,12 +2082,10 @@
1807
2082
  if ($showHideButton.type !== 'button') {
1808
2083
  throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
1809
2084
  }
1810
- this.$module = $module;
1811
2085
  this.$input = $input;
1812
2086
  this.$showHideButton = $showHideButton;
1813
- this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $module.dataset));
1814
2087
  this.i18n = new I18n(this.config.i18n, {
1815
- locale: closestAttributeValue($module, 'lang')
2088
+ locale: closestAttributeValue(this.$root, 'lang')
1816
2089
  });
1817
2090
  this.$showHideButton.removeAttribute('hidden');
1818
2091
  const $screenReaderStatusMessage = document.createElement('div');
@@ -1890,8 +2163,7 @@
1890
2163
  */
1891
2164
 
1892
2165
  /**
1893
- * @typedef {import('../../common/index.mjs').Schema} Schema
1894
- * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
2166
+ * @import { Schema } from '../../common/configuration.mjs'
1895
2167
  */
1896
2168
  PasswordInput.moduleName = 'govuk-password-input';
1897
2169
  PasswordInput.defaults = Object.freeze({
@@ -1917,7 +2189,7 @@
1917
2189
  *
1918
2190
  * @preserve
1919
2191
  */
1920
- class Radios extends GOVUKFrontendComponent {
2192
+ class Radios extends Component {
1921
2193
  /**
1922
2194
  * Radios can be associated with a 'conditionally revealed' content block –
1923
2195
  * for example, a radio for 'Phone' could reveal an additional form field for
@@ -1930,27 +2202,18 @@
1930
2202
  * (for example if the user has navigated back), and set up event handlers to
1931
2203
  * keep the reveal in sync with the radio state.
1932
2204
  *
1933
- * @param {Element | null} $module - HTML element to use for radios
2205
+ * @param {Element | null} $root - HTML element to use for radios
1934
2206
  */
1935
- constructor($module) {
1936
- super();
1937
- this.$module = void 0;
2207
+ constructor($root) {
2208
+ super($root);
1938
2209
  this.$inputs = void 0;
1939
- if (!($module instanceof HTMLElement)) {
1940
- throw new ElementError({
1941
- componentName: 'Radios',
1942
- element: $module,
1943
- identifier: 'Root element (`$module`)'
1944
- });
1945
- }
1946
- const $inputs = $module.querySelectorAll('input[type="radio"]');
2210
+ const $inputs = this.$root.querySelectorAll('input[type="radio"]');
1947
2211
  if (!$inputs.length) {
1948
2212
  throw new ElementError({
1949
- componentName: 'Radios',
2213
+ component: Radios,
1950
2214
  identifier: 'Form inputs (`<input type="radio">`)'
1951
2215
  });
1952
2216
  }
1953
- this.$module = $module;
1954
2217
  this.$inputs = $inputs;
1955
2218
  this.$inputs.forEach($input => {
1956
2219
  const targetId = $input.getAttribute('data-aria-controls');
@@ -1959,7 +2222,7 @@
1959
2222
  }
1960
2223
  if (!document.getElementById(targetId)) {
1961
2224
  throw new ElementError({
1962
- componentName: 'Radios',
2225
+ component: Radios,
1963
2226
  identifier: `Conditional reveal (\`id="${targetId}"\`)`
1964
2227
  });
1965
2228
  }
@@ -1968,7 +2231,7 @@
1968
2231
  });
1969
2232
  window.addEventListener('pageshow', () => this.syncAllConditionalReveals());
1970
2233
  this.syncAllConditionalReveals();
1971
- this.$module.addEventListener('click', event => this.handleClick(event));
2234
+ this.$root.addEventListener('click', event => this.handleClick(event));
1972
2235
  }
1973
2236
  syncAllConditionalReveals() {
1974
2237
  this.$inputs.forEach($input => this.syncConditionalRevealWithInputState($input));
@@ -2005,35 +2268,105 @@
2005
2268
  Radios.moduleName = 'govuk-radios';
2006
2269
 
2007
2270
  /**
2008
- * Skip link component
2271
+ * Service Navigation component
2009
2272
  *
2010
2273
  * @preserve
2011
2274
  */
2012
- class SkipLink extends GOVUKFrontendComponent {
2275
+ class ServiceNavigation extends Component {
2013
2276
  /**
2014
- * @param {Element | null} $module - HTML element to use for skip link
2015
- * @throws {ElementError} when $module is not set or the wrong type
2016
- * @throws {ElementError} when $module.hash does not contain a hash
2017
- * @throws {ElementError} when the linked element is missing or the wrong type
2277
+ * @param {Element | null} $root - HTML element to use for header
2018
2278
  */
2019
- constructor($module) {
2020
- var _this$$module$getAttr;
2021
- super();
2022
- this.$module = void 0;
2023
- if (!($module instanceof HTMLAnchorElement)) {
2279
+ constructor($root) {
2280
+ super($root);
2281
+ this.$menuButton = void 0;
2282
+ this.$menu = void 0;
2283
+ this.menuIsOpen = false;
2284
+ this.mql = null;
2285
+ const $menuButton = this.$root.querySelector('.govuk-js-service-navigation-toggle');
2286
+ if (!$menuButton) {
2287
+ return this;
2288
+ }
2289
+ const menuId = $menuButton.getAttribute('aria-controls');
2290
+ if (!menuId) {
2024
2291
  throw new ElementError({
2025
- componentName: 'Skip link',
2026
- element: $module,
2027
- expectedType: 'HTMLAnchorElement',
2028
- identifier: 'Root element (`$module`)'
2292
+ component: ServiceNavigation,
2293
+ identifier: 'Navigation button (`<button class="govuk-js-service-navigation-toggle">`) attribute (`aria-controls`)'
2294
+ });
2295
+ }
2296
+ const $menu = document.getElementById(menuId);
2297
+ if (!$menu) {
2298
+ throw new ElementError({
2299
+ component: ServiceNavigation,
2300
+ element: $menu,
2301
+ identifier: `Navigation (\`<ul id="${menuId}">\`)`
2302
+ });
2303
+ }
2304
+ this.$menu = $menu;
2305
+ this.$menuButton = $menuButton;
2306
+ this.setupResponsiveChecks();
2307
+ this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
2308
+ }
2309
+ setupResponsiveChecks() {
2310
+ const breakpoint = getBreakpoint('tablet');
2311
+ if (!breakpoint.value) {
2312
+ throw new ElementError({
2313
+ component: ServiceNavigation,
2314
+ identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
2029
2315
  });
2030
2316
  }
2031
- this.$module = $module;
2032
- const hash = this.$module.hash;
2033
- const href = (_this$$module$getAttr = this.$module.getAttribute('href')) != null ? _this$$module$getAttr : '';
2317
+ this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
2318
+ if ('addEventListener' in this.mql) {
2319
+ this.mql.addEventListener('change', () => this.checkMode());
2320
+ } else {
2321
+ this.mql.addListener(() => this.checkMode());
2322
+ }
2323
+ this.checkMode();
2324
+ }
2325
+ checkMode() {
2326
+ if (!this.mql || !this.$menu || !this.$menuButton) {
2327
+ return;
2328
+ }
2329
+ if (this.mql.matches) {
2330
+ this.$menu.removeAttribute('hidden');
2331
+ this.$menuButton.setAttribute('hidden', '');
2332
+ } else {
2333
+ this.$menuButton.removeAttribute('hidden');
2334
+ this.$menuButton.setAttribute('aria-expanded', this.menuIsOpen.toString());
2335
+ if (this.menuIsOpen) {
2336
+ this.$menu.removeAttribute('hidden');
2337
+ } else {
2338
+ this.$menu.setAttribute('hidden', '');
2339
+ }
2340
+ }
2341
+ }
2342
+ handleMenuButtonClick() {
2343
+ this.menuIsOpen = !this.menuIsOpen;
2344
+ this.checkMode();
2345
+ }
2346
+ }
2347
+ ServiceNavigation.moduleName = 'govuk-service-navigation';
2348
+
2349
+ /**
2350
+ * Skip link component
2351
+ *
2352
+ * @preserve
2353
+ * @augments Component<HTMLAnchorElement>
2354
+ */
2355
+ class SkipLink extends Component {
2356
+ /**
2357
+ * @param {Element | null} $root - HTML element to use for skip link
2358
+ * @throws {ElementError} when $root is not set or the wrong type
2359
+ * @throws {ElementError} when $root.hash does not contain a hash
2360
+ * @throws {ElementError} when the linked element is missing or the wrong type
2361
+ */
2362
+ constructor($root) {
2363
+ var _this$$root$getAttrib;
2364
+ super($root);
2365
+ const hash = this.$root.hash;
2366
+ const href = (_this$$root$getAttrib = this.$root.getAttribute('href')) != null ? _this$$root$getAttrib : '';
2034
2367
  let url;
2035
2368
  try {
2036
- url = new window.URL(this.$module.href);
2369
+ url = new window.URL(this.$root.href);
2037
2370
  } catch (error) {
2038
2371
  throw new ElementError(`Skip link: Target link (\`href="${href}"\`) is invalid`);
2039
2372
  }
@@ -2047,12 +2380,12 @@
2047
2380
  const $linkedElement = document.getElementById(linkedElementId);
2048
2381
  if (!$linkedElement) {
2049
2382
  throw new ElementError({
2050
- componentName: 'Skip link',
2383
+ component: SkipLink,
2051
2384
  element: $linkedElement,
2052
2385
  identifier: `Target content (\`id="${linkedElementId}"\`)`
2053
2386
  });
2054
2387
  }
2055
- this.$module.addEventListener('click', () => setFocus($linkedElement, {
2388
+ this.$root.addEventListener('click', () => setFocus($linkedElement, {
2056
2389
  onBeforeFocus() {
2057
2390
  $linkedElement.classList.add('govuk-skip-link-focused-element');
2058
2391
  },
@@ -2062,6 +2395,7 @@
2062
2395
  }));
2063
2396
  }
2064
2397
  }
2398
+ SkipLink.elementType = HTMLAnchorElement;
2065
2399
  SkipLink.moduleName = 'govuk-skip-link';
2066
2400
 
2067
2401
  /**
@@ -2069,13 +2403,12 @@
2069
2403
  *
2070
2404
  * @preserve
2071
2405
  */
2072
- class Tabs extends GOVUKFrontendComponent {
2406
+ class Tabs extends Component {
2073
2407
  /**
2074
- * @param {Element | null} $module - HTML element to use for tabs
2408
+ * @param {Element | null} $root - HTML element to use for tabs
2075
2409
  */
2076
- constructor($module) {
2077
- super();
2078
- this.$module = void 0;
2410
+ constructor($root) {
2411
+ super($root);
2079
2412
  this.$tabs = void 0;
2080
2413
  this.$tabList = void 0;
2081
2414
  this.$tabListItems = void 0;
@@ -2085,36 +2418,28 @@
2085
2418
  this.boundTabKeydown = void 0;
2086
2419
  this.boundOnHashChange = void 0;
2087
2420
  this.mql = null;
2088
- if (!$module) {
2089
- throw new ElementError({
2090
- componentName: 'Tabs',
2091
- element: $module,
2092
- identifier: 'Root element (`$module`)'
2093
- });
2094
- }
2095
- const $tabs = $module.querySelectorAll('a.govuk-tabs__tab');
2421
+ const $tabs = this.$root.querySelectorAll('a.govuk-tabs__tab');
2096
2422
  if (!$tabs.length) {
2097
2423
  throw new ElementError({
2098
- componentName: 'Tabs',
2424
+ component: Tabs,
2099
2425
  identifier: 'Links (`<a class="govuk-tabs__tab">`)'
2100
2426
  });
2101
2427
  }
2102
- this.$module = $module;
2103
2428
  this.$tabs = $tabs;
2104
2429
  this.boundTabClick = this.onTabClick.bind(this);
2105
2430
  this.boundTabKeydown = this.onTabKeydown.bind(this);
2106
2431
  this.boundOnHashChange = this.onHashChange.bind(this);
2107
- const $tabList = this.$module.querySelector('.govuk-tabs__list');
2108
- const $tabListItems = this.$module.querySelectorAll('li.govuk-tabs__list-item');
2432
+ const $tabList = this.$root.querySelector('.govuk-tabs__list');
2433
+ const $tabListItems = this.$root.querySelectorAll('li.govuk-tabs__list-item');
2109
2434
  if (!$tabList) {
2110
2435
  throw new ElementError({
2111
- componentName: 'Tabs',
2436
+ component: Tabs,
2112
2437
  identifier: 'List (`<ul class="govuk-tabs__list">`)'
2113
2438
  });
2114
2439
  }
2115
2440
  if (!$tabListItems.length) {
2116
2441
  throw new ElementError({
2117
- componentName: 'Tabs',
2442
+ component: Tabs,
2118
2443
  identifier: 'List items (`<li class="govuk-tabs__list-item">`)'
2119
2444
  });
2120
2445
  }
@@ -2126,7 +2451,7 @@
2126
2451
  const breakpoint = getBreakpoint('tablet');
2127
2452
  if (!breakpoint.value) {
2128
2453
  throw new ElementError({
2129
- componentName: 'Tabs',
2454
+ component: Tabs,
2130
2455
  identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
2131
2456
  });
2132
2457
  }
@@ -2201,7 +2526,7 @@
2201
2526
  this.showPanel($tab);
2202
2527
  }
2203
2528
  getTab(hash) {
2204
- return this.$module.querySelector(`a.govuk-tabs__tab[href="${hash}"]`);
2529
+ return this.$root.querySelector(`a.govuk-tabs__tab[href="${hash}"]`);
2205
2530
  }
2206
2531
  setAttributes($tab) {
2207
2532
  const panelId = getFragmentFromUrl($tab.href);
@@ -2260,16 +2585,12 @@
2260
2585
  onTabKeydown(event) {
2261
2586
  switch (event.key) {
2262
2587
  case 'ArrowLeft':
2263
- case 'ArrowUp':
2264
2588
  case 'Left':
2265
- case 'Up':
2266
2589
  this.activatePreviousTab();
2267
2590
  event.preventDefault();
2268
2591
  break;
2269
2592
  case 'ArrowRight':
2270
- case 'ArrowDown':
2271
2593
  case 'Right':
2272
- case 'Down':
2273
2594
  this.activateNextTab();
2274
2595
  event.preventDefault();
2275
2596
  break;
@@ -2316,7 +2637,7 @@
2316
2637
  if (!panelId) {
2317
2638
  return null;
2318
2639
  }
2319
- return this.$module.querySelector(`#${panelId}`);
2640
+ return this.$root.querySelector(`#${panelId}`);
2320
2641
  }
2321
2642
  showPanel($tab) {
2322
2643
  const $panel = this.getPanel($tab);
@@ -2349,7 +2670,7 @@
2349
2670
  $tab.setAttribute('tabindex', '0');
2350
2671
  }
2351
2672
  getCurrentTab() {
2352
- return this.$module.querySelector('.govuk-tabs__list-item--selected a.govuk-tabs__tab');
2673
+ return this.$root.querySelector('.govuk-tabs__list-item--selected a.govuk-tabs__tab');
2353
2674
  }
2354
2675
  }
2355
2676
  Tabs.moduleName = 'govuk-tabs';
@@ -2360,19 +2681,28 @@
2360
2681
  * Use the `data-module` attributes to find, instantiate and init all of the
2361
2682
  * components provided as part of GOV.UK Frontend.
2362
2683
  *
2363
- * @param {Config & { scope?: Element }} [config] - Config for all components (with optional scope)
2684
+ * @param {Config & { scope?: Element, onError?: OnErrorCallback<CompatibleClass> }} [config] - Config for all components (with optional scope)
2364
2685
  */
2365
2686
  function initAll(config) {
2366
2687
  var _config$scope;
2367
2688
  config = typeof config !== 'undefined' ? config : {};
2368
2689
  if (!isSupported()) {
2369
- console.log(new SupportError());
2690
+ if (config.onError) {
2691
+ config.onError(new SupportError(), {
2692
+ config
2693
+ });
2694
+ } else {
2695
+ console.log(new SupportError());
2696
+ }
2370
2697
  return;
2371
2698
  }
2372
- const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [SkipLink], [Tabs]];
2373
- const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
2699
+ const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [FileUpload, config.fileUpload], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [ServiceNavigation], [SkipLink], [Tabs]];
2700
+ const options = {
2701
+ scope: (_config$scope = config.scope) != null ? _config$scope : document,
2702
+ onError: config.onError
2703
+ };
2374
2704
  components.forEach(([Component, config]) => {
2375
- createAll(Component, config, $scope);
2705
+ createAll(Component, config, options);
2376
2706
  });
2377
2707
  }
2378
2708
 
@@ -2385,25 +2715,58 @@
2385
2715
  *
2386
2716
  * Any component errors will be caught and logged to the console.
2387
2717
  *
2388
- * @template {CompatibleClass} T
2389
- * @param {T} Component - class of the component to create
2390
- * @param {T["defaults"]} [config] - config for the component
2391
- * @param {Element|Document} [$scope] - scope of the document to search within
2392
- * @returns {Array<InstanceType<T>>} - array of instantiated components
2718
+ * @template {CompatibleClass} ComponentClass
2719
+ * @param {ComponentClass} Component - class of the component to create
2720
+ * @param {ComponentConfig<ComponentClass>} [config] - Config supplied to component
2721
+ * @param {OnErrorCallback<ComponentClass> | Element | Document | CreateAllOptions<ComponentClass> } [createAllOptions] - options for createAll including scope of the document to search within and callback function if error throw by component on init
2722
+ * @returns {Array<InstanceType<ComponentClass>>} - array of instantiated components
2393
2723
  */
2394
- function createAll(Component, config, $scope = document) {
2724
+ function createAll(Component, config, createAllOptions) {
2725
+ let $scope = document;
2726
+ let onError;
2727
+ if (typeof createAllOptions === 'object') {
2728
+ var _createAllOptions$sco;
2729
+ createAllOptions = createAllOptions;
2730
+ $scope = (_createAllOptions$sco = createAllOptions.scope) != null ? _createAllOptions$sco : $scope;
2731
+ onError = createAllOptions.onError;
2732
+ }
2733
+ if (typeof createAllOptions === 'function') {
2734
+ onError = createAllOptions;
2735
+ }
2736
+ if (createAllOptions instanceof HTMLElement) {
2737
+ $scope = createAllOptions;
2738
+ }
2395
2739
  const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
2740
+ if (!isSupported()) {
2741
+ if (onError) {
2742
+ onError(new SupportError(), {
2743
+ component: Component,
2744
+ config
2745
+ });
2746
+ } else {
2747
+ console.log(new SupportError());
2748
+ }
2749
+ return [];
2750
+ }
2396
2751
  return Array.from($elements).map($element => {
2397
2752
  try {
2398
- return 'defaults' in Component && typeof config !== 'undefined' ? new Component($element, config) : new Component($element);
2753
+ return typeof config !== 'undefined' ? new Component($element, config) : new Component($element);
2399
2754
  } catch (error) {
2400
- console.log(error);
2755
+ if (onError) {
2756
+ onError(error, {
2757
+ element: $element,
2758
+ component: Component,
2759
+ config
2760
+ });
2761
+ } else {
2762
+ console.log(error);
2763
+ }
2401
2764
  return null;
2402
2765
  }
2403
2766
  }).filter(Boolean);
2404
2767
  }
2405
2768
  /**
2406
- * @typedef {{new (...args: any[]): any, defaults?: object, moduleName: string}} CompatibleClass
2769
+ * @typedef {{new (...args: any[]): any, moduleName: string}} CompatibleClass
2407
2770
  */
2408
2771
  /**
2409
2772
  * Config for all components via `initAll()`
@@ -2414,43 +2777,70 @@
2414
2777
  * @property {CharacterCountConfig} [characterCount] - Character Count config
2415
2778
  * @property {ErrorSummaryConfig} [errorSummary] - Error Summary config
2416
2779
  * @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config
2780
+ * @property {FileUploadConfig} [fileUpload] - File Upload config
2417
2781
  * @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
2418
2782
  * @property {PasswordInputConfig} [passwordInput] - Password input config
2419
2783
  */
2420
2784
  /**
2421
2785
  * Config for individual components
2422
2786
  *
2423
- * @typedef {import('./components/accordion/accordion.mjs').AccordionConfig} AccordionConfig
2424
- * @typedef {import('./components/accordion/accordion.mjs').AccordionTranslations} AccordionTranslations
2425
- * @typedef {import('./components/button/button.mjs').ButtonConfig} ButtonConfig
2426
- * @typedef {import('./components/character-count/character-count.mjs').CharacterCountConfig} CharacterCountConfig
2427
- * @typedef {import('./components/character-count/character-count.mjs').CharacterCountTranslations} CharacterCountTranslations
2428
- * @typedef {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} ErrorSummaryConfig
2429
- * @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageConfig} ExitThisPageConfig
2430
- * @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageTranslations} ExitThisPageTranslations
2431
- * @typedef {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
2432
- * @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
2787
+ * @import { AccordionConfig } from './components/accordion/accordion.mjs'
2788
+ * @import { ButtonConfig } from './components/button/button.mjs'
2789
+ * @import { CharacterCountConfig } from './components/character-count/character-count.mjs'
2790
+ * @import { ErrorSummaryConfig } from './components/error-summary/error-summary.mjs'
2791
+ * @import { ExitThisPageConfig } from './components/exit-this-page/exit-this-page.mjs'
2792
+ * @import { NotificationBannerConfig } from './components/notification-banner/notification-banner.mjs'
2793
+ * @import { PasswordInputConfig } from './components/password-input/password-input.mjs'
2794
+ * @import { FileUploadConfig } from './components/file-upload/file-upload.mjs'
2433
2795
  */
2434
2796
  /**
2435
2797
  * Component config keys, e.g. `accordion` and `characterCount`
2436
2798
  *
2437
2799
  * @typedef {keyof Config} ConfigKey
2438
2800
  */
2801
+ /**
2802
+ * @template {CompatibleClass} ComponentClass
2803
+ * @typedef {ConstructorParameters<ComponentClass>[1]} ComponentConfig
2804
+ */
2805
+ /**
2806
+ * @template {CompatibleClass} ComponentClass
2807
+ * @typedef {object} ErrorContext
2808
+ * @property {Element} [element] - Element used for component module initialisation
2809
+ * @property {ComponentClass} [component] - Class of component
2810
+ * @property {ComponentConfig<ComponentClass>} config - Config supplied to component
2811
+ */
2812
+ /**
2813
+ * @template {CompatibleClass} ComponentClass
2814
+ * @callback OnErrorCallback
2815
+ * @param {unknown} error - Thrown error
2816
+ * @param {ErrorContext<ComponentClass>} context - Object containing the element, component class and configuration
2817
+ */
2818
+ /**
2819
+ * @template {CompatibleClass} ComponentClass
2820
+ * @typedef {object} CreateAllOptions
2821
+ * @property {Element | Document} [scope] - scope of the document to search within
2822
+ * @property {OnErrorCallback<ComponentClass>} [onError] - callback function if error throw by component on init
2823
+ */
2439
2824
 
2440
2825
  exports.Accordion = Accordion;
2441
2826
  exports.Button = Button;
2442
2827
  exports.CharacterCount = CharacterCount;
2443
2828
  exports.Checkboxes = Checkboxes;
2829
+ exports.Component = Component;
2830
+ exports.ConfigurableComponent = ConfigurableComponent;
2444
2831
  exports.ErrorSummary = ErrorSummary;
2445
2832
  exports.ExitThisPage = ExitThisPage;
2833
+ exports.FileUpload = FileUpload;
2446
2834
  exports.Header = Header;
2447
2835
  exports.NotificationBanner = NotificationBanner;
2448
2836
  exports.PasswordInput = PasswordInput;
2449
2837
  exports.Radios = Radios;
2838
+ exports.ServiceNavigation = ServiceNavigation;
2450
2839
  exports.SkipLink = SkipLink;
2451
2840
  exports.Tabs = Tabs;
2452
2841
  exports.createAll = createAll;
2453
2842
  exports.initAll = initAll;
2843
+ exports.isSupported = isSupported;
2454
2844
  exports.version = version;
2455
2845
 
2456
2846
  }));