govuk_tech_docs 2.2.2 → 2.4.2

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.nvmrc +1 -1
  4. data/.travis.yml +2 -0
  5. data/CHANGELOG.md +24 -0
  6. data/example/config/tech-docs.yml +1 -0
  7. data/example/source/single-page-nav.html.md +13 -0
  8. data/govuk_tech_docs.gemspec +2 -1
  9. data/lib/assets/javascripts/_modules/collapsible-navigation.js +7 -7
  10. data/lib/assets/javascripts/_modules/in-page-navigation.js +2 -4
  11. data/lib/assets/stylesheets/_govuk_tech_docs.scss +9 -10
  12. data/lib/assets/stylesheets/modules/_search.scss +4 -25
  13. data/lib/assets/stylesheets/modules/_technical-documentation.scss +1 -1
  14. data/lib/assets/stylesheets/modules/_toc.scss +11 -11
  15. data/lib/govuk_tech_docs/table_of_contents/heading.rb +5 -1
  16. data/lib/govuk_tech_docs/table_of_contents/heading_tree_renderer.rb +2 -2
  17. data/lib/govuk_tech_docs/table_of_contents/helpers.rb +22 -11
  18. data/lib/govuk_tech_docs/tech_docs_html_renderer.rb +1 -1
  19. data/lib/govuk_tech_docs/version.rb +1 -1
  20. data/lib/source/layouts/_header.erb +1 -2
  21. data/lib/source/layouts/layout.erb +3 -1
  22. data/node_modules/govuk-frontend/govuk/_base.scss +3 -0
  23. data/node_modules/govuk-frontend/govuk/all.js +306 -94
  24. data/node_modules/govuk-frontend/govuk/all.scss +1 -3
  25. data/node_modules/govuk-frontend/govuk/components/_all.scss +33 -29
  26. data/node_modules/govuk-frontend/govuk/components/accordion/_accordion.scss +2 -208
  27. data/node_modules/govuk-frontend/govuk/components/accordion/_index.scss +197 -0
  28. data/node_modules/govuk-frontend/govuk/components/accordion/accordion.js +1 -1
  29. data/node_modules/govuk-frontend/govuk/components/back-link/_back-link.scss +2 -65
  30. data/node_modules/govuk-frontend/govuk/components/back-link/_index.scss +99 -0
  31. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/_breadcrumbs.scss +2 -118
  32. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/_index.scss +138 -0
  33. data/node_modules/govuk-frontend/govuk/components/button/_button.scss +2 -284
  34. data/node_modules/govuk-frontend/govuk/components/button/_index.scss +288 -0
  35. data/node_modules/govuk-frontend/govuk/components/character-count/_character-count.scss +2 -31
  36. data/node_modules/govuk-frontend/govuk/components/character-count/_index.scss +25 -0
  37. data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +17 -9
  38. data/node_modules/govuk-frontend/govuk/components/checkboxes/_checkboxes.scss +2 -308
  39. data/node_modules/govuk-frontend/govuk/components/checkboxes/_index.scss +320 -0
  40. data/node_modules/govuk-frontend/govuk/components/checkboxes/checkboxes.js +129 -24
  41. data/node_modules/govuk-frontend/govuk/components/cookie-banner/_cookie-banner.scss +2 -0
  42. data/node_modules/govuk-frontend/govuk/components/cookie-banner/_index.scss +51 -0
  43. data/node_modules/govuk-frontend/govuk/components/date-input/_date-input.scss +2 -30
  44. data/node_modules/govuk-frontend/govuk/components/date-input/_index.scss +26 -0
  45. data/node_modules/govuk-frontend/govuk/components/details/_details.scss +2 -88
  46. data/node_modules/govuk-frontend/govuk/components/details/_index.scss +87 -0
  47. data/node_modules/govuk-frontend/govuk/components/error-message/_error-message.scss +2 -15
  48. data/node_modules/govuk-frontend/govuk/components/error-message/_index.scss +11 -0
  49. data/node_modules/govuk-frontend/govuk/components/error-summary/_error-summary.scss +2 -59
  50. data/node_modules/govuk-frontend/govuk/components/error-summary/_index.scss +43 -0
  51. data/node_modules/govuk-frontend/govuk/components/fieldset/_fieldset.scss +2 -68
  52. data/node_modules/govuk-frontend/govuk/components/fieldset/_index.scss +64 -0
  53. data/node_modules/govuk-frontend/govuk/components/file-upload/_file-upload.scss +2 -81
  54. data/node_modules/govuk-frontend/govuk/components/file-upload/_index.scss +49 -0
  55. data/node_modules/govuk-frontend/govuk/components/footer/_footer.scss +2 -244
  56. data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +241 -0
  57. data/node_modules/govuk-frontend/govuk/components/header/_header.scss +2 -318
  58. data/node_modules/govuk-frontend/govuk/components/header/_index.scss +331 -0
  59. data/node_modules/govuk-frontend/govuk/components/header/header.js +665 -316
  60. data/node_modules/govuk-frontend/govuk/components/hint/_hint.scss +2 -50
  61. data/node_modules/govuk-frontend/govuk/components/hint/_index.scss +44 -0
  62. data/node_modules/govuk-frontend/govuk/components/input/_index.scss +191 -0
  63. data/node_modules/govuk-frontend/govuk/components/input/_input.scss +2 -103
  64. data/node_modules/govuk-frontend/govuk/components/inset-text/_index.scss +24 -0
  65. data/node_modules/govuk-frontend/govuk/components/inset-text/_inset-text.scss +2 -28
  66. data/node_modules/govuk-frontend/govuk/components/label/_index.scss +41 -0
  67. data/node_modules/govuk-frontend/govuk/components/label/_label.scss +2 -45
  68. data/node_modules/govuk-frontend/govuk/components/notification-banner/_index.scss +89 -0
  69. data/node_modules/govuk-frontend/govuk/components/notification-banner/_notification-banner.scss +2 -0
  70. data/node_modules/govuk-frontend/govuk/components/notification-banner/notification-banner.js +61 -0
  71. data/node_modules/govuk-frontend/govuk/components/panel/_index.scss +44 -0
  72. data/node_modules/govuk-frontend/govuk/components/panel/_panel.scss +2 -44
  73. data/node_modules/govuk-frontend/govuk/components/phase-banner/_index.scss +27 -0
  74. data/node_modules/govuk-frontend/govuk/components/phase-banner/_phase-banner.scss +2 -31
  75. data/node_modules/govuk-frontend/govuk/components/radios/_index.scss +342 -0
  76. data/node_modules/govuk-frontend/govuk/components/radios/_radios.scss +2 -346
  77. data/node_modules/govuk-frontend/govuk/components/radios/radios.js +76 -28
  78. data/node_modules/govuk-frontend/govuk/components/select/_index.scss +49 -0
  79. data/node_modules/govuk-frontend/govuk/components/select/_select.scss +2 -57
  80. data/node_modules/govuk-frontend/govuk/components/skip-link/_index.scss +36 -0
  81. data/node_modules/govuk-frontend/govuk/components/skip-link/_skip-link.scss +2 -37
  82. data/node_modules/govuk-frontend/govuk/components/summary-list/_index.scss +145 -0
  83. data/node_modules/govuk-frontend/govuk/components/summary-list/_summary-list.scss +2 -157
  84. data/node_modules/govuk-frontend/govuk/components/table/_index.scss +71 -0
  85. data/node_modules/govuk-frontend/govuk/components/table/_table.scss +2 -54
  86. data/node_modules/govuk-frontend/govuk/components/tabs/_index.scss +130 -0
  87. data/node_modules/govuk-frontend/govuk/components/tabs/_tabs.scss +2 -142
  88. data/node_modules/govuk-frontend/govuk/components/tag/_index.scss +86 -0
  89. data/node_modules/govuk-frontend/govuk/components/tag/_tag.scss +2 -91
  90. data/node_modules/govuk-frontend/govuk/components/textarea/_index.scss +47 -0
  91. data/node_modules/govuk-frontend/govuk/components/textarea/_textarea.scss +2 -55
  92. data/node_modules/govuk-frontend/govuk/components/warning-text/_index.scss +66 -0
  93. data/node_modules/govuk-frontend/govuk/components/warning-text/_warning-text.scss +2 -60
  94. data/node_modules/govuk-frontend/govuk/core/_global-styles.scss +5 -3
  95. data/node_modules/govuk-frontend/govuk/core/_links.scss +13 -3
  96. data/node_modules/govuk-frontend/govuk/core/_lists.scss +17 -3
  97. data/node_modules/govuk-frontend/govuk/core/_section-break.scss +5 -3
  98. data/node_modules/govuk-frontend/govuk/core/_template.scss +5 -4
  99. data/node_modules/govuk-frontend/govuk/core/_typography.scss +5 -3
  100. data/node_modules/govuk-frontend/govuk/helpers/_clearfix.scss +1 -1
  101. data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +1 -1
  102. data/node_modules/govuk-frontend/govuk/helpers/_device-pixels.scss +3 -3
  103. data/node_modules/govuk-frontend/govuk/helpers/_focused.scss +1 -1
  104. data/node_modules/govuk-frontend/govuk/helpers/_font-faces.scss +9 -11
  105. data/node_modules/govuk-frontend/govuk/helpers/_grid.scss +2 -1
  106. data/node_modules/govuk-frontend/govuk/helpers/_links.scss +246 -33
  107. data/node_modules/govuk-frontend/govuk/helpers/_media-queries.scss +2 -6
  108. data/node_modules/govuk-frontend/govuk/helpers/_shape-arrow.scss +1 -1
  109. data/node_modules/govuk-frontend/govuk/helpers/_spacing.scss +3 -2
  110. data/node_modules/govuk-frontend/govuk/helpers/_typography.scss +8 -7
  111. data/node_modules/govuk-frontend/govuk/helpers/_visually-hidden.scss +1 -1
  112. data/node_modules/govuk-frontend/govuk/objects/_all.scss +1 -0
  113. data/node_modules/govuk-frontend/govuk/objects/_button-group.scss +101 -0
  114. data/node_modules/govuk-frontend/govuk/objects/_form-group.scss +1 -4
  115. data/node_modules/govuk-frontend/govuk/objects/_grid.scss +3 -6
  116. data/node_modules/govuk-frontend/govuk/objects/_main-wrapper.scss +5 -4
  117. data/node_modules/govuk-frontend/govuk/objects/_width-container.scss +6 -4
  118. data/node_modules/govuk-frontend/govuk/overrides/_display.scss +6 -4
  119. data/node_modules/govuk-frontend/govuk/overrides/_spacing.scss +5 -3
  120. data/node_modules/govuk-frontend/govuk/overrides/_typography.scss +5 -3
  121. data/node_modules/govuk-frontend/govuk/overrides/_width.scss +6 -3
  122. data/node_modules/govuk-frontend/govuk/settings/_all.scss +2 -0
  123. data/node_modules/govuk-frontend/govuk/settings/_colours-applied.scss +11 -5
  124. data/node_modules/govuk-frontend/govuk/settings/_colours-organisations.scss +3 -0
  125. data/node_modules/govuk-frontend/govuk/settings/_colours-palette.scss +42 -35
  126. data/node_modules/govuk-frontend/govuk/settings/_compatibility.scss +0 -1
  127. data/node_modules/govuk-frontend/govuk/settings/_ie8.scss +1 -1
  128. data/node_modules/govuk-frontend/govuk/settings/_links.scss +62 -0
  129. data/node_modules/govuk-frontend/govuk/settings/_measurements.scss +4 -5
  130. data/node_modules/govuk-frontend/govuk/settings/_typography-font-families.scss +2 -2
  131. data/node_modules/govuk-frontend/govuk/settings/_typography-font.scss +14 -5
  132. data/node_modules/govuk-frontend/govuk/settings/_typography-responsive.scss +6 -2
  133. data/node_modules/govuk-frontend/govuk/tools/_compatibility.scss +1 -1
  134. data/node_modules/govuk-frontend/govuk/tools/_font-url.scss +1 -4
  135. data/node_modules/govuk-frontend/govuk/tools/_ie8.scss +1 -1
  136. data/node_modules/govuk-frontend/govuk/tools/_image-url.scss +1 -4
  137. data/node_modules/govuk-frontend/govuk/tools/_px-to-em.scss +1 -1
  138. data/node_modules/govuk-frontend/govuk/tools/_px-to-rem.scss +1 -1
  139. data/node_modules/govuk-frontend/govuk/utilities/_visually-hidden.scss +0 -1
  140. data/node_modules/govuk-frontend/govuk/vendor/_sass-mq.scss +0 -4
  141. data/package-lock.json +358 -288
  142. data/package.json +2 -2
  143. metadata +56 -4
