govuk_tech_docs 2.2.2 → 2.4.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of govuk_tech_docs might be problematic. Click here for more details.

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();