Skip to content

Proposed Focus returns to trigger

Description

This rule checks that when a modal closes the focus returns to the trigger that opened the modal.

Applicability

This rule applies when focus is trapped within a region activated by a trigger control that is dismissible.

Expectation

The element that triggers the target element to become visible, is still present in the screen behind the triggered content and is the same element that receives focus when the target element stops being visible.

Background

Assumptions

There are no assumptions.

Accessibility Support

Some assistive technologies on known browsers still exhibit inconsistencies. Specifically, if an element is removed from the accessibility tree after being activated (e.g., a close button), and focus is moved via JavaScript to an element that is not inherently operable (e.g., a button works, but a div with tabindex=”-1” may not), the assistive technology might automatically shift focus to the closest visible element, regardless of the intended JavaScript behavior.

Bibliography

Accessibility Requirements Mapping

Input Aspects

The following aspects are required in using this rule.

Test Cases

These Javascript and CSS files are used in several examples:

File /test-assets/9au0ou/aria-apg-dialog.js:

/*
 *   This content is licensed according to the W3C Software License at
 *   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
 */

'use strict';

var aria = aria || {};

aria.Utils = aria.Utils || {};

(function () {
  /*
   * When util functions move focus around, set this true so the focus listener
   * can ignore the events.
   */
  aria.Utils.IgnoreUtilFocusChanges = false;

  aria.Utils.dialogOpenClass = 'has-dialog';

  /**
   * @description Set focus on descendant nodes until the first focusable element is
   *       found.
   * @param element
   *          DOM node for which to find the first focusable descendant.
   * @returns {boolean}
   *  true if a focusable element is found and focus is set.
   */
  aria.Utils.focusFirstDescendant = function (element) {
    for (var i = 0; i < element.childNodes.length; i++) {
      var child = element.childNodes[i];
      if (
        aria.Utils.attemptFocus(child) ||
        aria.Utils.focusFirstDescendant(child)
      ) {
        return true;
      }
    }
    return false;
  }; // end focusFirstDescendant

  /**
   * @description Find the last descendant node that is focusable.
   * @param element
   *          DOM node for which to find the last focusable descendant.
   * @returns {boolean}
   *  true if a focusable element is found and focus is set.
   */
  aria.Utils.focusLastDescendant = function (element) {
    for (var i = element.childNodes.length - 1; i >= 0; i--) {
      var child = element.childNodes[i];
      if (
        aria.Utils.attemptFocus(child) ||
        aria.Utils.focusLastDescendant(child)
      ) {
        return true;
      }
    }
    return false;
  }; // end focusLastDescendant

  /**
   * @description Set Attempt to set focus on the current node.
   * @param element
   *          The node to attempt to focus on.
   * @returns {boolean}
   *  true if element is focused.
   */
  aria.Utils.attemptFocus = function (element) {
    if (!aria.Utils.isFocusable(element)) {
      return false;
    }

    aria.Utils.IgnoreUtilFocusChanges = true;
    try {
      element.focus();
    } catch (e) {
      // continue regardless of error
    }
    aria.Utils.IgnoreUtilFocusChanges = false;
    return document.activeElement === element;
  }; // end attemptFocus

  /* Modals can open modals. Keep track of them with this array. */
  aria.OpenDialogList = aria.OpenDialogList || new Array(0);

  /**
   * @returns {object} the last opened dialog (the current dialog)
   */
  aria.getCurrentDialog = function () {
    if (aria.OpenDialogList && aria.OpenDialogList.length) {
      return aria.OpenDialogList[aria.OpenDialogList.length - 1];
    }
  };

  aria.closeCurrentDialog = function () {
    var currentDialog = aria.getCurrentDialog();
    if (currentDialog) {
      currentDialog.close();
      return true;
    }

    return false;
  };

  aria.handleEscape = function (event) {
    var key = event.which || event.keyCode;

    if (key === aria.KeyCode.ESC && aria.closeCurrentDialog()) {
      event.stopPropagation();
    }
  };

  document.addEventListener('keyup', aria.handleEscape);

  /**
   * @class
   * @description Dialog object providing modal focus management.
   *
   * Assumptions: The element serving as the dialog container is present in the
   * DOM and hidden. The dialog container has role='dialog'.
   * @param dialogId
   *          The ID of the element serving as the dialog container.
   * @param focusAfterClosed
   *          Either the DOM node or the ID of the DOM node to focus when the
   *          dialog closes.
   * @param focusFirst
   *          Optional parameter containing either the DOM node or the ID of the
   *          DOM node to focus when the dialog opens. If not specified, the
   *          first focusable element in the dialog will receive focus.
   */
  aria.Dialog = function (dialogId, focusAfterClosed, focusFirst) {
    this.dialogNode = document.getElementById(dialogId);
    if (this.dialogNode === null) {
      throw new Error('No element found with id="' + dialogId + '".');
    }

    var validRoles = ['dialog', 'alertdialog'];
    var isDialog = (this.dialogNode.getAttribute('role') || '')
      .trim()
      .split(/\s+/g)
      .some(function (token) {
        return validRoles.some(function (role) {
          return token === role;
        });
      });
    if (!isDialog) {
      throw new Error(
        'Dialog() requires a DOM element with ARIA role of dialog or alertdialog.'
      );
    }

    // Wrap in an individual backdrop element if one doesn't exist
    // Native <dialog> elements use the ::backdrop pseudo-element, which
    // works similarly.
    var backdropClass = 'dialog-backdrop';
    if (this.dialogNode.parentNode.classList.contains(backdropClass)) {
      this.backdropNode = this.dialogNode.parentNode;
    } else {
      this.backdropNode = document.createElement('div');
      this.backdropNode.className = backdropClass;
      this.dialogNode.parentNode.insertBefore(
        this.backdropNode,
        this.dialogNode
      );
      this.backdropNode.appendChild(this.dialogNode);
    }
    this.backdropNode.classList.add('active');

    // Disable scroll on the body element
    document.body.classList.add(aria.Utils.dialogOpenClass);

    if (typeof focusAfterClosed === 'string') {
      this.focusAfterClosed = document.getElementById(focusAfterClosed);
    } else if (typeof focusAfterClosed === 'object') {
      this.focusAfterClosed = focusAfterClosed;
    } else {
      throw new Error(
        'the focusAfterClosed parameter is required for the aria.Dialog constructor.'
      );
    }

    if (typeof focusFirst === 'string') {
      this.focusFirst = document.getElementById(focusFirst);
    } else if (typeof focusFirst === 'object') {
      this.focusFirst = focusFirst;
    } else {
      this.focusFirst = null;
    }

    // Bracket the dialog node with two invisible, focusable nodes.
    // While this dialog is open, we use these to make sure that focus never
    // leaves the document even if dialogNode is the first or last node.
    var preDiv = document.createElement('div');
    this.preNode = this.dialogNode.parentNode.insertBefore(
      preDiv,
      this.dialogNode
    );
    this.preNode.tabIndex = 0;
    var postDiv = document.createElement('div');
    this.postNode = this.dialogNode.parentNode.insertBefore(
      postDiv,
      this.dialogNode.nextSibling
    );
    this.postNode.tabIndex = 0;

    // If this modal is opening on top of one that is already open,
    // get rid of the document focus listener of the open dialog.
    if (aria.OpenDialogList.length > 0) {
      aria.getCurrentDialog().removeListeners();
    }

    this.addListeners();
    aria.OpenDialogList.push(this);
    this.clearDialog();
    this.dialogNode.className = 'default_dialog'; // make visible

    if (this.focusFirst) {
      this.focusFirst.focus();
    } else {
      aria.Utils.focusFirstDescendant(this.dialogNode);
    }

    this.lastFocus = document.activeElement;
  }; // end Dialog constructor

  aria.Dialog.prototype.clearDialog = function () {
    Array.prototype.map.call(
      this.dialogNode.querySelectorAll('input'),
      function (input) {
        input.value = '';
      }
    );
  };

  /**
   * @description
   *  Hides the current top dialog,
   *  removes listeners of the top dialog,
   *  restore listeners of a parent dialog if one was open under the one that just closed,
   *  and sets focus on the element specified for focusAfterClosed.
   */
  aria.Dialog.prototype.close = function () {
    aria.OpenDialogList.pop();
    this.removeListeners();
    aria.Utils.remove(this.preNode);
    aria.Utils.remove(this.postNode);
    this.dialogNode.className = 'hidden';
    this.backdropNode.classList.remove('active');
    this.focusAfterClosed.focus();

    // If a dialog was open underneath this one, restore its listeners.
    if (aria.OpenDialogList.length > 0) {
      aria.getCurrentDialog().addListeners();
    } else {
      document.body.classList.remove(aria.Utils.dialogOpenClass);
    }
  }; // end close

  /**
   * @description
   *  Hides the current dialog and replaces it with another.
   * @param newDialogId
   *  ID of the dialog that will replace the currently open top dialog.
   * @param newFocusAfterClosed
   *  Optional ID or DOM node specifying where to place focus when the new dialog closes.
   *  If not specified, focus will be placed on the element specified by the dialog being replaced.
   * @param newFocusFirst
   *  Optional ID or DOM node specifying where to place focus in the new dialog when it opens.
   *  If not specified, the first focusable element will receive focus.
   */
  aria.Dialog.prototype.replace = function (
    newDialogId,
    newFocusAfterClosed,
    newFocusFirst
  ) {
    aria.OpenDialogList.pop();
    this.removeListeners();
    aria.Utils.remove(this.preNode);
    aria.Utils.remove(this.postNode);
    this.dialogNode.className = 'hidden';
    this.backdropNode.classList.remove('active');

    var focusAfterClosed = newFocusAfterClosed || this.focusAfterClosed;
    new aria.Dialog(newDialogId, focusAfterClosed, newFocusFirst);
  }; // end replace

  aria.Dialog.prototype.addListeners = function () {
    document.addEventListener('focus', this.trapFocus, true);
  }; // end addListeners

  aria.Dialog.prototype.removeListeners = function () {
    document.removeEventListener('focus', this.trapFocus, true);
  }; // end removeListeners

  aria.Dialog.prototype.trapFocus = function (event) {
    if (aria.Utils.IgnoreUtilFocusChanges) {
      return;
    }
    var currentDialog = aria.getCurrentDialog();
    if (currentDialog.dialogNode.contains(event.target)) {
      currentDialog.lastFocus = event.target;
    } else {
      aria.Utils.focusFirstDescendant(currentDialog.dialogNode);
      if (currentDialog.lastFocus == document.activeElement) {
        aria.Utils.focusLastDescendant(currentDialog.dialogNode);
      }
      currentDialog.lastFocus = document.activeElement;
    }
  }; // end trapFocus

  window.openDialog = function (dialogId, focusAfterClosed, focusFirst) {
    new aria.Dialog(dialogId, focusAfterClosed, focusFirst);
  };

  window.closeDialog = function (closeButton) {
    var topDialog = aria.getCurrentDialog();
    if (topDialog.dialogNode.contains(closeButton)) {
      topDialog.close();
    }
  }; // end closeDialog

  window.replaceDialog = function (
    newDialogId,
    newFocusAfterClosed,
    newFocusFirst
  ) {
    var topDialog = aria.getCurrentDialog();
    if (topDialog.dialogNode.contains(document.activeElement)) {
      topDialog.replace(newDialogId, newFocusAfterClosed, newFocusFirst);
    }
  }; // end replaceDialog
})();

