From c57d8b5224613e31c34d3da8dfe81ea769e76d77 Mon Sep 17 00:00:00 2001 From: Kimberly Grey Date: Thu, 31 Mar 2022 15:44:21 +0100 Subject: [PATCH] Debounce character counter update Debounces the character counter update until 250 milliseconds after the user has stopped typing. This helps prevent multiple rapid-fire updates being queued up by screen readers and read out afterwards, and prevents "stuttering" by screen readers which attempt to read out the updated counter and the user's input simultaneously. The handleFocus method's bugfix for Dragon Naturally Speaking now runs a check to see when the last user input was provided, and will not update the counter if the user has recently typed anything. This prevents the DNS fix from causing the same queuing and stuttering behaviour. This also fixes a newly identified bug where the keyup, focus and blur event listeners were all being bound twice due to the sync method being called both on script initialisation and on pageshow/DOMContentLoaded events. The sync method has been removed and this now calls updateCountMessage directly, which is the part that actually requires syncronisation. --- .../character-count/character-count.js | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/govuk/components/character-count/character-count.js b/src/govuk/components/character-count/character-count.js index 6abe7e0ba1..957d3e5df2 100644 --- a/src/govuk/components/character-count/character-count.js +++ b/src/govuk/components/character-count/character-count.js @@ -6,6 +6,8 @@ function CharacterCount ($module) { this.$module = $module this.$textarea = $module.querySelector('.govuk-js-character-count') this.$countMessage = null + this.lastInputTimestamp = null + this.debouncedInputTimer = null } CharacterCount.prototype.defaults = { @@ -60,21 +62,17 @@ CharacterCount.prototype.init = function () { // Remove hard limit if set $module.removeAttribute('maxlength') + this.bindChangeEvents() + // When the page is restored after navigating 'back' in some browsers the // state of the character count is not restored until *after* the DOMContentLoaded - // event is fired, so we need to sync after the pageshow event in browsers - // that support it. + // event is fired, so we need to manually update it after the pageshow event + // in browsers that support it. if ('onpageshow' in window) { - window.addEventListener('pageshow', this.sync.bind(this)) + window.addEventListener('pageshow', this.updateCountMessage.bind(this)) } else { - window.addEventListener('DOMContentLoaded', this.sync.bind(this)) + window.addEventListener('DOMContentLoaded', this.updateCountMessage.bind(this)) } - - this.sync() -} - -CharacterCount.prototype.sync = function () { - this.bindChangeEvents() this.updateCountMessage() } @@ -109,7 +107,7 @@ CharacterCount.prototype.count = function (text) { // Bind input propertychange to the elements and update based on the change CharacterCount.prototype.bindChangeEvents = function () { var $textarea = this.$textarea - $textarea.addEventListener('keyup', this.checkIfValueChanged.bind(this)) + $textarea.addEventListener('keyup', this.handleKeyUp.bind(this)) // Bind focus/blur events to start/stop polling $textarea.addEventListener('focus', this.handleFocus.bind(this)) @@ -177,9 +175,25 @@ CharacterCount.prototype.updateCountMessage = function () { countMessage.innerHTML = 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb } +// Debounce updating the character counter until after a user has stopped typing +// for a short period of time. This helps prevent screen readers from queuing up +// multiple text updates in rapid succession. +CharacterCount.prototype.handleKeyUp = function () { + this.lastInputTimestamp = Date.now() + clearTimeout(this.debouncedInputTimer) + this.debouncedInputTimer = setTimeout(this.updateCountMessage.bind(this), 250) +} + CharacterCount.prototype.handleFocus = function () { - // Check if value changed on focus - this.valueChecker = setInterval(this.checkIfValueChanged.bind(this), 1000) + // If the field is focused, and a keyup event hasn't been detected for at + // least 1000 ms (1 second), then run the manual change check. + // This is so that the update triggered by the manual comparison doesn't + // conflict with debounced KeyboardEvent updates. + this.valueChecker = setInterval(function () { + if (!this.lastInputTimestamp || (Date.now() - 1000) >= this.lastInputTimestamp) { + this.checkIfValueChanged.bind(this) + } + }.bind(this), 1000) } CharacterCount.prototype.handleBlur = function () {