Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 901
Anya Avatar asked May 31 '19 16:05

Anya


2 Answers

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;
}
like image 111
dashdashzako Avatar answered Oct 18 '22 11:10

dashdashzako


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.

like image 2
brennanyoung Avatar answered Oct 18 '22 09:10

brennanyoung