hmrc-frontend 5.1.0 → 5.2.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.
package/hmrc/VERSION.txt CHANGED
@@ -1 +1 @@
1
- 5.1.0
1
+ 5.2.0
package/hmrc/all.js CHANGED
@@ -2757,10 +2757,9 @@
2757
2757
  function CharacterCount($module) {
2758
2758
  this.$module = $module;
2759
2759
  this.$textarea = $module.querySelector('.hmrc-js-character-count');
2760
-
2761
- if (this.$textarea) {
2762
- this.$countMessage = $module.querySelector("[id=\"".concat(this.$textarea.id, "-info\"]"));
2763
- }
2760
+ this.$visibleCountMessage = null;
2761
+ this.$screenReaderCountMessage = null;
2762
+ this.lastInputTimestamp = null;
2764
2763
  }
2765
2764
 
2766
2765
  CharacterCount.prototype.defaults = {
@@ -2769,18 +2768,35 @@
2769
2768
  }; // Initialize component
2770
2769
 
2771
2770
  CharacterCount.prototype.init = function () {
2772
- // Check for module
2771
+ // Check that required elements are present
2772
+ if (!this.$textarea) {
2773
+ return;
2774
+ } // Check for module
2775
+
2776
+
2773
2777
  var $module = this.$module;
2774
2778
  var $textarea = this.$textarea;
2775
- var $countMessage = this.$countMessage;
2776
-
2777
- if (!$textarea || !$countMessage) {
2778
- return;
2779
- } // We move count message right after the field
2779
+ var $fallbackLimitMessage = document.getElementById($textarea.id + '-info'); // Move the fallback count message to be immediately after the textarea
2780
2780
  // Kept for backwards compatibility
2781
2781
 
2782
+ $textarea.insertAdjacentElement('afterend', $fallbackLimitMessage); // Create the *screen reader* specific live-updating counter
2783
+ // This doesn't need any styling classes, as it is never visible
2784
+
2785
+ var $screenReaderCountMessage = document.createElement('div');
2786
+ $screenReaderCountMessage.className = 'hmrc-character-count__sr-status govuk-visually-hidden';
2787
+ $screenReaderCountMessage.setAttribute('aria-live', 'polite');
2788
+ this.$screenReaderCountMessage = $screenReaderCountMessage;
2789
+ $fallbackLimitMessage.insertAdjacentElement('afterend', $screenReaderCountMessage); // Create our live-updating counter element, copying the classes from the
2790
+ // fallback element for backwards compatibility as these may have been configured
2782
2791
 
2783
- $textarea.insertAdjacentElement('afterend', $countMessage); // Read options set using dataset ('data-' values)
2792
+ var $visibleCountMessage = document.createElement('div');
2793
+ $visibleCountMessage.className = $fallbackLimitMessage.className;
2794
+ $visibleCountMessage.classList.add('hmrc-character-count__status');
2795
+ $visibleCountMessage.setAttribute('aria-hidden', 'true');
2796
+ this.$visibleCountMessage = $visibleCountMessage;
2797
+ $fallbackLimitMessage.insertAdjacentElement('afterend', $visibleCountMessage); // Hide the fallback limit message
2798
+
2799
+ $fallbackLimitMessage.classList.add('govuk-visually-hidden'); // Read options set using dataset ('data-' values)
2784
2800
 
2785
2801
  this.options = this.getDataset($module); // Determine the limit attribute (characters or words)
2786
2802
 
@@ -2798,22 +2814,18 @@
2798
2814
  } // Remove hard limit if set
2799
2815
 
2800
2816
 
2801
- $module.removeAttribute('maxlength'); // When the page is restored after navigating 'back' in some browsers the
2817
+ $textarea.removeAttribute('maxlength');
2818
+ this.bindChangeEvents(); // When the page is restored after navigating 'back' in some browsers the
2802
2819
  // state of the character count is not restored until *after* the DOMContentLoaded
2803
- // event is fired, so we need to sync after the pageshow event in browsers
2804
- // that support it.
2820
+ // event is fired, so we need to manually update it after the pageshow event
2821
+ // in browsers that support it.
2805
2822
 
