govuk_publishing_components 29.9.0 → 29.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/govuk_publishing_components/analytics/page-content.js +4 -4
- data/app/assets/stylesheets/govuk_publishing_components/components/_contextual-sidebar.scss +20 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +3 -8
- data/app/assets/stylesheets/govuk_publishing_components/components/govspeak/_attachment.scss +7 -1
- data/app/controllers/govuk_publishing_components/audit_controller.rb +3 -2
- data/app/controllers/govuk_publishing_components/component_guide_controller.rb +0 -9
- data/app/models/govuk_publishing_components/audit_comparer.rb +92 -34
- data/app/views/govuk_publishing_components/audit/_applications.html.erb +20 -9
- data/app/views/govuk_publishing_components/component_guide/index.html.erb +1 -19
- data/app/views/govuk_publishing_components/components/_layout_footer.html.erb +20 -2
- data/app/views/govuk_publishing_components/components/contextual_sidebar/_ukraine_cta.html.erb +18 -19
- data/config/locales/ar.yml +1 -2
- data/config/locales/az.yml +1 -2
- data/config/locales/be.yml +1 -2
- data/config/locales/bg.yml +1 -2
- data/config/locales/bn.yml +1 -2
- data/config/locales/cs.yml +1 -2
- data/config/locales/cy.yml +1 -2
- data/config/locales/da.yml +1 -2
- data/config/locales/de.yml +1 -2
- data/config/locales/dr.yml +1 -2
- data/config/locales/el.yml +1 -2
- data/config/locales/en.yml +9 -2
- data/config/locales/es-419.yml +1 -2
- data/config/locales/es.yml +1 -2
- data/config/locales/et.yml +1 -2
- data/config/locales/fa.yml +1 -2
- data/config/locales/fi.yml +1 -2
- data/config/locales/fr.yml +1 -2
- data/config/locales/gd.yml +1 -2
- data/config/locales/gu.yml +1 -2
- data/config/locales/he.yml +1 -2
- data/config/locales/hi.yml +1 -2
- data/config/locales/hr.yml +1 -2
- data/config/locales/hu.yml +1 -2
- data/config/locales/hy.yml +1 -2
- data/config/locales/id.yml +1 -2
- data/config/locales/is.yml +1 -2
- data/config/locales/it.yml +1 -2
- data/config/locales/ja.yml +1 -2
- data/config/locales/ka.yml +1 -2
- data/config/locales/kk.yml +1 -2
- data/config/locales/ko.yml +1 -2
- data/config/locales/lt.yml +1 -2
- data/config/locales/lv.yml +1 -2
- data/config/locales/ms.yml +1 -2
- data/config/locales/mt.yml +1 -2
- data/config/locales/nl.yml +1 -2
- data/config/locales/no.yml +1 -2
- data/config/locales/pa-pk.yml +1 -2
- data/config/locales/pa.yml +1 -2
- data/config/locales/pl.yml +1 -2
- data/config/locales/ps.yml +1 -2
- data/config/locales/pt.yml +1 -2
- data/config/locales/ro.yml +1 -2
- data/config/locales/ru.yml +1 -2
- data/config/locales/si.yml +1 -2
- data/config/locales/sk.yml +1 -2
- data/config/locales/sl.yml +1 -2
- data/config/locales/so.yml +1 -2
- data/config/locales/sq.yml +1 -2
- data/config/locales/sr.yml +1 -2
- data/config/locales/sv.yml +1 -2
- data/config/locales/sw.yml +1 -2
- data/config/locales/ta.yml +1 -2
- data/config/locales/th.yml +1 -2
- data/config/locales/tk.yml +1 -2
- data/config/locales/tr.yml +1 -2
- data/config/locales/uk.yml +1 -2
- data/config/locales/ur.yml +1 -2
- data/config/locales/uz.yml +1 -2
- data/config/locales/vi.yml +1 -2
- data/config/locales/zh-hk.yml +1 -2
- data/config/locales/zh-tw.yml +1 -2
- data/config/locales/zh.yml +1 -2
- data/lib/govuk_publishing_components/presenters/attachment_helper.rb +1 -2
- data/lib/govuk_publishing_components/presenters/public_layout_helper.rb +35 -16
- data/lib/govuk_publishing_components/version.rb +1 -1
- data/node_modules/govuk-frontend/govuk/all.js +120 -49
- data/node_modules/govuk-frontend/govuk/components/back-link/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/breadcrumbs/_index.scss +0 -2
- data/node_modules/govuk-frontend/govuk/components/breadcrumbs/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/button/_index.scss +6 -16
- data/node_modules/govuk-frontend/govuk/components/button/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +120 -49
- data/node_modules/govuk-frontend/govuk/components/character-count/fixtures.json +33 -17
- data/node_modules/govuk-frontend/govuk/components/character-count/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/character-count/template.njk +1 -4
- data/node_modules/govuk-frontend/govuk/components/checkboxes/_index.scss +3 -2
- data/node_modules/govuk-frontend/govuk/components/checkboxes/fixtures.json +22 -10
- data/node_modules/govuk-frontend/govuk/components/checkboxes/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/date-input/fixtures.json +23 -23
- data/node_modules/govuk-frontend/govuk/components/date-input/template.njk +1 -1
- data/node_modules/govuk-frontend/govuk/components/details/macro-options.json +4 -4
- data/node_modules/govuk-frontend/govuk/components/error-message/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/error-summary/macro-options.json +3 -3
- data/node_modules/govuk-frontend/govuk/components/fieldset/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +12 -22
- data/node_modules/govuk-frontend/govuk/components/header/_index.scss +13 -3
- data/node_modules/govuk-frontend/govuk/components/header/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/hint/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/input/_index.scss +4 -13
- data/node_modules/govuk-frontend/govuk/components/input/macro-options.json +5 -5
- data/node_modules/govuk-frontend/govuk/components/inset-text/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/label/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/panel/_index.scss +1 -1
- data/node_modules/govuk-frontend/govuk/components/panel/macro-options.json +4 -4
- data/node_modules/govuk-frontend/govuk/components/phase-banner/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/radios/_index.scss +5 -4
- data/node_modules/govuk-frontend/govuk/components/radios/fixtures.json +17 -12
- data/node_modules/govuk-frontend/govuk/components/radios/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/skip-link/_index.scss +1 -3
- data/node_modules/govuk-frontend/govuk/components/skip-link/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/summary-list/macro-options.json +5 -5
- data/node_modules/govuk-frontend/govuk/components/table/macro-options.json +4 -4
- data/node_modules/govuk-frontend/govuk/components/tabs/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/tag/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/warning-text/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +3 -3
- data/node_modules/govuk-frontend/govuk/helpers/_links.scss +7 -5
- data/node_modules/govuk-frontend/govuk/helpers/_media-queries.scss +2 -2
- data/node_modules/govuk-frontend/govuk/helpers/_shape-arrow.scss +1 -1
- data/node_modules/govuk-frontend/govuk/helpers/_spacing.scss +3 -3
- data/node_modules/govuk-frontend/govuk/helpers/_typography.scss +2 -2
- data/node_modules/govuk-frontend/govuk/objects/_button-group.scss +10 -26
- data/node_modules/govuk-frontend/govuk/objects/_template.scss +1 -1
- data/node_modules/govuk-frontend/govuk/objects/_width-container.scss +0 -4
- data/node_modules/govuk-frontend/govuk/tools/_exports.scss +1 -1
- data/node_modules/govuk-frontend/govuk/tools/_font-url.scss +1 -1
- data/node_modules/govuk-frontend/govuk/tools/_image-url.scss +1 -1
- data/node_modules/govuk-frontend/govuk/tools/_px-to-em.scss +2 -2
- data/node_modules/govuk-frontend/govuk/tools/_px-to-rem.scss +1 -1
- data/node_modules/govuk-frontend/govuk-esm/all.mjs +88 -0
- data/node_modules/govuk-frontend/govuk-esm/common.mjs +28 -0
- data/node_modules/govuk-frontend/govuk-esm/components/accordion/accordion.mjs +374 -0
- data/node_modules/govuk-frontend/govuk-esm/components/button/button.mjs +64 -0
- data/node_modules/govuk-frontend/govuk-esm/components/character-count/character-count.mjs +251 -0
- data/node_modules/govuk-frontend/govuk-esm/components/checkboxes/checkboxes.mjs +164 -0
- data/node_modules/govuk-frontend/govuk-esm/components/details/details.mjs +147 -0
- data/node_modules/govuk-frontend/govuk-esm/components/error-summary/error-summary.mjs +168 -0
- data/node_modules/govuk-frontend/govuk-esm/components/header/header.mjs +52 -0
- data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs +55 -0
- data/node_modules/govuk-frontend/govuk-esm/components/radios/radios.mjs +122 -0
- data/node_modules/govuk-frontend/govuk-esm/components/skip-link/skip-link.mjs +94 -0
- data/node_modules/govuk-frontend/govuk-esm/components/tabs/tabs.mjs +282 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/DOMTokenList.js +264 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Document.js +26 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/classList.js +93 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/closest.js +24 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/matches.js +23 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/nextElementSibling.js +22 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/previousElementSibling.js +22 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element.js +114 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Event.js +252 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Function/prototype/bind.js +159 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Object/defineProperty.js +86 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Window.js +20 -0
- data/node_modules/govuk-frontend/package.json +8 -1
- metadata +27 -2
@@ -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
|
data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs
ADDED
@@ -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
|