@@ -891,7 +891,7 @@ Accordion.prototype.initHeaderAttributes = function ($headerWrapper, index) {
891
891
  icon.className = this.iconClass;
892
892
  icon.setAttribute('aria-hidden', 'true');
893
893
 
894
- $heading.appendChild(icon);
894
+ $button.appendChild(icon);
895
895
  };
896
896
 
897
897
  // When section toggled, set and store state
@@ -1498,7 +1498,7 @@ function CharacterCount ($module) {
1498
1498
  this.$module = $module;
1499
1499
  this.$textarea = $module.querySelector('.govuk-js-character-count');
1500
1500
  if (this.$textarea) {
1501
- this.$countMessage = $module.querySelector('[id=' + this.$textarea.id + '-info]');
1501
+ this.$countMessage = $module.querySelector('[id="' + this.$textarea.id + '-info"]');
1502
1502
  }
1503
1503
  }
1504
1504
 
@@ -1542,13 +1542,22 @@ CharacterCount.prototype.init = function () {
1542
1542
  // Remove hard limit if set
1543
1543
  $module.removeAttribute('maxlength');
1544
1544
 
1545
- // Bind event changes to the textarea
1546
- var boundChangeEvents = this.bindChangeEvents.bind(this);
1547
- boundChangeEvents();
1545
+ // When the page is restored after navigating 'back' in some browsers the
1546
+ // state of the character count is not restored until *after* the DOMContentLoaded
1547
+ // event is fired, so we need to sync after the pageshow event in browsers
1548
+ // that support it.
1549
+ if ('onpageshow' in window) {
1550
+ window.addEventListener('pageshow', this.sync.bind(this));
1551
+ } else {
1552
+ window.addEventListener('DOMContentLoaded', this.sync.bind(this));
1553
+ }
1548
1554
 
1549
- // Update count message
1550
- var boundUpdateCountMessage = this.updateCountMessage.bind(this);
1551
- boundUpdateCountMessage();
1555
+ this.sync();
1556
+ };
1557
+
1558
+ CharacterCount.prototype.sync = function () {
1559
+ this.bindChangeEvents();
1560
+ this.updateCountMessage();
1552
1561
  };