2806
2823
  if ('onpageshow' in window) {
2807
- window.addEventListener('pageshow', this.sync.bind(this));
2824
+ window.addEventListener('pageshow', this.updateCountMessage.bind(this));
2808
2825
  } else {
2809
- window.addEventListener('DOMContentLoaded', this.sync.bind(this));
2826
+ window.addEventListener('DOMContentLoaded', this.updateCountMessage.bind(this));
2810
2827
  }
2811
2828
 
2812
- this.sync();
2813
- };
2814
-
2815
- CharacterCount.prototype.sync = function () {
2816
- this.bindChangeEvents();
2817
2829
  this.updateCountMessage();
2818
2830
  }; // Read data attributes
2819
2831
 
@@ -2854,7 +2866,7 @@
2854
2866
 
2855
2867
  CharacterCount.prototype.bindChangeEvents = function () {
2856
2868
  var $textarea = this.$textarea;
2857
- $textarea.addEventListener('keyup', this.checkIfValueChanged.bind(this)); // Bind focus/blur events to start/stop polling
2869
+ $textarea.addEventListener('keyup', this.handleKeyUp.bind(this)); // Bind focus/blur events to start/stop polling
2858
2870
 
2859
2871
  $textarea.addEventListener('focus', this.handleFocus.bind(this));
2860
2872
  $textarea.addEventListener('blur', this.handleBlur.bind(this));
@@ -2870,43 +2882,63 @@
2870
2882
  this.$textarea.oldValue = this.$textarea.value;
2871
2883
  this.updateCountMessage();
2872
2884
  }
2873
- }; // Update message box
2885
+ }; // Helper function to update both the visible and screen reader-specific
2886
+ // counters simultaneously (e.g. on init)
2874
2887
 
2875
2888
 
2876
2889
  CharacterCount.prototype.updateCountMessage = function () {
2877
- var countElement = this.$textarea;
2878
- var options = this.options;
2879
- var countMessage = this.$countMessage; // Determine the remaining number of characters/words
2880
-
2881
- var currentLength = this.count(countElement.value);
2882
- var maxLength = this.maxLength;
2883
- var remainingNumber = maxLength - currentLength; // Set threshold if presented in options
2890
+ this.updateVisibleCountMessage();
2891
+ this.updateScreenReaderCountMessage();
2892
+ }; // Update visible counter
2884
2893
 
2885
- var thresholdPercent = options.threshold ? options.threshold : 0;
2886
- var thresholdValue = maxLength * thresholdPercent / 100;
2887
2894
 
