govuk_publishing_components 29.9.0 → 29.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (179) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -5
  3. data/app/assets/javascripts/govuk_publishing_components/analytics/page-content.js +4 -4
  4. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/gtm-click-tracking.js +46 -24
  5. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/gtm-page-views.js +98 -0
  6. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js +3 -0
  7. data/app/assets/javascripts/govuk_publishing_components/components/accordion.js +28 -1
  8. data/app/assets/javascripts/govuk_publishing_components/vendor/lux/lux-measurer.js +2 -2
  9. data/app/assets/stylesheets/govuk_publishing_components/_all_components_print.scss +1 -0
  10. data/app/assets/stylesheets/govuk_publishing_components/components/_contextual-sidebar.scss +20 -0
  11. data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +3 -8
  12. data/app/assets/stylesheets/govuk_publishing_components/components/govspeak/_attachment.scss +7 -1
  13. data/app/assets/stylesheets/govuk_publishing_components/components/print/_organisation-logo.scss +4 -0
  14. data/app/controllers/govuk_publishing_components/audit_controller.rb +3 -3
  15. data/app/controllers/govuk_publishing_components/component_guide_controller.rb +0 -9
  16. data/app/models/govuk_publishing_components/audit_comparer.rb +91 -34
  17. data/app/views/govuk_publishing_components/audit/_applications.html.erb +20 -9
  18. data/app/views/govuk_publishing_components/component_guide/index.html.erb +1 -19
  19. data/app/views/govuk_publishing_components/components/_accordion.html.erb +7 -5
  20. data/app/views/govuk_publishing_components/components/_attachment.html.erb +1 -3
  21. data/app/views/govuk_publishing_components/components/_character_count.html.erb +1 -1
  22. data/app/views/govuk_publishing_components/components/_date_input.html.erb +0 -1
  23. data/app/views/govuk_publishing_components/components/_error_alert.html.erb +6 -3
  24. data/app/views/govuk_publishing_components/components/_input.html.erb +0 -2
  25. data/app/views/govuk_publishing_components/components/_layout_footer.html.erb +23 -5
  26. data/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb +3 -8
  27. data/app/views/govuk_publishing_components/components/contextual_sidebar/_ukraine_cta.html.erb +18 -19
  28. data/app/views/govuk_publishing_components/components/docs/accordion.yml +22 -13
  29. data/app/views/govuk_publishing_components/components/docs/attachment.yml +0 -11
  30. data/app/views/govuk_publishing_components/components/docs/error_alert.yml +4 -0
  31. data/config/locales/ar.yml +1 -2
  32. data/config/locales/az.yml +1 -2
  33. data/config/locales/be.yml +1 -2
  34. data/config/locales/bg.yml +1 -2
  35. data/config/locales/bn.yml +1 -2
  36. data/config/locales/cs.yml +1 -2
  37. data/config/locales/cy.yml +1 -2
  38. data/config/locales/da.yml +1 -2
  39. data/config/locales/de.yml +1 -2
  40. data/config/locales/dr.yml +1 -2
  41. data/config/locales/el.yml +1 -2
  42. data/config/locales/en.yml +9 -2
  43. data/config/locales/es-419.yml +1 -2
  44. data/config/locales/es.yml +1 -2
  45. data/config/locales/et.yml +1 -2
  46. data/config/locales/fa.yml +1 -2
  47. data/config/locales/fi.yml +1 -2
  48. data/config/locales/fr.yml +1 -2
  49. data/config/locales/gd.yml +1 -2
  50. data/config/locales/gu.yml +1 -2
  51. data/config/locales/he.yml +1 -2
  52. data/config/locales/hi.yml +1 -2
  53. data/config/locales/hr.yml +1 -2
  54. data/config/locales/hu.yml +1 -2
  55. data/config/locales/hy.yml +1 -2
  56. data/config/locales/id.yml +1 -2
  57. data/config/locales/is.yml +1 -2
  58. data/config/locales/it.yml +1 -2
  59. data/config/locales/ja.yml +1 -2
  60. data/config/locales/ka.yml +1 -2
  61. data/config/locales/kk.yml +1 -2
  62. data/config/locales/ko.yml +1 -2
  63. data/config/locales/lt.yml +1 -2
  64. data/config/locales/lv.yml +1 -2
  65. data/config/locales/ms.yml +1 -2
  66. data/config/locales/mt.yml +1 -2
  67. data/config/locales/nl.yml +1 -2
  68. data/config/locales/no.yml +1 -2
  69. data/config/locales/pa-pk.yml +1 -2
  70. data/config/locales/pa.yml +1 -2
  71. data/config/locales/pl.yml +1 -2
  72. data/config/locales/ps.yml +1 -2
  73. data/config/locales/pt.yml +1 -2
  74. data/config/locales/ro.yml +1 -2
  75. data/config/locales/ru.yml +1 -2
  76. data/config/locales/si.yml +1 -2
  77. data/config/locales/sk.yml +1 -2
  78. data/config/locales/sl.yml +1 -2
  79. data/config/locales/so.yml +1 -2
  80. data/config/locales/sq.yml +1 -2
  81. data/config/locales/sr.yml +1 -2
  82. data/config/locales/sv.yml +1 -2
  83. data/config/locales/sw.yml +1 -2
  84. data/config/locales/ta.yml +1 -2
  85. data/config/locales/th.yml +1 -2
  86. data/config/locales/tk.yml +1 -2
  87. data/config/locales/tr.yml +1 -2
  88. data/config/locales/uk.yml +1 -2
  89. data/config/locales/ur.yml +1 -2
  90. data/config/locales/uz.yml +1 -2
  91. data/config/locales/vi.yml +1 -2
  92. data/config/locales/zh-hk.yml +1 -2
  93. data/config/locales/zh-tw.yml +1 -2
  94. data/config/locales/zh.yml +1 -2
  95. data/lib/govuk_publishing_components/presenters/attachment_helper.rb +0 -21
  96. data/lib/govuk_publishing_components/presenters/meta_tags.rb +6 -0
  97. data/lib/govuk_publishing_components/presenters/public_layout_helper.rb +35 -16
  98. data/lib/govuk_publishing_components/version.rb +1 -1
  99. data/node_modules/govuk-frontend/govuk/all.js +120 -49
  100. data/node_modules/govuk-frontend/govuk/components/back-link/macro-options.json +2 -2
  101. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/_index.scss +0 -2
  102. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/macro-options.json +2 -2
  103. data/node_modules/govuk-frontend/govuk/components/button/_index.scss +6 -16
  104. data/node_modules/govuk-frontend/govuk/components/button/macro-options.json +2 -2
  105. data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +120 -49
  106. data/node_modules/govuk-frontend/govuk/components/character-count/fixtures.json +33 -17
  107. data/node_modules/govuk-frontend/govuk/components/character-count/macro-options.json +2 -2
  108. data/node_modules/govuk-frontend/govuk/components/character-count/template.njk +1 -4
  109. data/node_modules/govuk-frontend/govuk/components/checkboxes/_index.scss +3 -2
  110. data/node_modules/govuk-frontend/govuk/components/checkboxes/fixtures.json +22 -10
  111. data/node_modules/govuk-frontend/govuk/components/checkboxes/macro-options.json +2 -2
  112. data/node_modules/govuk-frontend/govuk/components/date-input/fixtures.json +23 -23
  113. data/node_modules/govuk-frontend/govuk/components/date-input/template.njk +1 -1
  114. data/node_modules/govuk-frontend/govuk/components/details/macro-options.json +4 -4
  115. data/node_modules/govuk-frontend/govuk/components/error-message/macro-options.json +2 -2
  116. data/node_modules/govuk-frontend/govuk/components/error-summary/macro-options.json +3 -3
  117. data/node_modules/govuk-frontend/govuk/components/fieldset/macro-options.json +2 -2
  118. data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +12 -22
  119. data/node_modules/govuk-frontend/govuk/components/header/_index.scss +13 -3
  120. data/node_modules/govuk-frontend/govuk/components/header/macro-options.json +2 -2
  121. data/node_modules/govuk-frontend/govuk/components/hint/macro-options.json +2 -2
  122. data/node_modules/govuk-frontend/govuk/components/input/_index.scss +4 -13
  123. data/node_modules/govuk-frontend/govuk/components/input/macro-options.json +5 -5
  124. data/node_modules/govuk-frontend/govuk/components/inset-text/macro-options.json +2 -2
  125. data/node_modules/govuk-frontend/govuk/components/label/macro-options.json +2 -2
  126. data/node_modules/govuk-frontend/govuk/components/panel/_index.scss +1 -1
  127. data/node_modules/govuk-frontend/govuk/components/panel/macro-options.json +4 -4
  128. data/node_modules/govuk-frontend/govuk/components/phase-banner/macro-options.json +2 -2
  129. data/node_modules/govuk-frontend/govuk/components/radios/_index.scss +5 -4
  130. data/node_modules/govuk-frontend/govuk/components/radios/fixtures.json +17 -12
  131. data/node_modules/govuk-frontend/govuk/components/radios/macro-options.json +2 -2
  132. data/node_modules/govuk-frontend/govuk/components/skip-link/_index.scss +1 -3
  133. data/node_modules/govuk-frontend/govuk/components/skip-link/macro-options.json +2 -2
  134. data/node_modules/govuk-frontend/govuk/components/summary-list/macro-options.json +5 -5
  135. data/node_modules/govuk-frontend/govuk/components/table/macro-options.json +4 -4
  136. data/node_modules/govuk-frontend/govuk/components/tabs/macro-options.json +2 -2
  137. data/node_modules/govuk-frontend/govuk/components/tag/macro-options.json +2 -2
  138. data/node_modules/govuk-frontend/govuk/components/warning-text/macro-options.json +2 -2
  139. data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +3 -3
  140. data/node_modules/govuk-frontend/govuk/helpers/_links.scss +7 -5
  141. data/node_modules/govuk-frontend/govuk/helpers/_media-queries.scss +2 -2
  142. data/node_modules/govuk-frontend/govuk/helpers/_shape-arrow.scss +1 -1
  143. data/node_modules/govuk-frontend/govuk/helpers/_spacing.scss +3 -3
  144. data/node_modules/govuk-frontend/govuk/helpers/_typography.scss +2 -2
  145. data/node_modules/govuk-frontend/govuk/objects/_button-group.scss +10 -26
  146. data/node_modules/govuk-frontend/govuk/objects/_template.scss +1 -1
  147. data/node_modules/govuk-frontend/govuk/objects/_width-container.scss +0 -4
  148. data/node_modules/govuk-frontend/govuk/tools/_exports.scss +1 -1
  149. data/node_modules/govuk-frontend/govuk/tools/_font-url.scss +1 -1
  150. data/node_modules/govuk-frontend/govuk/tools/_image-url.scss +1 -1
  151. data/node_modules/govuk-frontend/govuk/tools/_px-to-em.scss +2 -2
  152. data/node_modules/govuk-frontend/govuk/tools/_px-to-rem.scss +1 -1
  153. data/node_modules/govuk-frontend/govuk-esm/all.mjs +88 -0
  154. data/node_modules/govuk-frontend/govuk-esm/common.mjs +28 -0
  155. data/node_modules/govuk-frontend/govuk-esm/components/accordion/accordion.mjs +374 -0
  156. data/node_modules/govuk-frontend/govuk-esm/components/button/button.mjs +64 -0
  157. data/node_modules/govuk-frontend/govuk-esm/components/character-count/character-count.mjs +251 -0
  158. data/node_modules/govuk-frontend/govuk-esm/components/checkboxes/checkboxes.mjs +164 -0
  159. data/node_modules/govuk-frontend/govuk-esm/components/details/details.mjs +147 -0
  160. data/node_modules/govuk-frontend/govuk-esm/components/error-summary/error-summary.mjs +168 -0
  161. data/node_modules/govuk-frontend/govuk-esm/components/header/header.mjs +52 -0
  162. data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs +55 -0
  163. data/node_modules/govuk-frontend/govuk-esm/components/radios/radios.mjs +122 -0
  164. data/node_modules/govuk-frontend/govuk-esm/components/skip-link/skip-link.mjs +94 -0
  165. data/node_modules/govuk-frontend/govuk-esm/components/tabs/tabs.mjs +282 -0
  166. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/DOMTokenList.js +264 -0
  167. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Document.js +26 -0
  168. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/classList.js +93 -0
  169. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/closest.js +24 -0
  170. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/matches.js +23 -0
  171. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/nextElementSibling.js +22 -0
  172. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/previousElementSibling.js +22 -0
  173. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element.js +114 -0
  174. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Event.js +252 -0
  175. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Function/prototype/bind.js +159 -0
  176. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Object/defineProperty.js +86 -0
  177. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Window.js +20 -0
  178. data/node_modules/govuk-frontend/package.json +8 -1
  179. metadata +29 -3
