6

The rise in the use of AJAX to dynamically update the website instead of using standard hyperlinks and separate Web pages has resulted in accessibility problems for users of assistive technology (e.g. screen readers).

I am inquiring here the public opinion of an accessible solution for the best possible user experience. Unfortunately the most relevant accessibility resources, for me at least, such as Google, ADG*, WebAIM, W3 - WCAG, MDN etc. did not provide the specific answer. The topic is not targeted to single page applications only. Many websites face similar issues when loading the dynamic content e.g. "Load more" button, tab content, modal content, pagination page change etc.

The issue

Dynamic content update instead of a standard Web page request?

  1. A screen reader user clicks the link and nothing happens "apparently".
    It creates a poor user experience as the link might appear broken. The expectation was opening a new page and positioning to the top of the content. A user is not aware that a part of the existing page is about to get updated soon. Screen readers do not announce content updates automatically (except in specific cases).

    Requirement #1: A user must be aware that a content loading has started, possibly with a delay e.g. after a request is taking longer than X seconds.

  2. A user focus remains on a link with no easy way of jumping to the updated content once the update was completed. There might be a bunch of other elements to skip over. Clicking the link a user expected to access the content straight away.

    Requirement #2: A user focus must move to the new content, preferably a heading element, after the update has completed.

The solution - Easy part

The requirement "A user focus must move to the new content" seems straightforward:

Use Javascript to move the focus to the heading element when AJAX request is completed!
In order to focus a heading, which is otherwise non-focusable element, it must have tabindex="-1" attribute.

The following resources answers "Why is it important to move the focus?", "How to do it?" and "Why is it better to focus a heading element instead of focusing the entire container?":

Managing focus and focusing the heading element: This is one of the rare cases where it's ok to remove the CSS outline (on heading). — Youtube - Rob Dodson from A11ycasts

Two solutions to help non-sighted or keyboard-only users get to where they're going:
Emulate a native page load and move focus to the top of the document.
Move Focus on the content (heading or possibly form control). — daverupert.com - Accessible Page Navigation in SPAs

The most reliable way I found to overcome this challenge was to set the focus on the heading of the area that's changing. — Marwan Aziz @medium.com - Accessibility considerations when building SPA

If the focus is set to an element, the screen reader announces it. This can be an easy way to inform users about something on the page: simply set the focus on it. Be sure though that this does not result in disorientation for the screen reader user: it shouldn't happen "out of the blue", but only when the screen reader user has done some interaction that caused the page change.
And by the way, you should not set the focus on a big container of information. — accessibility-developer-guide.com

The developer can simply allow the update to occur and not inform the user of it, alert the user of the update through some sort of embedded audio sound, or can set focus directly to the updated content. — WebAIM - Dynamic Content Updates

For single page applications, browser focus should be set on the <h1> heading after a new view is loaded. — Yale University

The user's focus is directed to new content added to the page. — Google Chrome DevTools - Accessibility Audit - Additional items to manually check


What if loading takes too long? - The tricky part

The requirement was also "to inform a user that a content loading has started". It makes sense as the user might be wondering why nothing happens after clicking the link. How would a user know whether to wait or the link is simply broken?

Are there any opinions? Ideas?

What I tried so far?

  • BAD SOLUTION: Use a placeholder where the new content suppose to appear and move the focus there immediately. The placeholder contains an initial heading text "Please wait. Loading..." and it is being read immediately when focused. So far so good. But after testing with JAWS, NVDA and various browsers I decided against the solution because it simply wouldn't work as expected. It was somewhat buggy. Often after the new content update was completed the screen reader would be still reading "Please wait. Loading..." text and sometimes even the new content was not read at all even though I re-focused to the new updated heading again
  • BAD SOLUTION: Show a loading modal dialog if a request takes longer than X seconds. User would be informed that a loading modal appeared and the modal title would be "Please wait. Loading...". After the content update was completed the modal would be closed and the heading of the new content would be focused. This worked kind of OK but troubles the user experience. Opening the modal takes a while, especially when animated so it could happen that the modal opens after the content was already loaded making a confusion. It does not really seem to be user friendly. It interrupts the user by popping-up in the middle of the screen and hiding the current content. It also mismanage the focus for screen reader causing disorientation. Because focus must be moved to a modal. Simply, it's senseless showing the modal dialog with OK button just to inform a user about loading. Modals are usually used for the user to interact with

Lastly... ARIA live region - Not perfect but the best so far...

