govuk_publishing_components 32.0.0 → 33.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/component_guide/accessibility-test.js +0 -1
  3. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-core.js +175 -0
  4. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-ecommerce-tracker.js +1 -1
  5. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js +5 -13
  6. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.js +80 -309
  7. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.js +2 -2
  8. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-specialist-link-tracker.js +140 -0
  9. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js +3 -0
  10. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js +1 -0
  11. data/app/assets/javascripts/govuk_publishing_components/components/accordion.js +12 -1
  12. data/app/assets/javascripts/govuk_publishing_components/components/layout-super-navigation-header.js +13 -4
  13. data/app/assets/javascripts/govuk_publishing_components/components/single-page-notification-button.js +24 -8
  14. data/app/assets/javascripts/govuk_publishing_components/vendor/lux/lux-reporter.js +83 -86
  15. data/app/assets/stylesheets/govuk_publishing_components/components/_big-number.scss +2 -5
  16. data/app/assets/stylesheets/govuk_publishing_components/components/_image-card.scss +1 -5
  17. data/app/assets/stylesheets/govuk_publishing_components/components/_input.scss +3 -5
  18. data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +10 -30
  19. data/app/assets/stylesheets/govuk_publishing_components/components/_search.scss +0 -7
  20. data/app/assets/stylesheets/govuk_publishing_components/components/_share-links.scss +0 -6
  21. data/app/views/govuk_publishing_components/components/_accordion.html.erb +14 -1
  22. data/app/views/govuk_publishing_components/components/_error_summary.html.erb +27 -26
  23. data/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb +2 -2
  24. data/app/views/govuk_publishing_components/components/_phase_banner.html.erb +1 -1
  25. data/app/views/govuk_publishing_components/components/_share_links.html.erb +18 -15
  26. data/app/views/govuk_publishing_components/components/_single_page_notification_button.html.erb +1 -1
  27. data/app/views/govuk_publishing_components/components/docs/accordion.yml +15 -3
  28. data/app/views/govuk_publishing_components/components/docs/button.yml +10 -0
  29. data/app/views/govuk_publishing_components/components/docs/share_links.yml +59 -30
  30. data/app/views/govuk_publishing_components/components/docs/single_page_notification_button.yml +10 -1
  31. data/app/views/govuk_publishing_components/components/feedback/_yes_no_banner.html.erb +3 -3
  32. data/config/locales/ar.yml +4 -1
  33. data/config/locales/az.yml +4 -1
  34. data/config/locales/be.yml +4 -1
  35. data/config/locales/bg.yml +4 -1
  36. data/config/locales/bn.yml +4 -1
  37. data/config/locales/cs.yml +4 -1
  38. data/config/locales/cy.yml +4 -1
  39. data/config/locales/da.yml +4 -1
  40. data/config/locales/de.yml +4 -1
  41. data/config/locales/dr.yml +4 -1
  42. data/config/locales/el.yml +4 -1
  43. data/config/locales/en.yml +20 -17
  44. data/config/locales/es-419.yml +4 -1
  45. data/config/locales/es.yml +4 -1
  46. data/config/locales/et.yml +4 -1
  47. data/config/locales/fa.yml +4 -1
  48. data/config/locales/fi.yml +4 -1
  49. data/config/locales/fr.yml +4 -1
  50. data/config/locales/gd.yml +4 -1
  51. data/config/locales/gu.yml +4 -1
  52. data/config/locales/he.yml +4 -1
  53. data/config/locales/hi.yml +4 -1
  54. data/config/locales/hr.yml +4 -1
  55. data/config/locales/hu.yml +4 -1
  56. data/config/locales/hy.yml +4 -1
  57. data/config/locales/id.yml +4 -1
  58. data/config/locales/is.yml +4 -1
  59. data/config/locales/it.yml +4 -1
  60. data/config/locales/ja.yml +4 -1
  61. data/config/locales/ka.yml +4 -1
  62. data/config/locales/kk.yml +4 -1
  63. data/config/locales/ko.yml +4 -1
  64. data/config/locales/lt.yml +4 -1
  65. data/config/locales/lv.yml +4 -1
  66. data/config/locales/ms.yml +4 -1
  67. data/config/locales/mt.yml +4 -1
  68. data/config/locales/nl.yml +4 -1
  69. data/config/locales/no.yml +4 -1
  70. data/config/locales/pa-pk.yml +4 -1
  71. data/config/locales/pa.yml +4 -1
  72. data/config/locales/pl.yml +4 -1
  73. data/config/locales/ps.yml +4 -1
  74. data/config/locales/pt.yml +4 -1
  75. data/config/locales/ro.yml +4 -1
  76. data/config/locales/ru.yml +4 -1
  77. data/config/locales/si.yml +4 -1
  78. data/config/locales/sk.yml +4 -1
  79. data/config/locales/sl.yml +4 -1
  80. data/config/locales/so.yml +4 -1
  81. data/config/locales/sq.yml +4 -1
  82. data/config/locales/sr.yml +4 -1
  83. data/config/locales/sv.yml +4 -1
  84. data/config/locales/sw.yml +4 -1
  85. data/config/locales/ta.yml +4 -1
  86. data/config/locales/th.yml +4 -1
  87. data/config/locales/tk.yml +4 -1
  88. data/config/locales/tr.yml +4 -1
  89. data/config/locales/uk.yml +4 -1
  90. data/config/locales/ur.yml +4 -1
  91. data/config/locales/uz.yml +4 -1
  92. data/config/locales/vi.yml +4 -1
  93. data/config/locales/zh-hk.yml +4 -1
  94. data/config/locales/zh-tw.yml +4 -1
  95. data/config/locales/zh.yml +4 -1
  96. data/lib/govuk_publishing_components/presenters/button_helper.rb +7 -1
  97. data/lib/govuk_publishing_components/presenters/single_page_notification_button_helper.rb +25 -1
  98. data/lib/govuk_publishing_components/version.rb +1 -1
  99. data/node_modules/axe-core/axe.js +4567 -4678
  100. data/node_modules/axe-core/axe.min.js +2 -2
  101. data/node_modules/axe-core/package.json +2 -2
  102. data/node_modules/axe-core/sri-history.json +8 -0
  103. data/node_modules/govuk-frontend/README.md +1 -2
  104. data/node_modules/govuk-frontend/govuk/all.js +1398 -273
  105. data/node_modules/govuk-frontend/govuk/common/closest-attribute-value.js +70 -0
  106. data/node_modules/govuk-frontend/govuk/common/index.js +172 -0
  107. data/node_modules/govuk-frontend/govuk/common/normalise-dataset.js +373 -0
  108. data/node_modules/govuk-frontend/govuk/common.js +138 -3
  109. data/node_modules/govuk-frontend/govuk/components/accordion/accordion.js +753 -25
  110. data/node_modules/govuk-frontend/govuk/components/accordion/fixtures.json +54 -22
  111. data/node_modules/govuk-frontend/govuk/components/accordion/macro-options.json +36 -0
  112. data/node_modules/govuk-frontend/govuk/components/accordion/template.njk +7 -1
  113. data/node_modules/govuk-frontend/govuk/components/back-link/fixtures.json +12 -12
  114. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/fixtures.json +22 -22
  115. data/node_modules/govuk-frontend/govuk/components/button/_index.scss +23 -5
  116. data/node_modules/govuk-frontend/govuk/components/button/button.js +365 -107
  117. data/node_modules/govuk-frontend/govuk/components/button/fixtures.json +85 -66
  118. data/node_modules/govuk-frontend/govuk/components/button/template.njk +1 -1
  119. data/node_modules/govuk-frontend/govuk/components/character-count/_index.scss +9 -0
  120. data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +1033 -121
  121. data/node_modules/govuk-frontend/govuk/components/character-count/fixtures.json +112 -36
  122. data/node_modules/govuk-frontend/govuk/components/character-count/macro-options.json +42 -0
  123. data/node_modules/govuk-frontend/govuk/components/character-count/template.njk +27 -3
  124. data/node_modules/govuk-frontend/govuk/components/checkboxes/checkboxes.js +30 -2
  125. data/node_modules/govuk-frontend/govuk/components/checkboxes/fixtures.json +96 -93
  126. data/node_modules/govuk-frontend/govuk/components/cookie-banner/fixtures.json +46 -46
  127. data/node_modules/govuk-frontend/govuk/components/date-input/fixtures.json +50 -50
  128. data/node_modules/govuk-frontend/govuk/components/details/details.js +43 -13
  129. data/node_modules/govuk-frontend/govuk/components/details/fixtures.json +20 -20
  130. data/node_modules/govuk-frontend/govuk/components/error-message/fixtures.json +20 -20
  131. data/node_modules/govuk-frontend/govuk/components/error-summary/error-summary.js +268 -6
  132. data/node_modules/govuk-frontend/govuk/components/error-summary/fixtures.json +44 -35
  133. data/node_modules/govuk-frontend/govuk/components/error-summary/template.njk +25 -21
  134. data/node_modules/govuk-frontend/govuk/components/fieldset/fixtures.json +51 -39
  135. data/node_modules/govuk-frontend/govuk/components/file-upload/fixtures.json +26 -26
  136. data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +1 -1
  137. data/node_modules/govuk-frontend/govuk/components/footer/fixtures.json +46 -46
  138. data/node_modules/govuk-frontend/govuk/components/footer/macro-options.json +2 -2
  139. data/node_modules/govuk-frontend/govuk/components/header/fixtures.json +93 -38
  140. data/node_modules/govuk-frontend/govuk/components/header/header.js +6 -0
  141. data/node_modules/govuk-frontend/govuk/components/header/macro-options.json +8 -2
  142. data/node_modules/govuk-frontend/govuk/components/header/template.njk +4 -2
  143. data/node_modules/govuk-frontend/govuk/components/hint/fixtures.json +12 -12
  144. data/node_modules/govuk-frontend/govuk/components/input/fixtures.json +80 -80
  145. data/node_modules/govuk-frontend/govuk/components/inset-text/fixtures.json +12 -12
  146. data/node_modules/govuk-frontend/govuk/components/label/fixtures.json +34 -34
  147. data/node_modules/govuk-frontend/govuk/components/notification-banner/fixtures.json +56 -46
  148. data/node_modules/govuk-frontend/govuk/components/notification-banner/notification-banner.js +252 -2
  149. data/node_modules/govuk-frontend/govuk/components/notification-banner/template.njk +1 -1
  150. data/node_modules/govuk-frontend/govuk/components/pagination/_index.scss +10 -7
  151. data/node_modules/govuk-frontend/govuk/components/pagination/fixtures.json +33 -26
  152. data/node_modules/govuk-frontend/govuk/components/panel/fixtures.json +18 -18
  153. data/node_modules/govuk-frontend/govuk/components/phase-banner/fixtures.json +14 -14
  154. data/node_modules/govuk-frontend/govuk/components/radios/fixtures.json +94 -91
  155. data/node_modules/govuk-frontend/govuk/components/radios/radios.js +30 -2
  156. data/node_modules/govuk-frontend/govuk/components/select/fixtures.json +32 -32
  157. data/node_modules/govuk-frontend/govuk/components/skip-link/fixtures.json +22 -20
  158. data/node_modules/govuk-frontend/govuk/components/skip-link/skip-link.js +10 -4
  159. data/node_modules/govuk-frontend/govuk/components/summary-list/fixtures.json +50 -50
  160. data/node_modules/govuk-frontend/govuk/components/table/_index.scss +1 -1
  161. data/node_modules/govuk-frontend/govuk/components/table/fixtures.json +40 -40
  162. data/node_modules/govuk-frontend/govuk/components/tabs/fixtures.json +29 -29
  163. data/node_modules/govuk-frontend/govuk/components/tabs/tabs.js +28 -0
  164. data/node_modules/govuk-frontend/govuk/components/tag/fixtures.json +28 -28
  165. data/node_modules/govuk-frontend/govuk/components/textarea/fixtures.json +34 -34
  166. data/node_modules/govuk-frontend/govuk/components/warning-text/fixtures.json +14 -14
  167. data/node_modules/govuk-frontend/govuk/core/_section-break.scss +1 -1
  168. data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +2 -2
  169. data/node_modules/govuk-frontend/govuk/helpers/_links.scss +6 -6
  170. data/node_modules/govuk-frontend/govuk/i18n.js +390 -0
  171. data/node_modules/govuk-frontend/govuk/macros/i18n.njk +15 -0
  172. data/node_modules/govuk-frontend/govuk/settings/_all.scss +1 -0
  173. data/node_modules/govuk-frontend/govuk/settings/_colours-palette.scss +12 -0
  174. data/node_modules/govuk-frontend/govuk/settings/_compatibility.scss +26 -0
  175. data/node_modules/govuk-frontend/govuk/settings/_typography-font.scss +23 -0
  176. data/node_modules/govuk-frontend/govuk/settings/_typography-responsive.scss +12 -0
  177. data/node_modules/govuk-frontend/govuk/settings/_warnings.scss +53 -0
  178. data/node_modules/govuk-frontend/govuk/tools/_compatibility.scss +20 -6
  179. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Date/now.js +21 -0
  180. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Element/prototype/dataset.js +300 -0
  181. data/node_modules/govuk-frontend/govuk/vendor/polyfills/String/prototype/trim.js +21 -0
  182. data/node_modules/govuk-frontend/govuk-esm/all.mjs +50 -27
  183. data/node_modules/govuk-frontend/govuk-esm/common/closest-attribute-value.mjs +15 -0
  184. data/node_modules/govuk-frontend/govuk-esm/common/index.mjs +159 -0
  185. data/node_modules/govuk-frontend/govuk-esm/common/normalise-dataset.mjs +58 -0
  186. data/node_modules/govuk-frontend/govuk-esm/common.mjs +6 -28
  187. data/node_modules/govuk-frontend/govuk-esm/components/accordion/accordion.mjs +113 -43
  188. data/node_modules/govuk-frontend/govuk-esm/components/button/button.mjs +67 -30
  189. data/node_modules/govuk-frontend/govuk-esm/components/character-count/character-count.mjs +325 -123
  190. data/node_modules/govuk-frontend/govuk-esm/components/checkboxes/checkboxes.mjs +9 -3
  191. data/node_modules/govuk-frontend/govuk-esm/components/details/details.mjs +22 -8
  192. data/node_modules/govuk-frontend/govuk-esm/components/error-summary/error-summary.mjs +48 -6
  193. data/node_modules/govuk-frontend/govuk-esm/components/header/header.mjs +6 -0
  194. data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs +32 -2
  195. data/node_modules/govuk-frontend/govuk-esm/components/radios/radios.mjs +9 -3
  196. data/node_modules/govuk-frontend/govuk-esm/components/skip-link/skip-link.mjs +10 -4
  197. data/node_modules/govuk-frontend/govuk-esm/components/tabs/tabs.mjs +8 -2
  198. data/node_modules/govuk-frontend/govuk-esm/i18n.mjs +380 -0
  199. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Date/now.mjs +13 -0
  200. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/dataset.mjs +68 -0
  201. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/String/prototype/trim.mjs +13 -0
  202. data/node_modules/govuk-frontend/govuk-prototype-kit/init.js +7 -0
  203. data/node_modules/govuk-frontend/govuk-prototype-kit/init.scss +12 -0
  204. data/node_modules/govuk-frontend/govuk-prototype-kit.config.json +138 -7
  205. data/node_modules/govuk-frontend/package.json +1 -1
  206. metadata +22 -3
