govuk_tech_docs 6.5.2.1 → 6.6.0.rc.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/publish-gem.yaml +1 -1
  3. data/lib/assets/stylesheets/_govuk_tech_docs.scss +39 -1
  4. data/lib/govuk_tech_docs/version.rb +1 -1
  5. data/node_modules/govuk-frontend/dist/govuk/all.bundle.js +202 -137
  6. data/node_modules/govuk-frontend/dist/govuk/all.bundle.mjs +202 -137
  7. data/node_modules/govuk-frontend/dist/govuk/common/configuration.mjs +5 -1
  8. data/node_modules/govuk-frontend/dist/govuk/common/create-element.mjs +13 -0
  9. data/node_modules/govuk-frontend/dist/govuk/common/govuk-frontend-version.mjs +1 -1
  10. data/node_modules/govuk-frontend/dist/govuk/component.mjs +2 -1
  11. data/node_modules/govuk-frontend/dist/govuk/components/_index.import.scss +2 -0
  12. data/node_modules/govuk-frontend/dist/govuk/components/_index.scss +2 -0
  13. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js +87 -62
  14. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs +87 -62
  15. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.mjs +60 -56
  16. data/node_modules/govuk-frontend/dist/govuk/components/button/button.bundle.js +17 -6
  17. data/node_modules/govuk-frontend/dist/govuk/components/button/button.bundle.mjs +17 -6
  18. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js +90 -32
  19. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs +90 -32
  20. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.mjs +64 -27
  21. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js +12 -5
  22. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs +12 -5
  23. data/node_modules/govuk-frontend/dist/govuk/components/cookie-banner/_mixin.scss +6 -0
  24. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js +17 -6
  25. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs +17 -6
  26. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js +43 -18
  27. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs +43 -18
  28. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs +16 -12
  29. data/node_modules/govuk-frontend/dist/govuk/components/feedback/_feedback.import.scss +6 -0
  30. data/node_modules/govuk-frontend/dist/govuk/components/feedback/_index.import.scss +8 -0
  31. data/node_modules/govuk-frontend/dist/govuk/components/feedback/_index.scss +5 -0
  32. data/node_modules/govuk-frontend/dist/govuk/components/feedback/_mixin.scss +48 -0
  33. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js +54 -30
  34. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs +54 -30
  35. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs +27 -24
  36. data/node_modules/govuk-frontend/dist/govuk/components/footer/_mixin.scss +6 -0
  37. data/node_modules/govuk-frontend/dist/govuk/components/language-navigation/_index.import.scss +8 -0
  38. data/node_modules/govuk-frontend/dist/govuk/components/language-navigation/_index.scss +6 -0
  39. data/node_modules/govuk-frontend/dist/govuk/components/language-navigation/_language-navigation.import.scss +6 -0
  40. data/node_modules/govuk-frontend/dist/govuk/components/language-navigation/_mixin.scss +68 -0
  41. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js +17 -6
  42. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs +17 -6
  43. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js +32 -9
  44. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs +32 -9
  45. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.mjs +5 -3
  46. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.bundle.js +12 -5
  47. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs +12 -5
  48. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/_mixin.scss +48 -3
  49. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js +12 -5
  50. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs +12 -5
  51. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js +14 -6
  52. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs +14 -6
  53. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs +2 -1
  54. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js +12 -5
  55. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs +12 -5
  56. data/node_modules/govuk-frontend/dist/govuk/custom-properties/_frontend-version.mixin.scss +1 -1
  57. data/node_modules/govuk-frontend/dist/govuk/custom-properties/_functional-colours.mixin.scss +1 -0
  58. data/node_modules/govuk-frontend/dist/govuk/errors/index.mjs +8 -5
  59. data/node_modules/govuk-frontend/dist/govuk/govuk-frontend.min.js +1 -1
  60. data/node_modules/govuk-frontend/dist/govuk/helpers/_colour.scss +1 -1
  61. data/node_modules/govuk-frontend/dist/govuk/init.mjs +8 -8
  62. data/node_modules/govuk-frontend/dist/govuk/settings/_colours-organisations.scss +16 -2
  63. data/package-lock.json +4 -4
  64. data/package.json +1 -1
  65. metadata +10 -1