File /test-assets/9au0ou/aria-apg-utils.js:

'use strict';
/**
 * @namespace aria
 */

var aria = aria || {};

/**
 * @description
 *  Key code constants
 */
aria.KeyCode = {
  BACKSPACE: 8,
  TAB: 9,
  RETURN: 13,
  SHIFT: 16,
  ESC: 27,
  SPACE: 32,
  PAGE_UP: 33,
  PAGE_DOWN: 34,
  END: 35,
  HOME: 36,
  LEFT: 37,
  UP: 38,
  RIGHT: 39,
  DOWN: 40,
  DELETE: 46,
};

aria.Utils = aria.Utils || {};

// Polyfill src https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
aria.Utils.matches = function (element, selector) {
  if (!Element.prototype.matches) {
    Element.prototype.matches =
      Element.prototype.matchesSelector ||
      Element.prototype.mozMatchesSelector ||
      Element.prototype.msMatchesSelector ||
      Element.prototype.oMatchesSelector ||
      Element.prototype.webkitMatchesSelector ||
      function (s) {
        var matches = element.parentNode.querySelectorAll(s);
        var i = matches.length;
        while (--i >= 0 && matches.item(i) !== this) {
          // empty
        }
        return i > -1;
      };
  }

  return element.matches(selector);
};