@@ -0,0 +1,168 @@
1
+ import '../../vendor/polyfills/Function/prototype/bind'
2
+ import '../../vendor/polyfills/Event' // addEventListener
3
+ import '../../vendor/polyfills/Element/prototype/closest'
4
+
5
+ function ErrorSummary ($module) {
6
+ this.$module = $module
7
+ }
8
+
9
+ ErrorSummary.prototype.init = function () {
10
+ var $module = this.$module
11
+ if (!$module) {
12
+ return
13
+ }
14
+
15
+ this.setFocus()
16
+ $module.addEventListener('click', this.handleClick.bind(this))
17
+ }
18
+
19
+ /**
20
+ * Focus the error summary
21
+ */
22
+ ErrorSummary.prototype.setFocus = function () {
23
+ var $module = this.$module
24
+
25
+ if ($module.getAttribute('data-disable-auto-focus') === 'true') {
26
+ return
27
+ }
28
+
29
+ // Set tabindex to -1 to make the element programmatically focusable, but
30
+ // remove it on blur as the error summary doesn't need to be focused again.
31
+ $module.setAttribute('tabindex', '-1')
32
+
33
+ $module.addEventListener('blur', function () {
34
+ $module.removeAttribute('tabindex')
35
+ })
36
+
37
+ $module.focus()
38
+ }
39
+
40
+ /**
41
+ * Click event handler
42
+ *
43
+ * @param {MouseEvent} event - Click event
44
+ */
45
+ ErrorSummary.prototype.handleClick = function (event) {
46
+ var target = event.target
47
+ if (this.focusTarget(target)) {
48
+ event.preventDefault()
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Focus the target element
54
+ *
55
+ * By default, the browser will scroll the target into view. Because our labels
56
+ * or legends appear above the input, this means the user will be presented with
57
+ * an input without any context, as the label or legend will be off the top of
58
+ * the screen.
59
+ *
60
+ * Manually handling the click event, scrolling the question into view and then
61
+ * focussing the element solves this.
62
+ *
63
+ * This also results in the label and/or legend being announced correctly in
64
+ * NVDA (as tested in 2018.3.2) - without this only the field type is announced
65
+ * (e.g. "Edit, has autocomplete").
66
+ *
67
+ * @param {HTMLElement} $target - Event target
68
+ * @returns {boolean} True if the target was able to be focussed
69
+ */
70
+ ErrorSummary.prototype.focusTarget = function ($target) {
71
+ // If the element that was clicked was not a link, return early
72
+ if ($target.tagName !== 'A' || $target.href === false) {
73
+ return false
74
+ }
75
+
76
+ var inputId = this.getFragmentFromUrl($target.href)
77
+ var $input = document.getElementById(inputId)
78
+ if (!$input) {
79
+ return false
80
+ }
81
+
82
+ var $legendOrLabel = this.getAssociatedLegendOrLabel($input)
83
+ if (!$legendOrLabel) {
84
+ return false
85
+ }
86
+
87
+ // Scroll the legend or label into view *before* calling focus on the input to
88
+ // avoid extra scrolling in browsers that don't support `preventScroll` (which
89
+ // at time of writing is most of them...)
90
+ $legendOrLabel.scrollIntoView()
91
+ $input.focus({ preventScroll: true })
92
+
93
+ return true
94
+ }
95
+
96
+ /**
97
+ * Get fragment from URL
98
+ *
99
+ * Extract the fragment (everything after the hash) from a URL, but not including
100
+ * the hash.
101
+ *
102
+ * @param {string} url - URL
103
+ * @returns {string} Fragment from URL, without the hash
104
+ */
105
+ ErrorSummary.prototype.getFragmentFromUrl = function (url) {
106
+ if (url.indexOf('#') === -1) {
107
+ return false
108
+ }
109
+
110
+ return url.split('#').pop()
111
+ }
112
+
113
+ /**
114
+ * Get associated legend or label
115
+ *
116
+ * Returns the first element that exists from this list:
117
+ *
118
+ * - The `<legend>` associated with the closest `<fieldset>` ancestor, as long
119
+ * as the top of it is no more than half a viewport height away from the
120
+ * bottom of the input
121
+ * - The first `<label>` that is associated with the input using for="inputId"
122
+ * - The closest parent `<label>`
123
+ *
124
+ * @param {HTMLElement} $input - The input
125
+ * @returns {HTMLElement} Associated legend or label, or null if no associated
126
+ * legend or label can be found
127
+ */
128
+ ErrorSummary.prototype.getAssociatedLegendOrLabel = function ($input) {
129
+ var $fieldset = $input.closest('fieldset')
130
+
131
+ if ($fieldset) {
132
+ var legends = $fieldset.getElementsByTagName('legend')
133
+
134
+ if (legends.length) {
135
+ var $candidateLegend = legends[0]
136
+
137
+ // If the input type is radio or checkbox, always use the legend if there
138
+ // is one.
139
+ if ($input.type === 'checkbox' || $input.type === 'radio') {
140
+ return $candidateLegend
141
+ }
142
+
143
+ // For other input types, only scroll to the fieldset’s legend (instead of
144
+ // the label associated with the input) if the input would end up in the
145
+ // top half of the screen.
146
+ //
147
+ // This should avoid situations where the input either ends up off the
148
+ // screen, or obscured by a software keyboard.
149
+ var legendTop = $candidateLegend.getBoundingClientRect().top
150
+ var inputRect = $input.getBoundingClientRect()
151
+
152
+ // If the browser doesn't support Element.getBoundingClientRect().height
153
+ // or window.innerHeight (like IE8), bail and just link to the label.
154
+ if (inputRect.height && window.innerHeight) {
155
+ var inputBottom = inputRect.top + inputRect.height
156
+
157
+ if (inputBottom - legendTop < window.innerHeight / 2) {
158
+ return $candidateLegend
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ return document.querySelector("label[for='" + $input.getAttribute('id') + "']") ||
165
+ $input.closest('label')
166
+ }
167
+
168
+ export default ErrorSummary
@@ -0,0 +1,52 @@
1
+ import '../../vendor/polyfills/Event'
2
+ import '../../vendor/polyfills/Element/prototype/classList'
3
+ import '../../vendor/polyfills/Function/prototype/bind'
4
+
5
+ function Header ($module) {
6
+ this.$module = $module
7
+ this.$menuButton = $module && $module.querySelector('.govuk-js-header-toggle')
8
+ this.$menu = this.$menuButton && $module.querySelector(
9
+ '#' + this.$menuButton.getAttribute('aria-controls')
10
+ )
11
+ }
12
+
13
+ /**
14
+ * Initialise header
15
+ *
16
+ * Check for the presence of the header, menu and menu button – if any are
17
+ * missing then there's nothing to do so return early.
18
+ */
19
+ Header.prototype.init = function () {
20
+ if (!this.$module || !this.$menuButton || !this.$menu) {
21
+ return
22
+ }
23
+
24
+ this.syncState(this.$menu.classList.contains('govuk-header__navigation-list--open'))
25
+ this.$menuButton.addEventListener('click', this.handleMenuButtonClick.bind(this))
26
+ }
27
+
28
+ /**
29
+ * Sync menu state
30
+ *
31
+ * Sync the menu button class and the accessible state of the menu and the menu
32
+ * button with the visible state of the menu
33
+ *
34
+ * @param {boolean} isVisible Whether the menu is currently visible
35
+ */
36
+ Header.prototype.syncState = function (isVisible) {
37
+ this.$menuButton.classList.toggle('govuk-header__menu-button--open', isVisible)
38
+ this.$menuButton.setAttribute('aria-expanded', isVisible)
39
+ }
40
+
41
+ /**
42
+ * Handle menu button click
43
+ *
44
+ * When the menu button is clicked, change the visibility of the menu and then
45
+ * sync the accessibility state and menu button state
46
+ */
47
+ Header.prototype.handleMenuButtonClick = function () {
48
+ var isVisible = this.$menu.classList.toggle('govuk-header__navigation-list--open')
49
+ this.syncState(isVisible)
50
+ }
51
+
52
+ export default Header
@@ -0,0 +1,55 @@
1
+ import '../../vendor/polyfills/Event' // addEventListener
2
+
3
+ function NotificationBanner ($module) {
4
+ this.$module = $module
5
+ }
6
+
7
+ /**
8
+ * Initialise the component
9
+ */
10
+ NotificationBanner.prototype.init = function () {
11
+ var $module = this.$module
12
+ // Check for module
13
+ if (!$module) {
14
+ return
15
+ }
16
+
17
+ this.setFocus()
18
+ }
19
+
20
+ /**
21
+ * Focus the element
22
+ *
23
+ * If `role="alert"` is set, focus the element to help some assistive technologies
24
+ * prioritise announcing it.
25
+ *
26
+ * You can turn off the auto-focus functionality by setting `data-disable-auto-focus="true"` in the
27
+ * component HTML. You might wish to do this based on user research findings, or to avoid a clash
28
+ * with another element which should be focused when the page loads.
29
+ */
30
+ NotificationBanner.prototype.setFocus = function () {
31
+ var $module = this.$module
32
+
33
+ if ($module.getAttribute('data-disable-auto-focus') === 'true') {
34
+ return
35
+ }
36
+
37
+ if ($module.getAttribute('role') !== 'alert') {
38
+ return
39
+ }
40
+
41
+ // Set tabindex to -1 to make the element focusable with JavaScript.
42
+ // Remove the tabindex on blur as the component doesn't need to be focusable after the page has
43
+ // loaded.
44
+ if (!$module.getAttribute('tabindex')) {
45
+ $module.setAttribute('tabindex', '-1')
46
+
47
+ $module.addEventListener('blur', function () {
48
+ $module.removeAttribute('tabindex')
49
+ })
50
+ }
51
+
52
+ $module.focus()
53
+ }
54
+
55
+ export default NotificationBanner
@@ -0,0 +1,122 @@
1
+ import '../../vendor/polyfills/Function/prototype/bind'
2
+ // addEventListener, event.target normalization and DOMContentLoaded
3
+ import '../../vendor/polyfills/Event'
4
+ import '../../vendor/polyfills/Element/prototype/classList'
5
+ import { nodeListForEach } from '../../common'
6
+
7
+ function Radios ($module) {
8
+ this.$module = $module
9
+ this.$inputs = $module.querySelectorAll('input[type="radio"]')
10
+ }
11
+
12
+ /**
13
+ * Initialise Radios
14
+ *
15
+ * Radios can be associated with a 'conditionally revealed' content block – for
16
+ * example, a radio for 'Phone' could reveal an additional form field for the
17
+ * user to enter their phone number.
18
+ *
19
+ * These associations are made using a `data-aria-controls` attribute, which is
20
+ * promoted to an aria-controls attribute during initialisation.
21
+ *
22
+ * We also need to restore the state of any conditional reveals on the page (for
23
+ * example if the user has navigated back), and set up event handlers to keep
24
+ * the reveal in sync with the radio state.
25
+ */
26
+ Radios.prototype.init = function () {
27
+ var $module = this.$module
28
+ var $inputs = this.$inputs
29
+
30
+ nodeListForEach($inputs, function ($input) {
31
+ var target = $input.getAttribute('data-aria-controls')
32
+
33
+ // Skip radios without data-aria-controls attributes, or where the
34
+ // target element does not exist.
35
+ if (!target || !document.getElementById(target)) {
36
+ return
37
+ }
38
+
39
+ // Promote the data-aria-controls attribute to a aria-controls attribute
40
+ // so that the relationship is exposed in the AOM
41
+ $input.setAttribute('aria-controls', target)
42
+ $input.removeAttribute('data-aria-controls')
43
+ })
44
+
45
+ // When the page is restored after navigating 'back' in some browsers the
46
+ // state of form controls is not restored until *after* the DOMContentLoaded
47
+ // event is fired, so we need to sync after the pageshow event in browsers
48
+ // that support it.
49
+ if ('onpageshow' in window) {
50
+ window.addEventListener('pageshow', this.syncAllConditionalReveals.bind(this))
51
+ } else {
52
+ window.addEventListener('DOMContentLoaded', this.syncAllConditionalReveals.bind(this))
53
+ }
54
+
55
+ // Although we've set up handlers to sync state on the pageshow or
56
+ // DOMContentLoaded event, init could be called after those events have fired,
57
+ // for example if they are added to the page dynamically, so sync now too.
58
+ this.syncAllConditionalReveals()
59
+
60
+ // Handle events
61
+ $module.addEventListener('click', this.handleClick.bind(this))
62
+ }
63
+
64
+ /**
65
+ * Sync the conditional reveal states for all inputs in this $module.
66
+ */
67
+ Radios.prototype.syncAllConditionalReveals = function () {
68
+ nodeListForEach(this.$inputs, this.syncConditionalRevealWithInputState.bind(this))
69
+ }
70
+
71
+ /**
72
+ * Sync conditional reveal with the input state
73
+ *
74
+ * Synchronise the visibility of the conditional reveal, and its accessible
75
+ * state, with the input's checked state.
76
+ *
77
+ * @param {HTMLInputElement} $input Radio input
78
+ */
79
+ Radios.prototype.syncConditionalRevealWithInputState = function ($input) {
80
+ var $target = document.getElementById($input.getAttribute('aria-controls'))
81
+
82
+ if ($target && $target.classList.contains('govuk-radios__conditional')) {
83
+ var inputIsChecked = $input.checked
84
+
85
+ $input.setAttribute('aria-expanded', inputIsChecked)
86
+ $target.classList.toggle('govuk-radios__conditional--hidden', !inputIsChecked)
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Click event handler
92
+ *
93
+ * Handle a click within the $module – if the click occurred on a radio, sync
94
+ * the state of the conditional reveal for all radio buttons in the same form
95
+ * with the same name (because checking one radio could have un-checked a radio
96
+ * in another $module)
97
+ *
98
+ * @param {MouseEvent} event Click event
99
+ */
100
+ Radios.prototype.handleClick = function (event) {
101
+ var $clickedInput = event.target
102
+
103
+ // Ignore clicks on things that aren't radio buttons
104
+ if ($clickedInput.type !== 'radio') {
105
+ return
106
+ }
107
+
108
+ // We only need to consider radios with conditional reveals, which will have
109
+ // aria-controls attributes.
110
+ var $allInputs = document.querySelectorAll('input[type="radio"][aria-controls]')
111
+
112
+ nodeListForEach($allInputs, function ($input) {
113
+ var hasSameFormOwner = ($input.form === $clickedInput.form)
114
+ var hasSameName = ($input.name === $clickedInput.name)
115
+
116
+ if (hasSameName && hasSameFormOwner) {
117
+ this.syncConditionalRevealWithInputState($input)
118
+ }
119
+ }.bind(this))
120
+ }
121
+
122
+ export default Radios
@@ -0,0 +1,94 @@
1
+ import '../../vendor/polyfills/Function/prototype/bind'
2
+ import '../../vendor/polyfills/Element/prototype/classList'
3
+ import '../../vendor/polyfills/Event' // addEventListener and event.target normalization
4
+
5
+ function SkipLink ($module) {
6
+ this.$module = $module
7
+ this.$linkedElement = null
8
+ this.linkedElementListener = false
9
+ }
10
+
11
+ /**
12
+ * Initialise the component
13
+ */
14
+ SkipLink.prototype.init = function () {
15
+ // Check for module
16
+ if (!this.$module) {
17
+ return
18
+ }
19
+
20
+ // Check for linked element
21
+ this.$linkedElement = this.getLinkedElement()
22
+ if (!this.$linkedElement) {
23
+ return
24
+ }
25
+
26
+ this.$module.addEventListener('click', this.focusLinkedElement.bind(this))
27
+ }
28
+
29
+ /**
30
+ * Get linked element
31
+ *
32
+ * @returns {HTMLElement} $linkedElement - DOM element linked to from the skip link
33
+ */
34
+ SkipLink.prototype.getLinkedElement = function () {
35
+ var linkedElementId = this.getFragmentFromUrl()
36
+
37
+ if (!linkedElementId) {
38
+ return false
39
+ }
40
+
41
+ return document.getElementById(linkedElementId)
42
+ }
43
+
44
+ /**
45
+ * Focus the linked element
46
+ *
47
+ * Set tabindex and helper CSS class. Set listener to remove them on blur.
48
+ */
49
+ SkipLink.prototype.focusLinkedElement = function () {
50
+ var $linkedElement = this.$linkedElement
51
+
52
+ if (!$linkedElement.getAttribute('tabindex')) {
53
+ // Set the element tabindex to -1 so it can be focused with JavaScript.
54
+ $linkedElement.setAttribute('tabindex', '-1')
55
+ $linkedElement.classList.add('govuk-skip-link-focused-element')
56
+
57
+ // Add listener for blur on the focused element (unless the listener has previously been added)
58
+ if (!this.linkedElementListener) {
59
+ this.$linkedElement.addEventListener('blur', this.removeFocusProperties.bind(this))
60
+ this.linkedElementListener = true
61
+ }
62
+ }
63
+ $linkedElement.focus()
64
+ }
65
+
66
+ /**
67
+ * Remove the tabindex that makes the linked element focusable because the element only needs to be
68
+ * focusable until it has received programmatic focus and a screen reader has announced it.
69
+ *
70
+ * Remove the CSS class that removes the native focus styles.
71
+ */
72
+ SkipLink.prototype.removeFocusProperties = function () {
73
+ this.$linkedElement.removeAttribute('tabindex')
74
+ this.$linkedElement.classList.remove('govuk-skip-link-focused-element')
75
+ }
76
+
77
+ /**
78
+ * Get fragment from URL
79
+ *
80
+ * Extract the fragment (everything after the hash symbol) from a URL, but not including
81
+ * the symbol.
82
+ *
83
+ * @returns {string} Fragment from URL, without the hash symbol
84
+ */
85
+ SkipLink.prototype.getFragmentFromUrl = function () {
86
+ // Bail if the anchor link doesn't have a hash
87
+ if (!this.$module.hash) {
88
+ return false
89
+ }
90
+
91
+ return this.$module.hash.split('#').pop()
92
+ }
93
+
94
+ export default SkipLink