wearebraid / vue-formulate

⚡️ The easiest way to build forms with Vue.
https://vueformulate.com
MIT License
2.25k stars 245 forks source link

Accessibility: Radio/Checkbox groups have semantically improper label associations #359

Open aminimalanimal opened 3 years ago

aminimalanimal commented 3 years ago

Describe the bug

The output for a radio/checkbox group comes out in this structure:

div.formulate-input(data-classification="group" data-type="checkbox")
  div.formulate-input-wrapper
    label.formulate-input-label.formulate-input-label--before(for="formulate--guide-inputs-types-box--2") This is a label for all the options

    div.formulate-input-element.formulate-input-element--group.formulate-input-group

      div.formulate-input-group-item.formulate-input(data-classification="box" data-type="checkbox")
        div.formulate-input-wrapper
          div.formulate-input-element.formulate-input-element--checkbox(data-type="checkbox")
            input#formulate--guide-inputs-types-box--2_first(type="checkbox" value="first")
            label.formulate-input-element-decorator(for="formulate--guide-inputs-types-box--2_first")
          label.formulate-input-label.formulate-input-label--after(for="formulate--guide-inputs-types-box--2_first") First

      div.formulate-input-group-item.formulate-input(data-classification="box" data-type="checkbox")
        div.formulate-input-wrapper
          div.formulate-input-element.formulate-input-element--checkbox(data-type="checkbox")
            input#formulate--guide-inputs-types-box--2_second(type="checkbox" value="second")
            label.formulate-input-element-decorator(for="formulate--guide-inputs-types-box--2_second")
          label.formulate-input-label.formulate-input-label--after(for="formulate--guide-inputs-types-box--2_second") Second

There are a couple of issues with this output from an accessibility perspective.

  1. The first label is not associated with anything. A screen reader user that lands on one of these checkboxes/radios will not necessarily hear the context of the entire question. This is especially problematic for forms that might ask several questions with the same answers (such as “agree, neutral, disagree”). There are two options to properly ensure that the context of the question is linked to the radios/checkboxes.
    • WAI-ARIA: Add :aria-labelledby="uniqueId" and role="group" attributes to either formulate-input or formulate-input-wrapper. Add the uniqueId as the id of the main label. This is probably the path of least resistance.
      div.formulate-input(role="group" aria-labelledby="unique-id" data-classification="group" data-type="checkbox")
      div.formulate-input-wrapper
        label.formulate-input-label.formulate-input-label--before(id="unique-id") This is a label for all the options
    • Semantic HTML: Use fieldset and legend. the div.formulate-input-wrapper would then have to be fieldset.formulate-input-wrapper and the first label would become the legend. This comes with its own styling headaches.
      div.formulate-input(data-classification="group" data-type="checkbox")
      fieldset.formulate-input-wrapper
        legend.formulate-input-label.formulate-input-label--before(for="formulate--guide-inputs-types-box--2") This is a label for all the options

      Both of these approaches work in VoiceOver in Chrome. Uncertain which is overall better supported and since the screen reader landscape is so glitchy, I doubt either would be 100%, but it’s important to implement one of them.

  2. The formulate-input-element-decorator labels are linked to the input with for. I recognize that this is being done to ensure that clicking on the decorator label toggles the associated input (and that the decorator label is going to be used for styling purposes), but it poses a larger issue for screen readers because most screen readers don't support multiple labels. It is likely then, that a screen reader would choose the first label to associate with the input, which in this case would provide no valuable information to the user. The simplest solution to this would likely be to place the decorator span or div inside the label that contains the radio button/checkbox’s label content. (These days it’s becoming more and more possible to use pseudo-elements to style these elements, so the decorators may not even be strictly necessary.)
    div.formulate-input-group-item.formulate-input(data-classification="box" data-type="checkbox")
      div.formulate-input-wrapper
        div.formulate-input-element.formulate-input-element--checkbox(data-type="checkbox")
          input#formulate--guide-inputs-types-box--2_first(type="checkbox" value="first")
        label.formulate-input-label.formulate-input-label--after(for="formulate--guide-inputs-types-box--2_first")
          span.formulate-input-element-decorator
          | First

To Reproduce

Issue 1: Unassociated legend

  1. Go to Box | Vue Formulate
  2. Turn on a screen reader
  3. Hit tab until landing on a checkbox or radio in a group
  4. Note that the context / main label / legend isn’t read

Issue 2: Multiple associated labels for one input

  1. Install aXe accessibility testing tool into your browser of choice
  2. Go to Box | Vue Formulate
  3. Open dev tools and run aXe on the page
  4. Several instances of “Form field should not have multiple label elements” appear

Expected behavior

Screen reader users should be able to jump to radio/checkbox groups and be able to hear the context of the question being asked of them, as well as the label associated with the radio/checkbox input. Deque’s aXe should not throw warnings/errors.

aminimalanimal commented 3 years ago

I also just want to chime in and say that vue-formulate is looking pretty dang nifty overall. It’s a similar but superior system to one I've been working on for quite a while, so I'm pretty excited about it. I'd been writing my own to achieve better accessibility standards than I've found in other frameworks. I see a lot of promise here though, and especially like that I can write custom components where needed. Good job to the team. Kudos. 🎉

justin-schroeder commented 3 years ago

@aminimalanimal I appreciate the heads up on these issues. I think I speak for all the contributors of Vue Formulate when I say we're keenly interested in ensuring accessibility is a key priority. Especially considering making improvements in this one library can improve forms on hundreds or thousands of other sites.

