Search code examples
formsradio-buttonaccessibilityshow-hide

Accessible yes/no radio buttons that show/hide corresponding form fields


I'm working on a very old site, trying to update it to be accessible for screen reader users. In particular, I'm trying to figure out the best way to code some radio buttons where the yes/no values conditionally show other form fields.

I have a prototype at: https://jsfiddle.net/vpyx47qr/

Note: It is just a prototype, though the html is loosely structured around the real form I'm updating.

Some extra fields should display when the "yes" radio button is checked. Those fields are not shown when "no" is checked. If you click the radios in the prototype you'll see what I'm trying to do.

The "yes" radio button has aria-expanded="false" and aria-controls="SpouseYesFields" attributes on it.

The corresponding "yes" form fields are down in <div id="SpouseYesFields" role="region" aria-labelledby="SpouseYes" class="spouse-fields" hidden>

$(function() {
  var $radios = $('[name=spouse]');

  $radios.click(function() {
    if (this.value === 'true') {
      $('#SpouseYes').attr('aria-expanded', true);
      $('#SpouseYesFields').attr('hidden', false);
    } else {
      $('#SpouseYes').attr('aria-expanded', false);
      $('#SpouseYesFields').attr('hidden', true);
    }
  });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<form>
  <fieldset>
    <legend>step 5</legend>
    <p>Are you applying with your spouse?</p>

    <ol role="radiogroup" aria-required="true">
      <li>
        <label for="SpouseYes">
          <input id="SpouseYes"
            name="spouse"
            type="radio"
            value="true"
            aria-describedby="error-subscriber-applying-with-spouse"
            
            aria-expanded="false"
            aria-controls="SpouseYesFields">
          Yes
        </label>
      </li>
      <li>
        <label for="SpouseNo">
          <input id="SpouseNo" name="spouse" type="radio" value="false" aria-describedby="error-subscriber-applying-with-spouse">
          No
        </label>
      </li>

      <div id="error-subscriber-applying-with-spouse" class="intake-error-message" hidden>
        Please select one.
      </div>

      <!--spouse progressive disclosure-->
      <div id="SpouseYesFields" role="region" aria-labelledby="SpouseYes" class="spouse-fields" hidden>
        <li>
          <label for="label1">some label</label>
          <input id="label1" type="text">
        </li>
        <li>
          <label for="label2">some label2</label>
          <input id="label2" type="text">
        </li>
        <li>
          <label for="label3">some label3</label>
          <input id="label3" type="text">
        </li>
      </div>
    </ol>
  </fieldset>
</form>

The site already uses jquery and I will continue to use that for updating this form.

All I'm doing is toggling the radio button's aria-expanded attribute aria-expanded="true|false" and adding or removing a hidden attribute on the wrapping div around those extra "yes" fields. If you inspect these elements in the prototype then click between yes/no you'll see this.

Is this the best way to provide this sort of progressive disclosure function in an accessible manner or is there a better way?


Solution

  • The GOV.UK Design System Team published an update on the accessibility of conditionally revealed questions a while ago.

    On their component page for radios, they give some updated guidelines for Conditionally revealing a related question

    Keep it simple. If the related question is complicated or has more than one part, show it on the next page in the process instead.

    You might consider separating the form into several pages, depending on your use case.

    What’s also noteworthy on their component, is that the revealed fields come directly after the radio button that reveals them, which makes the relationship more clear.

    GOV.UK are tracking further research in the Radios component issue

    aria-expanded is actually not intended for radio, so support will be lacking as well. So everything depends on the layout and structure.

    Don’t simply hide referenced error messages

    As QuentinC mentions, content referenced via aria-describedby is used for a description even if it is hidden.

    A. Hidden Not Referenced: If the current node is hidden and is:

    1. Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden.

    Accessible Name and Description Computation 1.2

    So you should also remove the reference when hiding the message.

    See also Usable and Accessible Form Validation and Error Recovery

    Use <fieldset>

    Should you decide to keep everything on the same page, you probably should use group instead of region, or even better, <fieldset> to group the dependent inputs.

    region is a landmark role, “sufficiently important […] to have it listed in a summary of the page”. This seems exaggerated, especially under the premise that it’s a conditional block of content.

    If you reveal these inputs right after the toggle, role=radiogroup isn’t pertinent either. You should replace the overall grouping by a <fieldset> as well.

    Focus in a radiogroup MUST be managed, and if the dependent fields are part of the same grouping, you’ll need to Tab to focus them.

    Provide missing accessible names

    A radiogroup, group and a form MUST have an accessible name, which is missing in your example.

    When using a screen reader in form mode, users jump directly to the radio button via Tab. The text before will only be read if it is assigned to a grouping role.

    <p id="radio-question">Are you applying with your spouse?</p>
    <ol role="group" aria-labelledby="radio-question">
    

    Always bind to data events, not UIEvent

    In general, you should avoid binding to user events like click in favour of DOM events like change. This makes your code more robust against side effects, like other scripts changing the value.

    Avoid jQuery for simple tasks

    If you don’t have a real advantage of using a library for something, you should always resort to vanilla JavaScript.

    Because doing so will only harden the dependence on the library. Switching to another library will always get harder, forcing a complete rewrite at some point, without reason.

    const radios = document.querySelectorAll('[name=spouse]'),
      spouseYes = document.getElementById('SpouseYes'),
      spouseYesFields = document.getElementById('SpouseYesFields');
    
      radios.forEach(r => r.addEventListener('change', function() {
        const target = event.target;
    
        if (target.value === 'true') {
          target.setAttribute('aria-expanded', true);
          spouseYesFields.hidden = false;
        } else {
          spouseYes.setAttribute('aria-expanded', false);
          spouseYesFields.hidden = true;
        }
      }));
    <form aria-labelledby="form-title">
      <h2 id="form-title">Something form</h2>
    
      <fieldset>
        <legend>Step 5</legend>
        <p id="radio-question">Are you applying with your spouse?</p>
    
        <ol role="group" aria-labelledby="radio-question">
          <li>
            <label for="SpouseYes">
              <input id="SpouseYes"
                name="spouse"
                type="radio"
                value="true"
                aria-expanded="false"
                aria-controls="SpouseYesFields">
              Yes
            </label>
          </li>
    
          <!--spouse progressive disclosure-->
          <fieldset id="SpouseYesFields" hidden>
            <legend>Information about your spouse</legend>
            <li>
              <label for="label1">some label</label>
              <input id="label1" type="text">
            </li>
            <li>
              <label for="label2">some label2</label>
              <input id="label2" type="text">
            </li>
            <li>
              <label for="label3">some label3</label>
              <input id="label3" type="text">
            </li>
          </fieldset>
    
          <li>
            <label for="SpouseNo">
              <input id="SpouseNo" name="spouse" type="radio" value="false">
              No
            </label>
          </li>
        </ol>
      </fieldset>
    </form>

    Get a screen reader and test the result

    Today, it’s quite easy to get a screen reader. TalkBack is pre-installed on Android phones, VoiceOver on iOS and macOS, Narrator on Windows.

    To test with a more commonly used screen reader, NVDA is the free option.