Skip to content

Commit

Permalink
Debounce character counter update
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
querkmachine committed Mar 31, 2022
1 parent 428e744 commit c57d8b5
Showing 1 changed file with 27 additions and 13 deletions.
40 changes: 27 additions & 13 deletions src/govuk/components/character-count/character-count.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 () {
Expand Down

0 comments on commit c57d8b5

Please sign in to comment.