Search code examples
javascripthtmlwai-aria

aria-describedby repeats when aria-live region updates for the first time


Intro

I am putting together a password input which is complemented by a password strength indicator and a password criteria list. For accessibility, the desired flow is:

  1. User focuses on password input and screen reader reads out password criteria;
  2. As user types, password strength improvements are announced (invalid -> poor -> good -> strong).

Code

A simplified version of the HTML and JS is as follows:

const passwordInput = document.getElementById('password');
const strengthValue = document.getElementById('strength');

passwordInput.addEventListener('input', (e) => {
  const index = e.target.value.length < 3 ? e.target.value.length : 3
  const strength = ['invalid', 'poor', 'good', 'strong'][index]
  strengthValue.innerHTML = `Password strength is ${strength}`
});
<label for="password">Password</label>
<input id="password" type="password" aria-describedby="criteria" />
<p id="strength" aria-live="polite">Password strength is invalid</p>
<p id="criteria">Your password must contain both upper and lowercase letters</p>

This can also be seen in this Codepen.

The Problem

The above code results in the following flow:

  1. User focuses on password input and criteria are announced;
  2. User types one character, strength change is announced, then criteria are announced again;
  3. Further typing results in strength announcements but not criteria.

The problem is at step 2. The criteria should not be repeated. Everything else is fine.

Comments

  • This is happening on MacOS while using VoiceOver on Chrome, Firefox, Safari and Edge.
  • This only happens with the first character that's entered. If the strength announcement is delayed until two characters are entered, there is no repetition of the criteria.
  • I came across this in React and boiled it down to HTML/Vanilla JS for debugging purposes, my rationale being that if it's happening in HTML/Vanilla JS then the added complexity of React would only muddy the waters. If seeing it in React is helpful, here is a CodeSandbox.
  • I have a hack to get around it whereby the id of the id="criteria" node is removed when a value is detected in the password input. This works but it feels like there should be a better solution somewhere out there.

If anyone is able to shed any light on why this is happening and whether there is an elegant/proper solution out there I would be really interested to know more. I haven't been successful in finding more technical explanation of how the aria-live and aria-describedby nodes play together so I have hit a bit of a wall. Thanks in advance.


Solution

  • I tried VoiceOver on iOS (I don't have a Mac) and it worked ok. After I got to "password strength is strong", any further additions to the password were not announced. Same thing happened with JAWS on Chrome.

    The way it's coded, I didn't expect to hear "password strength is..." but rather only "poor", "good", or "strong". You have a live region but you are not using aria-atomic="true". Technically, a live region without aria-atomic should only announce the text that actually changes. So if the message changes from "password strength is good" to "password strength is strong", the only text that really changed is from "good" to "strong" so only the word "strong" should be announced. NVDA always announces the full text but I wouldn't count on that happening unless you have aria-atomic.

    Sometimes there are "eccentricities" in screen readers where your code is correct but the screen reader is not announcing things like you'd expect. The fact that you're worried about how it sounds is fantastic but you need to be careful that you don't try to code around the way a screen reader is currently behaving, especially if code inspection shows you're doing everything right.

    The fact that the description (aria-describedby) is read twice makes it sound like the password field loses focus and then focus moves back to it. The description should only be read when you focus on an element, so if it's read twice, it "feels" like it's a focus issue. But nothing in your code is messing with the focus so it could be a VoiceOver/MacOS issue/bug.