govuk_publishing_components 29.9.0 → 29.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/govuk_publishing_components/analytics/page-content.js +4 -4
  3. data/app/assets/stylesheets/govuk_publishing_components/components/_contextual-sidebar.scss +20 -0
  4. data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +3 -8
  5. data/app/assets/stylesheets/govuk_publishing_components/components/govspeak/_attachment.scss +7 -1
  6. data/app/controllers/govuk_publishing_components/audit_controller.rb +3 -2
  7. data/app/controllers/govuk_publishing_components/component_guide_controller.rb +0 -9
  8. data/app/models/govuk_publishing_components/audit_comparer.rb +92 -34
  9. data/app/views/govuk_publishing_components/audit/_applications.html.erb +20 -9
  10. data/app/views/govuk_publishing_components/component_guide/index.html.erb +1 -19
  11. data/app/views/govuk_publishing_components/components/_layout_footer.html.erb +20 -2
  12. data/app/views/govuk_publishing_components/components/contextual_sidebar/_ukraine_cta.html.erb +18 -19
  13. data/config/locales/ar.yml +1 -2
  14. data/config/locales/az.yml +1 -2
  15. data/config/locales/be.yml +1 -2
  16. data/config/locales/bg.yml +1 -2
  17. data/config/locales/bn.yml +1 -2
  18. data/config/locales/cs.yml +1 -2
  19. data/config/locales/cy.yml +1 -2
  20. data/config/locales/da.yml +1 -2
  21. data/config/locales/de.yml +1 -2
  22. data/config/locales/dr.yml +1 -2
  23. data/config/locales/el.yml +1 -2
  24. data/config/locales/en.yml +9 -2
  25. data/config/locales/es-419.yml +1 -2
  26. data/config/locales/es.yml +1 -2
  27. data/config/locales/et.yml +1 -2
  28. data/config/locales/fa.yml +1 -2
  29. data/config/locales/fi.yml +1 -2
  30. data/config/locales/fr.yml +1 -2
  31. data/config/locales/gd.yml +1 -2
  32. data/config/locales/gu.yml +1 -2
  33. data/config/locales/he.yml +1 -2
  34. data/config/locales/hi.yml +1 -2
  35. data/config/locales/hr.yml +1 -2
  36. data/config/locales/hu.yml +1 -2
  37. data/config/locales/hy.yml +1 -2
  38. data/config/locales/id.yml +1 -2
  39. data/config/locales/is.yml +1 -2
  40. data/config/locales/it.yml +1 -2
  41. data/config/locales/ja.yml +1 -2
  42. data/config/locales/ka.yml +1 -2
  43. data/config/locales/kk.yml +1 -2
  44. data/config/locales/ko.yml +1 -2
  45. data/config/locales/lt.yml +1 -2
  46. data/config/locales/lv.yml +1 -2
  47. data/config/locales/ms.yml +1 -2
  48. data/config/locales/mt.yml +1 -2
  49. data/config/locales/nl.yml +1 -2
  50. data/config/locales/no.yml +1 -2
  51. data/config/locales/pa-pk.yml +1 -2
  52. data/config/locales/pa.yml +1 -2
  53. data/config/locales/pl.yml +1 -2
  54. data/config/locales/ps.yml +1 -2
  55. data/config/locales/pt.yml +1 -2
  56. data/config/locales/ro.yml +1 -2
  57. data/config/locales/ru.yml +1 -2
  58. data/config/locales/si.yml +1 -2
  59. data/config/locales/sk.yml +1 -2
  60. data/config/locales/sl.yml +1 -2
  61. data/config/locales/so.yml +1 -2
  62. data/config/locales/sq.yml +1 -2
  63. data/config/locales/sr.yml +1 -2
  64. data/config/locales/sv.yml +1 -2
  65. data/config/locales/sw.yml +1 -2
  66. data/config/locales/ta.yml +1 -2
  67. data/config/locales/th.yml +1 -2
  68. data/config/locales/tk.yml +1 -2
  69. data/config/locales/tr.yml +1 -2
  70. data/config/locales/uk.yml +1 -2
  71. data/config/locales/ur.yml +1 -2
  72. data/config/locales/uz.yml +1 -2
  73. data/config/locales/vi.yml +1 -2
  74. data/config/locales/zh-hk.yml +1 -2
  75. data/config/locales/zh-tw.yml +1 -2
  76. data/config/locales/zh.yml +1 -2
  77. data/lib/govuk_publishing_components/presenters/attachment_helper.rb +1 -2
  78. data/lib/govuk_publishing_components/presenters/public_layout_helper.rb +35 -16
  79. data/lib/govuk_publishing_components/version.rb +1 -1
  80. data/node_modules/govuk-frontend/govuk/all.js +120 -49
  81. data/node_modules/govuk-frontend/govuk/components/back-link/macro-options.json +2 -2
  82. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/_index.scss +0 -2
  83. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/macro-options.json +2 -2
  84. data/node_modules/govuk-frontend/govuk/components/button/_index.scss +6 -16
  85. data/node_modules/govuk-frontend/govuk/components/button/macro-options.json +2 -2
  86. data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +120 -49
  87. data/node_modules/govuk-frontend/govuk/components/character-count/fixtures.json +33 -17
  88. data/node_modules/govuk-frontend/govuk/components/character-count/macro-options.json +2 -2
  89. data/node_modules/govuk-frontend/govuk/components/character-count/template.njk +1 -4
  90. data/node_modules/govuk-frontend/govuk/components/checkboxes/_index.scss +3 -2
  91. data/node_modules/govuk-frontend/govuk/components/checkboxes/fixtures.json +22 -10
  92. data/node_modules/govuk-frontend/govuk/components/checkboxes/macro-options.json +2 -2
  93. data/node_modules/govuk-frontend/govuk/components/date-input/fixtures.json +23 -23
  94. data/node_modules/govuk-frontend/govuk/components/date-input/template.njk +1 -1
  95. data/node_modules/govuk-frontend/govuk/components/details/macro-options.json +4 -4
  96. data/node_modules/govuk-frontend/govuk/components/error-message/macro-options.json +2 -2
  97. data/node_modules/govuk-frontend/govuk/components/error-summary/macro-options.json +3 -3
  98. data/node_modules/govuk-frontend/govuk/components/fieldset/macro-options.json +2 -2
  99. data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +12 -22
  100. data/node_modules/govuk-frontend/govuk/components/header/_index.scss +13 -3
  101. data/node_modules/govuk-frontend/govuk/components/header/macro-options.json +2 -2
  102. data/node_modules/govuk-frontend/govuk/components/hint/macro-options.json +2 -2
  103. data/node_modules/govuk-frontend/govuk/components/input/_index.scss +4 -13
  104. data/node_modules/govuk-frontend/govuk/components/input/macro-options.json +5 -5
  105. data/node_modules/govuk-frontend/govuk/components/inset-text/macro-options.json +2 -2
  106. data/node_modules/govuk-frontend/govuk/components/label/macro-options.json +2 -2
  107. data/node_modules/govuk-frontend/govuk/components/panel/_index.scss +1 -1
  108. data/node_modules/govuk-frontend/govuk/components/panel/macro-options.json +4 -4
  109. data/node_modules/govuk-frontend/govuk/components/phase-banner/macro-options.json +2 -2
  110. data/node_modules/govuk-frontend/govuk/components/radios/_index.scss +5 -4
  111. data/node_modules/govuk-frontend/govuk/components/radios/fixtures.json +17 -12
  112. data/node_modules/govuk-frontend/govuk/components/radios/macro-options.json +2 -2
  113. data/node_modules/govuk-frontend/govuk/components/skip-link/_index.scss +1 -3
  114. data/node_modules/govuk-frontend/govuk/components/skip-link/macro-options.json +2 -2
  115. data/node_modules/govuk-frontend/govuk/components/summary-list/macro-options.json +5 -5
  116. data/node_modules/govuk-frontend/govuk/components/table/macro-options.json +4 -4
  117. data/node_modules/govuk-frontend/govuk/components/tabs/macro-options.json +2 -2
  118. data/node_modules/govuk-frontend/govuk/components/tag/macro-options.json +2 -2
  119. data/node_modules/govuk-frontend/govuk/components/warning-text/macro-options.json +2 -2
  120. data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +3 -3
  121. data/node_modules/govuk-frontend/govuk/helpers/_links.scss +7 -5
  122. data/node_modules/govuk-frontend/govuk/helpers/_media-queries.scss +2 -2
  123. data/node_modules/govuk-frontend/govuk/helpers/_shape-arrow.scss +1 -1
  124. data/node_modules/govuk-frontend/govuk/helpers/_spacing.scss +3 -3
  125. data/node_modules/govuk-frontend/govuk/helpers/_typography.scss +2 -2
  126. data/node_modules/govuk-frontend/govuk/objects/_button-group.scss +10 -26
  127. data/node_modules/govuk-frontend/govuk/objects/_template.scss +1 -1
  128. data/node_modules/govuk-frontend/govuk/objects/_width-container.scss +0 -4
  129. data/node_modules/govuk-frontend/govuk/tools/_exports.scss +1 -1
  130. data/node_modules/govuk-frontend/govuk/tools/_font-url.scss +1 -1
  131. data/node_modules/govuk-frontend/govuk/tools/_image-url.scss +1 -1
  132. data/node_modules/govuk-frontend/govuk/tools/_px-to-em.scss +2 -2
  133. data/node_modules/govuk-frontend/govuk/tools/_px-to-rem.scss +1 -1
  134. data/node_modules/govuk-frontend/govuk-esm/all.mjs +88 -0
  135. data/node_modules/govuk-frontend/govuk-esm/common.mjs +28 -0
  136. data/node_modules/govuk-frontend/govuk-esm/components/accordion/accordion.mjs +374 -0
  137. data/node_modules/govuk-frontend/govuk-esm/components/button/button.mjs +64 -0
  138. data/node_modules/govuk-frontend/govuk-esm/components/character-count/character-count.mjs +251 -0
  139. data/node_modules/govuk-frontend/govuk-esm/components/checkboxes/checkboxes.mjs +164 -0
  140. data/node_modules/govuk-frontend/govuk-esm/components/details/details.mjs +147 -0
  141. data/node_modules/govuk-frontend/govuk-esm/components/error-summary/error-summary.mjs +168 -0
  142. data/node_modules/govuk-frontend/govuk-esm/components/header/header.mjs +52 -0
  143. data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs +55 -0
  144. data/node_modules/govuk-frontend/govuk-esm/components/radios/radios.mjs +122 -0
  145. data/node_modules/govuk-frontend/govuk-esm/components/skip-link/skip-link.mjs +94 -0
  146. data/node_modules/govuk-frontend/govuk-esm/components/tabs/tabs.mjs +282 -0
  147. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/DOMTokenList.js +264 -0
  148. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Document.js +26 -0
  149. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/classList.js +93 -0
  150. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/closest.js +24 -0
  151. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/matches.js +23 -0
  152. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/nextElementSibling.js +22 -0
  153. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/previousElementSibling.js +22 -0
  154. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element.js +114 -0
  155. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Event.js +252 -0
  156. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Function/prototype/bind.js +159 -0
  157. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Object/defineProperty.js +86 -0
  158. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Window.js +20 -0
  159. data/node_modules/govuk-frontend/package.json +8 -1
  160. metadata +27 -2