aria.Utils.remove = function (item) {
  if (item.remove && typeof item.remove === 'function') {
    return item.remove();
  }
  if (
    item.parentNode &&
    item.parentNode.removeChild &&
    typeof item.parentNode.removeChild === 'function'
  ) {
    return item.parentNode.removeChild(item);
  }
  return false;
};

aria.Utils.isFocusable = function (element) {
  if (element.tabIndex < 0) {
    return false;
  }

  if (element.disabled) {
    return false;
  }

  switch (element.nodeName) {
    case 'A':
      return !!element.href && element.rel != 'ignore';
    case 'INPUT':
      return element.type != 'hidden';
    case 'BUTTON':
    case 'SELECT':
    case 'TEXTAREA':
      return true;
    default:
      return false;
  }
};

aria.Utils.getAncestorBySelector = function (element, selector) {
  if (!aria.Utils.matches(element, selector + ' ' + element.tagName)) {
    // Element is not inside an element that matches selector
    return null;
  }

  // Move up the DOM tree until a parent matching the selector is found
  var currentNode = element;
  var ancestor = null;
  while (ancestor === null) {
    if (aria.Utils.matches(currentNode.parentNode, selector)) {
      ancestor = currentNode.parentNode;
    } else {
      currentNode = currentNode.parentNode;
    }
  }

  return ancestor;
};