@@ -28,6 +28,9 @@ function isArray(option) {
28
28
  function isObject(option) {
29
29
  return !!option && typeof option === 'object' && !isArray(option);
30
30
  }
31
+ function isScope($scope) {
32
+ return !!$scope && ($scope instanceof Element || $scope instanceof Document);
33
+ }
31
34
  function formatErrorMessage(Component, message) {
32
35
  return `${Component.moduleName}: ${message}`;
33
36
  }
@@ -46,11 +49,14 @@ class SupportError extends GOVUKFrontendError {
46
49
  /**
47
50
  * Checks if GOV.UK Frontend is supported on this page
48
51
  *
49
- * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
52
+ * @param {Element | Document | null | string} [scopeOrMessage] - HTML element `<body>` checked for browser support or support error message
50
53
  */
51
- constructor($scope = document.body) {
52
- const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
53
- super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
54
+ constructor(scopeOrMessage = document.body) {
55
+ let supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
56
+ if (!isScope(scopeOrMessage)) {
57
+ supportMessage = 'GOV.UK Frontend initialised without `<script type="module">`';
58
+ }
59
+ super(typeof scopeOrMessage === 'string' ? scopeOrMessage : supportMessage);
54
60
  this.name = 'SupportError';
55
61
  }
56
62
  }
@@ -91,6 +97,7 @@ class InitError extends GOVUKFrontendError {
91
97
  * @import { ComponentWithModuleName } from '../common/index.mjs'
92
98
  */
93
99
 
100
+ const _self = typeof globalThis !== 'undefined' ? globalThis : self;
94
101
  class Component {
95
102
  /**
96
103
  * Returns the root element of the component
@@ -144,7 +151,7 @@ class Component {
144
151
  /**
145
152
  * @typedef {typeof Component & ChildClass} ChildClassConstructor
146
153
  */
147
- Component.elementType = HTMLElement;
154
+ Component.elementType = _self.HTMLElement;
148
155
 
149
156
  const configOverride = Symbol.for('configOverride');
150
157
  class ConfigurableComponent extends Component {
@@ -169,10 +176,14 @@ class ConfigurableComponent extends Component {
169
176
  throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
170
177
  }
171
178
  const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
172
- this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
179
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {});
180
+ this._config = mergeConfigs(this._config, this[configOverride](datasetConfig), datasetConfig);
173
181
  }
174
182
  }
175
183
  function normaliseString(value, property) {
184
+ if (property != null && property.type && !['string', 'number', 'boolean'].includes(property.type)) {
185
+ return;
186
+ }
176
187
  const trimmedValue = value ? value.trim() : '';
177
188
  let output;
178
189
  let outputType = property == null ? void 0 : property.type;
@@ -312,6 +323,17 @@ function extractConfigByNamespace(schema, dataset, namespace) {
312
323
  * @import { CompatibleClass, Config, CreateAllOptions, OnErrorCallback } from '../init.mjs'
313
324
  */
314
325
 
326
+ function createElement(tagName, attributes = {}, children = []) {
327
+ const $element = document.createElement(tagName);
328
+ Object.entries(attributes).forEach(([name, value]) => {
329
+ $element.setAttribute(name, value);
330
+ });
331
+ for (const child of children) {
332
+ $element.append(child);
333
+ }
334
+ return $element;
335
+ }
336
+
315
337
  class I18n {
316
338
  constructor(translations = {}, config = {}) {
317
339
  var _config$locale;
@@ -402,6 +424,14 @@ class CharacterCount extends ConfigurableComponent {
402
424
  maxwords: undefined
403
425
  };
404
426
  }
427
+ if ('maxwords' in datasetConfig || this.config.maxwords !== undefined) {
428
+ console.warn(formatErrorMessage(CharacterCount, 'Option `maxwords` is deprecated. Use `maxlength` with `countType: "words"` instead.'));
429
+ if (!('maxlength' in datasetConfig)) {
430
+ var _datasetConfig$maxwor;
431
+ configOverrides.maxlength = (_datasetConfig$maxwor = datasetConfig.maxwords) != null ? _datasetConfig$maxwor : this.config.maxwords;
432
+ configOverrides.countType = 'words';
433
+ }
434
+ }
405
435
  return configOverrides;
406
436
  }
407
437
 
@@ -410,10 +440,11 @@ class CharacterCount extends ConfigurableComponent {
410
440
  * @param {CharacterCountConfig} [config] - Character count config
411
441
  */
412
442
  constructor($root, config = {}) {
413
- var _ref, _this$config$maxwords;
443
+ var _this$config$maxlengt;
414
444
  super($root, config);
415
445
  this.$textarea = void 0;
416
446
  this.count = 0;
447
+ this.segmenter = null;
417
448
  this.$visibleCountMessage = void 0;
418
449
  this.$screenReaderCountMessage = void 0;
419
450
  this.lastInputTimestamp = null;
@@ -437,7 +468,15 @@ class CharacterCount extends ConfigurableComponent {
437
468
  this.i18n = new I18n(this.config.i18n, {
438
469
  locale: closestAttributeValue(this.$root, 'lang')
439
470
  });
440
- this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
471
+ if (this.config.countType === 'characters' || this.config.countType === 'words' && this.config.maxwords === undefined) {
472
+ if (!('Segmenter' in Intl)) {
473
+ throw new SupportError(formatErrorMessage(CharacterCount, 'Support for "Intl.Segmenter" required'));
474
+ }
475
+ this.segmenter = new Intl.Segmenter(this.i18n.locale, {
476
+ granularity: this.config.countType === 'words' ? 'word' : 'grapheme'
477
+ });
478
+ }
479
+ this.maxLength = (_this$config$maxlengt = this.config.maxlength) != null ? _this$config$maxlengt : Infinity;
441
480
  this.$textarea = $textarea;
442
481
  const textareaDescriptionId = `${this.$textarea.id}-info`;
443
482
  const $textareaDescription = document.getElementById(textareaDescriptionId);
@@ -455,15 +494,16 @@ class CharacterCount extends ConfigurableComponent {
455
494
  });
456
495
  }
457
496
  this.$textarea.insertAdjacentElement('afterend', $textareaDescription);
458
- const $screenReaderCountMessage = document.createElement('div');
459
- $screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden';
460
- $screenReaderCountMessage.setAttribute('aria-live', 'polite');
497
+ const $screenReaderCountMessage = createElement('div', {
498
+ class: 'govuk-character-count__sr-status govuk-visually-hidden',
499
+ 'aria-live': 'polite'
500
+ });
461
501
  this.$screenReaderCountMessage = $screenReaderCountMessage;
462
502
  $textareaDescription.insertAdjacentElement('afterend', $screenReaderCountMessage);
463
- const $visibleCountMessage = document.createElement('div');
464
- $visibleCountMessage.className = $textareaDescription.className;
465
- $visibleCountMessage.classList.add('govuk-character-count__status');
466
- $visibleCountMessage.setAttribute('aria-hidden', 'true');
503
+ const $visibleCountMessage = createElement('div', {
504
+ class: `${$textareaDescription.className} govuk-character-count__status`,
505
+ 'aria-hidden': 'true'
506
+ });
467
507
  this.$visibleCountMessage = $visibleCountMessage;
468
508
  $textareaDescription.insertAdjacentElement('afterend', $visibleCountMessage);
469
509
  $textareaDescription.classList.add('govuk-visually-hidden');
@@ -531,26 +571,38 @@ class CharacterCount extends ConfigurableComponent {
531
571
  }
532
572
  updateCount() {
533
573
  const text = this.$textarea.value;
534
- if (this.config.maxwords) {
535
- var _text$match;
536
- const tokens = (_text$match = text.match(/\S+/g)) != null ? _text$match : [];
537
- this.count = tokens.length;
574
+ if (this.config.countType === 'characters') {
575
+ this.count = this.segmenter ? Array.from(this.segmenter.segment(text)).length : 0;
576
+ return;
577
+ }
578
+ if (this.config.countType === 'words') {
579
+ if (this.config.maxwords !== undefined) {
580
+ var _text$match$length, _text$match;
581
+ this.count = (_text$match$length = (_text$match = text.match(/\S+/g)) == null ? void 0 : _text$match.length) != null ? _text$match$length : 0;
582
+ return;
583
+ }
584
+ const segments = this.segmenter ? Array.from(this.segmenter.segment(text)) : [];
585
+ this.count = segments.filter(segment => segment.isWordLike).length;
538
586
  return;
539
587
  }
540
588
  this.count = text.length;
541
589
  }
542
590
  getCountMessage() {
543
591
  const remainingNumber = this.maxLength - this.count;
544
- const countType = this.config.maxwords ? 'words' : 'characters';
545
- return this.formatCountMessage(remainingNumber, countType);
592
+ return this.formatCountMessage(remainingNumber);
546
593
  }
547
594
  formatCountMessage(remainingNumber, countType) {
595
+ countType = countType != null ? countType : this.config.countType;
596
+ let translationKeyPrefix = 'characters';
597
+ let translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit';
598
+ if (countType === 'words') {
599
+ translationKeyPrefix = 'words';
600
+ }
548
601
  if (remainingNumber === 0) {
549
- return this.i18n.t(`${countType}AtLimit`);
602
+ translationKeySuffix = 'AtLimit';
550
603
  }
551
- const translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit';
552
- return this.i18n.t(`${countType}${translationKeySuffix}`, {
553
- count: Math.abs(remainingNumber)
604
+ return this.i18n.t(`${translationKeyPrefix}${translationKeySuffix}`, {
605
+ count: remainingNumber === 0 ? undefined : Math.abs(remainingNumber)
554
606
  });
555
607
  }
556
608
  isOverThreshold() {
@@ -569,13 +621,15 @@ class CharacterCount extends ConfigurableComponent {
569
621
  *
570
622
  * @see {@link CharacterCount.defaults}
571
623
  * @typedef {object} CharacterCountConfig
572
- * @property {number} [maxlength] - The maximum number of characters.
573
- * If maxwords is provided, the maxlength option will be ignored.
574
- * @property {number} [maxwords] - The maximum number of words. If maxwords is
575
- * provided, the maxlength option will be ignored.
624
+ * @property {number} [maxlength] - The maximum number of characters (or words
625
+ * if `countType` is set to `"words"`).
626
+ * @property {number} [maxwords] - Deprecated. Use `maxlength` and
627
+ * `countType: "words"` instead.
576
628
  * @property {number} [threshold=0] - The percentage value of the limit at
577
629
  * which point the count message is displayed. If this attribute is set, the
578
630
  * count message will be hidden by default.
631
+ * @property {'characters' | 'length' | 'words'} [countType] - The count type
632
+ * (`"characters"`, `"length"` or `"words"`) used to count the text.
579
633
  * @property {CharacterCountTranslations} [i18n=CharacterCount.defaults.i18n] - Character count translations
580
634
  */
581
635
 
@@ -621,9 +675,9 @@ class CharacterCount extends ConfigurableComponent {
621
675
  * @property {TranslationPluralForms} [textareaDescription] - Message made
622
676
  * available to assistive technologies, if none is already present in the
623
677
  * HTML, to describe that the component accepts only a limited amount of
624
- * content. It is visible on the page when JavaScript is unavailable. The
625
- * component will replace the `%{count}` placeholder with the value of the
626
- * `maxlength` or `maxwords` parameter.
678
+ * content. It is visible on the page if `countType` is not supported or
679
+ * JavaScript is unavailable. The component will replace the `%{count}`
680
+ * placeholder with the value of the `maxlength` parameter.
627
681
  */
628
682
 
629
683
  /**
@@ -633,6 +687,7 @@ class CharacterCount extends ConfigurableComponent {
633
687
  CharacterCount.moduleName = 'govuk-character-count';
634
688
  CharacterCount.defaults = Object.freeze({
635
689
  threshold: 0,
690
+ countType: 'length',
636
691
  i18n: {
637
692
  charactersUnderLimit: {
638
693
  one: 'You have %{count} character remaining',
@@ -670,6 +725,9 @@ CharacterCount.schema = Object.freeze({
670
725
  },
671
726
  threshold: {
672
727
  type: 'number'
728
+ },
729
+ countType: {
730
+ type: 'string'
673
731
  }
674
732
  },
675
733
  anyOf: [{
@@ -1,7 +1,8 @@
1
1
  import { closestAttributeValue } from '../../common/closest-attribute-value.mjs';
2
2
  import { ConfigurableComponent, configOverride, validateConfig } from '../../common/configuration.mjs';
3
+ import { createElement } from '../../common/create-element.mjs';
3
4
  import { formatErrorMessage } from '../../common/index.mjs';
4
- import { ElementError, ConfigError } from '../../errors/index.mjs';
5
+ import { ElementError, ConfigError, SupportError } from '../../errors/index.mjs';
5
6
  import { I18n } from '../../i18n.mjs';
6
7
 
7
8
  /**
@@ -26,6 +27,14 @@ class CharacterCount extends ConfigurableComponent {
26
27
  maxwords: undefined
27
28
  };
28
29
  }
30
+ if ('maxwords' in datasetConfig || this.config.maxwords !== undefined) {
31
+ console.warn(formatErrorMessage(CharacterCount, 'Option `maxwords` is deprecated. Use `maxlength` with `countType: "words"` instead.'));
32
+ if (!('maxlength' in datasetConfig)) {
33
+ var _datasetConfig$maxwor;
34
+ configOverrides.maxlength = (_datasetConfig$maxwor = datasetConfig.maxwords) != null ? _datasetConfig$maxwor : this.config.maxwords;
35
+ configOverrides.countType = 'words';
36
+ }
37
+ }
29
38
  return configOverrides;
30
39
  }
31
40
 
@@ -34,10 +43,11 @@ class CharacterCount extends ConfigurableComponent {
34
43
  * @param {CharacterCountConfig} [config] - Character count config
35
44
  */
36
45
  constructor($root, config = {}) {
37
- var _ref, _this$config$maxwords;
46
+ var _this$config$maxlengt;
38
47
  super($root, config);
39
48
  this.$textarea = void 0;
40
49
  this.count = 0;
50
+ this.segmenter = null;
41
51
  this.$visibleCountMessage = void 0;
42
52
  this.$screenReaderCountMessage = void 0;
43
53
  this.lastInputTimestamp = null;
@@ -61,7 +71,15 @@ class CharacterCount extends ConfigurableComponent {
61
71
  this.i18n = new I18n(this.config.i18n, {
62
72
  locale: closestAttributeValue(this.$root, 'lang')
63
73
  });
64
- this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
74
+ if (this.config.countType === 'characters' || this.config.countType === 'words' && this.config.maxwords === undefined) {
75
+ if (!('Segmenter' in Intl)) {
76
+ throw new SupportError(formatErrorMessage(CharacterCount, 'Support for "Intl.Segmenter" required'));
77
+ }
78
+ this.segmenter = new Intl.Segmenter(this.i18n.locale, {
79
+ granularity: this.config.countType === 'words' ? 'word' : 'grapheme'
80
+ });
81
+ }
82
+ this.maxLength = (_this$config$maxlengt = this.config.maxlength) != null ? _this$config$maxlengt : Infinity;
65
83
  this.$textarea = $textarea;
66
84
  const textareaDescriptionId = `${this.$textarea.id}-info`;
67
85
  const $textareaDescription = document.getElementById(textareaDescriptionId);
@@ -79,15 +97,16 @@ class CharacterCount extends ConfigurableComponent {
79
97
  });
80
98
  }
81
99
  this.$textarea.insertAdjacentElement('afterend', $textareaDescription);
82
- const $screenReaderCountMessage = document.createElement('div');
83
- $screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden';
84
- $screenReaderCountMessage.setAttribute('aria-live', 'polite');
100
+ const $screenReaderCountMessage = createElement('div', {
101
+ class: 'govuk-character-count__sr-status govuk-visually-hidden',
102
+ 'aria-live': 'polite'
103
+ });
85
104
  this.$screenReaderCountMessage = $screenReaderCountMessage;
86
105
  $textareaDescription.insertAdjacentElement('afterend', $screenReaderCountMessage);
87
- const $visibleCountMessage = document.createElement('div');
88
- $visibleCountMessage.className = $textareaDescription.className;
89
- $visibleCountMessage.classList.add('govuk-character-count__status');
90
- $visibleCountMessage.setAttribute('aria-hidden', 'true');
106
+ const $visibleCountMessage = createElement('div', {
107
+ class: `${$textareaDescription.className} govuk-character-count__status`,
108
+ 'aria-hidden': 'true'
109
+ });
91
110
  this.$visibleCountMessage = $visibleCountMessage;
92
111
  $textareaDescription.insertAdjacentElement('afterend', $visibleCountMessage);
93
112
  $textareaDescription.classList.add('govuk-visually-hidden');
@@ -155,26 +174,38 @@ class CharacterCount extends ConfigurableComponent {
155
174
  }
156
175
  updateCount() {
157
176
  const text = this.$textarea.value;
158
- if (this.config.maxwords) {
159
- var _text$match;
160
- const tokens = (_text$match = text.match(/\S+/g)) != null ? _text$match : [];
161
- this.count = tokens.length;
177
+ if (this.config.countType === 'characters') {
178
+ this.count = this.segmenter ? Array.from(this.segmenter.segment(text)).length : 0;
179
+ return;
180
+ }
181
+ if (this.config.countType === 'words') {
182
+ if (this.config.maxwords !== undefined) {
183
+ var _text$match$length, _text$match;
184
+ this.count = (_text$match$length = (_text$match = text.match(/\S+/g)) == null ? void 0 : _text$match.length) != null ? _text$match$length : 0;
185
+ return;
186
+ }
187
+ const segments = this.segmenter ? Array.from(this.segmenter.segment(text)) : [];
188
+ this.count = segments.filter(segment => segment.isWordLike).length;
162
189
  return;
163
190
  }
164
191
  this.count = text.length;
165
192
  }
166
193
  getCountMessage() {
167
194
  const remainingNumber = this.maxLength - this.count;
168
- const countType = this.config.maxwords ? 'words' : 'characters';
169
- return this.formatCountMessage(remainingNumber, countType);
195
+ return this.formatCountMessage(remainingNumber);
170
196
  }
171
197
  formatCountMessage(remainingNumber, countType) {
198
+ countType = countType != null ? countType : this.config.countType;
199
+ let translationKeyPrefix = 'characters';
200
+ let translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit';
201
+ if (countType === 'words') {
202
+ translationKeyPrefix = 'words';
203
+ }
172
204
  if (remainingNumber === 0) {
173
- return this.i18n.t(`${countType}AtLimit`);
205
+ translationKeySuffix = 'AtLimit';
174
206
  }
175
- const translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit';
176
- return this.i18n.t(`${countType}${translationKeySuffix}`, {
177
- count: Math.abs(remainingNumber)
207
+ return this.i18n.t(`${translationKeyPrefix}${translationKeySuffix}`, {
208
+ count: remainingNumber === 0 ? undefined : Math.abs(remainingNumber)
178
209
  });
179
210
  }
180
211
  isOverThreshold() {
@@ -193,13 +224,15 @@ class CharacterCount extends ConfigurableComponent {
193
224
  *
194
225
  * @see {@link CharacterCount.defaults}
195
226
  * @typedef {object} CharacterCountConfig
196
- * @property {number} [maxlength] - The maximum number of characters.
197
- * If maxwords is provided, the maxlength option will be ignored.
198
- * @property {number} [maxwords] - The maximum number of words. If maxwords is
199
- * provided, the maxlength option will be ignored.
227
+ * @property {number} [maxlength] - The maximum number of characters (or words
228
+ * if `countType` is set to `"words"`).
229
+ * @property {number} [maxwords] - Deprecated. Use `maxlength` and
230
+ * `countType: "words"` instead.
200
231
  * @property {number} [threshold=0] - The percentage value of the limit at
201
232
  * which point the count message is displayed. If this attribute is set, the
202
233
  * count message will be hidden by default.
234
+ * @property {'characters' | 'length' | 'words'} [countType] - The count type
235
+ * (`"characters"`, `"length"` or `"words"`) used to count the text.
203
236
  * @property {CharacterCountTranslations} [i18n=CharacterCount.defaults.i18n] - Character count translations
204
237
  */
205
238
 
@@ -245,9 +278,9 @@ class CharacterCount extends ConfigurableComponent {
245
278
  * @property {TranslationPluralForms} [textareaDescription] - Message made
246
279
  * available to assistive technologies, if none is already present in the
247
280
  * HTML, to describe that the component accepts only a limited amount of
248
- * content. It is visible on the page when JavaScript is unavailable. The
249
- * component will replace the `%{count}` placeholder with the value of the
250
- * `maxlength` or `maxwords` parameter.
281
+ * content. It is visible on the page if `countType` is not supported or
282
+ * JavaScript is unavailable. The component will replace the `%{count}`
283
+ * placeholder with the value of the `maxlength` parameter.
251
284
  */
252
285
 
253
286
  /**
@@ -257,6 +290,7 @@ class CharacterCount extends ConfigurableComponent {
257
290
  CharacterCount.moduleName = 'govuk-character-count';
258
291
  CharacterCount.defaults = Object.freeze({
259
292
  threshold: 0,
293
+ countType: 'length',
260
294
  i18n: {
261
295
  charactersUnderLimit: {
262
296
  one: 'You have %{count} character remaining',
@@ -294,6 +328,9 @@ CharacterCount.schema = Object.freeze({
294
328
  },
295
329
  threshold: {
296
330
  type: 'number'
331
+ },
332
+ countType: {
333
+ type: 'string'
297
334
  }
298
335
  },
299
336
  anyOf: [{
@@ -29,6 +29,9 @@
29
29
  function isObject(option) {
30
30
  return !!option && typeof option === 'object' && !isArray(option);
31
31
  }
32
+ function isScope($scope) {
33
+ return !!$scope && ($scope instanceof Element || $scope instanceof Document);
34
+ }
32
35
  function formatErrorMessage(Component, message) {
33
36
  return `${Component.moduleName}: ${message}`;
34
37
  }
@@ -47,11 +50,14 @@
47
50
  /**
48
51
  * Checks if GOV.UK Frontend is supported on this page
49
52
  *
50
- * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
53
+ * @param {Element | Document | null | string} [scopeOrMessage] - HTML element `<body>` checked for browser support or support error message
51
54
  */
52
- constructor($scope = document.body) {
53
- const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
54
- super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
55
+ constructor(scopeOrMessage = document.body) {
56
+ let supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
57
+ if (!isScope(scopeOrMessage)) {
58
+ supportMessage = 'GOV.UK Frontend initialised without `<script type="module">`';
59
+ }
60
+ super(typeof scopeOrMessage === 'string' ? scopeOrMessage : supportMessage);
55
61
  this.name = 'SupportError';
56
62
  }
57
63
  }
@@ -86,6 +92,7 @@
86
92
  * @import { ComponentWithModuleName } from '../common/index.mjs'
87
93
  */
88
94
 
95
+ const _self = typeof globalThis !== 'undefined' ? globalThis : self;
89
96
  class Component {
90
97
  /**
91
98
  * Returns the root element of the component
@@ -139,7 +146,7 @@
139
146
  /**
140
147
  * @typedef {typeof Component & ChildClass} ChildClassConstructor
141
148
  */
142
- Component.elementType = HTMLElement;
149
+ Component.elementType = _self.HTMLElement;
143
150
 
144
151
  /**
145
152
  * Checkboxes component
@@ -23,6 +23,9 @@ function isArray(option) {
23
23
  function isObject(option) {
24
24
  return !!option && typeof option === 'object' && !isArray(option);
25
25
  }
26
+ function isScope($scope) {
27
+ return !!$scope && ($scope instanceof Element || $scope instanceof Document);
28
+ }
26
29
  function formatErrorMessage(Component, message) {
27
30
  return `${Component.moduleName}: ${message}`;
28
31
  }
@@ -41,11 +44,14 @@ class SupportError extends GOVUKFrontendError {
41
44
  /**
42
45
  * Checks if GOV.UK Frontend is supported on this page
43
46
  *
44
- * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
47
+ * @param {Element | Document | null | string} [scopeOrMessage] - HTML element `<body>` checked for browser support or support error message
45
48
  */
46
- constructor($scope = document.body) {
47
- const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
48
- super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
49
+ constructor(scopeOrMessage = document.body) {
50
+ let supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
51
+ if (!isScope(scopeOrMessage)) {
52
+ supportMessage = 'GOV.UK Frontend initialised without `<script type="module">`';
53
+ }
54
+ super(typeof scopeOrMessage === 'string' ? scopeOrMessage : supportMessage);
49
55
  this.name = 'SupportError';
50
56
  }
51
57
  }
@@ -80,6 +86,7 @@ class InitError extends GOVUKFrontendError {
80
86
  * @import { ComponentWithModuleName } from '../common/index.mjs'
81
87
  */
82
88
 
89
+ const _self = typeof globalThis !== 'undefined' ? globalThis : self;
83
90
  class Component {
84
91
  /**
85
92
  * Returns the root element of the component
@@ -133,7 +140,7 @@ class Component {
133
140
  /**
134
141
  * @typedef {typeof Component & ChildClass} ChildClassConstructor
135
142
  */
136
- Component.elementType = HTMLElement;
143
+ Component.elementType = _self.HTMLElement;
137
144
 
138
145
  /**
139
146
  * Checkboxes component
@@ -16,6 +16,12 @@
16
16
 
17
17
  color: base.govuk-functional-colour(surface-text);
18
18
  background-color: base.govuk-functional-colour(surface-background);
19
+
20
+ @media print {
21
+ border-bottom-color: currentcolor;
22
+ color: base.govuk-functional-colour(print-text);
23
+ background-color: transparent;
24
+ }
19
25
  }
20
26
 
21
27
  // Support older browsers which don't hide elements with the `hidden` attribute
@@ -53,6 +53,9 @@
53
53
  function isObject(option) {
54
54
  return !!option && typeof option === 'object' && !isArray(option);
55
55
  }
56
+ function isScope($scope) {
57
+ return !!$scope && ($scope instanceof Element || $scope instanceof Document);
58
+ }
56
59
  function formatErrorMessage(Component, message) {
57
60
  return `${Component.moduleName}: ${message}`;
58
61
  }
@@ -71,11 +74,14 @@
71
74
  /**
72
75
  * Checks if GOV.UK Frontend is supported on this page
73
76
  *
74
- * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
77
+ * @param {Element | Document | null | string} [scopeOrMessage] - HTML element `<body>` checked for browser support or support error message
75
78
  */
76
- constructor($scope = document.body) {
77
- const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
78
- super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
79
+ constructor(scopeOrMessage = document.body) {
80
+ let supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
81
+ if (!isScope(scopeOrMessage)) {
82
+ supportMessage = 'GOV.UK Frontend initialised without `<script type="module">`';
83
+ }
84
+ super(typeof scopeOrMessage === 'string' ? scopeOrMessage : supportMessage);
79
85
  this.name = 'SupportError';
80
86
  }
81
87
  }
@@ -116,6 +122,7 @@
116
122
  * @import { ComponentWithModuleName } from '../common/index.mjs'
117
123
  */
118
124
 
125
+ const _self = typeof globalThis !== 'undefined' ? globalThis : self;
119
126
  class Component {
120
127
  /**
121
128
  * Returns the root element of the component
@@ -169,7 +176,7 @@
169
176
  /**
170
177
  * @typedef {typeof Component & ChildClass} ChildClassConstructor
171
178
  */
172
- Component.elementType = HTMLElement;
179
+ Component.elementType = _self.HTMLElement;
173
180
 
174
181
  const configOverride = Symbol.for('configOverride');
175
182
  class ConfigurableComponent extends Component {
@@ -194,10 +201,14 @@
194
201
  throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
195
202
  }
196
203
  const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
197
- this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
204
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {});
205
+ this._config = mergeConfigs(this._config, this[configOverride](datasetConfig), datasetConfig);
198
206
  }
199
207
  }
200
208
  function normaliseString(value, property) {
209
+ if (property != null && property.type && !['string', 'number', 'boolean'].includes(property.type)) {
210
+ return;
211
+ }
201
212
  const trimmedValue = value ? value.trim() : '';
202
213
  let output;
203
214
  let outputType = property == null ? void 0 : property.type;
@@ -47,6 +47,9 @@ function isArray(option) {
47
47
  function isObject(option) {
48
48
  return !!option && typeof option === 'object' && !isArray(option);
49
49
  }
50
+ function isScope($scope) {
51
+ return !!$scope && ($scope instanceof Element || $scope instanceof Document);
52
+ }
50
53
  function formatErrorMessage(Component, message) {
51
54
  return `${Component.moduleName}: ${message}`;
52
55
  }
@@ -65,11 +68,14 @@ class SupportError extends GOVUKFrontendError {
65
68
  /**
66
69
  * Checks if GOV.UK Frontend is supported on this page
67
70
  *
68
- * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
71
+ * @param {Element | Document | null | string} [scopeOrMessage] - HTML element `<body>` checked for browser support or support error message
69
72
  */
70
- constructor($scope = document.body) {
71
- const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
72
- super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
73
+ constructor(scopeOrMessage = document.body) {
74
+ let supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
75
+ if (!isScope(scopeOrMessage)) {
76
+ supportMessage = 'GOV.UK Frontend initialised without `<script type="module">`';
77
+ }
78
+ super(typeof scopeOrMessage === 'string' ? scopeOrMessage : supportMessage);
73
79
  this.name = 'SupportError';
74
80
  }
75
81
  }
@@ -110,6 +116,7 @@ class InitError extends GOVUKFrontendError {
110
116
  * @import { ComponentWithModuleName } from '../common/index.mjs'
111
117
  */
112
118
 
119
+ const _self = typeof globalThis !== 'undefined' ? globalThis : self;
113
120
  class Component {
114
121
  /**
115
122
  * Returns the root element of the component
@@ -163,7 +170,7 @@ class Component {
163
170
  /**
164
171
  * @typedef {typeof Component & ChildClass} ChildClassConstructor
165
172
  */
166
- Component.elementType = HTMLElement;
173
+ Component.elementType = _self.HTMLElement;
167
174
 
168
175
  const configOverride = Symbol.for('configOverride');
169
176
  class ConfigurableComponent extends Component {
@@ -188,10 +195,14 @@ class ConfigurableComponent extends Component {
188
195
  throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
189
196
  }
190
197
  const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
191
- this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
198
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {});
199
+ this._config = mergeConfigs(this._config, this[configOverride](datasetConfig), datasetConfig);
192
200
  }
193
201
  }
194
202
  function normaliseString(value, property) {
203
+ if (property != null && property.type && !['string', 'number', 'boolean'].includes(property.type)) {
204
+ return;
205
+ }
195
206
  const trimmedValue = value ? value.trim() : '';
196
207
  let output;
197
208
  let outputType = property == null ? void 0 : property.type;