@@ -1,8 +1,106 @@
1
+ import '../../vendor/polyfills/Date/now.mjs'
1
2
  import '../../vendor/polyfills/Function/prototype/bind.mjs'
2
- import '../../vendor/polyfills/Event.mjs' // addEventListener and event.target normaliziation
3
+ import '../../vendor/polyfills/Event.mjs' // addEventListener and event.target normalisation
3
4
  import '../../vendor/polyfills/Element/prototype/classList.mjs'
5
+ import { extractConfigByNamespace, mergeConfigs } from '../../common/index.mjs'
6
+ import { I18n } from '../../i18n.mjs'
7
+ import { normaliseDataset } from '../../common/normalise-dataset.mjs'
8
+ import { closestAttributeValue } from '../../common/closest-attribute-value.mjs'
9
+
10
+ /**
11
+ * @constant
12
+ * @type {CharacterCountTranslations}
13
+ * @see Default value for {@link CharacterCountConfig.i18n}
14
+ * @default
15
+ */
16
+ var CHARACTER_COUNT_TRANSLATIONS = {
17
+ // Characters
18
+ charactersUnderLimit: {
19
+ one: 'You have %{count} character remaining',
20
+ other: 'You have %{count} characters remaining'
21
+ },
22
+ charactersAtLimit: 'You have 0 characters remaining',
23
+ charactersOverLimit: {
24
+ one: 'You have %{count} character too many',
25
+ other: 'You have %{count} characters too many'
26
+ },
27
+ // Words
28
+ wordsUnderLimit: {
29
+ one: 'You have %{count} word remaining',
30
+ other: 'You have %{count} words remaining'
31
+ },
32
+ wordsAtLimit: 'You have 0 words remaining',
33
+ wordsOverLimit: {
34
+ one: 'You have %{count} word too many',
35
+ other: 'You have %{count} words too many'
36
+ },
37
+ textareaDescription: {
38
+ other: ''
39
+ }
40
+ }
41
+
42
+ /**
43
+ * JavaScript enhancements for the CharacterCount component
44
+ *
45
+ * Tracks the number of characters or words in the `.govuk-js-character-count`
46
+ * `<textarea>` inside the element. Displays a message with the remaining number
47
+ * of characters/words available, or the number of characters/words in excess.
48
+ *
49
+ * You can configure the message to only appear after a certain percentage
50
+ * of the available characters/words has been entered.
51
+ *
52
+ * @class
53
+ * @param {HTMLElement} $module - The element this component controls
54
+ * @param {CharacterCountConfig} [config] - Character count config
55
+ */
56
+ function CharacterCount ($module, config) {
57
+ if (!$module) {
58
+ return this
59
+ }
60
+
61
+ var defaultConfig = {
62
+ threshold: 0,
63
+ i18n: CHARACTER_COUNT_TRANSLATIONS
64
+ }
65
+
66
+ // Read config set using dataset ('data-' values)
67
+ var datasetConfig = normaliseDataset($module.dataset)
68
+
69
+ // To ensure data-attributes take complete precedence, even if they change the
70
+ // type of count, we need to reset the `maxlength` and `maxwords` from the
71
+ // JavaScript config.
72
+ //
73
+ // We can't mutate `config`, though, as it may be shared across multiple
74
+ // components inside `initAll`.
75
+ var configOverrides = {}
76
+ if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
77
+ configOverrides = {
78
+ maxlength: false,
79
+ maxwords: false
80
+ }
81
+ }
82
+
83
+ this.config = mergeConfigs(
84
+ defaultConfig,
85
+ config || {},
86
+ configOverrides,
87
+ datasetConfig
88
+ )
89
+
90
+ this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), {
91
+ // Read the fallback if necessary rather than have it set in the defaults
92
+ locale: closestAttributeValue($module, 'lang')
93
+ })
94
+
95
+ // Determine the limit attribute (characters or words)
96
+ if (this.config.maxwords) {
97
+ this.maxLength = this.config.maxwords
98
+ } else if (this.config.maxlength) {
99
+ this.maxLength = this.config.maxlength
100
+ } else {
101
+ return
102
+ }
4
103
 