1553
1562
 
1554
1563
  // Read data attributes
@@ -1596,8 +1605,7 @@ CharacterCount.prototype.checkIfValueChanged = function () {
1596
1605
  if (!this.$textarea.oldValue) this.$textarea.oldValue = '';
1597
1606
  if (this.$textarea.value !== this.$textarea.oldValue) {
1598
1607
  this.$textarea.oldValue = this.$textarea.value;
1599
- var boundUpdateCountMessage = this.updateCountMessage.bind(this);
1600
- boundUpdateCountMessage();
1608
+ this.updateCountMessage();
1601
1609
  }
1602
1610
  };
1603
1611
 
@@ -1666,52 +1674,157 @@ function Checkboxes ($module) {
1666
1674
  this.$inputs = $module.querySelectorAll('input[type="checkbox"]');
1667
1675
  }
1668
1676
 
1677
+ /**
1678
+ * Initialise Checkboxes
1679
+ *
1680
+ * Checkboxes can be associated with a 'conditionally revealed' content block –
1681
+ * for example, a checkbox for 'Phone' could reveal an additional form field for
1682
+ * the user to enter their phone number.
1683
+ *
1684
+ * These associations are made using a `data-aria-controls` attribute, which is
1685
+ * promoted to an aria-controls attribute during initialisation.
1686
+ *
1687
+ * We also need to restore the state of any conditional reveals on the page (for
1688
+ * example if the user has navigated back), and set up event handlers to keep
1689
+ * the reveal in sync with the checkbox state.
1690
+ */
1669
1691
  Checkboxes.prototype.init = function () {
1670
1692
  var $module = this.$module;
1671
1693
  var $inputs = this.$inputs;
1672
1694
 
1673
- /**
1674
- * Loop over all items with [data-controls]
1675
- * Check if they have a matching conditional reveal
1676
- * If they do, assign attributes.
1677
- **/
1678
1695
  nodeListForEach($inputs, function ($input) {
1679
- var controls = $input.getAttribute('data-aria-controls');
1696
+ var target = $input.getAttribute('data-aria-controls');
1680
1697
 
1681
- // Check if input controls anything
1682
- // Check if content exists, before setting attributes.
1683
- if (!controls || !$module.querySelector('#' + controls)) {
1698
+ // Skip checkboxes without data-aria-controls attributes, or where the
1699
+ // target element does not exist.
1700
+ if (!target || !$module.querySelector('#' + target)) {
1684
1701
  return
1685
1702
  }
1686
1703
 
1687
- // If we have content that is controlled, set attributes.
1688
- $input.setAttribute('aria-controls', controls);
1704
+ // Promote the data-aria-controls attribute to a aria-controls attribute
1705
+ // so that the relationship is exposed in the AOM
1706
+ $input.setAttribute('aria-controls', target);
1689
1707
  $input.removeAttribute('data-aria-controls');
1690
- this.setAttributes($input);
1691
- }.bind(this));
1708
+ });
1709
+
1710
+ // When the page is restored after navigating 'back' in some browsers the
1711
+ // state of form controls is not restored until *after* the DOMContentLoaded
1712
+ // event is fired, so we need to sync after the pageshow event in browsers
1713
+ // that support it.
1714
+ if ('onpageshow' in window) {
1715
+ window.addEventListener('pageshow', this.syncAllConditionalReveals.bind(this));
1716
+ } else {
1717
+ window.addEventListener('DOMContentLoaded', this.syncAllConditionalReveals.bind(this));
1718
+ }
1719
+
1720
+ // Although we've set up handlers to sync state on the pageshow or
1721
+ // DOMContentLoaded event, init could be called after those events have fired,
1722
+ // for example if they are added to the page dynamically, so sync now too.
1723
+ this.syncAllConditionalReveals();
1692
1724
 