2888
- if (thresholdValue > currentLength) {
2889
- countMessage.classList.add('hmrc-character-count__message--disabled'); // Ensure threshold is hidden for users of assistive technologies
2895
+ CharacterCount.prototype.updateVisibleCountMessage = function () {
2896
+ var $textarea = this.$textarea;
2897
+ var $visibleCountMessage = this.$visibleCountMessage;
2898
+ var remainingNumber = this.maxLength - this.count($textarea.value); // If input is over the threshold, remove the disabled class which renders the
2899
+ // counter invisible.
2890
2900
 
2891
- countMessage.setAttribute('aria-hidden', true);
2901
+ if (this.isOverThreshold()) {
2902
+ $visibleCountMessage.classList.remove('hmrc-character-count__message--disabled');
2892
2903
  } else {
2893
- countMessage.classList.remove('hmrc-character-count__message--disabled'); // Ensure threshold is visible for users of assistive technologies
2894
-
2895
- countMessage.removeAttribute('aria-hidden');
2904
+ $visibleCountMessage.classList.add('hmrc-character-count__message--disabled');
2896
2905
  } // Update styles
2897
2906
 
2898
2907
 
2899
2908
  if (remainingNumber < 0) {
2900
- countElement.classList.add('govuk-textarea--error');
2901
- countMessage.classList.remove('govuk-hint');
2902
- countMessage.classList.add('govuk-error-message');
2909
+ $textarea.classList.add('govuk-textarea--error');
2910
+ $visibleCountMessage.classList.remove('govuk-hint');
2911
+ $visibleCountMessage.classList.add('govuk-error-message');
2912
+ } else {
2913
+ $textarea.classList.remove('govuk-textarea--error');
2914
+ $visibleCountMessage.classList.remove('govuk-error-message');
2915
+ $visibleCountMessage.classList.add('govuk-hint');
2916
+ } // Update message
2917
+
2918
+
2919
+ $visibleCountMessage.innerHTML = this.formattedUpdateMessage();
2920
+ }; // Update screen reader-specific counter
2921
+
2922
+
2923
+ CharacterCount.prototype.updateScreenReaderCountMessage = function () {
2924
+ var $screenReaderCountMessage = this.$screenReaderCountMessage; // If over the threshold, remove the aria-hidden attribute, allowing screen
2925
+ // readers to announce the content of the element.
2926
+
2927
+ if (this.isOverThreshold()) {
2928
+ $screenReaderCountMessage.removeAttribute('aria-hidden');
2903
2929
  } else {
2904
- countElement.classList.remove('govuk-textarea--error');
2905
- countMessage.classList.remove('govuk-error-message');
2906
- countMessage.classList.add('govuk-hint');
2930
+ $screenReaderCountMessage.setAttribute('aria-hidden', true);
2907
2931
  } // Update message
2908
2932
 
2909
2933
 
2934
+ $screenReaderCountMessage.innerHTML = this.formattedUpdateMessage();
2935
+ }; // Format update message
2936
+
2937
+
2938
+ CharacterCount.prototype.formattedUpdateMessage = function () {
2939
+ var $textarea = this.$textarea;
2940
+ var options = this.options;
2941
+ var remainingNumber = this.maxLength - this.count($textarea.value);
2910
2942
  var isWelsh = this.options.language === 'cy';
2911
2943
  var charVerb;
2912
2944
  var charNoun;
@@ -2931,12 +2963,41 @@
2931
2963
  }
2932
2964
 
2933
2965
  var displayNumber = Math.abs(remainingNumber);
2934
- countMessage.innerHTML = charStartMessage + displayNumber + ' ' + charNoun + ' ' + charVerb;
2966
+ return charStartMessage + displayNumber + ' ' + charNoun + ' ' + charVerb;
2967
+ }; // Checks whether the value is over the configured threshold for the input.
2968
+ // If there is no configured threshold, it is set to 0 and this function will
2969
+ // always return true.
2970
+
2971
+
2972
+ CharacterCount.prototype.isOverThreshold = function () {
2973
+ var $textarea = this.$textarea;
2974
+ var options = this.options; // Determine the remaining number of characters/words
2975
+
2976
+ var currentLength = this.count($textarea.value);
2977
+ var maxLength = this.maxLength; // Set threshold if presented in options
2978
+
2979
+ var thresholdPercent = options.threshold ? options.threshold : 0;
2980
+ var thresholdValue = maxLength * thresholdPercent / 100;
2981
+ return thresholdValue <= currentLength;
2982
+ }; // Update the visible character counter and keep track of when the last update
2983
+ // happened for each keypress
2984
+
2985
+
2986
+ CharacterCount.prototype.handleKeyUp = function () {
2987
+ this.updateVisibleCountMessage();
2988
+ this.lastInputTimestamp = Date.now();
2935
2989
  };
2936
2990
 
2937
2991
  CharacterCount.prototype.handleFocus = function () {
2938
- // Check if value changed on focus
2939
- this.valueChecker = setInterval(this.checkIfValueChanged.bind(this), 1000);
2992
+ // If the field is focused, and a keyup event hasn't been detected for at
2993
+ // least 1000 ms (1 second), then run the manual change check.
2994
+ // This is so that the update triggered by the manual comparison doesn't
2995
+ // conflict with debounced KeyboardEvent updates.
2996
+ this.valueChecker = setInterval(function () {
2997
+ if (!this.lastInputTimestamp || Date.now() - 500 >= this.lastInputTimestamp) {
2998
+ this.checkIfValueChanged();
2999
+ }
3000
+ }.bind(this), 1000);
2940
3001
  };
2941
3002
 