5
- function CharacterCount ($module) {
6
104
  this.$module = $module
7
105
  this.$textarea = $module.querySelector('.govuk-js-character-count')
8
106
  this.$visibleCountMessage = null
@@ -10,26 +108,28 @@ function CharacterCount ($module) {
10
108
  this.lastInputTimestamp = null
11
109
  }
12
110
 
13
- CharacterCount.prototype.defaults = {
14
- characterCountAttribute: 'data-maxlength',
15
- wordCountAttribute: 'data-maxwords'
16
- }
17
-
18
- // Initialize component
111
+ /**
112
+ * Initialise component
113
+ */
19
114
  CharacterCount.prototype.init = function () {
20
115
  // Check that required elements are present
21
116
  if (!this.$textarea) {
22
117
  return
23
118
  }
24
119
 
25
- // Check for module
26
- var $module = this.$module
27
120
  var $textarea = this.$textarea
28
- var $fallbackLimitMessage = document.getElementById($textarea.id + '-info')
121
+ var $textareaDescription = document.getElementById($textarea.id + '-info')
122
+
123
+ // Inject a decription for the textarea if none is present already
124
+ // for when the component was rendered with no maxlength, maxwords
125
+ // nor custom textareaDescriptionText
126
+ if ($textareaDescription.innerText.match(/^\s*$/)) {
127
+ $textareaDescription.innerText = this.i18n.t('textareaDescription', { count: this.maxLength })
128
+ }
29
129
 
30
- // Move the fallback count message to be immediately after the textarea
130
+ // Move the textarea description to be immediately after the textarea
31
131
  // Kept for backwards compatibility
32
- $textarea.insertAdjacentElement('afterend', $fallbackLimitMessage)
132
+ $textarea.insertAdjacentElement('afterend', $textareaDescription)
33
133
 
34
134
  // Create the *screen reader* specific live-updating counter
35
135
  // This doesn't need any styling classes, as it is never visible
@@ -37,36 +137,20 @@ CharacterCount.prototype.init = function () {
37
137
  $screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden'
38
138
  $screenReaderCountMessage.setAttribute('aria-live', 'polite')
39
139
  this.$screenReaderCountMessage = $screenReaderCountMessage
40
- $fallbackLimitMessage.insertAdjacentElement('afterend', $screenReaderCountMessage)
140
+ $textareaDescription.insertAdjacentElement('afterend', $screenReaderCountMessage)
41
141
 
42
142
  // Create our live-updating counter element, copying the classes from the
43
- // fallback element for backwards compatibility as these may have been configured
143
+ // textarea description for backwards compatibility as these may have been
144
+ // configured
44
145
  var $visibleCountMessage = document.createElement('div')
45
- $visibleCountMessage.className = $fallbackLimitMessage.className
146
+ $visibleCountMessage.className = $textareaDescription.className
46
147
  $visibleCountMessage.classList.add('govuk-character-count__status')
47
148
  $visibleCountMessage.setAttribute('aria-hidden', 'true')
48
149
  this.$visibleCountMessage = $visibleCountMessage
49
- $fallbackLimitMessage.insertAdjacentElement('afterend', $visibleCountMessage)
150
+ $textareaDescription.insertAdjacentElement('afterend', $visibleCountMessage)
50
151
 
51
- // Hide the fallback limit message
52
- $fallbackLimitMessage.classList.add('govuk-visually-hidden')
53
-
54
- // Read options set using dataset ('data-' values)
55
- this.options = this.getDataset($module)
56
-
57
- // Determine the limit attribute (characters or words)
58
- var countAttribute = this.defaults.characterCountAttribute
59
- if (this.options.maxwords) {
60
- countAttribute = this.defaults.wordCountAttribute
61
- }
62
-
63
- // Save the element limit
64
- this.maxLength = $module.getAttribute(countAttribute)
65
-
66
- // Check for limit
67
- if (!this.maxLength) {
68
- return
69
- }
152
+ // Hide the textarea description
153
+ $textareaDescription.classList.add('govuk-visually-hidden')
70
154
 
71
155
  // Remove hard limit if set
72
156
  $textarea.removeAttribute('maxlength')
@@ -74,9 +158,9 @@ CharacterCount.prototype.init = function () {
74
158
  this.bindChangeEvents()
75
159
 
76
160
  // When the page is restored after navigating 'back' in some browsers the
77
- // state of the character count is not restored until *after* the DOMContentLoaded
78
- // event is fired, so we need to manually update it after the pageshow event
79
- // in browsers that support it.
161
+ // state of the character count is not restored until *after* the
162
+ // DOMContentLoaded event is fired, so we need to manually update it after the
163
+ // pageshow event in browsers that support it.
80
164
  if ('onpageshow' in window) {
81
165
  window.addEventListener('pageshow', this.updateCountMessage.bind(this))
82
166
  } else {
@@ -85,35 +169,12 @@ CharacterCount.prototype.init = function () {
85
169
  this.updateCountMessage()
86
170
  }
87
171
 
88
- // Read data attributes
89
- CharacterCount.prototype.getDataset = function (element) {
90
- var dataset = {}
91
- var attributes = element.attributes
92
- if (attributes) {
93
- for (var i = 0; i < attributes.length; i++) {
94
- var attribute = attributes[i]
95
- var match = attribute.name.match(/^data-(.+)/)
96
- if (match) {
97
- dataset[match[1]] = attribute.value
98
- }
99
- }
100
- }
101
- return dataset
102
- }
103
-
104
- // Counts characters or words in text
105
- CharacterCount.prototype.count = function (text) {
106
- var length
107
- if (this.options.maxwords) {
108
- var tokens = text.match(/\S+/g) || [] // Matches consecutive non-whitespace chars
109
- length = tokens.length
110
- } else {
111
- length = text.length
112
- }
113
- return length
114
- }
115
-
116
- // Bind input propertychange to the elements and update based on the change
172
+ /**
173
+ * Bind change events
174
+ *
175
+ * Set up event listeners on the $textarea so that the count messages update
176
+ * when the user types.
177
+ */
117
178
  CharacterCount.prototype.bindChangeEvents = function () {
118
179
  var $textarea = this.$textarea
119
180
  $textarea.addEventListener('keyup', this.handleKeyUp.bind(this))
@@ -123,10 +184,52 @@ CharacterCount.prototype.bindChangeEvents = function () {
123
184
  $textarea.addEventListener('blur', this.handleBlur.bind(this))
124
185
  }
125
186
 
126
- // Speech recognition software such as Dragon NaturallySpeaking will modify the
127
- // fields by directly changing its `value`. These changes don't trigger events
128
- // in JavaScript, so we need to poll to handle when and if they occur.
129
- CharacterCount.prototype.checkIfValueChanged = function () {
187
+ /**
188
+ * Handle key up event
189
+ *
190
+ * Update the visible character counter and keep track of when the last update
191
+ * happened for each keypress
192
+ */
193
+ CharacterCount.prototype.handleKeyUp = function () {
194
+ this.updateVisibleCountMessage()
195
+ this.lastInputTimestamp = Date.now()
196
+ }
197
+
198
+ /**
199
+ * Handle focus event
200
+ *
201
+ * Speech recognition software such as Dragon NaturallySpeaking will modify the
202
+ * fields by directly changing its `value`. These changes don't trigger events
203
+ * in JavaScript, so we need to poll to handle when and if they occur.
204
+ *
205
+ * Once the keyup event hasn't been detected for at least 1000 ms (1s), check if
206
+ * the textarea value has changed and update the count message if it has.
207
+ *
208
+ * This is so that the update triggered by the manual comparison doesn't
209
+ * conflict with debounced KeyboardEvent updates.
210
+ */
211
+ CharacterCount.prototype.handleFocus = function () {
212
+ this.valueChecker = setInterval(function () {
213
+ if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
214
+ this.updateIfValueChanged()
215
+ }
216
+ }.bind(this), 1000)
217
+ }
218
+
219
+ /**
220
+ * Handle blur event
221
+ *
222
+ * Stop checking the textarea value once the textarea no longer has focus
223
+ */
224
+ CharacterCount.prototype.handleBlur = function () {
225
+ // Cancel value checking on blur
226
+ clearInterval(this.valueChecker)
227
+ }
228
+
229
+ /**
230
+ * Update count message if textarea value has changed
231
+ */
232
+ CharacterCount.prototype.updateIfValueChanged = function () {
130
233
  if (!this.$textarea.oldValue) this.$textarea.oldValue = ''
131
234
  if (this.$textarea.value !== this.$textarea.oldValue) {
132
235
  this.$textarea.oldValue = this.$textarea.value
@@ -134,14 +237,20 @@ CharacterCount.prototype.checkIfValueChanged = function () {
134
237
  }
135
238
  }
136
239
 
137
- // Helper function to update both the visible and screen reader-specific
138
- // counters simultaneously (e.g. on init)
240
+ /**
241
+ * Update count message
242
+ *
243
+ * Helper function to update both the visible and screen reader-specific
244
+ * counters simultaneously (e.g. on init)
245
+ */
139
246
  CharacterCount.prototype.updateCountMessage = function () {
140
247
  this.updateVisibleCountMessage()
141
248
  this.updateScreenReaderCountMessage()
142
249
  }
143
250
 
144
- // Update visible counter
251
+ /**
252
+ * Update visible count message
253
+ */
145
254
  CharacterCount.prototype.updateVisibleCountMessage = function () {
146
255
  var $textarea = this.$textarea
147
256
  var $visibleCountMessage = this.$visibleCountMessage
@@ -167,10 +276,12 @@ CharacterCount.prototype.updateVisibleCountMessage = function () {
167
276
  }
168
277
 
169
278
  // Update message
170
- $visibleCountMessage.innerHTML = this.formattedUpdateMessage()
279
+ $visibleCountMessage.innerText = this.getCountMessage()
171
280
  }
172
281
 
173
- // Update screen reader-specific counter
282
+ /**
283
+ * Update screen reader count message
284
+ */
174
285
  CharacterCount.prototype.updateScreenReaderCountMessage = function () {
175
286
  var $screenReaderCountMessage = this.$screenReaderCountMessage
176
287
 
@@ -183,69 +294,160 @@ CharacterCount.prototype.updateScreenReaderCountMessage = function () {
183
294
  }
184
295
 
185
296
  // Update message
186
- $screenReaderCountMessage.innerHTML = this.formattedUpdateMessage()
297
+ $screenReaderCountMessage.innerText = this.getCountMessage()
187
298
  }
188
299
 
189
- // Format update message
190
- CharacterCount.prototype.formattedUpdateMessage = function () {
191
- var $textarea = this.$textarea
192
- var options = this.options
193
- var remainingNumber = this.maxLength - this.count($textarea.value)
300
+ /**
301
+ * Count the number of characters (or words, if `config.maxwords` is set)
302
+ * in the given text
303
+ *
304
+ * @param {string} text - The text to count the characters of
305
+ * @returns {number} the number of characters (or words) in the text
306
+ */
307
+ CharacterCount.prototype.count = function (text) {
308
+ if (this.config.maxwords) {
309
+ var tokens = text.match(/\S+/g) || [] // Matches consecutive non-whitespace chars
310
+ return tokens.length
311
+ } else {
312
+ return text.length
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Get count message
318
+ *
319
+ * @returns {string} Status message
320
+ */
321
+ CharacterCount.prototype.getCountMessage = function () {
322
+ var remainingNumber = this.maxLength - this.count(this.$textarea.value)
323
+
324
+ var countType = this.config.maxwords ? 'words' : 'characters'
325
+ return this.formatCountMessage(remainingNumber, countType)
326
+ }
194
327
 
195
- var charVerb = 'remaining'
196
- var charNoun = 'character'
197
- var displayNumber = remainingNumber
198
- if (options.maxwords) {
199
- charNoun = 'word'
328
+ /**
329
+ * Formats the message shown to users according to what's counted
330
+ * and how many remain
331
+ *
332
+ * @param {number} remainingNumber - The number of words/characaters remaining
333
+ * @param {string} countType - "words" or "characters"
334
+ * @returns {string} Status message
335
+ */
336
+ CharacterCount.prototype.formatCountMessage = function (remainingNumber, countType) {
337
+ if (remainingNumber === 0) {
338
+ return this.i18n.t(countType + 'AtLimit')
200
339
  }
201
- charNoun = charNoun + ((remainingNumber === -1 || remainingNumber === 1) ? '' : 's')
202
340
 
203
- charVerb = (remainingNumber < 0) ? 'too many' : 'remaining'
204
- displayNumber = Math.abs(remainingNumber)
341
+ var translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit'
205
342
 
206
- return 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb
343
+ return this.i18n.t(countType + translationKeySuffix, { count: Math.abs(remainingNumber) })
207
344
  }
208
345
 
209
- // Checks whether the value is over the configured threshold for the input.
210
- // If there is no configured threshold, it is set to 0 and this function will
211
- // always return true.
346
+ /**
347
+ * Check if count is over threshold
348
+ *
349
+ * Checks whether the value is over the configured threshold for the input.
350
+ * If there is no configured threshold, it is set to 0 and this function will
351
+ * always return true.
352
+ *
353
+ * @returns {boolean} true if the current count is over the config.threshold
354
+ * (or no threshold is set)
355
+ */
212
356
  CharacterCount.prototype.isOverThreshold = function () {
357
+ // No threshold means we're always above threshold so save some computation
358
+ if (!this.config.threshold) {
359
+ return true
360
+ }
361
+
213
362
  var $textarea = this.$textarea
214
- var options = this.options
215
363
 
216
364
  // Determine the remaining number of characters/words
217
365
  var currentLength = this.count($textarea.value)
218
366
  var maxLength = this.maxLength
219
367
 
220
- // Set threshold if presented in options
221
- var thresholdPercent = options.threshold ? options.threshold : 0
222
- var thresholdValue = maxLength * thresholdPercent / 100
368
+ var thresholdValue = maxLength * this.config.threshold / 100
223
369
 
224
370
  return (thresholdValue <= currentLength)
225
371
  }
226
372
 
227
- // Update the visible character counter and keep track of when the last update
228
- // happened for each keypress
229
- CharacterCount.prototype.handleKeyUp = function () {
230
- this.updateVisibleCountMessage()
231
- this.lastInputTimestamp = Date.now()
232
- }
233
-
234
- CharacterCount.prototype.handleFocus = function () {
235
- // If the field is focused, and a keyup event hasn't been detected for at
236
- // least 1000 ms (1 second), then run the manual change check.
237
- // This is so that the update triggered by the manual comparison doesn't
238
- // conflict with debounced KeyboardEvent updates.
239
- this.valueChecker = setInterval(function () {
240
- if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
241
- this.checkIfValueChanged()
242
- }
243
- }.bind(this), 1000)
244
- }
245
-
246
- CharacterCount.prototype.handleBlur = function () {
247
- // Cancel value checking on blur
248
- clearInterval(this.valueChecker)
249
- }
250
-
251
373
  export default CharacterCount
374
+
375
+ /**
376
+ * Character count config
377
+ *
378
+ * @typedef {CharacterCountConfigWithMaxLength | CharacterCountConfigWithMaxWords} CharacterCountConfig
379
+ */
380
+
381
+ /**
382
+ * Character count config (with maximum number of characters)
383
+ *
384
+ * @typedef {object} CharacterCountConfigWithMaxLength
385
+ * @property {number} [maxlength] - The maximum number of characters.
386
+ * If maxwords is provided, the maxlength option will be ignored.
387
+ * @property {number} [threshold = 0] - The percentage value of the limit at
388
+ * which point the count message is displayed. If this attribute is set, the
389
+ * count message will be hidden by default.
390
+ * @property {CharacterCountTranslations} [i18n = CHARACTER_COUNT_TRANSLATIONS] - See constant {@link CHARACTER_COUNT_TRANSLATIONS}
391
+ */
392
+
393
+ /**
394
+ * Character count config (with maximum number of words)
395
+ *
396
+ * @typedef {object} CharacterCountConfigWithMaxWords
397
+ * @property {number} [maxwords] - The maximum number of words. If maxwords is
398
+ * provided, the maxlength option will be ignored.
399
+ * @property {number} [threshold = 0] - The percentage value of the limit at
400
+ * which point the count message is displayed. If this attribute is set, the
401
+ * count message will be hidden by default.
402
+ * @property {CharacterCountTranslations} [i18n = CHARACTER_COUNT_TRANSLATIONS] - See constant {@link CHARACTER_COUNT_TRANSLATIONS}
403
+ */
404
+
405
+ /**
406
+ * Character count translations
407
+ *
408
+ * @typedef {object} CharacterCountTranslations
409
+ *
410
+ * Messages shown to users as they type. It provides feedback on how many words
411
+ * or characters they have remaining or if they are over the limit. This also
412
+ * includes a message used as an accessible description for the textarea.
413
+ * @property {TranslationPluralForms} [charactersUnderLimit] - Message displayed
414
+ * when the number of characters is under the configured maximum, `maxlength`.
415
+ * This message is displayed visually and through assistive technologies. The
416
+ * component will replace the `%{count}` placeholder with the number of
417
+ * remaining characters. This is a [pluralised list of
418
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
419
+ * @property {string} [charactersAtLimit] - Message displayed when the number of
420
+ * characters reaches the configured maximum, `maxlength`. This message is
421
+ * displayed visually and through assistive technologies.
422
+ * @property {TranslationPluralForms} [charactersOverLimit] - Message displayed
423
+ * when the number of characters is over the configured maximum, `maxlength`.
424
+ * This message is displayed visually and through assistive technologies. The
425
+ * component will replace the `%{count}` placeholder with the number of
426
+ * remaining characters. This is a [pluralised list of
427
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
428
+ * @property {TranslationPluralForms} [wordsUnderLimit] - Message displayed when
429
+ * the number of words is under the configured maximum, `maxlength`. This
430
+ * message is displayed visually and through assistive technologies. The
431
+ * component will replace the `%{count}` placeholder with the number of
432
+ * remaining words. This is a [pluralised list of
433
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
434
+ * @property {string} [wordsAtLimit] - Message displayed when the number of
435
+ * words reaches the configured maximum, `maxlength`. This message is
436
+ * displayed visually and through assistive technologies.
437
+ * @property {TranslationPluralForms} [wordsOverLimit] - Message displayed when
438
+ * the number of words is over the configured maximum, `maxlength`. This
439
+ * message is displayed visually and through assistive technologies. The
440
+ * component will replace the `%{count}` placeholder with the number of
441
+ * remaining words. This is a [pluralised list of
442
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
443
+ * @property {TranslationPluralForms} [textareaDescription] - Message made
444
+ * available to assistive technologies, if none is already present in the
445
+ * HTML, to describe that the component accepts only a limited amount of
446
+ * content. It is visible on the page when JavaScript is unavailable. The
447
+ * component will replace the `%{count}` placeholder with the value of the
448
+ * `maxlength` or `maxwords` parameter.
449
+ */
450
+
451
+ /**
452
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
453
+ */
@@ -2,8 +2,14 @@ import '../../vendor/polyfills/Function/prototype/bind.mjs'
2
2
  // addEventListener, event.target normalization and DOMContentLoaded
3
3
  import '../../vendor/polyfills/Event.mjs'
4
4
  import '../../vendor/polyfills/Element/prototype/classList.mjs'
5
- import { nodeListForEach } from '../../common.mjs'
5
+ import { nodeListForEach } from '../../common/index.mjs'
6
6
 
7
+ /**
8
+ * Checkboxes component
9
+ *
10
+ * @class
11
+ * @param {HTMLElement} $module - HTML element to use for checkboxes
12
+ */
7
13
  function Checkboxes ($module) {
8
14
  this.$module = $module
9
15
  this.$inputs = $module.querySelectorAll('input[type="checkbox"]')
@@ -73,7 +79,7 @@ Checkboxes.prototype.syncAllConditionalReveals = function () {
73
79
  * Synchronise the visibility of the conditional reveal, and its accessible
74
80
  * state, with the input's checked state.
75
81
  *
76
- * @param {HTMLInputElement} $input Checkbox input
82
+ * @param {HTMLInputElement} $input - Checkbox input
77
83
  */
78
84
  Checkboxes.prototype.syncConditionalRevealWithInputState = function ($input) {
79
85
  var $target = document.getElementById($input.getAttribute('aria-controls'))
@@ -131,7 +137,7 @@ Checkboxes.prototype.unCheckExclusiveInputs = function ($input) {
131
137
  * Handle a click within the $module – if the click occurred on a checkbox, sync
132
138
  * the state of any associated conditional reveal with the checkbox state.
133
139
  *
134
- * @param {MouseEvent} event Click event
140
+ * @param {MouseEvent} event - Click event
135
141
  */
136
142
  Checkboxes.prototype.handleClick = function (event) {
137
143
  var $target = event.target
@@ -6,11 +6,17 @@
6
6
  */
7
7
  import '../../vendor/polyfills/Function/prototype/bind.mjs'
8
8
  import '../../vendor/polyfills/Event.mjs' // addEventListener and event.target normaliziation
9
- import { generateUniqueID } from '../../common.mjs'
9
+ import { generateUniqueID } from '../../common/index.mjs'
10
10
 
11
11
  var KEY_ENTER = 13
12
12
  var KEY_SPACE = 32
13
13
 
14
+ /**
15
+ * Details component
16
+ *
17
+ * @class
18
+ * @param {HTMLElement} $module - HTML element to use for details
19
+ */
14
20
  function Details ($module) {
15
21
  this.$module = $module
16
22
  }
@@ -77,9 +83,10 @@ Details.prototype.polyfillDetails = function () {
77
83
  }
78
84
 
79
85
  /**
80
- * Define a statechange function that updates aria-expanded and style.display
81
- * @param {object} summary element
82
- */
86
+ * Define a statechange function that updates aria-expanded and style.display
87
+ *
88
+ * @returns {boolean} Returns true
89
+ */
83
90
  Details.prototype.polyfillSetAttributes = function () {
84
91
  if (this.$module.hasAttribute('open')) {
85
92
  this.$module.removeAttribute('open')
@@ -95,10 +102,11 @@ Details.prototype.polyfillSetAttributes = function () {
95
102
  }
96
103
 
97
104
  /**
98
- * Handle cross-modal click events
99
- * @param {object} node element
100
- * @param {function} callback function
101
- */
105
+ * Handle cross-modal click events
106
+ *
107
+ * @param {object} node - element
108
+ * @param {polyfillHandleInputsCallback} callback - function
109
+ */
102
110
  Details.prototype.polyfillHandleInputs = function (node, callback) {
103
111
  node.addEventListener('keypress', function (event) {
104
112
  var target = event.target
@@ -133,3 +141,9 @@ Details.prototype.polyfillHandleInputs = function (node, callback) {
133
141
  }
134
142
 
135
143
  export default Details
144
+
145
+ /**
146
+ * @callback polyfillHandleInputsCallback
147
+ * @param {KeyboardEvent} event - Keyboard event
148
+ * @returns {undefined}
149
+ */