1693
- // Handle events
1694
1725
  $module.addEventListener('click', this.handleClick.bind(this));
1695
1726
  };
1696
1727
 
1697
- Checkboxes.prototype.setAttributes = function ($input) {
1698
- var inputIsChecked = $input.checked;
1699
- $input.setAttribute('aria-expanded', inputIsChecked);
1728
+ /**
1729
+ * Sync the conditional reveal states for all inputs in this $module.
1730
+ */
1731
+ Checkboxes.prototype.syncAllConditionalReveals = function () {
1732
+ nodeListForEach(this.$inputs, this.syncConditionalRevealWithInputState.bind(this));
1733
+ };
1700
1734
 
1701
- var $content = this.$module.querySelector('#' + $input.getAttribute('aria-controls'));
1702
- if ($content) {
1703
- $content.classList.toggle('govuk-checkboxes__conditional--hidden', !inputIsChecked);
1735
+ /**
1736
+ * Sync conditional reveal with the input state
1737
+ *
1738
+ * Synchronise the visibility of the conditional reveal, and its accessible
1739
+ * state, with the input's checked state.
1740
+ *
1741
+ * @param {HTMLInputElement} $input Checkbox input
1742
+ */
1743
+ Checkboxes.prototype.syncConditionalRevealWithInputState = function ($input) {
1744
+ var $target = this.$module.querySelector('#' + $input.getAttribute('aria-controls'));
1745
+
1746
+ if ($target && $target.classList.contains('govuk-checkboxes__conditional')) {
1747
+ var inputIsChecked = $input.checked;
1748
+
1749
+ $input.setAttribute('aria-expanded', inputIsChecked);
1750
+ $target.classList.toggle('govuk-checkboxes__conditional--hidden', !inputIsChecked);
1704
1751
  }
1705
1752
  };
1706
1753
 
1754
+ /**
1755
+ * Uncheck other checkboxes
1756
+ *
1757
+ * Find any other checkbox inputs with the same name value, and uncheck them.
1758
+ * This is useful for when a “None of these" checkbox is checked.
1759
+ */
1760
+ Checkboxes.prototype.unCheckAllInputsExcept = function ($input) {
1761
+ var allInputsWithSameName = document.querySelectorAll('input[type="checkbox"][name="' + $input.name + '"]');
1762
+
1763
+ nodeListForEach(allInputsWithSameName, function ($inputWithSameName) {
1764
+ var hasSameFormOwner = ($input.form === $inputWithSameName.form);
1765
+ if (hasSameFormOwner && $inputWithSameName !== $input) {
1766
+ $inputWithSameName.checked = false;
1767
+ }
1768
+ });
1769
+
1770
+ this.syncAllConditionalReveals();
1771
+ };
1772
+
1773
+ /**
1774
+ * Uncheck exclusive inputs
1775
+ *
1776
+ * Find any checkbox inputs with the same name value and the 'exclusive' behaviour,
1777
+ * and uncheck them. This helps prevent someone checking both a regular checkbox and a
1778
+ * "None of these" checkbox in the same fieldset.
1779
+ */
1780
+ Checkboxes.prototype.unCheckExclusiveInputs = function ($input) {
1781
+ var allInputsWithSameNameAndExclusiveBehaviour = document.querySelectorAll(
1782
+ 'input[data-behaviour="exclusive"][type="checkbox"][name="' + $input.name + '"]'
1783
+ );
1784
+
1785
+ nodeListForEach(allInputsWithSameNameAndExclusiveBehaviour, function ($exclusiveInput) {
1786
+ var hasSameFormOwner = ($input.form === $exclusiveInput.form);
1787
+ if (hasSameFormOwner) {
1788
+ $exclusiveInput.checked = false;
1789
+ }
1790
+ });
1791
+
1792
+ this.syncAllConditionalReveals();
1793
+ };
1794
+
1795
+ /**
1796
+ * Click event handler
1797
+ *
1798
+ * Handle a click within the $module – if the click occurred on a checkbox, sync
1799
+ * the state of any associated conditional reveal with the checkbox state.
1800
+ *
1801
+ * @param {MouseEvent} event Click event
1802
+ */
1707
1803
  Checkboxes.prototype.handleClick = function (event) {
1708
1804
  var $target = event.target;
1709
1805
 
1710
- // If a checkbox with aria-controls, handle click
1711
- var isCheckbox = $target.getAttribute('type') === 'checkbox';
1806
+ // Ignore clicks on things that aren't checkbox inputs
1807
+ if ($target.type !== 'checkbox') {
1808
+ return
1809
+ }
1810
+
1811
+ // If the checkbox conditionally-reveals some content, sync the state
1712
1812
  var hasAriaControls = $target.getAttribute('aria-controls');
1713
- if (isCheckbox && hasAriaControls) {
1714
- this.setAttributes($target);
1813
+ if (hasAriaControls) {
1814
+ this.syncConditionalRevealWithInputState($target);
1815
+ }
1816
+
1817
+ // No further behaviour needed for unchecking
1818
+ if (!$target.checked) {
1819
+ return
1820
+ }
1821
+
1822
+ // Handle 'exclusive' checkbox behaviour (ie "None of these")
1823
+ var hasBehaviourExclusive = ($target.getAttribute('data-behaviour') === 'exclusive');
1824
+ if (hasBehaviourExclusive) {
1825
+ this.unCheckAllInputsExcept($target);
1826
+ } else {
1827
+ this.unCheckExclusiveInputs($target);
1715
1828
  }
1716
1829
  };
1717
1830
 
@@ -1904,122 +2017,216 @@ ErrorSummary.prototype.getAssociatedLegendOrLabel = function ($input) {
1904
2017
  $input.closest('label')
1905
2018
  };
1906
2019
 
1907
- function Header ($module) {
2020
+ function NotificationBanner ($module) {
1908
2021
  this.$module = $module;
1909
2022
  }
1910
2023
 
1911
- Header.prototype.init = function () {
1912
- // Check for module
2024
+ /**
2025
+ * Initialise the component
2026
+ */
2027
+ NotificationBanner.prototype.init = function () {
1913
2028
  var $module = this.$module;
2029
+ // Check for module
1914
2030
  if (!$module) {
1915
2031
  return
1916
2032
  }
1917
2033
 
1918
- // Check for button
1919
- var $toggleButton = $module.querySelector('.govuk-js-header-toggle');
1920
- if (!$toggleButton) {
2034
+ this.setFocus();
2035
+ };
2036
+
2037
+ /**
2038
+ * Focus the element
2039
+ *
2040
+ * If `role="alert"` is set, focus the element to help some assistive technologies
2041
+ * prioritise announcing it.
2042
+ *
2043
+ * You can turn off the auto-focus functionality by setting `data-disable-auto-focus="true"` in the
2044
+ * component HTML. You might wish to do this based on user research findings, or to avoid a clash
2045
+ * with another element which should be focused when the page loads.
2046
+ */
2047
+ NotificationBanner.prototype.setFocus = function () {
2048
+ var $module = this.$module;
2049
+
2050
+ if ($module.getAttribute('data-disable-auto-focus') === 'true') {
1921
2051
  return
1922
2052
  }
1923
2053
 
1924
- // Handle $toggleButton click events
1925
- $toggleButton.addEventListener('click', this.handleClick.bind(this));
2054
+ if ($module.getAttribute('role') !== 'alert') {
2055
+ return
2056
+ }
2057
+
2058
+ // Set tabindex to -1 to make the element focusable with JavaScript.
2059
+ // Remove the tabindex on blur as the component doesn't need to be focusable after the page has
2060
+ // loaded.
2061
+ if (!$module.getAttribute('tabindex')) {
2062
+ $module.setAttribute('tabindex', '-1');
2063
+
2064
+ $module.addEventListener('blur', function () {
2065
+ $module.removeAttribute('tabindex');
2066
+ });
2067
+ }
2068
+
2069
+ $module.focus();
1926
2070
  };
1927
2071
 
2072
+ function Header ($module) {
2073
+ this.$module = $module;
2074
+ this.$menuButton = $module && $module.querySelector('.govuk-js-header-toggle');
2075
+ this.$menu = this.$menuButton && $module.querySelector(
2076
+ '#' + this.$menuButton.getAttribute('aria-controls')
2077
+ );
2078
+ }
2079
+
1928
2080
  /**
1929
- * Toggle class
1930
- * @param {object} node element
1931
- * @param {string} className to toggle
1932
- */
1933
- Header.prototype.toggleClass = function (node, className) {
1934
- if (node.className.indexOf(className) > 0) {
1935
- node.className = node.className.replace(' ' + className, '');
1936
- } else {
1937
- node.className += ' ' + className;
2081
+ * Initialise header
2082
+ *
2083
+ * Check for the presence of the header, menu and menu button – if any are
2084
+ * missing then there's nothing to do so return early.
2085
+ */
2086
+ Header.prototype.init = function () {
2087
+ if (!this.$module || !this.$menuButton || !this.$menu) {
2088
+ return
1938
2089
  }
2090
+
2091
+ this.syncState(this.$menu.classList.contains('govuk-header__navigation--open'));
2092
+ this.$menuButton.addEventListener('click', this.handleMenuButtonClick.bind(this));
1939
2093
  };
1940
2094
 
1941
2095
  /**
1942
- * An event handler for click event on $toggleButton
1943
- * @param {object} event event
1944
- */
1945
- Header.prototype.handleClick = function (event) {
1946
- var $module = this.$module;
1947
- var $toggleButton = event.target || event.srcElement;
1948
- var $target = $module.querySelector('#' + $toggleButton.getAttribute('aria-controls'));
1949
-
1950
- // If a button with aria-controls, handle click
1951
- if ($toggleButton && $target) {
1952
- this.toggleClass($target, 'govuk-header__navigation--open');
1953
- this.toggleClass($toggleButton, 'govuk-header__menu-button--open');
2096
+ * Sync menu state
2097
+ *
2098
+ * Sync the menu button class and the accessible state of the menu and the menu
2099
+ * button with the visible state of the menu
2100
+ *
2101
+ * @param {boolean} isVisible Whether the menu is currently visible
2102
+ */
2103
+ Header.prototype.syncState = function (isVisible) {
2104
+ this.$menuButton.classList.toggle('govuk-header__menu-button--open', isVisible);
2105
+ this.$menuButton.setAttribute('aria-expanded', isVisible);
2106
+ };
1954
2107
 
1955
- $toggleButton.setAttribute('aria-expanded', $toggleButton.getAttribute('aria-expanded') !== 'true');
1956
- $target.setAttribute('aria-hidden', $target.getAttribute('aria-hidden') === 'false');
1957
- }
2108
+ /**
2109
+ * Handle menu button click
2110
+ *
2111
+ * When the menu button is clicked, change the visibility of the menu and then
2112
+ * sync the accessibility state and menu button state
2113
+ */
2114
+ Header.prototype.handleMenuButtonClick = function () {
2115
+ var isVisible = this.$menu.classList.toggle('govuk-header__navigation--open');
2116
+ this.syncState(isVisible);
1958
2117
  };
1959
2118
 
1960
2119
  function Radios ($module) {
1961
2120
  this.$module = $module;
2121
+ this.$inputs = $module.querySelectorAll('input[type="radio"]');
1962
2122
  }
1963
2123
 
2124
+ /**
2125
+ * Initialise Radios
2126
+ *
2127
+ * Radios can be associated with a 'conditionally revealed' content block – for
2128
+ * example, a radio for 'Phone' could reveal an additional form field for the
2129
+ * user to enter their phone number.
2130
+ *
2131
+ * These associations are made using a `data-aria-controls` attribute, which is
2132
+ * promoted to an aria-controls attribute during initialisation.
2133
+ *
2134
+ * We also need to restore the state of any conditional reveals on the page (for
2135
+ * example if the user has navigated back), and set up event handlers to keep
2136
+ * the reveal in sync with the radio state.
2137
+ */
1964
2138
  Radios.prototype.init = function () {
1965
2139
  var $module = this.$module;
1966
- var $inputs = $module.querySelectorAll('input[type="radio"]');
2140
+ var $inputs = this.$inputs;
1967
2141
 
1968
- /**
1969
- * Loop over all items with [data-controls]
1970
- * Check if they have a matching conditional reveal
1971
- * If they do, assign attributes.
1972
- **/
1973
2142
  nodeListForEach($inputs, function ($input) {
1974
- var controls = $input.getAttribute('data-aria-controls');
2143
+ var target = $input.getAttribute('data-aria-controls');
1975
2144
 
1976
- // Check if input controls anything
1977
- // Check if content exists, before setting attributes.
1978
- if (!controls || !$module.querySelector('#' + controls)) {
2145
+ // Skip radios without data-aria-controls attributes, or where the
2146
+ // target element does not exist.
2147
+ if (!target || !$module.querySelector('#' + target)) {
1979
2148
  return
1980
2149
  }
1981
2150
 
1982
- // If we have content that is controlled, set attributes.
1983
- $input.setAttribute('aria-controls', controls);
2151
+ // Promote the data-aria-controls attribute to a aria-controls attribute
2152
+ // so that the relationship is exposed in the AOM
2153
+ $input.setAttribute('aria-controls', target);
1984
2154
  $input.removeAttribute('data-aria-controls');
1985
- this.setAttributes($input);
1986
- }.bind(this));
2155
+ });
2156
+
2157
+ // When the page is restored after navigating 'back' in some browsers the
2158
+ // state of form controls is not restored until *after* the DOMContentLoaded
2159
+ // event is fired, so we need to sync after the pageshow event in browsers
2160
+ // that support it.
2161
+ if ('onpageshow' in window) {
2162
+ window.addEventListener('pageshow', this.syncAllConditionalReveals.bind(this));
2163
+ } else {
2164
+ window.addEventListener('DOMContentLoaded', this.syncAllConditionalReveals.bind(this));
2165
+ }
2166
+
2167
+ // Although we've set up handlers to sync state on the pageshow or
2168
+ // DOMContentLoaded event, init could be called after those events have fired,
2169
+ // for example if they are added to the page dynamically, so sync now too.
2170
+ this.syncAllConditionalReveals();
1987
2171
 
1988
2172
  // Handle events
1989
2173
  $module.addEventListener('click', this.handleClick.bind(this));
1990
2174
  };
1991
2175
 
1992
- Radios.prototype.setAttributes = function ($input) {
1993
- var $content = document.querySelector('#' + $input.getAttribute('aria-controls'));
2176
+ /**
2177
+ * Sync the conditional reveal states for all inputs in this $module.
2178
+ */
2179
+ Radios.prototype.syncAllConditionalReveals = function () {
2180
+ nodeListForEach(this.$inputs, this.syncConditionalRevealWithInputState.bind(this));
2181
+ };
2182
+
2183
+ /**
2184
+ * Sync conditional reveal with the input state
2185
+ *
2186
+ * Synchronise the visibility of the conditional reveal, and its accessible
2187
+ * state, with the input's checked state.
2188
+ *
2189
+ * @param {HTMLInputElement} $input Radio input
2190
+ */
2191
+ Radios.prototype.syncConditionalRevealWithInputState = function ($input) {
2192
+ var $target = document.querySelector('#' + $input.getAttribute('aria-controls'));
1994
2193
 
1995
- if ($content && $content.classList.contains('govuk-radios__conditional')) {
2194
+ if ($target && $target.classList.contains('govuk-radios__conditional')) {
1996
2195
  var inputIsChecked = $input.checked;
1997
2196
 
1998
2197
  $input.setAttribute('aria-expanded', inputIsChecked);
1999
-
2000
- $content.classList.toggle('govuk-radios__conditional--hidden', !inputIsChecked);
2198
+ $target.classList.toggle('govuk-radios__conditional--hidden', !inputIsChecked);
2001
2199
  }
2002
2200
  };
2003
2201
 
2202
+ /**
2203
+ * Click event handler
2204
+ *
2205
+ * Handle a click within the $module – if the click occurred on a radio, sync
2206
+ * the state of the conditional reveal for all radio buttons in the same form
2207
+ * with the same name (because checking one radio could have un-checked a radio
2208
+ * in another $module)
2209
+ *
2210
+ * @param {MouseEvent} event Click event
2211
+ */
2004
2212
  Radios.prototype.handleClick = function (event) {
2005
2213
  var $clickedInput = event.target;
2006
- // We only want to handle clicks for radio inputs
2214
+
2215
+ // Ignore clicks on things that aren't radio buttons
2007
2216
  if ($clickedInput.type !== 'radio') {
2008
2217
  return
2009
2218
  }
2010
- // Because checking one radio can uncheck a radio in another $module,
2011
- // we need to call set attributes on all radios in the same form, or document if they're not in a form.
2012
- //
2013
- // We also only want radios which have aria-controls, as they support conditional reveals.
2219
+
2220
+ // We only need to consider radios with conditional reveals, which will have
2221
+ // aria-controls attributes.
2014
2222
  var $allInputs = document.querySelectorAll('input[type="radio"][aria-controls]');
2223
+
2015
2224
  nodeListForEach($allInputs, function ($input) {
2016
- // Only inputs with the same form owner should change.
2017
2225
  var hasSameFormOwner = ($input.form === $clickedInput.form);
2018
-
2019
- // In radios, only radios with the same name will affect each other.
2020
2226
  var hasSameName = ($input.name === $clickedInput.name);
2227
+
2021
2228
  if (hasSameName && hasSameFormOwner) {
2022
- this.setAttributes($input);
2229
+ this.syncConditionalRevealWithInputState($input);
2023
2230
  }
2024
2231
  }.bind(this));
2025
2232
  };
@@ -2379,6 +2586,11 @@ function initAll (options) {
2379
2586
  var $toggleButton = scope.querySelector('[data-module="govuk-header"]');
2380
2587
  new Header($toggleButton).init();
2381
2588
 
2589
+ var $notificationBanners = scope.querySelectorAll('[data-module="govuk-notification-banner"]');
2590
+ nodeListForEach($notificationBanners, function ($notificationBanner) {
2591
+ new NotificationBanner($notificationBanner).init();
2592
+ });
2593
+
2382
2594
  var $radios = scope.querySelectorAll('[data-module="govuk-radios"]');
2383
2595
  nodeListForEach($radios, function ($radio) {
2384
2596
  new Radios($radio).init();