@@ -2,7 +2,7 @@ module GovukPublishingComponents
2
2
  module Presenters
3
3
  class PublicLayoutHelper
4
4
  FOOTER_NAVIGATION_COLUMNS = [2, 1].freeze
5
-
5
+ FOOTER_TRACK_ACTIONS = %w[topicsLink governmentActivityLink].freeze
6
6
  FOOTER_META = {
7
7
  items: [
8
8
  {
@@ -46,25 +46,44 @@ module GovukPublishingComponents
46
46
  attr_reader :footer_navigation, :footer_meta, :cookie_banner_data
47
47
 
48
48
  def initialize(local_assigns)
49
- @footer_navigation = local_assigns[:footer_navigation] || I18n.t("components.layout_footer.navigation_links").each_with_index.map do |menu, i|
49
+ @footer_navigation = local_assigns[:footer_navigation] || navigation_link_generation_from_locale(I18n.t("components.layout_footer.navigation_links"))
50
+ @footer_meta = local_assigns[:footer_meta] || { items: add_data_attributes_to_links(FOOTER_META[:items], "supportLink") }
51
+ @cookie_banner_data = local_assigns[:cookie_banner_data] || {}
52
+ end
53
+
54
+ def navigation_link_generation_from_locale(links)
55
+ links.each_with_index.map do |menu, i|
50
56
  {
51
57
  title: menu[:title],
52
- columns: FOOTER_NAVIGATION_COLUMNS[i],
53
- items: menu[:menu_contents].map do |item|
54
- item.merge({
55
- attributes: {
56
- data: {
57
- track_category: "footerClicked",
58
- track_action: "footerLinks",
59
- track_label: item[:text],
60
- },
61
- },
62
- })
63
- end,
58
+ columns: footer_navigation_columns[i],
59
+ items: add_data_attributes_to_links(menu[:menu_contents], footer_track_actions[i]),
64
60
  }
65
61
  end
66
- @footer_meta = local_assigns[:footer_meta] || FOOTER_META
67
- @cookie_banner_data = local_assigns[:cookie_banner_data] || {}
62
+ end
63
+
64
+ def footer_navigation_columns
65
+ FOOTER_NAVIGATION_COLUMNS
66
+ end
67
+
68
+ def footer_track_actions
69
+ FOOTER_TRACK_ACTIONS
70
+ end
71
+
72
+ def generate_data_attribute(link, track_action)
73
+ {
74
+ track_category: "footerClicked",
75
+ track_action: track_action,
76
+ track_label: link[:href],
77
+ track_options: {
78
+ dimension29: link[:text],
79
+ },
80
+ }
81
+ end
82
+
83
+ def add_data_attributes_to_links(items, track_action)
84
+ items.map do |item|
85
+ item.deep_merge({ attributes: { data: generate_data_attribute(item, track_action) } })
86
+ end
68
87
  end
69
88
  end
70
89
  end
@@ -1,3 +1,3 @@
1
1
  module GovukPublishingComponents
2
- VERSION = "29.9.0".freeze
2
+ VERSION = "29.10.0".freeze
3
3
  end
@@ -1601,9 +1601,9 @@ Details.prototype.polyfillHandleInputs = function (node, callback) {
1601
1601
  function CharacterCount ($module) {
1602
1602
  this.$module = $module;
1603
1603
  this.$textarea = $module.querySelector('.govuk-js-character-count');
1604
- if (this.$textarea) {
1605
- this.$countMessage = document.getElementById(this.$textarea.id + '-info');
1606
- }
1604
+ this.$visibleCountMessage = null;
1605
+ this.$screenReaderCountMessage = null;
1606
+ this.lastInputTimestamp = null;
1607
1607
  }
1608
1608
 
1609
1609
  CharacterCount.prototype.defaults = {
@@ -1613,18 +1613,39 @@ CharacterCount.prototype.defaults = {
1613
1613
 
1614
1614
  // Initialize component
1615
1615
  CharacterCount.prototype.init = function () {
1616
+ // Check that required elements are present
1617
+ if (!this.$textarea) {
1618
+ return
1619
+ }
1620
+
1616
1621
  // Check for module
1617
1622
  var $module = this.$module;
1618
1623
  var $textarea = this.$textarea;
1619
- var $countMessage = this.$countMessage;
1620
-
1621
- if (!$textarea || !$countMessage) {
1622
- return
1623
- }
1624
+ var $fallbackLimitMessage = document.getElementById($textarea.id + '-info');
1624
1625
 
1625
- // We move count message right after the field
1626
+ // Move the fallback count message to be immediately after the textarea
1626
1627
  // Kept for backwards compatibility
1627
- $textarea.insertAdjacentElement('afterend', $countMessage);
1628
+ $textarea.insertAdjacentElement('afterend', $fallbackLimitMessage);
1629
+
1630
+ // Create the *screen reader* specific live-updating counter
1631
+ // This doesn't need any styling classes, as it is never visible
1632
+ var $screenReaderCountMessage = document.createElement('div');
1633
+ $screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden';
1634
+ $screenReaderCountMessage.setAttribute('aria-live', 'polite');
1635
+ this.$screenReaderCountMessage = $screenReaderCountMessage;
1636
+ $fallbackLimitMessage.insertAdjacentElement('afterend', $screenReaderCountMessage);
1637
+
1638
+ // Create our live-updating counter element, copying the classes from the
1639
+ // fallback element for backwards compatibility as these may have been configured
1640
+ var $visibleCountMessage = document.createElement('div');
1641
+ $visibleCountMessage.className = $fallbackLimitMessage.className;
1642
+ $visibleCountMessage.classList.add('govuk-character-count__status');
1643
+ $visibleCountMessage.setAttribute('aria-hidden', 'true');
1644
+ this.$visibleCountMessage = $visibleCountMessage;
1645
+ $fallbackLimitMessage.insertAdjacentElement('afterend', $visibleCountMessage);
1646
+
1647
+ // Hide the fallback limit message
1648
+ $fallbackLimitMessage.classList.add('govuk-visually-hidden');
1628
1649
 
1629
1650
  // Read options set using dataset ('data-' values)
1630
1651
  this.options = this.getDataset($module);
@@ -1644,23 +1665,19 @@ CharacterCount.prototype.init = function () {
1644
1665
  }
1645
1666
 
1646
1667
  // Remove hard limit if set
1647
- $module.removeAttribute('maxlength');
1668
+ $textarea.removeAttribute('maxlength');
1669
+
1670
+ this.bindChangeEvents();
1648
1671
 
1649
1672
  // When the page is restored after navigating 'back' in some browsers the
1650
1673
  // state of the character count is not restored until *after* the DOMContentLoaded
1651
- // event is fired, so we need to sync after the pageshow event in browsers
1652
- // that support it.
1674
+ // event is fired, so we need to manually update it after the pageshow event
1675
+ // in browsers that support it.
1653
1676
  if ('onpageshow' in window) {
1654
- window.addEventListener('pageshow', this.sync.bind(this));
1677
+ window.addEventListener('pageshow', this.updateCountMessage.bind(this));
1655
1678
  } else {
1656
- window.addEventListener('DOMContentLoaded', this.sync.bind(this));
1679
+ window.addEventListener('DOMContentLoaded', this.updateCountMessage.bind(this));
1657
1680
  }
1658
-
1659
- this.sync();
1660
- };
1661
-
1662
- CharacterCount.prototype.sync = function () {
1663
- this.bindChangeEvents();
1664
1681
  this.updateCountMessage();
1665
1682
  };
1666
1683
 
@@ -1695,7 +1712,7 @@ CharacterCount.prototype.count = function (text) {
1695
1712
  // Bind input propertychange to the elements and update based on the change
1696
1713
  CharacterCount.prototype.bindChangeEvents = function () {
1697
1714
  var $textarea = this.$textarea;
1698
- $textarea.addEventListener('keyup', this.checkIfValueChanged.bind(this));
1715
+ $textarea.addEventListener('keyup', this.handleKeyUp.bind(this));
1699
1716
 
1700
1717
  // Bind focus/blur events to start/stop polling
1701
1718
  $textarea.addEventListener('focus', this.handleFocus.bind(this));
@@ -1713,42 +1730,64 @@ CharacterCount.prototype.checkIfValueChanged = function () {
1713
1730
  }
1714
1731
  };
1715
1732
 
1716
- // Update message box
1733
+ // Helper function to update both the visible and screen reader-specific
1734
+ // counters simultaneously (e.g. on init)
1717
1735
  CharacterCount.prototype.updateCountMessage = function () {
1718
- var countElement = this.$textarea;
1719
- var options = this.options;
1720
- var countMessage = this.$countMessage;
1736
+ this.updateVisibleCountMessage();
1737
+ this.updateScreenReaderCountMessage();
1738
+ };
1721
1739
 
1722
- // Determine the remaining number of characters/words
1723
- var currentLength = this.count(countElement.value);
1724
- var maxLength = this.maxLength;
1725
- var remainingNumber = maxLength - currentLength;
1740
+ // Update visible counter
1741
+ CharacterCount.prototype.updateVisibleCountMessage = function () {
1742
+ var $textarea = this.$textarea;
1743
+ var $visibleCountMessage = this.$visibleCountMessage;
1744
+ var remainingNumber = this.maxLength - this.count($textarea.value);
1726
1745
 
1727
- // Set threshold if presented in options
1728
- var thresholdPercent = options.threshold ? options.threshold : 0;
1729
- var thresholdValue = maxLength * thresholdPercent / 100;
1730
- if (thresholdValue > currentLength) {
1731
- countMessage.classList.add('govuk-character-count__message--disabled');
1732
- // Ensure threshold is hidden for users of assistive technologies
1733
- countMessage.setAttribute('aria-hidden', true);
1746
+ // If input is over the threshold, remove the disabled class which renders the
1747
+ // counter invisible.
1748
+ if (this.isOverThreshold()) {
1749
+ $visibleCountMessage.classList.remove('govuk-character-count__message--disabled');
1734
1750
  } else {
1735
- countMessage.classList.remove('govuk-character-count__message--disabled');
1736
- // Ensure threshold is visible for users of assistive technologies
1737
- countMessage.removeAttribute('aria-hidden');
1751
+ $visibleCountMessage.classList.add('govuk-character-count__message--disabled');
1738
1752
  }
1739
1753
 
1740
1754
  // Update styles
1741
1755
  if (remainingNumber < 0) {
1742
- countElement.classList.add('govuk-textarea--error');
1743
- countMessage.classList.remove('govuk-hint');
1744
- countMessage.classList.add('govuk-error-message');
1756
+ $textarea.classList.add('govuk-textarea--error');
1757
+ $visibleCountMessage.classList.remove('govuk-hint');
1758
+ $visibleCountMessage.classList.add('govuk-error-message');
1759
+ } else {
1760
+ $textarea.classList.remove('govuk-textarea--error');
1761
+ $visibleCountMessage.classList.remove('govuk-error-message');
1762
+ $visibleCountMessage.classList.add('govuk-hint');
1763
+ }
1764
+
1765
+ // Update message
1766
+ $visibleCountMessage.innerHTML = this.formattedUpdateMessage();
1767
+ };
1768
+
1769
+ // Update screen reader-specific counter
1770
+ CharacterCount.prototype.updateScreenReaderCountMessage = function () {
1771
+ var $screenReaderCountMessage = this.$screenReaderCountMessage;
1772
+
1773
+ // If over the threshold, remove the aria-hidden attribute, allowing screen
1774
+ // readers to announce the content of the element.
1775
+ if (this.isOverThreshold()) {
1776
+ $screenReaderCountMessage.removeAttribute('aria-hidden');
1745
1777
  } else {
1746
- countElement.classList.remove('govuk-textarea--error');
1747
- countMessage.classList.remove('govuk-error-message');
1748
- countMessage.classList.add('govuk-hint');
1778
+ $screenReaderCountMessage.setAttribute('aria-hidden', true);
1749
1779
  }
1750
1780
 
1751
1781
  // Update message
1782
+ $screenReaderCountMessage.innerHTML = this.formattedUpdateMessage();
1783
+ };
1784
+
1785
+ // Format update message
1786
+ CharacterCount.prototype.formattedUpdateMessage = function () {
1787
+ var $textarea = this.$textarea;
1788
+ var options = this.options;
1789
+ var remainingNumber = this.maxLength - this.count($textarea.value);
1790
+
1752
1791
  var charVerb = 'remaining';
1753
1792
  var charNoun = 'character';
1754
1793
  var displayNumber = remainingNumber;
@@ -1760,12 +1799,44 @@ CharacterCount.prototype.updateCountMessage = function () {
1760
1799
  charVerb = (remainingNumber < 0) ? 'too many' : 'remaining';
1761
1800
  displayNumber = Math.abs(remainingNumber);
1762
1801
 
1763
- countMessage.innerHTML = 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb;
1802
+ return 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb
1803
+ };
1804
+
1805
+ // Checks whether the value is over the configured threshold for the input.
1806
+ // If there is no configured threshold, it is set to 0 and this function will
1807
+ // always return true.
1808
+ CharacterCount.prototype.isOverThreshold = function () {
1809
+ var $textarea = this.$textarea;
1810
+ var options = this.options;
1811
+
1812
+ // Determine the remaining number of characters/words
1813
+ var currentLength = this.count($textarea.value);
1814
+ var maxLength = this.maxLength;
1815
+
1816
+ // Set threshold if presented in options
1817
+ var thresholdPercent = options.threshold ? options.threshold : 0;
1818
+ var thresholdValue = maxLength * thresholdPercent / 100;
1819
+
1820
+ return (thresholdValue <= currentLength)
1821
+ };
1822
+
1823
+ // Update the visible character counter and keep track of when the last update
1824
+ // happened for each keypress
1825
+ CharacterCount.prototype.handleKeyUp = function () {
1826
+ this.updateVisibleCountMessage();
1827
+ this.lastInputTimestamp = Date.now();
1764
1828
  };
1765
1829
 
1766
1830
  CharacterCount.prototype.handleFocus = function () {
1767
- // Check if value changed on focus
1768
- this.valueChecker = setInterval(this.checkIfValueChanged.bind(this), 1000);
1831
+ // If the field is focused, and a keyup event hasn't been detected for at
1832
+ // least 1000 ms (1 second), then run the manual change check.
1833
+ // This is so that the update triggered by the manual comparison doesn't
1834
+ // conflict with debounced KeyboardEvent updates.
1835
+ this.valueChecker = setInterval(function () {
1836
+ if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
1837
+ this.checkIfValueChanged();
1838
+ }
1839
+ }.bind(this), 1000);
1769
1840
  };
1770
1841
 
1771
1842
  CharacterCount.prototype.handleBlur = function () {
@@ -3,13 +3,13 @@
3
3
  "name": "text",
4
4
  "type": "string",
5
5
  "required": false,
6
- "description": "Text to use within the back link component. If `html` is provided, the `text` argument will be ignored. Defaults to 'Back'."
6
+ "description": "Text to use within the back link component. If `html` is provided, the `text` option will be ignored. Defaults to 'Back'."
7
7
  },
8
8
  {
9
9
  "name": "html",
10
10
  "type": "string",
11
11
  "required": false,
12
- "description": "HTML to use within the back link component. If `html` is provided, the `text` argument will be ignored. Defaults to 'Back'."
12
+ "description": "HTML to use within the back link component. If `html` is provided, the `text` option will be ignored. Defaults to 'Back'."
13
13
  },
14
14
  {
15
15
  "name": "href",
@@ -128,8 +128,6 @@
128
128
  }
129
129
 
130
130
  .govuk-breadcrumbs__list {
131
- display: -webkit-box;
132
- display: -webkit-flex;
133
131
  display: -ms-flexbox;
134
132
  display: flex;
135
133
  }
@@ -9,13 +9,13 @@
9
9
  "name": "text",
10
10
  "type": "string",
11
11
  "required": true,
12
- "description": "If `html` is set, this is not required. Text to use within the breadcrumbs item. If `html` is provided, the `text` argument will be ignored."
12
+ "description": "If `html` is set, this is not required. Text to use within the breadcrumbs item. If `html` is provided, the `text` option will be ignored."
13
13
  },
14
14
  {
15
15
  "name": "html",
16
16
  "type": "string",
17
17
  "required": true,
18
- "description": "If `text` is set, this is not required. HTML to use within the breadcrumbs item. If `html` is provided, the `text` argument will be ignored."
18
+ "description": "If `text` is set, this is not required. HTML to use within the breadcrumbs item. If `html` is provided, the `text` option will be ignored."
19
19
  },
20
20
  {
21
21
  "name": "href",
@@ -235,22 +235,14 @@
235
235
  @include govuk-typography-weight-bold;
236
236
  @include govuk-typography-responsive($size: 24, $override-line-height: 1);
237
237
 
238
- display: -webkit-inline-box;
239
-
240
- display: -webkit-inline-flex;
241
-
242
238
  display: -ms-inline-flexbox;
243
239
 
244
240
  display: inline-flex;
245
241
  min-height: auto;
246
242
 
247
- -webkit-box-pack: center;
248
-
249
- -webkit-justify-content: center;
250
-
251
- -ms-flex-pack: center;
243
+ -ms-flex-pack: center;
252
244
 
253
- justify-content: center;
245
+ justify-content: center;
254
246
  }
255
247
 
256
248
  .govuk-button__start-icon {
@@ -260,12 +252,10 @@
260
252
  margin-left: govuk-spacing(2);
261
253
  }
262
254
  vertical-align: middle;
263
- -webkit-flex-shrink: 0;
264
- -ms-flex-negative: 0;
265
- flex-shrink: 0;
266
- -webkit-align-self: center;
267
- -ms-flex-item-align: center;
268
- align-self: center;
255
+ -ms-flex-negative: 0;
256
+ flex-shrink: 0;
257
+ -ms-flex-item-align: center;
258
+ align-self: center;
269
259
  // Work around SVGs not inheriting color from parent in forced color mode
270
260
  // (https://github.com/w3c/csswg-drafts/issues/6310)
271
261
  forced-color-adjust: auto;
@@ -9,13 +9,13 @@
9
9
  "name": "text",
10
10
  "type": "string",
11
11
  "required": true,
12
- "description": "If `html` is set, this is not required. Text for the button or link. If `html` is provided, the `text` argument will be ignored and `element` will be automatically set to `button` unless `href` is also set, or it has already been defined. This argument has no effect if `element` is set to `input`."
12
+ "description": "If `html` is set, this is not required. Text for the button or link. If `html` is provided, the `text` option will be ignored and `element` will be automatically set to `button` unless `href` is also set, or it has already been defined. This option has no effect if `element` is set to `input`."
13
13
  },
14
14
  {
15
15
  "name": "html",
16
16
  "type": "string",
17
17
  "required": true,
18
- "description": "If `text` is set, this is not required. HTML for the button or link. If `html` is provided, the `text` argument will be ignored and `element` will be automatically set to `button` unless `href` is also set, or it has already been defined. This argument has no effect if `element` is set to `input`."
18
+ "description": "If `text` is set, this is not required. HTML for the button or link. If `html` is provided, the `text` option will be ignored and `element` will be automatically set to `button` unless `href` is also set, or it has already been defined. This option has no effect if `element` is set to `input`."
19
19
  },
20
20
  {
21
21
  "name": "name",
@@ -1017,9 +1017,9 @@ if (detect) return
1017
1017
  function CharacterCount ($module) {
1018
1018
  this.$module = $module;
1019
1019
  this.$textarea = $module.querySelector('.govuk-js-character-count');
1020
- if (this.$textarea) {
1021
- this.$countMessage = document.getElementById(this.$textarea.id + '-info');
1022
- }
1020
+ this.$visibleCountMessage = null;
1021
+ this.$screenReaderCountMessage = null;
1022
+ this.lastInputTimestamp = null;
1023
1023
  }
1024
1024
 
1025
1025
  CharacterCount.prototype.defaults = {
@@ -1029,18 +1029,39 @@ CharacterCount.prototype.defaults = {
1029
1029
 
1030
1030
  // Initialize component
1031
1031
  CharacterCount.prototype.init = function () {
1032
+ // Check that required elements are present
1033
+ if (!this.$textarea) {
1034
+ return
1035
+ }
1036
+
1032
1037
  // Check for module
1033
1038
  var $module = this.$module;
1034
1039
  var $textarea = this.$textarea;
1035
- var $countMessage = this.$countMessage;
1036
-
1037
- if (!$textarea || !$countMessage) {
1038
- return
1039
- }
1040
+ var $fallbackLimitMessage = document.getElementById($textarea.id + '-info');
1040
1041
 
1041
- // We move count message right after the field
1042
+ // Move the fallback count message to be immediately after the textarea
1042
1043
  // Kept for backwards compatibility
1043
- $textarea.insertAdjacentElement('afterend', $countMessage);
1044
+ $textarea.insertAdjacentElement('afterend', $fallbackLimitMessage);
1045
+
1046
+ // Create the *screen reader* specific live-updating counter
1047
+ // This doesn't need any styling classes, as it is never visible
1048
+ var $screenReaderCountMessage = document.createElement('div');
1049
+ $screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden';
1050
+ $screenReaderCountMessage.setAttribute('aria-live', 'polite');
1051
+ this.$screenReaderCountMessage = $screenReaderCountMessage;
1052
+ $fallbackLimitMessage.insertAdjacentElement('afterend', $screenReaderCountMessage);
1053
+
1054
+ // Create our live-updating counter element, copying the classes from the
1055
+ // fallback element for backwards compatibility as these may have been configured
1056
+ var $visibleCountMessage = document.createElement('div');
1057
+ $visibleCountMessage.className = $fallbackLimitMessage.className;
1058
+ $visibleCountMessage.classList.add('govuk-character-count__status');
1059
+ $visibleCountMessage.setAttribute('aria-hidden', 'true');
1060
+ this.$visibleCountMessage = $visibleCountMessage;
1061
+ $fallbackLimitMessage.insertAdjacentElement('afterend', $visibleCountMessage);
1062
+
1063
+ // Hide the fallback limit message
1064
+ $fallbackLimitMessage.classList.add('govuk-visually-hidden');
1044
1065
 
1045
1066
  // Read options set using dataset ('data-' values)
1046
1067
  this.options = this.getDataset($module);
@@ -1060,23 +1081,19 @@ CharacterCount.prototype.init = function () {
1060
1081
  }
1061
1082
 
1062
1083
  // Remove hard limit if set
1063
- $module.removeAttribute('maxlength');
1084
+ $textarea.removeAttribute('maxlength');
1085
+
1086
+ this.bindChangeEvents();
1064
1087
 
1065
1088
  // When the page is restored after navigating 'back' in some browsers the
1066
1089
  // state of the character count is not restored until *after* the DOMContentLoaded
1067
- // event is fired, so we need to sync after the pageshow event in browsers
1068
- // that support it.
1090
+ // event is fired, so we need to manually update it after the pageshow event
1091
+ // in browsers that support it.
1069
1092
  if ('onpageshow' in window) {
1070
- window.addEventListener('pageshow', this.sync.bind(this));
1093
+ window.addEventListener('pageshow', this.updateCountMessage.bind(this));
1071
1094
  } else {
1072
- window.addEventListener('DOMContentLoaded', this.sync.bind(this));
1095
+ window.addEventListener('DOMContentLoaded', this.updateCountMessage.bind(this));
1073
1096
  }
1074
-
1075
- this.sync();
1076
- };
1077
-
1078
- CharacterCount.prototype.sync = function () {
1079
- this.bindChangeEvents();
1080
1097
  this.updateCountMessage();
1081
1098
  };
1082
1099
 
@@ -1111,7 +1128,7 @@ CharacterCount.prototype.count = function (text) {
1111
1128
  // Bind input propertychange to the elements and update based on the change
1112
1129
  CharacterCount.prototype.bindChangeEvents = function () {
1113
1130
  var $textarea = this.$textarea;
1114
- $textarea.addEventListener('keyup', this.checkIfValueChanged.bind(this));
1131
+ $textarea.addEventListener('keyup', this.handleKeyUp.bind(this));
1115
1132
 
1116
1133
  // Bind focus/blur events to start/stop polling
1117
1134
  $textarea.addEventListener('focus', this.handleFocus.bind(this));
@@ -1129,42 +1146,64 @@ CharacterCount.prototype.checkIfValueChanged = function () {
1129
1146
  }
1130
1147
  };
1131
1148
 
1132
- // Update message box
1149
+ // Helper function to update both the visible and screen reader-specific
1150
+ // counters simultaneously (e.g. on init)
1133
1151
  CharacterCount.prototype.updateCountMessage = function () {
1134
- var countElement = this.$textarea;
1135
- var options = this.options;
1136
- var countMessage = this.$countMessage;
1152
+ this.updateVisibleCountMessage();
1153
+ this.updateScreenReaderCountMessage();
1154
+ };
1137
1155
 
1138
- // Determine the remaining number of characters/words
1139
- var currentLength = this.count(countElement.value);
1140
- var maxLength = this.maxLength;
1141
- var remainingNumber = maxLength - currentLength;
1156
+ // Update visible counter
1157
+ CharacterCount.prototype.updateVisibleCountMessage = function () {
1158
+ var $textarea = this.$textarea;
1159
+ var $visibleCountMessage = this.$visibleCountMessage;
1160
+ var remainingNumber = this.maxLength - this.count($textarea.value);
1142
1161
 
1143
- // Set threshold if presented in options
1144
- var thresholdPercent = options.threshold ? options.threshold : 0;
1145
- var thresholdValue = maxLength * thresholdPercent / 100;
1146
- if (thresholdValue > currentLength) {
1147
- countMessage.classList.add('govuk-character-count__message--disabled');
1148
- // Ensure threshold is hidden for users of assistive technologies
1149
- countMessage.setAttribute('aria-hidden', true);
1162
+ // If input is over the threshold, remove the disabled class which renders the
1163
+ // counter invisible.
1164
+ if (this.isOverThreshold()) {
1165
+ $visibleCountMessage.classList.remove('govuk-character-count__message--disabled');
1150
1166
  } else {
1151
- countMessage.classList.remove('govuk-character-count__message--disabled');
1152
- // Ensure threshold is visible for users of assistive technologies
1153
- countMessage.removeAttribute('aria-hidden');
1167
+ $visibleCountMessage.classList.add('govuk-character-count__message--disabled');
1154
1168
  }
1155
1169
 
1156
1170
  // Update styles
1157
1171
  if (remainingNumber < 0) {
1158
- countElement.classList.add('govuk-textarea--error');
1159
- countMessage.classList.remove('govuk-hint');
1160
- countMessage.classList.add('govuk-error-message');
1172
+ $textarea.classList.add('govuk-textarea--error');
1173
+ $visibleCountMessage.classList.remove('govuk-hint');
1174
+ $visibleCountMessage.classList.add('govuk-error-message');
1175
+ } else {
1176
+ $textarea.classList.remove('govuk-textarea--error');
1177
+ $visibleCountMessage.classList.remove('govuk-error-message');
1178
+ $visibleCountMessage.classList.add('govuk-hint');
1179
+ }
1180
+
1181
+ // Update message
1182
+ $visibleCountMessage.innerHTML = this.formattedUpdateMessage();
1183
+ };
1184
+
1185
+ // Update screen reader-specific counter
1186
+ CharacterCount.prototype.updateScreenReaderCountMessage = function () {
1187
+ var $screenReaderCountMessage = this.$screenReaderCountMessage;
1188
+
1189
+ // If over the threshold, remove the aria-hidden attribute, allowing screen
1190
+ // readers to announce the content of the element.
1191
+ if (this.isOverThreshold()) {
1192
+ $screenReaderCountMessage.removeAttribute('aria-hidden');
1161
1193
  } else {
1162
- countElement.classList.remove('govuk-textarea--error');
1163
- countMessage.classList.remove('govuk-error-message');
1164
- countMessage.classList.add('govuk-hint');
1194
+ $screenReaderCountMessage.setAttribute('aria-hidden', true);
1165
1195
  }
1166
1196
 
1167
1197
  // Update message
1198
+ $screenReaderCountMessage.innerHTML = this.formattedUpdateMessage();
1199
+ };
1200
+
1201
+ // Format update message
1202
+ CharacterCount.prototype.formattedUpdateMessage = function () {
1203
+ var $textarea = this.$textarea;
1204
+ var options = this.options;
1205
+ var remainingNumber = this.maxLength - this.count($textarea.value);
1206
+
1168
1207
  var charVerb = 'remaining';
1169
1208
  var charNoun = 'character';
1170
1209
  var displayNumber = remainingNumber;
@@ -1176,12 +1215,44 @@ CharacterCount.prototype.updateCountMessage = function () {
1176
1215
  charVerb = (remainingNumber < 0) ? 'too many' : 'remaining';
1177
1216
  displayNumber = Math.abs(remainingNumber);
1178
1217
 
1179
- countMessage.innerHTML = 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb;
1218
+ return 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb
1219
+ };
1220
+
1221
+ // Checks whether the value is over the configured threshold for the input.
1222
+ // If there is no configured threshold, it is set to 0 and this function will
1223
+ // always return true.
1224
+ CharacterCount.prototype.isOverThreshold = function () {
1225
+ var $textarea = this.$textarea;
1226
+ var options = this.options;
1227
+
1228
+ // Determine the remaining number of characters/words
1229
+ var currentLength = this.count($textarea.value);
1230
+ var maxLength = this.maxLength;
1231
+
1232
+ // Set threshold if presented in options
1233
+ var thresholdPercent = options.threshold ? options.threshold : 0;
1234
+ var thresholdValue = maxLength * thresholdPercent / 100;
1235
+
1236
+ return (thresholdValue <= currentLength)
1237
+ };
1238
+
1239
+ // Update the visible character counter and keep track of when the last update
1240
+ // happened for each keypress
1241
+ CharacterCount.prototype.handleKeyUp = function () {
1242
+ this.updateVisibleCountMessage();
1243
+ this.lastInputTimestamp = Date.now();
1180
1244
  };
1181
1245
 
1182
1246
  CharacterCount.prototype.handleFocus = function () {
1183
- // Check if value changed on focus
1184
- this.valueChecker = setInterval(this.checkIfValueChanged.bind(this), 1000);
1247
+ // If the field is focused, and a keyup event hasn't been detected for at
1248
+ // least 1000 ms (1 second), then run the manual change check.
1249
+ // This is so that the update triggered by the manual comparison doesn't
1250
+ // conflict with debounced KeyboardEvent updates.
1251
+ this.valueChecker = setInterval(function () {
1252
+ if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
1253
+ this.checkIfValueChanged();
1254
+ }
1255
+ }.bind(this), 1000);
1185
1256
  };
1186
1257
 
1187
1258
  CharacterCount.prototype.handleBlur = function () {