1

Issue:

We are triggering a snackbar from a button within a dialog and the snackbar is not being read. It reads fine when triggered from a button outside the dialog.

Desired behavior:

When the user clicks the "OPEN SNACKBAR" button from within the dialog the snackbar is read by the screen reader.

Codesandbox:

https://codesandbox.io/s/dialog-snackbar-testing-sje9p?file=/demo.js

Steps to reproduce issue:

Note: If you would like to see how the snackbar should be read by the screen reader Click the first "OPEN SNACKBAR" button presented.

  1. Open NVDA screen reader or JAWS screen reader
  2. Go to https://codesandbox.io/s/dialog-snackbar-testing-sje9p?file=/demo.js
  3. Click "OPEN DIALOG" button
  4. Click "OPEN SNACKBAR" button

Testing details:

  • Button trigger: JAWS + Chrome = PASS
  • Button trigger: JAWS + Firefox = PASS
  • Button trigger: JAWS + Edge = PASS
  • Button trigger: NVDA + Chrome = PASS
  • Button trigger: NVDA + Firefox = PASS
  • Button trigger: NVDA + Edge = PASS
  • Dialog button trigger: JAWS + Chrome = FAIL
  • Dialog button trigger: JAWS + Firefox = FAIL
  • Dialog button trigger: JAWS + Edge = FAIL
  • Dialog button trigger: NVDA + Chrome = FAIL
  • Dialog button trigger: NVDA + Firefox = FAIL
  • Dialog button trigger: NVDA + Edge = FAIL

2 Answers2

1

I think you are getting lucky that your first example works. It looks like you are not using aria-live correctly in either test case. aria-live is meant to exist on the page and not be added dynamically itself.

That is, for aria-live to work correctly all the time no matter which browser or screen reader or platform, your page should look something like this:

Before

<div>stuff</div>
<div aria-live="polite"> <!-- intentionally empty --> </div>
<div>stuff</div>

And when you add the snackbar message, it would be injected into the empty <div> and would be announced properly.

After

<div>stuff</div>
<div aria-live="polite"> snackbar stuff </div>
<div>stuff</div>

What doesn't work reliably is adding aria-live elements dynamically. The following may or may not work on some browser + screen reader combinations:

Before

<div>stuff</div>
<div>stuff</div>

After

<div>stuff</div>
<div aria-live="polite"> snackbar stuff </div>
<div>stuff</div>

Note that my examples use aria-live directly. In your sample, your snackbar is using role="alert" which gives you an implicit aria-live="assertive". For the purposes of having the text announced, it doesn't matter if you have "polite" or "assertive". The main issue is adding a role="alert" (ie, aria-live) dynamically. It generally doesn't work.

The fact that you hear it with the button example but not the dialog example is weird but the first thing you have to eliminate is the dynamic aria-live. If you do that and it still doesn't work in both cases, I can take another look.

slugolicious
  • 15,824
  • 2
  • 29
  • 43
  • First off thank you for the response, it is much appreciated. I can easily set my role=“alert” to role=“none” and wrap the snackbar in a div with aria-live=“polite”. Based on your comment this seems to be what you are suggesting as a first step. I will test this and post the results. – user1569333 Oct 23 '21 at 00:31
  • No, that's not what I was trying to say. Sorry if I wasn't clear. Whether you use `role="alert"` or `aria-live` directly doesn't matter. The problem *might* be that you are adding the alert dynamically. The element with `alert` needs to exist on your page upon page load. When you want to display the message, then you inject new elements or text into that alert container. – slugolicious Oct 24 '21 at 17:43
  • Ahhh understood thanks for clarifying. – user1569333 Oct 24 '21 at 18:14
  • Attempted this with no luck. The message is read when I trigger it from a button but not when I trigger it from a button within a dialog. – user1569333 Oct 25 '21 at 14:50
  • Do you have new code I can test? – slugolicious Oct 26 '21 at 01:07
1

The issue is multiple live regions on the page.

When you add your first snackbar you expose role="alert" (which is the same as aria-live="assertive" effectively).

This should work pretty well across all browsers (a single alert).

However, multiple aria-live regions tend to fail in numerous screen readers, especially when dynamically added and removed.

The best way to deal with this is to move all screen reader alerts into a single aria-live region and set up a message queue to handle alerts at the application level, this way you only ever have one aria-live region (2 if you need a polite and an assertive option) and won't run into as many issues.

A quick Example / the concept

HTML

<!-- You can add and remove these at will, this is the dynamically added snackbar -->
<div class="snack"></div>

<!-- This will **always** exist on the page and we will update the contents from our message queue -->
<div id="alertAnnouncer" aria-live="assertive">

</div>

Pseudo code

// Adds the snack bar with the specified text. This has nothing to do with the screen reader message queue, it is just to represent your current function to add a snack bar.
addSnackBar("Alert Text"); 

// this is how we add messages to the queue.
addScreenReaderAlert("Alert Text"); 

// we hold our messages here for processing.
var messageQueue = []; 

function addScreenReaderAlert(textToAnnounce){

    messageQueue.push(textToAnnounce); 

    //CODE TO ADD: start the interval to process the queue if it is not running, if no update has happened in the last x seconds process the message immediately and set the last time processed or something similar.
    
}

function processMessageQueue(){
    if(messageQueue.length > 0){

       //CODE TO ADD: get oldest message (you would use slice etc. and remove it entirely from the queue)

      // ultimately we want this
       let oldestMessage = messageQueue[0];

       // we change the inner HTML of the aria-live region so the oldest message is announced.
       return document.querySelector('#alertAnnouncer').innerHTML = oldestMessage; 
    }else{
      //CODE TO ADD: clear the interval
    }

    //NOTE: to save  CPU cycles I would stop the interval once the message queue reaches 0 length and then restart the message queue interval when you fire the addScreenReaderAlert() function.
}

// this is purely to show there is an interval on the queue, you would set and unset this as described above. 
Window.setInterval(processMessageQueue, 1000); 

// NOTE: The interval delay should be set to a value large enough to allow a screen reader to announce the messages. So for a simple snackbar this can be shorter, but if you want to announce longer messages you need to set this higher. Note that the interval is purely for spreading out multiple messages added in quick succession so you don't overwrite them too quickly.

Explanation of code

That all looks complicated and messy, my apologies!

But essentially we want a way of:

  • adding a message to a queue
  • processing that queue with a gap between each update to the aria-live region.
  • Ideally we process items immediately if there hasn't been a message in the last x seconds.

The gap between messages is to ensure you do not overwrite the message currently being spoken.

You can set up aria-live regions so that you can continue adding messages to them (appending the message rather than replacing), but this is not always stable in all screen readers (older NVDA, ORCA etc. don't always read new items after a certain length). This workaround is pretty robust once you fine tune the message timings etc.

The message queue, although a little bit of work to set up in the first place, is also a super easy way to manage all alerts etc. across your whole application without worrying about multiple alerts overlapping etc.

If you need more explanation (as I am not sure if I "waffled" on in this explanation ) just let me know.

GrahamTheDev
  • 22,724
  • 2
  • 32
  • 64