The latest idea was to implement a balloon notification message which becomes visible if an ongoing AJAX request takes longer than X seconds. It has an ARIA live region aria-live="polite", a loading icon and a text message "Please wait. Loading...". When the request is completed the message is changed to "Loading done." but hidden visually and remain visible only to a screen reader so it can announce it. I did not use role="status" due to lack of consistent and cross-browser support. The user focus would never move. It remains on a clicked element until the request was done but the user is still informed that loading is in progress or done and in a polite way. So far so good.

enter image description here

I tested the solution successfully on Windows 8.1 with:

  • Chrome 80.0 + NVDA 2019.3.1
  • Chrome 80.0 + JAWS 18.0
  • Chrome 80.0 + ChromeVox 53.0 (Chrome extension)
  • Firefox 74.0 + NVDA 2019.3.1
  • Firefox 74.0 + JAWS 18.0
  • IE 11.0 + NVDA 2019.3.1 (OK but sometimes reads the same text multiple times)
  • IE 11.0 + JAWS 18.0 (OK but sometimes reads the same text multiple times)
  • Windows Narrator - Kind of works but buggy. I didn't go into details

The most important things I've learned about ARIA live regions is that aria-live element must exist in the DOM on page load instead of being created by Javascript and also that there can be various issues if I decide to show/hide a live region instead of leaving it visible and only updating the content. That's why I was instead toggling the styles only by a class name. For example NVDA needs a small delay before a text update in a live region if the region was hidden and then shown. Otherwise the update is not recorded https://github.com/nvaccess/nvda/issues/8873. Then IE 11 somehow tends to repeat the live region content multiple times. Couldn't figure why. Also it was important to take care of the race conditions e.g. not to allow the text update into "Loading..." after the request was already done, or that a new request don't break the reporting of the current request.

I used the following code using Bootstrap 3.x and jQuery:

<!-- Hint: This element must not be added dynamically. Must be in the DOM initially so that a screen reader can register it -->
<div id="ajax-status-alert" aria-live="polite" data-toggle-class="alert alert-warning">
    <span class="ajax-status-alert__text"></span>
    <i class="ajax-status-alert__icon glyphicon glyphicon-refresh glyphicon-spin" role="presentation" aria-hidden="true"></i>
</div>
#ajax-status-alert {
    position: fixed;
    bottom: 10px;
    right: 15px;
    z-index: 1001;
}

.ajax-status-alert__text + .ajax-status-alert__icon {
    display: inline-block;
    margin-left: 5px;
}
$(document).ajaxStart(function (e) {
    console.log("LOG: FIRST AJAX REQUEST HAS STARTED"); // TODO: Remove this

    /* A delay to display a notification only if a request takes longer than X seconds */
    e.target.ajaxStatusAlertShowDelay = setTimeout(function () {
        let txtLoadingStarted = "Please wait. Loading..."; // TODO: Translate and clean-up already existing strings related to this functionality

        let $ajaxStatusAlert = $('#ajax-status-alert');
        let $ajaxStatusAlertText = $ajaxStatusAlert.find(".ajax-status-alert__text");
        let $ajaxStatusAlertIcon = $ajaxStatusAlert.find(".ajax-status-alert__icon");

        /* Reset and clean-up. Toggle class instead of show/hide otherwise expect issues with live region */
        $ajaxStatusAlert.attr("class", $ajaxStatusAlert.attr("data-toggle-class"));
        $ajaxStatusAlertText.removeClass("sr-only").text("");
        $ajaxStatusAlertIcon.show(0);
        console.log("LOG: RESET"); // TODO: Remove this

        $ajaxStatusAlertText.text(txtLoadingStarted);
        console.log("LOG: TEXT UPDATE"); // TODO: Remove this
    }, 1500); // TODO: Don't forget to update the value
});
$(document).ajaxStop(function (e) {
    console.log("LOG: ALL AJAX REQUESTS DONE"); // TODO: Remove this

    /* Prevent to show status alert */
    if ("undefined" !== typeof(e.target.ajaxStatusAlertShowDelay)) {
        clearTimeout(e.target.ajaxStatusAlertShowDelay);
    }

    let txtLoadingFinished = "Loading done."; // TODO: Translate

    let $ajaxStatusAlert = $('#ajax-status-alert');
    let $ajaxStatusAlertText = $ajaxStatusAlert.find(".ajax-status-alert__text");
    let $ajaxStatusAlertIcon = $ajaxStatusAlert.find(".ajax-status-alert__icon");

    /* Apply `sr-only` to loading done state so that it can remain visible to sr until read completely */
    $ajaxStatusAlertIcon.hide(0);
    if (!$ajaxStatusAlertText.hasClass("sr-only")) {
        $ajaxStatusAlertText.text(txtLoadingFinished).addClass("sr-only");
    }
    $ajaxStatusAlert.attr("class", "");
    console.log("LOG: TEXT UPDATE"); // TODO: Remove this
});

