govuk_publishing_components 29.7.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/explicit-cross-domain-links.js +72 -73
- data/app/assets/javascripts/govuk_publishing_components/analytics/page-content.js +13 -2
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/gtm-click-tracking.js +49 -0
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js +1 -0
- data/app/assets/javascripts/govuk_publishing_components/components/accordion.js +0 -1
- data/app/assets/javascripts/govuk_publishing_components/modules.js +0 -1
- data/app/assets/javascripts/govuk_publishing_components/vendor/lux/lux-measurer.js +37 -0
- data/app/assets/javascripts/govuk_publishing_components/vendor/lux/lux-reporter.js +166 -142
- data/app/assets/stylesheets/govuk_publishing_components/components/_cards.scss +11 -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 +8 -2
- 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 +3 -21
- data/app/views/govuk_publishing_components/component_guide/show.html.erb +1 -1
- data/app/views/govuk_publishing_components/components/_cards.html.erb +13 -11
- data/app/views/govuk_publishing_components/components/_layout_footer.html.erb +20 -2
- data/app/views/govuk_publishing_components/components/_summary_list.html.erb +4 -5
- data/app/views/govuk_publishing_components/components/contextual_sidebar/_ukraine_cta.html.erb +18 -19
- data/app/views/govuk_publishing_components/components/docs/contents_list.yml +1 -1
- data/app/views/govuk_publishing_components/components/docs/document_list.yml +4 -4
- data/app/views/govuk_publishing_components/components/docs/heading.yml +1 -1
- data/app/views/govuk_publishing_components/components/docs/image_card.yml +1 -1
- data/app/views/govuk_publishing_components/components/docs/meta_tags.yml +4 -4
- data/app/views/govuk_publishing_components/components/docs/metadata.yml +1 -1
- data/app/views/govuk_publishing_components/components/docs/share_links.yml +1 -1
- data/app/views/govuk_publishing_components/components/docs/subscription_links.yml +1 -1
- data/app/views/govuk_publishing_components/components/docs/translation_nav.yml +1 -2
- 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/generators/govuk_publishing_components/templates/_component.html.erb +1 -1
- data/lib/govuk_publishing_components/app_helpers/brand_helper.rb +1 -1
- data/lib/govuk_publishing_components/presenters/attachment_helper.rb +1 -3
- 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 +30 -3
@@ -0,0 +1,64 @@
|
|
1
|
+
import '../../vendor/polyfills/Event' // addEventListener and event.target normaliziation
|
2
|
+
import '../../vendor/polyfills/Function/prototype/bind'
|
3
|
+
|
4
|
+
var KEY_SPACE = 32
|
5
|
+
var DEBOUNCE_TIMEOUT_IN_SECONDS = 1
|
6
|
+
|
7
|
+
function Button ($module) {
|
8
|
+
this.$module = $module
|
9
|
+
this.debounceFormSubmitTimer = null
|
10
|
+
}
|
11
|
+
|
12
|
+
/**
|
13
|
+
* JavaScript 'shim' to trigger the click event of element(s) when the space key is pressed.
|
14
|
+
*
|
15
|
+
* Created since some Assistive Technologies (for example some Screenreaders)
|
16
|
+
* will tell a user to press space on a 'button', so this functionality needs to be shimmed
|
17
|
+
* See https://github.com/alphagov/govuk_elements/pull/272#issuecomment-233028270
|
18
|
+
*
|
19
|
+
* @param {object} event event
|
20
|
+
*/
|
21
|
+
Button.prototype.handleKeyDown = function (event) {
|
22
|
+
// get the target element
|
23
|
+
var target = event.target
|
24
|
+
// if the element has a role='button' and the pressed key is a space, we'll simulate a click
|
25
|
+
if (target.getAttribute('role') === 'button' && event.keyCode === KEY_SPACE) {
|
26
|
+
event.preventDefault()
|
27
|
+
// trigger the target's click event
|
28
|
+
target.click()
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
/**
|
33
|
+
* If the click quickly succeeds a previous click then nothing will happen.
|
34
|
+
* This stops people accidentally causing multiple form submissions by
|
35
|
+
* double clicking buttons.
|
36
|
+
*/
|
37
|
+
Button.prototype.debounce = function (event) {
|
38
|
+
var target = event.target
|
39
|
+
// Check the button that is clicked on has the preventDoubleClick feature enabled
|
40
|
+
if (target.getAttribute('data-prevent-double-click') !== 'true') {
|
41
|
+
return
|
42
|
+
}
|
43
|
+
|
44
|
+
// If the timer is still running then we want to prevent the click from submitting the form
|
45
|
+
if (this.debounceFormSubmitTimer) {
|
46
|
+
event.preventDefault()
|
47
|
+
return false
|
48
|
+
}
|
49
|
+
|
50
|
+
this.debounceFormSubmitTimer = setTimeout(function () {
|
51
|
+
this.debounceFormSubmitTimer = null
|
52
|
+
}.bind(this), DEBOUNCE_TIMEOUT_IN_SECONDS * 1000)
|
53
|
+
}
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Initialise an event listener for keydown at document level
|
57
|
+
* this will help listening for later inserted elements with a role="button"
|
58
|
+
*/
|
59
|
+
Button.prototype.init = function () {
|
60
|
+
this.$module.addEventListener('keydown', this.handleKeyDown)
|
61
|
+
this.$module.addEventListener('click', this.debounce)
|
62
|
+
}
|
63
|
+
|
64
|
+
export default Button
|
@@ -0,0 +1,251 @@
|
|
1
|
+
import '../../vendor/polyfills/Function/prototype/bind'
|
2
|
+
import '../../vendor/polyfills/Event' // addEventListener and event.target normaliziation
|
3
|
+
import '../../vendor/polyfills/Element/prototype/classList'
|
4
|
+
|
5
|
+
function CharacterCount ($module) {
|
6
|
+
this.$module = $module
|
7
|
+
this.$textarea = $module.querySelector('.govuk-js-character-count')
|
8
|
+
this.$visibleCountMessage = null
|
9
|
+
this.$screenReaderCountMessage = null
|
10
|
+
this.lastInputTimestamp = null
|
11
|
+
}
|
12
|
+
|
13
|
+
CharacterCount.prototype.defaults = {
|
14
|
+
characterCountAttribute: 'data-maxlength',
|
15
|
+
wordCountAttribute: 'data-maxwords'
|
16
|
+
}
|
17
|
+
|
18
|
+
// Initialize component
|
19
|
+
CharacterCount.prototype.init = function () {
|
20
|
+
// Check that required elements are present
|
21
|
+
if (!this.$textarea) {
|
22
|
+
return
|
23
|
+
}
|
24
|
+
|
25
|
+
// Check for module
|
26
|
+
var $module = this.$module
|
27
|
+
var $textarea = this.$textarea
|
28
|
+
var $fallbackLimitMessage = document.getElementById($textarea.id + '-info')
|
29
|
+
|
30
|
+
// Move the fallback count message to be immediately after the textarea
|
31
|
+
// Kept for backwards compatibility
|
32
|
+
$textarea.insertAdjacentElement('afterend', $fallbackLimitMessage)
|
33
|
+
|
34
|
+
// Create the *screen reader* specific live-updating counter
|
35
|
+
// This doesn't need any styling classes, as it is never visible
|
36
|
+
var $screenReaderCountMessage = document.createElement('div')
|
37
|
+
$screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden'
|
38
|
+
$screenReaderCountMessage.setAttribute('aria-live', 'polite')
|
39
|
+
this.$screenReaderCountMessage = $screenReaderCountMessage
|
40
|
+
$fallbackLimitMessage.insertAdjacentElement('afterend', $screenReaderCountMessage)
|
41
|
+
|
42
|
+
// Create our live-updating counter element, copying the classes from the
|
43
|
+
// fallback element for backwards compatibility as these may have been configured
|
44
|
+
var $visibleCountMessage = document.createElement('div')
|
45
|
+
$visibleCountMessage.className = $fallbackLimitMessage.className
|
46
|
+
$visibleCountMessage.classList.add('govuk-character-count__status')
|
47
|
+
$visibleCountMessage.setAttribute('aria-hidden', 'true')
|
48
|
+
this.$visibleCountMessage = $visibleCountMessage
|
49
|
+
$fallbackLimitMessage.insertAdjacentElement('afterend', $visibleCountMessage)
|
50
|
+
|
51
|
+
// Hide the fallback limit message
|
52
|
+
$fallbackLimitMessage.classList.add('govuk-visually-hidden')
|
53
|
+
|
54
|
+
// Read options set using dataset ('data-' values)
|
55
|
+
this.options = this.getDataset($module)
|
56
|
+
|
57
|
+
// Determine the limit attribute (characters or words)
|
58
|
+
var countAttribute = this.defaults.characterCountAttribute
|
59
|
+
if (this.options.maxwords) {
|
60
|
+
countAttribute = this.defaults.wordCountAttribute
|
61
|
+
}
|
62
|
+
|
63
|
+
// Save the element limit
|
64
|
+
this.maxLength = $module.getAttribute(countAttribute)
|
65
|
+
|
66
|
+
// Check for limit
|
67
|
+
if (!this.maxLength) {
|
68
|
+
return
|
69
|
+
}
|
70
|
+
|
71
|
+
// Remove hard limit if set
|
72
|
+
$textarea.removeAttribute('maxlength')
|
73
|
+
|
74
|
+
this.bindChangeEvents()
|
75
|
+
|
76
|
+
// When the page is restored after navigating 'back' in some browsers the
|
77
|
+
// state of the character count is not restored until *after* the DOMContentLoaded
|
78
|
+
// event is fired, so we need to manually update it after the pageshow event
|
79
|
+
// in browsers that support it.
|
80
|
+
if ('onpageshow' in window) {
|
81
|
+
window.addEventListener('pageshow', this.updateCountMessage.bind(this))
|
82
|
+
} else {
|
83
|
+
window.addEventListener('DOMContentLoaded', this.updateCountMessage.bind(this))
|
84
|
+
}
|
85
|
+
this.updateCountMessage()
|
86
|
+
}
|
87
|
+
|
88
|
+
// Read data attributes
|
89
|
+
CharacterCount.prototype.getDataset = function (element) {
|
90
|
+
var dataset = {}
|
91
|
+
var attributes = element.attributes
|
92
|
+
if (attributes) {
|
93
|
+
for (var i = 0; i < attributes.length; i++) {
|
94
|
+
var attribute = attributes[i]
|
95
|
+
var match = attribute.name.match(/^data-(.+)/)
|
96
|
+
if (match) {
|
97
|
+
dataset[match[1]] = attribute.value
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
return dataset
|
102
|
+
}
|
103
|
+
|
104
|
+
// Counts characters or words in text
|
105
|
+
CharacterCount.prototype.count = function (text) {
|
106
|
+
var length
|
107
|
+
if (this.options.maxwords) {
|
108
|
+
var tokens = text.match(/\S+/g) || [] // Matches consecutive non-whitespace chars
|
109
|
+
length = tokens.length
|
110
|
+
} else {
|
111
|
+
length = text.length
|
112
|
+
}
|
113
|
+
return length
|
114
|
+
}
|
115
|
+
|
116
|
+
// Bind input propertychange to the elements and update based on the change
|
117
|
+
CharacterCount.prototype.bindChangeEvents = function () {
|
118
|
+
var $textarea = this.$textarea
|
119
|
+
$textarea.addEventListener('keyup', this.handleKeyUp.bind(this))
|
120
|
+
|
121
|
+
// Bind focus/blur events to start/stop polling
|
122
|
+
$textarea.addEventListener('focus', this.handleFocus.bind(this))
|
123
|
+
$textarea.addEventListener('blur', this.handleBlur.bind(this))
|
124
|
+
}
|
125
|
+
|
126
|
+
// Speech recognition software such as Dragon NaturallySpeaking will modify the
|
127
|
+
// fields by directly changing its `value`. These changes don't trigger events
|
128
|
+
// in JavaScript, so we need to poll to handle when and if they occur.
|
129
|
+
CharacterCount.prototype.checkIfValueChanged = function () {
|
130
|
+
if (!this.$textarea.oldValue) this.$textarea.oldValue = ''
|
131
|
+
if (this.$textarea.value !== this.$textarea.oldValue) {
|
132
|
+
this.$textarea.oldValue = this.$textarea.value
|
133
|
+
this.updateCountMessage()
|
134
|
+
}
|
135
|
+
}
|
136
|
+
|
137
|
+
// Helper function to update both the visible and screen reader-specific
|
138
|
+
// counters simultaneously (e.g. on init)
|
139
|
+
CharacterCount.prototype.updateCountMessage = function () {
|
140
|
+
this.updateVisibleCountMessage()
|
141
|
+
this.updateScreenReaderCountMessage()
|
142
|
+
}
|
143
|
+
|
144
|
+
// Update visible counter
|
145
|
+
CharacterCount.prototype.updateVisibleCountMessage = function () {
|
146
|
+
var $textarea = this.$textarea
|
147
|
+
var $visibleCountMessage = this.$visibleCountMessage
|
148
|
+
var remainingNumber = this.maxLength - this.count($textarea.value)
|
149
|
+
|
150
|
+
// If input is over the threshold, remove the disabled class which renders the
|
151
|
+
// counter invisible.
|
152
|
+
if (this.isOverThreshold()) {
|
153
|
+
$visibleCountMessage.classList.remove('govuk-character-count__message--disabled')
|
154
|
+
} else {
|
155
|
+
$visibleCountMessage.classList.add('govuk-character-count__message--disabled')
|
156
|
+
}
|
157
|
+
|
158
|
+
// Update styles
|
159
|
+
if (remainingNumber < 0) {
|
160
|
+
$textarea.classList.add('govuk-textarea--error')
|
161
|
+
$visibleCountMessage.classList.remove('govuk-hint')
|
162
|
+
$visibleCountMessage.classList.add('govuk-error-message')
|
163
|
+
} else {
|
164
|
+
$textarea.classList.remove('govuk-textarea--error')
|
165
|
+
$visibleCountMessage.classList.remove('govuk-error-message')
|
166
|
+
$visibleCountMessage.classList.add('govuk-hint')
|
167
|
+
}
|
168
|
+
|
169
|
+
// Update message
|
170
|
+
$visibleCountMessage.innerHTML = this.formattedUpdateMessage()
|
171
|
+
}
|
172
|
+
|
173
|
+
// Update screen reader-specific counter
|
174
|
+
CharacterCount.prototype.updateScreenReaderCountMessage = function () {
|
175
|
+
var $screenReaderCountMessage = this.$screenReaderCountMessage
|
176
|
+
|
177
|
+
// If over the threshold, remove the aria-hidden attribute, allowing screen
|
178
|
+
// readers to announce the content of the element.
|
179
|
+
if (this.isOverThreshold()) {
|
180
|
+
$screenReaderCountMessage.removeAttribute('aria-hidden')
|
181
|
+
} else {
|
182
|
+
$screenReaderCountMessage.setAttribute('aria-hidden', true)
|
183
|
+
}
|
184
|
+
|
185
|
+
// Update message
|
186
|
+
$screenReaderCountMessage.innerHTML = this.formattedUpdateMessage()
|
187
|
+
}
|
188
|
+
|
189
|
+
// Format update message
|
190
|
+
CharacterCount.prototype.formattedUpdateMessage = function () {
|
191
|
+
var $textarea = this.$textarea
|
192
|
+
var options = this.options
|
193
|
+
var remainingNumber = this.maxLength - this.count($textarea.value)
|
194
|
+
|
195
|
+
var charVerb = 'remaining'
|
196
|
+
var charNoun = 'character'
|
197
|
+
var displayNumber = remainingNumber
|
198
|
+
if (options.maxwords) {
|
199
|
+
charNoun = 'word'
|
200
|
+
}
|
201
|
+
charNoun = charNoun + ((remainingNumber === -1 || remainingNumber === 1) ? '' : 's')
|
202
|
+
|
203
|
+
charVerb = (remainingNumber < 0) ? 'too many' : 'remaining'
|
204
|
+
displayNumber = Math.abs(remainingNumber)
|
205
|
+
|
206
|
+
return 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb
|
207
|
+
}
|
208
|
+
|
209
|
+
// Checks whether the value is over the configured threshold for the input.
|
210
|
+
// If there is no configured threshold, it is set to 0 and this function will
|
211
|
+
// always return true.
|
212
|
+
CharacterCount.prototype.isOverThreshold = function () {
|
213
|
+
var $textarea = this.$textarea
|
214
|
+
var options = this.options
|
215
|
+
|
216
|
+
// Determine the remaining number of characters/words
|
217
|
+
var currentLength = this.count($textarea.value)
|
218
|
+
var maxLength = this.maxLength
|
219
|
+
|
220
|
+
// Set threshold if presented in options
|
221
|
+
var thresholdPercent = options.threshold ? options.threshold : 0
|
222
|
+
var thresholdValue = maxLength * thresholdPercent / 100
|
223
|
+
|
224
|
+
return (thresholdValue <= currentLength)
|
225
|
+
}
|
226
|
+
|
227
|
+
// Update the visible character counter and keep track of when the last update
|
228
|
+
// happened for each keypress
|
229
|
+
CharacterCount.prototype.handleKeyUp = function () {
|
230
|
+
this.updateVisibleCountMessage()
|
231
|
+
this.lastInputTimestamp = Date.now()
|
232
|
+
}
|
233
|
+
|
234
|
+
CharacterCount.prototype.handleFocus = function () {
|
235
|
+
// If the field is focused, and a keyup event hasn't been detected for at
|
236
|
+
// least 1000 ms (1 second), then run the manual change check.
|
237
|
+
// This is so that the update triggered by the manual comparison doesn't
|
238
|
+
// conflict with debounced KeyboardEvent updates.
|
239
|
+
this.valueChecker = setInterval(function () {
|
240
|
+
if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
|
241
|
+
this.checkIfValueChanged()
|
242
|
+
}
|
243
|
+
}.bind(this), 1000)
|
244
|
+
}
|
245
|
+
|
246
|
+
CharacterCount.prototype.handleBlur = function () {
|
247
|
+
// Cancel value checking on blur
|
248
|
+
clearInterval(this.valueChecker)
|
249
|
+
}
|
250
|
+
|
251
|
+
export default CharacterCount
|
@@ -0,0 +1,164 @@
|
|
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 Checkboxes ($module) {
|
8
|
+
this.$module = $module
|
9
|
+
this.$inputs = $module.querySelectorAll('input[type="checkbox"]')
|
10
|
+
}
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Initialise Checkboxes
|
14
|
+
*
|
15
|
+
* Checkboxes can be associated with a 'conditionally revealed' content block –
|
16
|
+
* for example, a checkbox for 'Phone' could reveal an additional form field for
|
17
|
+
* the 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 checkbox state.
|
25
|
+
*/
|
26
|
+
Checkboxes.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 checkboxes 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
|
+
$module.addEventListener('click', this.handleClick.bind(this))
|
61
|
+
}
|
62
|
+
|
63
|
+
/**
|
64
|
+
* Sync the conditional reveal states for all inputs in this $module.
|
65
|
+
*/
|
66
|
+
Checkboxes.prototype.syncAllConditionalReveals = function () {
|
67
|
+
nodeListForEach(this.$inputs, this.syncConditionalRevealWithInputState.bind(this))
|
68
|
+
}
|
69
|
+
|
70
|
+
/**
|
71
|
+
* Sync conditional reveal with the input state
|
72
|
+
*
|
73
|
+
* Synchronise the visibility of the conditional reveal, and its accessible
|
74
|
+
* state, with the input's checked state.
|
75
|
+
*
|
76
|
+
* @param {HTMLInputElement} $input Checkbox input
|
77
|
+
*/
|
78
|
+
Checkboxes.prototype.syncConditionalRevealWithInputState = function ($input) {
|
79
|
+
var $target = document.getElementById($input.getAttribute('aria-controls'))
|
80
|
+
|
81
|
+
if ($target && $target.classList.contains('govuk-checkboxes__conditional')) {
|
82
|
+
var inputIsChecked = $input.checked
|
83
|
+
|
84
|
+
$input.setAttribute('aria-expanded', inputIsChecked)
|
85
|
+
$target.classList.toggle('govuk-checkboxes__conditional--hidden', !inputIsChecked)
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Uncheck other checkboxes
|
91
|
+
*
|
92
|
+
* Find any other checkbox inputs with the same name value, and uncheck them.
|
93
|
+
* This is useful for when a “None of these" checkbox is checked.
|
94
|
+
*/
|
95
|
+
Checkboxes.prototype.unCheckAllInputsExcept = function ($input) {
|
96
|
+
var allInputsWithSameName = document.querySelectorAll('input[type="checkbox"][name="' + $input.name + '"]')
|
97
|
+
|
98
|
+
nodeListForEach(allInputsWithSameName, function ($inputWithSameName) {
|
99
|
+
var hasSameFormOwner = ($input.form === $inputWithSameName.form)
|
100
|
+
if (hasSameFormOwner && $inputWithSameName !== $input) {
|
101
|
+
$inputWithSameName.checked = false
|
102
|
+
this.syncConditionalRevealWithInputState($inputWithSameName)
|
103
|
+
}
|
104
|
+
}.bind(this))
|
105
|
+
}
|
106
|
+
|
107
|
+
/**
|
108
|
+
* Uncheck exclusive inputs
|
109
|
+
*
|
110
|
+
* Find any checkbox inputs with the same name value and the 'exclusive' behaviour,
|
111
|
+
* and uncheck them. This helps prevent someone checking both a regular checkbox and a
|
112
|
+
* "None of these" checkbox in the same fieldset.
|
113
|
+
*/
|
114
|
+
Checkboxes.prototype.unCheckExclusiveInputs = function ($input) {
|
115
|
+
var allInputsWithSameNameAndExclusiveBehaviour = document.querySelectorAll(
|
116
|
+
'input[data-behaviour="exclusive"][type="checkbox"][name="' + $input.name + '"]'
|
117
|
+
)
|
118
|
+
|
119
|
+
nodeListForEach(allInputsWithSameNameAndExclusiveBehaviour, function ($exclusiveInput) {
|
120
|
+
var hasSameFormOwner = ($input.form === $exclusiveInput.form)
|
121
|
+
if (hasSameFormOwner) {
|
122
|
+
$exclusiveInput.checked = false
|
123
|
+
this.syncConditionalRevealWithInputState($exclusiveInput)
|
124
|
+
}
|
125
|
+
}.bind(this))
|
126
|
+
}
|
127
|
+
|
128
|
+
/**
|
129
|
+
* Click event handler
|
130
|
+
*
|
131
|
+
* Handle a click within the $module – if the click occurred on a checkbox, sync
|
132
|
+
* the state of any associated conditional reveal with the checkbox state.
|
133
|
+
*
|
134
|
+
* @param {MouseEvent} event Click event
|
135
|
+
*/
|
136
|
+
Checkboxes.prototype.handleClick = function (event) {
|
137
|
+
var $target = event.target
|
138
|
+
|
139
|
+
// Ignore clicks on things that aren't checkbox inputs
|
140
|
+
if ($target.type !== 'checkbox') {
|
141
|
+
return
|
142
|
+
}
|
143
|
+
|
144
|
+
// If the checkbox conditionally-reveals some content, sync the state
|
145
|
+
var hasAriaControls = $target.getAttribute('aria-controls')
|
146
|
+
if (hasAriaControls) {
|
147
|
+
this.syncConditionalRevealWithInputState($target)
|
148
|
+
}
|
149
|
+
|
150
|
+
// No further behaviour needed for unchecking
|
151
|
+
if (!$target.checked) {
|
152
|
+
return
|
153
|
+
}
|
154
|
+
|
155
|
+
// Handle 'exclusive' checkbox behaviour (ie "None of these")
|
156
|
+
var hasBehaviourExclusive = ($target.getAttribute('data-behaviour') === 'exclusive')
|
157
|
+
if (hasBehaviourExclusive) {
|
158
|
+
this.unCheckAllInputsExcept($target)
|
159
|
+
} else {
|
160
|
+
this.unCheckExclusiveInputs($target)
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
export default Checkboxes
|
@@ -0,0 +1,147 @@
|
|
1
|
+
/**
|
2
|
+
* JavaScript 'polyfill' for HTML5's <details> and <summary> elements
|
3
|
+
* and 'shim' to add accessiblity enhancements for all browsers
|
4
|
+
*
|
5
|
+
* http://caniuse.com/#feat=details
|
6
|
+
*/
|
7
|
+
import '../../vendor/polyfills/Function/prototype/bind'
|
8
|
+
import '../../vendor/polyfills/Event' // addEventListener and event.target normaliziation
|
9
|
+
import { generateUniqueID } from '../../common'
|
10
|
+
|
11
|
+
var KEY_ENTER = 13
|
12
|
+
var KEY_SPACE = 32
|
13
|
+
|
14
|
+
function Details ($module) {
|
15
|
+
this.$module = $module
|
16
|
+
}
|
17
|
+
|
18
|
+
Details.prototype.init = function () {
|
19
|
+
if (!this.$module) {
|
20
|
+
return
|
21
|
+
}
|
22
|
+
|
23
|
+
// If there is native details support, we want to avoid running code to polyfill native behaviour.
|
24
|
+
var hasNativeDetails = typeof this.$module.open === 'boolean'
|
25
|
+
|
26
|
+
if (hasNativeDetails) {
|
27
|
+
return
|
28
|
+
}
|
29
|
+
|
30
|
+
this.polyfillDetails()
|
31
|
+
}
|
32
|
+
|
33
|
+
Details.prototype.polyfillDetails = function () {
|
34
|
+
var $module = this.$module
|
35
|
+
|
36
|
+
// Save shortcuts to the inner summary and content elements
|
37
|
+
var $summary = this.$summary = $module.getElementsByTagName('summary').item(0)
|
38
|
+
var $content = this.$content = $module.getElementsByTagName('div').item(0)
|
39
|
+
|
40
|
+
// If <details> doesn't have a <summary> and a <div> representing the content
|
41
|
+
// it means the required HTML structure is not met so the script will stop
|
42
|
+
if (!$summary || !$content) {
|
43
|
+
return
|
44
|
+
}
|
45
|
+
|
46
|
+
// If the content doesn't have an ID, assign it one now
|
47
|
+
// which we'll need for the summary's aria-controls assignment
|
48
|
+
if (!$content.id) {
|
49
|
+
$content.id = 'details-content-' + generateUniqueID()
|
50
|
+
}
|
51
|
+
|
52
|
+
// Add ARIA role="group" to details
|
53
|
+
$module.setAttribute('role', 'group')
|
54
|
+
|
55
|
+
// Add role=button to summary
|
56
|
+
$summary.setAttribute('role', 'button')
|
57
|
+
|
58
|
+
// Add aria-controls
|
59
|
+
$summary.setAttribute('aria-controls', $content.id)
|
60
|
+
|
61
|
+
// Set tabIndex so the summary is keyboard accessible for non-native elements
|
62
|
+
//
|
63
|
+
// We have to use the camelcase `tabIndex` property as there is a bug in IE6/IE7 when we set the correct attribute lowercase:
|
64
|
+
// See http://web.archive.org/web/20170120194036/http://www.saliences.com/browserBugs/tabIndex.html for more information.
|
65
|
+
$summary.tabIndex = 0
|
66
|
+
|
67
|
+
// Detect initial open state
|
68
|
+
var openAttr = $module.getAttribute('open') !== null
|
69
|
+
if (openAttr === true) {
|
70
|
+
$summary.setAttribute('aria-expanded', 'true')
|
71
|
+
$content.setAttribute('aria-hidden', 'false')
|
72
|
+
} else {
|
73
|
+
$summary.setAttribute('aria-expanded', 'false')
|
74
|
+
$content.setAttribute('aria-hidden', 'true')
|
75
|
+
$content.style.display = 'none'
|
76
|
+
}
|
77
|
+
|
78
|
+
// Bind an event to handle summary elements
|
79
|
+
this.polyfillHandleInputs($summary, this.polyfillSetAttributes.bind(this))
|
80
|
+
}
|
81
|
+
|
82
|
+
/**
|
83
|
+
* Define a statechange function that updates aria-expanded and style.display
|
84
|
+
* @param {object} summary element
|
85
|
+
*/
|
86
|
+
Details.prototype.polyfillSetAttributes = function () {
|
87
|
+
var $module = this.$module
|
88
|
+
var $summary = this.$summary
|
89
|
+
var $content = this.$content
|
90
|
+
|
91
|
+
var expanded = $summary.getAttribute('aria-expanded') === 'true'
|
92
|
+
var hidden = $content.getAttribute('aria-hidden') === 'true'
|
93
|
+
|
94
|
+
$summary.setAttribute('aria-expanded', (expanded ? 'false' : 'true'))
|
95
|
+
$content.setAttribute('aria-hidden', (hidden ? 'false' : 'true'))
|
96
|
+
|
97
|
+
$content.style.display = (expanded ? 'none' : '')
|
98
|
+
|
99
|
+
var hasOpenAttr = $module.getAttribute('open') !== null
|
100
|
+
if (!hasOpenAttr) {
|
101
|
+
$module.setAttribute('open', 'open')
|
102
|
+
} else {
|
103
|
+
$module.removeAttribute('open')
|
104
|
+
}
|
105
|
+
|
106
|
+
return true
|
107
|
+
}
|
108
|
+
|
109
|
+
/**
|
110
|
+
* Handle cross-modal click events
|
111
|
+
* @param {object} node element
|
112
|
+
* @param {function} callback function
|
113
|
+
*/
|
114
|
+
Details.prototype.polyfillHandleInputs = function (node, callback) {
|
115
|
+
node.addEventListener('keypress', function (event) {
|
116
|
+
var target = event.target
|
117
|
+
// When the key gets pressed - check if it is enter or space
|
118
|
+
if (event.keyCode === KEY_ENTER || event.keyCode === KEY_SPACE) {
|
119
|
+
if (target.nodeName.toLowerCase() === 'summary') {
|
120
|
+
// Prevent space from scrolling the page
|
121
|
+
// and enter from submitting a form
|
122
|
+
event.preventDefault()
|
123
|
+
// Click to let the click event do all the necessary action
|
124
|
+
if (target.click) {
|
125
|
+
target.click()
|
126
|
+
} else {
|
127
|
+
// except Safari 5.1 and under don't support .click() here
|
128
|
+
callback(event)
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
})
|
133
|
+
|
134
|
+
// Prevent keyup to prevent clicking twice in Firefox when using space key
|
135
|
+
node.addEventListener('keyup', function (event) {
|
136
|
+
var target = event.target
|
137
|
+
if (event.keyCode === KEY_SPACE) {
|
138
|
+
if (target.nodeName.toLowerCase() === 'summary') {
|
139
|
+
event.preventDefault()
|
140
|
+
}
|
141
|
+
}
|
142
|
+
})
|
143
|
+
|
144
|
+
node.addEventListener('click', callback)
|
145
|
+
}
|
146
|
+
|
147
|
+
export default Details
|