govuk_publishing_components 32.1.0 → 33.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) 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 +4 -4
  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/single-page-notification-button.js +24 -8
  13. data/app/assets/javascripts/govuk_publishing_components/components/step-by-step-nav.js +22 -1
  14. data/app/assets/javascripts/govuk_publishing_components/vendor/lux/lux-reporter.js +140 -191
  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/views/govuk_publishing_components/components/_accordion.html.erb +14 -1
  21. data/app/views/govuk_publishing_components/components/_error_summary.html.erb +27 -26
  22. data/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb +2 -2
  23. data/app/views/govuk_publishing_components/components/_phase_banner.html.erb +1 -1
  24. data/app/views/govuk_publishing_components/components/_share_links.html.erb +11 -13
  25. data/app/views/govuk_publishing_components/components/_single_page_notification_button.html.erb +1 -1
  26. data/app/views/govuk_publishing_components/components/_step_by_step_nav.html.erb +4 -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/docs/step_by_step_nav.yml +34 -0
  32. data/app/views/govuk_publishing_components/components/feedback/_problem_form.html.erb +1 -1
  33. data/app/views/govuk_publishing_components/components/feedback/_survey_signup_form.html.erb +1 -1
  34. data/app/views/govuk_publishing_components/components/feedback/_yes_no_banner.html.erb +3 -3
  35. data/lib/govuk_publishing_components/presenters/button_helper.rb +9 -2
  36. data/lib/govuk_publishing_components/presenters/single_page_notification_button_helper.rb +25 -1
  37. data/lib/govuk_publishing_components/version.rb +1 -1
  38. data/node_modules/axe-core/axe.js +4559 -4673
  39. data/node_modules/axe-core/axe.min.js +2 -2
  40. data/node_modules/axe-core/package.json +2 -2
  41. data/node_modules/axe-core/sri-history.json +4 -0
  42. data/node_modules/govuk-frontend/README.md +1 -2
  43. data/node_modules/govuk-frontend/govuk/all.js +1398 -273
  44. data/node_modules/govuk-frontend/govuk/common/closest-attribute-value.js +70 -0
  45. data/node_modules/govuk-frontend/govuk/common/index.js +172 -0
  46. data/node_modules/govuk-frontend/govuk/common/normalise-dataset.js +373 -0
  47. data/node_modules/govuk-frontend/govuk/common.js +138 -3
  48. data/node_modules/govuk-frontend/govuk/components/accordion/accordion.js +753 -25
  49. data/node_modules/govuk-frontend/govuk/components/accordion/fixtures.json +54 -22
  50. data/node_modules/govuk-frontend/govuk/components/accordion/macro-options.json +36 -0
  51. data/node_modules/govuk-frontend/govuk/components/accordion/template.njk +7 -1
  52. data/node_modules/govuk-frontend/govuk/components/back-link/fixtures.json +12 -12
  53. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/fixtures.json +22 -22
  54. data/node_modules/govuk-frontend/govuk/components/button/_index.scss +23 -5
  55. data/node_modules/govuk-frontend/govuk/components/button/button.js +365 -107
  56. data/node_modules/govuk-frontend/govuk/components/button/fixtures.json +85 -66
  57. data/node_modules/govuk-frontend/govuk/components/button/template.njk +1 -1
  58. data/node_modules/govuk-frontend/govuk/components/character-count/_index.scss +9 -0
  59. data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +1033 -121
  60. data/node_modules/govuk-frontend/govuk/components/character-count/fixtures.json +112 -36
  61. data/node_modules/govuk-frontend/govuk/components/character-count/macro-options.json +42 -0
  62. data/node_modules/govuk-frontend/govuk/components/character-count/template.njk +27 -3
  63. data/node_modules/govuk-frontend/govuk/components/checkboxes/checkboxes.js +30 -2
  64. data/node_modules/govuk-frontend/govuk/components/checkboxes/fixtures.json +96 -93
  65. data/node_modules/govuk-frontend/govuk/components/cookie-banner/fixtures.json +46 -46
  66. data/node_modules/govuk-frontend/govuk/components/date-input/fixtures.json +50 -50
  67. data/node_modules/govuk-frontend/govuk/components/details/details.js +43 -13
  68. data/node_modules/govuk-frontend/govuk/components/details/fixtures.json +20 -20
  69. data/node_modules/govuk-frontend/govuk/components/error-message/fixtures.json +20 -20
  70. data/node_modules/govuk-frontend/govuk/components/error-summary/error-summary.js +268 -6
  71. data/node_modules/govuk-frontend/govuk/components/error-summary/fixtures.json +44 -35
  72. data/node_modules/govuk-frontend/govuk/components/error-summary/template.njk +25 -21
  73. data/node_modules/govuk-frontend/govuk/components/fieldset/fixtures.json +51 -39
  74. data/node_modules/govuk-frontend/govuk/components/file-upload/fixtures.json +26 -26
  75. data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +1 -1
  76. data/node_modules/govuk-frontend/govuk/components/footer/fixtures.json +46 -46
  77. data/node_modules/govuk-frontend/govuk/components/footer/macro-options.json +2 -2
  78. data/node_modules/govuk-frontend/govuk/components/header/fixtures.json +93 -38
  79. data/node_modules/govuk-frontend/govuk/components/header/header.js +6 -0
  80. data/node_modules/govuk-frontend/govuk/components/header/macro-options.json +8 -2
  81. data/node_modules/govuk-frontend/govuk/components/header/template.njk +4 -2
  82. data/node_modules/govuk-frontend/govuk/components/hint/fixtures.json +12 -12
  83. data/node_modules/govuk-frontend/govuk/components/input/fixtures.json +80 -80
  84. data/node_modules/govuk-frontend/govuk/components/inset-text/fixtures.json +12 -12
  85. data/node_modules/govuk-frontend/govuk/components/label/fixtures.json +34 -34
  86. data/node_modules/govuk-frontend/govuk/components/notification-banner/fixtures.json +56 -46
  87. data/node_modules/govuk-frontend/govuk/components/notification-banner/notification-banner.js +252 -2
  88. data/node_modules/govuk-frontend/govuk/components/notification-banner/template.njk +1 -1
  89. data/node_modules/govuk-frontend/govuk/components/pagination/_index.scss +10 -7
  90. data/node_modules/govuk-frontend/govuk/components/pagination/fixtures.json +33 -26
  91. data/node_modules/govuk-frontend/govuk/components/panel/fixtures.json +18 -18
  92. data/node_modules/govuk-frontend/govuk/components/phase-banner/fixtures.json +14 -14
  93. data/node_modules/govuk-frontend/govuk/components/radios/fixtures.json +94 -91
  94. data/node_modules/govuk-frontend/govuk/components/radios/radios.js +30 -2
  95. data/node_modules/govuk-frontend/govuk/components/select/fixtures.json +32 -32
  96. data/node_modules/govuk-frontend/govuk/components/skip-link/fixtures.json +22 -20
  97. data/node_modules/govuk-frontend/govuk/components/skip-link/skip-link.js +10 -4
  98. data/node_modules/govuk-frontend/govuk/components/summary-list/fixtures.json +50 -50
  99. data/node_modules/govuk-frontend/govuk/components/table/_index.scss +1 -1
  100. data/node_modules/govuk-frontend/govuk/components/table/fixtures.json +40 -40
  101. data/node_modules/govuk-frontend/govuk/components/tabs/fixtures.json +29 -29
  102. data/node_modules/govuk-frontend/govuk/components/tabs/tabs.js +28 -0
  103. data/node_modules/govuk-frontend/govuk/components/tag/fixtures.json +28 -28
  104. data/node_modules/govuk-frontend/govuk/components/textarea/fixtures.json +34 -34
  105. data/node_modules/govuk-frontend/govuk/components/warning-text/fixtures.json +14 -14
  106. data/node_modules/govuk-frontend/govuk/core/_section-break.scss +1 -1
  107. data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +2 -2
  108. data/node_modules/govuk-frontend/govuk/helpers/_links.scss +6 -6
  109. data/node_modules/govuk-frontend/govuk/i18n.js +390 -0
  110. data/node_modules/govuk-frontend/govuk/macros/i18n.njk +15 -0
  111. data/node_modules/govuk-frontend/govuk/settings/_all.scss +1 -0
  112. data/node_modules/govuk-frontend/govuk/settings/_colours-palette.scss +12 -0
  113. data/node_modules/govuk-frontend/govuk/settings/_compatibility.scss +26 -0
  114. data/node_modules/govuk-frontend/govuk/settings/_typography-font.scss +23 -0
  115. data/node_modules/govuk-frontend/govuk/settings/_typography-responsive.scss +12 -0
  116. data/node_modules/govuk-frontend/govuk/settings/_warnings.scss +53 -0
  117. data/node_modules/govuk-frontend/govuk/tools/_compatibility.scss +20 -6
  118. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Date/now.js +21 -0
  119. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Element/prototype/dataset.js +300 -0
  120. data/node_modules/govuk-frontend/govuk/vendor/polyfills/String/prototype/trim.js +21 -0
  121. data/node_modules/govuk-frontend/govuk-esm/all.mjs +50 -27
  122. data/node_modules/govuk-frontend/govuk-esm/common/closest-attribute-value.mjs +15 -0
  123. data/node_modules/govuk-frontend/govuk-esm/common/index.mjs +159 -0
  124. data/node_modules/govuk-frontend/govuk-esm/common/normalise-dataset.mjs +58 -0
  125. data/node_modules/govuk-frontend/govuk-esm/common.mjs +6 -28
  126. data/node_modules/govuk-frontend/govuk-esm/components/accordion/accordion.mjs +113 -43
  127. data/node_modules/govuk-frontend/govuk-esm/components/button/button.mjs +67 -30
  128. data/node_modules/govuk-frontend/govuk-esm/components/character-count/character-count.mjs +325 -123
  129. data/node_modules/govuk-frontend/govuk-esm/components/checkboxes/checkboxes.mjs +9 -3
  130. data/node_modules/govuk-frontend/govuk-esm/components/details/details.mjs +22 -8
  131. data/node_modules/govuk-frontend/govuk-esm/components/error-summary/error-summary.mjs +48 -6
  132. data/node_modules/govuk-frontend/govuk-esm/components/header/header.mjs +6 -0
  133. data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs +32 -2
  134. data/node_modules/govuk-frontend/govuk-esm/components/radios/radios.mjs +9 -3
  135. data/node_modules/govuk-frontend/govuk-esm/components/skip-link/skip-link.mjs +10 -4
  136. data/node_modules/govuk-frontend/govuk-esm/components/tabs/tabs.mjs +8 -2
  137. data/node_modules/govuk-frontend/govuk-esm/i18n.mjs +380 -0
  138. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Date/now.mjs +13 -0
  139. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/dataset.mjs +68 -0
  140. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/String/prototype/trim.mjs +13 -0
  141. data/node_modules/govuk-frontend/govuk-prototype-kit/init.js +7 -0
  142. data/node_modules/govuk-frontend/govuk-prototype-kit/init.scss +12 -0
  143. data/node_modules/govuk-frontend/govuk-prototype-kit.config.json +138 -7
  144. data/node_modules/govuk-frontend/package.json +1 -1
  145. 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
+ */