Example Start

Lunch Options

Drink Options

Example End

Example Description

Type: Best Practice

Simple example of a radio button group widget using aria-activedescendant to mangage focus.

Keyboard Support

  • Tab: Move between button items and text area.
  • Enter or space: Toggle aria-checked state of checkbox with keyboard focus.

Example Markup

Browser Compatibility

HTML Source Code


<div role="application">

  <h3 id="rg1_label">Lunch Options</h3>

  <ul class="radiogroup"
      id="rg1"
      role="radiogroup"
      aria-labelledby="rg1_label"
      aria-activedescendant="rg1-r4"
      tabindex="0";
   >
    <li id="rg1-r1"
      role="radio"
      aria-checked="false">
      <img role="presentation" src="http://www.oaa-accessibility.org/media/examples/images/radio-unchecked.gif" />
      Thai
    </li>
    <li id="rg1-r2"
      role="radio"
      aria-checked="false">
      <img role="presentation" src="http://www.oaa-accessibility.org/media/examples/images/radio-unchecked.gif" />
      Subway
    </li>
    <li id="rg1-r3"
      role="radio"
      aria-checked="false">
      <img role="presentation" src="http://www.oaa-accessibility.org/media/examples/images/radio-unchecked.gif" />
      Jimmy Johns
    </li>
    <li id="rg1-r4"
      role="radio"
      aria-checked="true">
      <img role="presentation" src="http://www.oaa-accessibility.org/media/examples/images/radio-checked.gif" />
      Radio Maria
    </li>
    <li id="rg1-r5"
      role="radio"
      aria-checked="false">
      <img role="presentation" src="http://www.oaa-accessibility.org/media/examples/images/radio-unchecked.gif" />
      Rainbow Gardens
    </li>  
  </ul>  

<!-- Start of second Radio Group  -->

  <h3 id="rg2_label">Drink Options</h3>
  
  <ul id="rg2"
      class="radiogroup"
      role="radiogroup"
      aria-labelledby="rg2_label"
      aria-activedescendant=""
      tabindex="0"
   >
    <li id="rg2-r1"
      role="radio"
      aria-checked="false">
      <img role="presentation" src="http://www.oaa-accessibility.org/media/examples/images/radio-unchecked.gif" />
      Water
    </li>
    <li id="rg2-r2"
      role="radio"
      aria-checked="false">
      <img role="presentation" src="http://www.oaa-accessibility.org/media/examples/images/radio-unchecked.gif" />
      Tea
    </li>
    <li id="rg2-r3"
      role="radio"
      aria-checked="false">
      <img role="presentation" src="http://www.oaa-accessibility.org/media/examples/images/radio-unchecked.gif" />
      Coffee
    </li>
    <li id="rg2-r4"
      role="radio"
      aria-checked="false">
      <img role="presentation" src="http://www.oaa-accessibility.org/media/examples/images/radio-unchecked.gif" />
      Cola
    </li>
    <li id="rg2-r5"
      role="radio"
      aria-checked="false">
      <img role="presentation" src="http://www.oaa-accessibility.org/media/examples/images/radio-unchecked.gif" />
      Ginger Ale
    </li>  
  </ul>  

</div>

CSS Code


ul.radiogroup {
    margin: 0;
    padding: 0;    
  width: 8em;
}

ul.radiogroup  li   {
    padding: 0;
    margin: 0;
    margin-left: 1em;
    padding: 4px;
    list-style: none;
    width: 6em;    
}
    
ul.radiogroup li:hover {
    padding: 2px;
    border: gray 2px solid;
}

ul.radiogroup li.selected {
    padding: 2px;
    border: black 2px solid;
   }

.offscreen {
  position: absolute;
    left: -200em;
    top: -20em;
}

Javascript Source Code


$(document).ready(function() {

  var group1 = new radioGroup('rg1');
  var group2 = new radioGroup('rg2');

}); // end ready

function keyCodes () {
  // Define values for keycodes
  this.enter      = 13;
  this.space      = 32;

  this.left       = 37;
  this.up         = 38;
  this.right      = 39;
  this.down       = 40;
}

//
// Function radioGroup() is a class to define an ARIA-enabled radiogroup widget.
//
// This widget attaches to an unordered list and makes each list entry a group
// of radio buttons.
//
// @param (id object) id is the html id of the <ul> to attach to
//
// @return N/A
//
function radioGroup(id) {

  var thisObj = this;

  ///////// define widget properties ///////////////

  this.$id = $('#' + id);

  // find all list items with a role of radio
  this.$buttons = this.$id.find('li').filter('[role=radio]');

  // Store the currently checked item
  this.$checked = this.$buttons.filter('[aria-checked=true]');

  this.checkButton = true; // set to false during ctrl+arrow operations;

  if (this.$checked.length > 0) {
    this.$active = $('#' + this.$id.attr('aria-activedescendant')); // the active button
  }
  else {
    this.$active = null; // no selected button
  }
  
  this.keys = new keyCodes();

  ///////////// Bind Event handlers ////////////////

  this.$buttons.click(function(e) {
    return thisObj.handleClick(e, $(this));
  });

  this.$id.keydown(function(e) {
    return thisObj.handleKeyDown(e, $(this));
  });

  this.$id.keypress(function(e) {
    return thisObj.handleKeyPress(e, $(this));
  });

  this.$id.focus(function(e) {
    return thisObj.handleFocus(e, $(this));
  });

  this.$id.blur(function(e) {
    return thisObj.handleBlur(e, $(this));
  });
}