What about using a button instead of a link?

  • The issue remains the same. It does not make a difference if a user clicked a link or a button
  • From my perspective it depends much on the action whether we can assume it's more semantic to use a button or a link. If the action simulates a new page load, e.g. most of the page content will get updated, then a user expects such behavior, similarly to opening a new page and thus a link seems appropriate. It should be considered updating the page title and handling the history state for the browsers "back" button as well
  • If the action will only replace a small part of the page e.g. collapsing/expanding element then a button might be a better option. In such cases focus management might not even be a good idea despite the content update. It's better to let the user control the focus
1
  • 2
    This is large topic, I can't pretend to offer any real insight here but I know who can. Mike Gifford of OpenConcept has been accessibility maintainer, gov.consultant & drupal core dev for years. I think he works with a blind dev for certain accessibility texting. If you reach out to @mgifford on twitter he'll have some background on this. I don't know if I'm allowed to refer people like that here... – It's Dylan Feb 28 '20 at 18:25
0

Somehow stumbled across this and thought I would throw a solution (or the best fix we have at the moment) down for you even if it is 9 months old!

You are 95% of the way there, aria-live regions are the way to go, the only part you have over-complicated is how to use them effectively here.

All updates to a screen reader for page statuses should ideally go through one aria-live region.

The way updates work is that if the content of that region is changed it will be announced.

So instead of trying to make your toasts / notifications the live region a much simpler way is to have a visually hidden live region to update screen readers and update that aria-live region as actions occur.

This solves your problem with having to toggle aria-hidden on the aria-live region and the associated bugs with that.

Instead you would make your toast / notification have aria-hidden on it (and add any relevant information to the aria-live region).

So for your above scenario you would:-

  • click a link, immediately add "loading" to the aria-live region using .innerHTML or similar in JavaScript. Leave the focus where it is.
  • When the section loads move focus to that region as you have already mentioned. If your HTML structure is correct ideally you should move focus to that sections h2 to h6 which should describe the new section.
  • If the loading is taking a long time simply replace the content of the aria-live region with some different text (maybe "still loading, please wait").
  • If the loading fails, same principle, update the aria-live region with "the content could not be loaded please try again" or something appropriate.

The same principles above apply to Single Page Applications, tell a user a page is loading, then when it has loaded move focus to that page's h1, which should be descriptive enough to let them know they have landed on the correct page.

One note here is that to avoid adding unnecessary tab stops to a page the heading should have tabindex="-1". That way it is programatically focusable but isn't in the pages focus order.

Another important note is that this is one of the few times an aria-live region should be aria-live="assertive" - you want the screen reader to interrupt with important updates (you could possibly say it should be role="alert" but I always reserve that for errors personally and the experience difference is minimal).

Another thing you may want to do is add aria-busy="true" to the region that is currently loading content via AJAX (if it is a partial page update). This means that if a screen reader user does try to navigate while waiting for things to load they will be alerted to the fact an item is loading in screen readers that support it.

So in your example you need to:

  • Remove the aria-live from #ajax-status-alert
  • Remove any toggles to sr-only for the alert in your JS, add aria-hidden="true" to the alert.
  • Add a new div <div id="aria-live-region" aria-live="assertive" class="visually-hidden"></div>. While testing you may want to leave off the class="visually-hidden" so you can see updates that a screen reader would hear (or comment out the visually-hidden class in your CSS).
  • Update your JavaScript to change the content of the new #aria-live-region div every time you update the loading status using something like $('#aria-live-region').html(txtLoadingStarted);
  • Add a .focus() command that points to a relevant heading. e.g. <h2 tabindex="-1">New section</h2> once the new page / section has loaded.
  • clear the aria-live region content ready for the next updates ($('#aria-live-region').html("");)
  • finally add the visually-hidden class to your CSS once you have tested everything and test with a screen reader.

Visually Hidden Class

This class is better than sr-only as explained in this answer I gave on Stack Overflow, so use this instead if you can.

.visually-hidden { 
    border: 0;
    padding: 0;
    margin: 0;
    position: absolute !important;
    height: 1px; 
    width: 1px;
    overflow: hidden;
    clip: rect(1px 1px 1px 1px); /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
    clip: rect(1px, 1px, 1px, 1px); /*maybe deprecated but we need to support legacy browsers */
    clip-path: inset(50%); /*modern browsers, clip-path works inwards from each corner*/
    white-space: nowrap; /* added line to stop words getting smushed together (as they go onto seperate lines and some screen readers do not understand line feeds as a space */
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.