aria.Utils.hasClass = function (element, className) {
  return new RegExp('(\\s|^)' + className + '(\\s|$)').test(element.className);
};

aria.Utils.addClass = function (element, className) {
  if (!aria.Utils.hasClass(element, className)) {
    element.className += ' ' + className;
  }
};

aria.Utils.removeClass = function (element, className) {
  var classRegex = new RegExp('(\\s|^)' + className + '(\\s|$)');
  element.className = element.className.replace(classRegex, ' ').trim();
};

aria.Utils.bindMethods = function (object /* , ...methodNames */) {
  var methodNames = Array.prototype.slice.call(arguments, 1);
  methodNames.forEach(function (method) {
    object[method] = object[method].bind(object);
  });
};

File /test-assets/9au0ou/aria-apg-dialog.css:

.hidden {
  display: none;
}

[role="dialog"] {
  box-sizing: border-box;
  padding: 15px;
  border: 1px solid #000;
  background-color: #fff;
  min-height: 100vh;
}

@media screen and (min-width: 640px) {
  [role="dialog"] {
    position: absolute;
    top: 2rem;
    left: 50vw; /* move to the middle of the screen (assumes relative parent is the body/viewport) */
    transform: translateX(
      -50%
    ); /* move backwards 50% of this element's width */

    min-width: calc(640px - (15px * 2)); /* == breakpoint - left+right margin */
    min-height: auto;
    box-shadow: 0 19px 38px rgb(0 0 0 / 12%), 0 15px 12px rgb(0 0 0 / 22%);
  }
}

.dialog_label {
  text-align: center;
}

.dialog_form {
  margin: 15px;
}

.dialog_form .label_text {
  box-sizing: border-box;
  padding-right: 0.5em;
  display: inline-block;
  font-size: 16px;
  font-weight: bold;
  width: 30%;
  text-align: right;
}

.dialog_form .label_info {
  box-sizing: border-box;
  padding-right: 0.5em;
  font-size: 12px;
  width: 30%;
  text-align: right;
  display: inline-block;
}

.dialog_form_item {
  margin: 10px 0;
  font-size: 0;
}

.dialog_form_item .wide_input {
  box-sizing: border-box;
  max-width: 70%;
  width: 27em;
}

.dialog_form_item .city_input {
  box-sizing: border-box;
  max-width: 70%;
  width: 17em;
}

.dialog_form_item .state_input {
  box-sizing: border-box;
  max-width: 70%;
  width: 15em;
}

.dialog_form_item .zip_input {
  box-sizing: border-box;
  max-width: 70%;
  width: 9em;
}

.dialog_form_actions {
  text-align: right;
  padding: 0 20px 20px;
}

.dialog_close_button {
  float: right;
  position: absolute;
  top: 10px;
  left: 92%;
  height: 25px;
}

.dialog_close_button img {
  border: 0;
}

.dialog_desc {
  padding: 10px 20px;
}

/* native <dialog> element uses the ::backdrop pseudo-element */