2942
3003
  CharacterCount.prototype.handleBlur = function () {
@@ -14,9 +14,9 @@ import 'govuk-frontend/govuk/vendor/polyfills/Element/prototype/classList';
14
14
  function CharacterCount($module) {
15
15
  this.$module = $module;
16
16
  this.$textarea = $module.querySelector('.hmrc-js-character-count');
17
- if (this.$textarea) {
18
- this.$countMessage = $module.querySelector(`[id="${this.$textarea.id}-info"]`);
19
- }
17
+ this.$visibleCountMessage = null;
18
+ this.$screenReaderCountMessage = null;
19
+ this.lastInputTimestamp = null;
20
20
  }
21
21
 
22
22
  CharacterCount.prototype.defaults = {
@@ -26,18 +26,39 @@ CharacterCount.prototype.defaults = {
26
26
 
27
27
  // Initialize component
28
28
  CharacterCount.prototype.init = function () {
29
+ // Check that required elements are present
30
+ if (!this.$textarea) {
31
+ return
32
+ }
33
+
29
34
  // Check for module
30
35
  var $module = this.$module;
31
36
  var $textarea = this.$textarea;
32
- var $countMessage = this.$countMessage;
33
-
34
- if (!$textarea || !$countMessage) {
35
- return;
36
- }
37
+ var $fallbackLimitMessage = document.getElementById($textarea.id + '-info');
37
38
 
38
- // We move count message right after the field
39
+ // Move the fallback count message to be immediately after the textarea
39
40
  // Kept for backwards compatibility
40
- $textarea.insertAdjacentElement('afterend', $countMessage);
41
+ $textarea.insertAdjacentElement('afterend', $fallbackLimitMessage);
42
+
43
+ // Create the *screen reader* specific live-updating counter
44
+ // This doesn't need any styling classes, as it is never visible
45
+ var $screenReaderCountMessage = document.createElement('div');
46
+ $screenReaderCountMessage.className = 'hmrc-character-count__sr-status govuk-visually-hidden';
47
+ $screenReaderCountMessage.setAttribute('aria-live', 'polite');
48
+ this.$screenReaderCountMessage = $screenReaderCountMessage;
49
+ $fallbackLimitMessage.insertAdjacentElement('afterend', $screenReaderCountMessage);
50
+
51
+ // Create our live-updating counter element, copying the classes from the
52
+ // fallback element for backwards compatibility as these may have been configured
53
+ var $visibleCountMessage = document.createElement('div');
54
+ $visibleCountMessage.className = $fallbackLimitMessage.className;
55
+ $visibleCountMessage.classList.add('hmrc-character-count__status');
56
+ $visibleCountMessage.setAttribute('aria-hidden', 'true');
57
+ this.$visibleCountMessage = $visibleCountMessage;
58
+ $fallbackLimitMessage.insertAdjacentElement('afterend', $visibleCountMessage);
59
+
60
+ // Hide the fallback limit message
61
+ $fallbackLimitMessage.classList.add('govuk-visually-hidden');
41
62
 
42
63
  // Read options set using dataset ('data-' values)
43
64
  this.options = this.getDataset($module);
@@ -57,23 +78,19 @@ CharacterCount.prototype.init = function () {
57
78
  }
58
79
 
59
80
  // Remove hard limit if set
60
- $module.removeAttribute('maxlength');
81
+ $textarea.removeAttribute('maxlength');
82
+
83
+ this.bindChangeEvents();
61
84
 
62
85
  // When the page is restored after navigating 'back' in some browsers the
63
86
  // state of the character count is not restored until *after* the DOMContentLoaded
64
- // event is fired, so we need to sync after the pageshow event in browsers
65
- // that support it.
87
+ // event is fired, so we need to manually update it after the pageshow event
88
+ // in browsers that support it.
66
89
  if ('onpageshow' in window) {
67
- window.addEventListener('pageshow', this.sync.bind(this));
90
+ window.addEventListener('pageshow', this.updateCountMessage.bind(this));
68
91
  } else {
69
- window.addEventListener('DOMContentLoaded', this.sync.bind(this));
92
+ window.addEventListener('DOMContentLoaded', this.updateCountMessage.bind(this));
70
93
  }
71
-
72
- this.sync();
73
- };
74
-
75
- CharacterCount.prototype.sync = function () {
76
- this.bindChangeEvents();
77
94
  this.updateCountMessage();
78
95
  };
79
96
 
@@ -108,7 +125,7 @@ CharacterCount.prototype.count = function (text) {
108
125
  // Bind input propertychange to the elements and update based on the change
109
126
  CharacterCount.prototype.bindChangeEvents = function () {
110
127
  var $textarea = this.$textarea;
111
- $textarea.addEventListener('keyup', this.checkIfValueChanged.bind(this));
128
+ $textarea.addEventListener('keyup', this.handleKeyUp.bind(this));
112
129
 
113
130
  // Bind focus/blur events to start/stop polling
114
131
  $textarea.addEventListener('focus', this.handleFocus.bind(this));
@@ -126,42 +143,64 @@ CharacterCount.prototype.checkIfValueChanged = function () {
126
143
  }
127
144
  };
128
145
 
129
- // Update message box
146
+ // Helper function to update both the visible and screen reader-specific
147
+ // counters simultaneously (e.g. on init)
130
148
  CharacterCount.prototype.updateCountMessage = function () {
131
- var countElement = this.$textarea;
132
- var options = this.options;
133
- var countMessage = this.$countMessage;
149
+ this.updateVisibleCountMessage();
150
+ this.updateScreenReaderCountMessage();
151
+ };
134
152
 
135
- // Determine the remaining number of characters/words
136
- var currentLength = this.count(countElement.value)
137
- var maxLength = this.maxLength
138
- var remainingNumber = maxLength - currentLength
153
+ // Update visible counter
154
+ CharacterCount.prototype.updateVisibleCountMessage = function () {
155
+ var $textarea = this.$textarea;
156
+ var $visibleCountMessage = this.$visibleCountMessage;
157
+ var remainingNumber = this.maxLength - this.count($textarea.value);
139
158
 
140
- // Set threshold if presented in options
141
- var thresholdPercent = options.threshold ? options.threshold : 0;
142
- var thresholdValue = (maxLength * thresholdPercent) / 100;
143
- if (thresholdValue > currentLength) {
144
- countMessage.classList.add('hmrc-character-count__message--disabled');
145
- // Ensure threshold is hidden for users of assistive technologies
146
- countMessage.setAttribute('aria-hidden', true);
159
+ // If input is over the threshold, remove the disabled class which renders the
160
+ // counter invisible.
161
+ if (this.isOverThreshold()) {
162
+ $visibleCountMessage.classList.remove('hmrc-character-count__message--disabled');
147
163
  } else {
148
- countMessage.classList.remove('hmrc-character-count__message--disabled');
149
- // Ensure threshold is visible for users of assistive technologies
150
- countMessage.removeAttribute('aria-hidden');
164
+ $visibleCountMessage.classList.add('hmrc-character-count__message--disabled');
151
165
  }
152
166
 
153
167
  // Update styles
154
168
  if (remainingNumber < 0) {
155
- countElement.classList.add('govuk-textarea--error');
156
- countMessage.classList.remove('govuk-hint');
157
- countMessage.classList.add('govuk-error-message');
169
+ $textarea.classList.add('govuk-textarea--error');
170
+ $visibleCountMessage.classList.remove('govuk-hint');
171
+ $visibleCountMessage.classList.add('govuk-error-message');
158
172
  } else {
159
- countElement.classList.remove('govuk-textarea--error');
160
- countMessage.classList.remove('govuk-error-message');
161
- countMessage.classList.add('govuk-hint');
173
+ $textarea.classList.remove('govuk-textarea--error');
174
+ $visibleCountMessage.classList.remove('govuk-error-message');
175
+ $visibleCountMessage.classList.add('govuk-hint');
162
176
  }
163
177
 
164
178
  // Update message
179
+ $visibleCountMessage.innerHTML = this.formattedUpdateMessage();
180
+ };
181
+
182
+ // Update screen reader-specific counter
183
+ CharacterCount.prototype.updateScreenReaderCountMessage = function () {
184
+ var $screenReaderCountMessage = this.$screenReaderCountMessage;
185
+
186
+ // If over the threshold, remove the aria-hidden attribute, allowing screen
187
+ // readers to announce the content of the element.
188
+ if (this.isOverThreshold()) {
189
+ $screenReaderCountMessage.removeAttribute('aria-hidden');
190
+ } else {
191
+ $screenReaderCountMessage.setAttribute('aria-hidden', true);
192
+ }
193
+
194
+ // Update message
195
+ $screenReaderCountMessage.innerHTML = this.formattedUpdateMessage();
196
+ };
197
+
198
+ // Format update message
199
+ CharacterCount.prototype.formattedUpdateMessage = function () {
200
+ var $textarea = this.$textarea;
201
+ var options = this.options;
202
+ var remainingNumber = this.maxLength - this.count($textarea.value);
203
+
165
204
  var isWelsh = this.options.language === 'cy';
166
205
  var charVerb;
167
206
  var charNoun;
@@ -188,12 +227,44 @@ CharacterCount.prototype.updateCountMessage = function () {
188
227
 
189
228
  const displayNumber = Math.abs(remainingNumber);
190
229
 
191
- countMessage.innerHTML = charStartMessage + displayNumber + ' ' + charNoun + ' ' + charVerb;
230
+ return charStartMessage + displayNumber + ' ' + charNoun + ' ' + charVerb;
231
+ };
232
+
233
+ // Checks whether the value is over the configured threshold for the input.
234
+ // If there is no configured threshold, it is set to 0 and this function will
235
+ // always return true.
236
+ CharacterCount.prototype.isOverThreshold = function () {
237
+ var $textarea = this.$textarea;
238
+ var options = this.options;
239
+
240
+ // Determine the remaining number of characters/words
241
+ var currentLength = this.count($textarea.value);
242
+ var maxLength = this.maxLength;
243
+
244
+ // Set threshold if presented in options
245
+ var thresholdPercent = options.threshold ? options.threshold : 0;
246
+ var thresholdValue = maxLength * thresholdPercent / 100;
247
+
248
+ return (thresholdValue <= currentLength)
249
+ };
250
+
251
+ // Update the visible character counter and keep track of when the last update
252
+ // happened for each keypress
253
+ CharacterCount.prototype.handleKeyUp = function () {
254
+ this.updateVisibleCountMessage();
255
+ this.lastInputTimestamp = Date.now();
192
256
  };
193
257
 
194
258
  CharacterCount.prototype.handleFocus = function () {
195
- // Check if value changed on focus
196
- this.valueChecker = setInterval(this.checkIfValueChanged.bind(this), 1000);
259
+ // If the field is focused, and a keyup event hasn't been detected for at
260
+ // least 1000 ms (1 second), then run the manual change check.
261
+ // This is so that the update triggered by the manual comparison doesn't
262
+ // conflict with debounced KeyboardEvent updates.
263
+ this.valueChecker = setInterval(function () {
264
+ if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
265
+ this.checkIfValueChanged();
266
+ }
267
+ }.bind(this), 1000);
197
268
  };
198
269
 
199
270
  CharacterCount.prototype.handleBlur = function () {
@@ -31,19 +31,13 @@
31
31
  {{ govukHint({
32
32
  text: 'Gallwch nodi hyd at ' + (params.maxlength or params.maxwords) + (' o eiriau' if params.maxwords else ' o gymeriadau'),
33
33
  id: params.id + '-info',
34
- classes: 'hmrc-character-count__message' + (' ' + params.countMessage.classes if params.countMessage.classes),
35
- attributes: {
36
- 'aria-live': 'polite'
37
- }
34
+ classes: 'hmrc-character-count__message' + (' ' + params.countMessage.classes if params.countMessage.classes)
38
35
  }) }}
39
36
  {% else %}
40
37
  {{ govukHint({
41
38
  text: 'You can enter up to ' + (params.maxlength or params.maxwords) + (' words' if params.maxwords else ' characters'),
42
39
  id: params.id + '-info',
43
- classes: 'hmrc-character-count__message' + (' ' + params.countMessage.classes if params.countMessage.classes),
44
- attributes: {
45
- 'aria-live': 'polite'
46
- }
40
+ classes: 'hmrc-character-count__message' + (' ' + params.countMessage.classes if params.countMessage.classes)
47
41
  }) }}
48
42
  {% endif %}
49
43
  </div>