How do I get a screen reader to correctly say 'button expanded' when aria-expanded is true
Asked Answered
B

3

8

I'm working on making our accordions accessible with aria labels such as aria-expanded. I have the value of aria-expanded change correctly as the accordion trigger heading gets clicked or the 'return' button is pressed on the keyboard. For some reason though ChromeVox, which I'm using to test, will only say 'button collapsed' initially but doesn't say 'button expanded' once the value changes after a click.

I've looked at examples on other sites such as https://www.w3.org/TR/wai-aria-practices/examples/accordion/accordion.html and ChromeVox reads both states of aria-expanded correctly there, so I'm thinking it's something about how my code is structured that is preventing ChromeVox from announcing the 'expanded' state.

Here is an example of one accordion section:

<div class="accordion-section">

    <button tabIndex="0" id="rules_list" class="accordion" aria-expanded="false" aria-controls="sectRules">
        <span class="title">Rules</span>
    </button>

    <div class="content" role="region" id="sectRules" aria-labledby="rules_list">

        <h4>What rules must all visitors follow?</h4>

        <ul class="list">
            <li style="list-style-type:disc; border-top:0px; margin-bottom:0px; padding-bottom:0px; padding-top:10px; overflow:visible;">rule 1</li>
            <li style="list-style-type:disc; border-top:0px; margin-bottom:0px; padding-bottom:0px; padding-top:10px; overflow:visible;">rule 2</li>
            <li style="list-style-type:disc; border-top:0px; margin-bottom:0px; padding-bottom:0px; padding-top:10px; overflow:visible;">rule 3 etc..</li>
        </ul>

    </div>

</div>

The relevant js is:

/* Generic Accordion */
$('.accordion .title').click(function() {
    $(this).parent().parent().children('.content').toggle();
    $(this).toggleClass('open');

    $(this).hasClass("open") ? $(this).parent().attr("aria-expanded", "true") : $(this).parent().attr("aria-expanded", "false");
});

/* Adding support for keyboard */
$('.accordion').on('keydown', function(e) {
    if (/^(13|32)$/.test(e.which)) {
        e.preventDefault();
        $(this).find('.title').click();
    }
});

The relevant CSS is :

.accordion + .content {
    display: none;
    margin-top: 10px;
    padding: 10px;
}

.accordion + .content.open {
    display: block;
    background-color: white;
}

I'm at a loss, any help would be much appreciated.

Bolme answered 31/5, 2019 at 16:52 Comment(0)
B
5

I second what brennanyoung said about the way you used the span element and why it probably is the reason your code doesn't work as you expect.

In my opinon, you really should consider using the button element to toggle the content, as this will avoid some extra work such as:

  • ensuring the span covers the whole button to prevent clicking the button which would result in nothing (saying it aloud sounds weird),
  • handling focusing the span,
  • ensuring the span properly acts like a button (click, press, and other button related events).

Also, programmatically triggering the click event in your code is a hint that something easier can be done.

hidden + aria-labelledby

I would tend to keep it as simple as possible by using both the hidden attribute on the content to be toggled, and the aria-expanded on the button toggling it.

Collapsible Sections of the "Inclusive components book" by Heydon Pickering is a very good read if you need… well, collapsible sections. Actually the whole book is awesome, you won't waste your time reading it if you haven't yet.

The hidden attribute is properly handled by the screen readers and will hide the element both visually and from the accessibility tree. You can use it on pretty much any recent web browser (https://caniuse.com/#search=hidden), which makes it a good candidate to avoid juggling with classes and the CSS display property.

If you want to use the aria-labelledby (with 2 "L" by the way, there is one missing in your code) on the content (and you should, since you declared it as a region), using the button text as a label works fine.

However, if you plan to use a text that describes the action (for example "Show the rules" or "Hide the rules" depending on the state), then this isn't relevant anymore and you will have to use another element as the label for this landmark. The h4 element in your code seems to do the job, and giving it an id will let screen readers identify the region more easily.

Example

I took the liberty of rewriting the example you provided to only use plain JS, with the small adjustments I mentioned. It is testable here.

<div class="accordion-section" id="accordion-1">

    <button id="rules-list" class="rules-toggle" aria-expanded="true" aria-controls="sect-rules">
        <span>Rules</span>
    </button>

    <section role="region" class="content" id="sect-rules" aria-labelledby="rules-list-title">

        <h4 id="rules-list-title">What rules must all visitors follow?</h4>

        <ul class="list">
            <li>rule 1</li>
            <li>rule 2</li>
            <li>rule 3</li>
        </ul>

    </section>

</div>
const myButton = document.querySelector('#accordion-1 .rules-toggle');
myButton.addEventListener('click', toggleRules);

function toggleRules(evt) {
  const button = evt.currentTarget;
  const accordionSection = button.parentElement;
  const content = accordionSection.querySelector('.content');

  if ('hidden' in content.attributes) {
    content.removeAttribute('hidden');
    button.setAttribute('aria-expanded', true);
  } else {
    content.setAttribute('hidden', true);
    button.setAttribute('aria-expanded', false);
  }
}
.rules-toggle + .content {
  margin-top: 10px;
  padding: 10px;
  background-color: white;
}

.list li {
  list-style-type: disc;
  border-top: 0;
  margin-bottom: 0;
  padding-bottom: 0;
  padding-top: 10px;
  overflow: visible;
}
Brindled answered 6/6, 2019 at 12:6 Comment(0)
B
2

You put the click handler on the span, not the button? That doesn't seem right, and might be the cause of the problem, since aria-expanded is placed on the button.

Voiceover is probably looking for aria-expanded on the event target, which is the span, not the button. Of course it does not find it.

This might explain why it announces the state when the button gets focus, but not when you click it.

So check whether adding click to the button, rather than the span, gives you the result you want. And if you do this, you can skip one of the parent() steps in the toggle().

In addition, I would set aria-expanded on the .content and keep it in sync with the aria-expanded attribute on the button.

Boyhood answered 6/6, 2019 at 10:10 Comment(3)
BTW my eyebrows raised when I saw that your content region is labeled by a button element. That doesn't seem right, although I doubt it is the cause of your problem.Boyhood
does it make sense to put the aria-expanded on the .content as well ? I guess aria-controls on the button should be enough. i can see the reasoning behind why we would want to do so, but, any examples where aria-expanded is also used on the controlled elements?Parasite
I would be inclined to but aria-expanded on both. There are some w3c examples that do this (and some that don't).Boyhood
B
0

This was my final solution, this works for multiple sections:

````
   let elementsArray = document.querySelectorAll(".rules-toggle");

   elementsArray.forEach(function(elem) {
      elem.addEventListener("click", function (evt) {
        const button = evt.currentTarget;
        const accordionSection = button.parentElement;
        const content = accordionSection.querySelector('.content');

       if ('hidden' in content.attributes) {
         content.removeAttribute('hidden');
         button.setAttribute('aria-expanded', true);
       } else {
        content.setAttribute('hidden', true);
        button.setAttribute('aria-expanded', false);
       }
     });
});

Bolme answered 21/6, 2019 at 19:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.