Your first issue can be addressed immediately via the role="group" and aria-labelledby="uniqueId" as you suggest. The release/2.5.0 branch is in deep development at the moment. I'll try to get these included.

The second issue is definitely more challenging since it requires a breaking change to the DOM that is already being output. However, the multiple label issue has come up before actually — specifically #313 which proposed to allow the disabling of the second checkbox all together. This is implemented in the unpublished, but public, release/2.5.0 branch. For the time being this is probably as far as the 2.x series can take the improvement.

However, as we close out 2.5 our attention is turning to a full re-write 3.x which can include breaking changes. Among a number of other improvements, one thing im keen to see improved in 3.x is overall accessibility. We've had some conversations with experts in the field and any additional assistance you want to provide on the 3.x development will be greatly appreciated.

justin-schroeder commented 3 years ago

@aminimalanimal Ive just pushed a new commit on the release/2.5.0 branch that includes the aria-labledby and role="group" attributes. I'd love if you could take a peek at it and render an opinion on it 👍

If you clone that branch, npm install, and run npm run dev you should be able to see a "specimen sheet" of all the inputs including some checkbox samples.

aminimalanimal commented 3 years ago

@justin-schroeder Just checked out release/2.5.0. This is a massive improvement. Thanks so much.

This implementation differs from my description above, but it's potentially better. This implementation assigns role="group to the container directly around the radios/checkboxes whereas I described it being on the outer container, but my inclination of where to place it was based on how fieldset operates, and that may not be the preferred way of handling role="group". In any case, the differences in how a screen reader interprets the two are subtle, and I'm honestly not certain which way would be preferred. I'm quite happy with this approach.

plweil commented 2 years ago

Adding an accessible label for the group as described here is an important improvement, to be sure. But it is also worth noting that this approach goes against best practices for ARIA. The first rule for ARIA is "if you can use native HTML, then use native HTML". There is no compelling reason that I can see to use ARIA here. The most straightforward, simplest, and best-supported solution for radio and checkbox groups is to use the native fieldset and legend elements. That's why they exist.

That's not to say that using role="group" and aria-labelledby here break anything or are likely to cause problems. But all too often, I see devs go straight for ARIA-based solutions when native HTML is all they need. For a more in-depth discussion, please see Arian Roselli's My Priority of Methods for Labeling a Control.

So please consider implementing a native HTML solution for v3.

justin-schroeder commented 2 years ago

@plweil Good news on this front — FormKit (the next major version) uses fieldset and legend 👍

aminimalanimal commented 2 years ago

The first rule for ARIA is "if you can use native HTML, then use native HTML". There is no compelling reason that I can see to use ARIA here.

I actually think there is a a compelling argument to use WAI-ARIA: fieldset and legend are notoriously difficult to style:

This messes up the ability to share labelling styles with other inputs via common classes making visual consistency more difficult. And... it's not something that a reset can fix, either.

Additionally, from an accessibility standpoint, the last time I tested support for nested fieldsets (granted, it was years ago), many screen readers neglected to read the outer legend.

With role="group" and aria-labelledby, that outer “legend”’s ID can be passed in as well, or used with aria-describedby instead to ensure that the context is maintained. Edit: On macOS Monterey, aria-describedby doesn't work on native fieldset or role="group". aria-labelledby works with either when both associated IDs are provided, so it's a moot point.

When would this matter? Communication preference forms come to mind. Repeated outer fieldsets like “Friend requests” or “When someone comments on my post” with inner fields like an on/off toggle + radio/checkbox groups for “Notify via” (Email, text, phone call), or “Frequency” (Instant, Daily, Weekly).

I think that instead of a “native is always preferred” ideology, if you want to know which to use, tests need to be run to guarantee the best overall support.

plweil commented 2 years ago

Thanks @aminimalanimal. You make some valid points.

While fieldset and legend have unique behaviors in regard to css, I honestly have never had serious difficulty in working with them. And while I have never tried it (or needed to), MDN says that you can use flex with it. And of course there are other ways to add icons to an element (e.g., pseudo-elements).

For most situations, I don't find styling a compelling reason not to use these elements. How fancy do we need legends and groups of checkboxes to be? We're talking about forms, not beautiful works of art, right? Of course, if you absolutely must style one of these a certain way and can't for some reason, no one is going to say that you can't use the ARIA-based approach. After all, this is not a matter of ideology (despite the four(?) "rules" of ARIA); this is really about accepted best practices.

Regarding nested fieldsets, it has been well-known for some time that screen readers don't handle them well. For that reason (and perhaps others), accessibility experts advise against using them. I would think that one could find ways to avoid using nested fieldsets and still create an effective, user-friendly form. In my own work, I've never needed to use them. But if you absolutely must, then I suppose that could be a compelling reason to go the ARIA route.

aminimalanimal commented 2 years ago

I ran the tests. Results here. Not tested: JAWS, Android. https://codepen.io/aminimalanimal/pen/NqbXKX

Overall, of modern browsers, iOS is the one that messes up what otherwise is pretty darn consistent support for both. iOS does not seem to support role="group". I may file a bug for that.

If you're supporting IE 11 (and hopefully you aren't), role="group" is actually preferable.

I have definitely run into complications with fieldset styling. I know there are workarounds to it, but that's not the point. If you're creating a system where the developer isn't going to have a lot of control over the HTML output, you need to be aware of the limitations your system imposes upon them, and fieldset is a special case.