//
// Function selectButton() is a member function to select and possibly check a button in the
// radioGroup.
//
// @param ($id object) $id is the jQuery object of the button to select
//
// @return N/A
//
radioGroup.prototype.selectButton = function($id) {

  if (this.checkButton == true) {
    // checking the button

    // remove the checked image from the previously checked button
    this.$checked.find('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/radio-unchecked.gif');

      // set the previous button's aria-checked attribute to false
      this.$checked.attr('aria-checked', 'false');

    // update the button image
    $id.find('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/radio-checked.gif');

      // set the new button's aria-checked attribute to true
      $id.attr('aria-checked', 'true');

    // update the stored $checked object
    this.$checked = $id;
  }

  if (this.$active != null) {
    // remove the selected styling from the previous active button
    this.$active.removeClass('selected');
  }

  // give this button the selected class
  $id.addClass('selected');

  // update the stored $active object
  this.$active = $id;

  // update the aria-activedescendant property of the group
  this.$id.attr('aria-activedescendant', $id.attr('id'));
  
  // reset the checkButton flag - in case it was false
  this.checkButton = true;

} // end selectButton()

//
// Function handleClick() is a member function to process keydown events for the radioGroup.
//
// @param (e object) e is the event object
//
// @param ($id object) $is is the jquery object of the triggering element
//
// @return (boolean) Returns false if consuming event; true if propagating
//
radioGroup.prototype.handleClick = function(e, $id) {

  if (e.altKey || e.ctrlKey || e.shiftKey) {
    // do nothing
    return true;
  }

  // select the clicked button
  this.selectButton($id);

  // give the group focus
  this.$id.focus();

  e.stopPropagation();
  return false;

} // end handleClick()

//
// Function handleKeyDown() is a member function to process keydown events for the radioGroup.
//
// @param (e object) e is the event object
//
// @param ($id object) $is is the jquery object of the triggering element
//
// @return (boolean) Returns false if consuming event; true if propagating
//
radioGroup.prototype.handleKeyDown = function(e, $id) {

  if (e.altKey) {
    // do nothing
    return true;
  }

  switch (e.keyCode) {
    case this.keys.space:
    case this.keys.enter: {
      if (e.ctrlkey || e.shiftKey) {
        // do nothing
        return true;
      }

      // select and check the active the button
      this.selectButton(this.$active);

      e.stopPropagation();
      return false;
    }
    case this.keys.left:
    case this.keys.up: {

      var $prev = this.$active.prev(); // the previous button

      if (e.shiftKey) {
        // do nothing
        return true;
      }

      // if this was the first item
      // select the last one in the group.
      if (this.$active.index() == 0) {
        $prev = this.$buttons.last();
      }

      if (e.ctrlKey) {
        // set checkButton to false so
        // focus does not check button
        this.checkButton = false;
      }

      // select the previous button
      this.selectButton($prev);

      e.stopPropagation();
      return false;
    }
    case this.keys.right:
    case this.keys.down: {

      var $next = this.$active.next(); // the next button

      if (e.shiftKey) {
        // do nothing
        return true;
      }

      // if this was the last item,
      // select the first one in the group.
      if (this.$active.index() == this.$buttons.length - 1) {
        $next = this.$buttons.first();
      }

      if (e.ctrlKey) {
        // set checkButton to false so
        // focus does not check button
        this.checkButton = false;
      }

      // give the next button focus
      this.selectButton($next);

      e.stopPropagation();
      return false;
    }
  } // end switch

  return true;

} // end handleKeyDown()

//
// Function handleKeyPress() is a member function to process keydown events for the radioGroup.
// This is needed to prevent browsers that process window events on keypress (such as Opera) from
// performing unwanted scrolling of the window, etc.
//
// @param (e object) e is the event object
//
// @param ($id object) $is is the jquery object of the triggering element
//
// @return (boolean) Returns false if consuming event; true if propagating
//
radioGroup.prototype.handleKeyPress = function(e, $id) {

  if (e.altKey) {
    // do nothing
    return true;
  }

  switch (e.keyCode) {
    case this.keys.space:
    case this.keys.enter: {
      if (e.ctrlKey || e.shiftKey) {
        // do nothing
        return true;
      }
    }
    case this.keys.left:
    case this.keys.up:
    case this.keys.right:
    case this.keys.down: {
      if (e.shiftKey) {
        // do nothing
        return true;
      }
      e.stopPropagation();
      return false;
    }
  } // end switch

  return true;

} // end handleKeyPress()

//
// Function handleFocus() is a member function to process focus events for the radioGroup.
//
// @param (e object) e is the event object
//
// @param ($id object) $is is the jquery object of the triggering element
//
// @return (boolean) Returns false if consuming event; true if propagating
//
radioGroup.prototype.handleFocus = function(e, $id) {

  if (this.$active == null) {
    // no previously selected button - select first one
    this.selectButton(this.$buttons.first());
  }
  else {
    // select active button
    this.selectButton(this.$active);
  }

  return true;
} // end handleFocus()

//
// Function handleBlur() is a member function to process blur events for the radioGroup.
//
// @param (e object) e is the event object
//
// @param ($id object) $is is the jquery object of the triggering element
//
// @return (boolean) Returns false if consuming event; true if propagating
//
radioGroup.prototype.handleBlur = function(e, $id) {

  // remove the focus styling from this buttons
  this.$buttons.removeClass('selected');

  return true;
} // end handleBlur()