/* dialog::backdrop, */
.dialog-backdrop {
  display: none;
  position: fixed;
  overflow-y: auto;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

@media screen and (min-width: 640px) {
  .dialog-backdrop {
    background: rgb(0 0 0 / 30%);
  }
}

.dialog-backdrop.active {
  display: block;
}

.no-scroll {
  overflow-y: auto !important;
}

/* this is added to the body when a dialog is open */
.has-dialog {
  overflow: hidden;
}

File /test-assets/9au0ou/9au0ou.css:

.close-button {
	padding: 0.3rem;
	margin: 0.3rem;
	/*
	display: inline-block;
	vertical-align: middle;
	overflow: hidden;
	text-decoration: none;
	color: inherit;
	background-color: inherit;
	text-align: center;
	cursor: pointer;
	white-space: nowrap
	*/
}

.topright {
	position: absolute;
	right: 0;
	top: 0
}

Passed

Passed Example 1

Open in a new tab

The button that is activated to display the modal gets the focus returned to it when the modal is dismissed using the escape key or activating close, OK or cancel button in the modal.

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>Passed Example 1</title>
		<script src="/test-assets/9au0ou/aria-apg-dialog.js"></script>
		<script src="/test-assets/9au0ou/aria-apg-utils.js"></script>
		<link href="/test-assets/9au0ou/aria-apg-dialog.css" rel="stylesheet" />
		<link href="/test-assets/9au0ou/9au0ou.css" rel="stylesheet" />
	</head>
	<body>
		<button type="button">Do nothing</button>
		<button type="button" onclick="openDialog('dialog1', this)">Open modal dialog</button>
		<button type="button">Do nothing</button>
		<div id="dialog_layer" class="dialogs">
			<div role="dialog" id="dialog1" aria-labelledby="dialog1_label" aria-modal="true"
					 class="hidden">
				<h2 id="dialog1_label" class="dialog_label">Dialog title</h2>
				<div class="dialog_form">Dialog content.</div>
				<button type="button" class="close-button topright" aria-label="Close" title="Close" 
						onclick="closeDialog(this)">
					&times;
				</button>
				<div style="text-align: right;">
					<button type="button" onclick="closeDialog(this)"></button>
					<button type="button" onclick="closeDialog(this)">Cancel</button>
				</div>
			</div>
		</div>
	</body>
</html>

Passed Example 2

Open in a new tab

The link that is activated to display the modal gets the focus returned to it when the modal is dismissed using the escape key or activating close, OK or cancel button in the modal.

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>Passed Example 2</title>
		<script src="/test-assets/9au0ou/aria-apg-dialog.js"></script>
		<script src="/test-assets/9au0ou/aria-apg-utils.js"></script>
		<link href="/test-assets/9au0ou/aria-apg-dialog.css" rel="stylesheet" />
		<link href="/test-assets/9au0ou/9au0ou.css" rel="stylesheet" />
	</head>
	<body>
		<a href="#">Do nothing</a>
		<a href="javascript:openDialog('dialog1', this)">Open modal dialog</a>
		<a href="#">Do nothing</a>
		<div id="dialog_layer" class="dialogs">
			<div role="dialog" id="dialog1" aria-labelledby="dialog1_label" aria-modal="true"
					 class="hidden">
				<h2 id="dialog1_label" class="dialog_label">Dialog title</h2>
				<div class="dialog_form">Dialog content.</div>
				<button type="button" class="close-button topright" aria-label="Close" title="Close" 
						onclick="closeDialog(this)">
					&times;
				</button>
				<div style="text-align: right;">
					<button type="button" onclick="closeDialog(this)">OK</button>
					<button type="button" onclick="closeDialog(this)">Cancel</button>
				</div>
			</div>
		</div>
	</body>
</html>

Failed

Failed Example 1

Open in a new tab

The button that is activated to display the modal gets the focus returned to the item after it when the modal is dismissed.

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>Failed Example 1</title>
		<script src="test-assets/9au0ou/aria-apg-dialog.js"></script>
		<script src="test-assets/9au0ou/aria-apg-utils.js"></script>
		<script>

			addEventListener('DOMContentLoaded', (event) => {
				document.getElementById('dialog1').on_close_via_escape_key = () => { 
					document.getElementById('donothingbutton2').focus();
				};
			});

		</script>
		<link href="test-assets/9au0ou/aria-apg-dialog.css" rel="stylesheet">
		<link href="test-assets/9au0ou/9au0ou.css" rel="stylesheet">
	</head>
	<body>
		<button id="donothingbutton1" class="top-level-button" type="button">Do nothing</button><br><br>
		<button class="top-level-button" type="button" onclick="openDialog('dialog1', this)">Open modal dialog</button><br><br>
		<button id="donothingbutton2" class="top-level-button" type="button">Do nothing</button><br><br>
		<div id="dialog_layer" class="dialogs">
			<div role="dialog" id="dialog1" aria-labelledby="dialog1_label" aria-modal="true" class="hidden">
				<div class="dialog_form">For this test: use any button, or the escape key, to close the dialog.</div>
				<button type="button" class="close-button topright" aria-label="Close" title="Close" onclick="closeDialog(this); document.getElementById('donothingbutton2').focus();">
					×
				</button>
				<div style="text-align: right;">
					<button type="button" onclick="closeDialog(this); document.getElementById('donothingbutton2').focus();">OK</button>
					<button type="button" onclick="closeDialog(this); document.getElementById('donothingbutton2').focus();">Cancel</button>
				</div>
			</div>
		</div>
	</body>
</html>

Failed Example 2

Open in a new tab

The button that is activated to display the modal gets the focus returned to the start of the page when the modal is dismissed.

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>Failed Example 2</title>
		<script src="test-assets/9au0ou/aria-apg-dialog.js"></script>
		<script src="test-assets/9au0ou/aria-apg-utils.js"></script>
		<script>

			addEventListener('DOMContentLoaded', (event) => {
				document.getElementById('dialog1').on_close_via_escape_key = () => { 
					document.getElementById('donothingbutton1').focus();
				};
			});

		</script>
		<link href="test-assets/9au0ou/aria-apg-dialog.css" rel="stylesheet">
		<link href="test-assets/9au0ou/9au0ou.css" rel="stylesheet">
	</head>
	<body>
		<button id="donothingbutton1" class="top-level-button" type="button">Do nothing</button><br><br>
		<button class="top-level-button" type="button" onclick="openDialog('dialog1', this)">Open modal dialog</button><br><br>
		<button id="donothingbutton2" class="top-level-button" type="button">Do nothing</button><br><br>
		<div id="dialog_layer" class="dialogs">
			<div role="dialog" id="dialog1" aria-labelledby="dialog1_label" aria-modal="true" class="hidden">
				<div class="dialog_form">For this test: use any button, or the escape key, to close the dialog.</div>
				<button type="button" class="close-button topright" aria-label="Close" title="Close" onclick="closeDialog(this); document.getElementById('donothingbutton1').focus();">
					×
				</button>
				<div style="text-align: right;">
					<button type="button" onclick="closeDialog(this); document.getElementById('donothingbutton1').focus();">OK</button>
					<button type="button" onclick="closeDialog(this); document.getElementById('donothingbutton1').focus();">Cancel</button>
				</div>
			</div>
		</div>
	</body>
</html>

Failed Example 3

Open in a new tab

The link that is activated to display the modal gets the focus returned to the item after it when the modal is dismissed.

Code needed!

Failed Example 4

Open in a new tab

The link that is activated to display the modal gets the focus returned to the start of the page when the modal is dismissed.

Code needed!

Failed Example 5

Open in a new tab

The page has a list of buttons that is activated to display a modal, the focus is returned to the incorrect button on dismissing the modal.

Code needed!

Inapplicable

Inapplicable Example 1

Open in a new tab

TBC

Code needed!

Glossary

Focused

An element is said to be focused when the element matches the :focus pseudo-class uninterruptedly for a period of 1 second after a user stopped interacting with the page.

The 1 second time span is an arbitrary limit which is not included in WCAG. Given the possibility of the focus state of elements being managed through scripts, testing the focused state of an element consistently would be impractical without a time limit.

Outcome

A conclusion that comes from evaluating an ACT Rule on a test subject or one of its constituent test target. An outcome can be one of the five following types:

Note: A rule has one passed or failed outcome for every test target. When a tester evaluates a test target it can also be reported as cantTell if the rule cannot be tested in its entirety. For example, when applicability was automated, but the expectations have to be evaluated manually.

When there are no test targets the rule has one inapplicable outcome. If the tester is unable to determine whether there are test targets there will be one cantTell outcome. And when no evaluation has occurred the test target has one untested outcome. This means that each test subject always has one or more outcomes.

Outcomes used in ACT Rules can be expressed using the outcome property of the [EARL10-Schema][].

Rule Versions

This is the first version of this ACT rule.

Implementations

There are currently no known implementations for this rule. If you would like to contribute an implementation, please read the ACT Implementations page for details.

Back to Top

This is an unpublished draft preview that might include content that is not yet approved. The published website is at w3.org/WAI/.