diff --git a/aria-practices.html b/aria-practices.html index fda41cf96f..1c980a81c8 100644 --- a/aria-practices.html +++ b/aria-practices.html @@ -2581,13 +2581,10 @@

Switch

Examples

diff --git a/examples/index.html b/examples/index.html index 3c39848590..b47359d3d1 100644 --- a/examples/index.html +++ b/examples/index.html @@ -359,6 +359,10 @@

Examples by Role

+ + switch + Switch + tab @@ -481,6 +485,7 @@

Examples By Properties and States

  • Editor Menubar (HC)
  • Radio Group Using aria-activedescendant (HC)
  • Radio Group Using Roving tabindex (HC)
  • +
  • Switch (HC)
  • Toolbar
  • @@ -608,6 +613,7 @@

    Examples By Properties and States

  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • Date Picker Spin Button
  • +
  • Switch (HC)
  • Toolbar
  • diff --git a/examples/switch/css/switch.css b/examples/switch/css/switch.css new file mode 100644 index 0000000000..8234d84f96 --- /dev/null +++ b/examples/switch/css/switch.css @@ -0,0 +1,73 @@ +[role="switch"] { + margin: 2px; + padding: 4px 4px 8px 8px; + border: 0 solid #005a9c; + border-radius: 5px; + width: 15em; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +[role="switch"] .label { + display: inline-block; + width: 8em; +} + +[role="switch"] .switch { + position: relative; + display: inline-block; + top: 6px; + border: 2px solid black; + border-radius: 12px; + height: 20px; + width: 40px; +} + +[role="switch"] .switch span { + position: absolute; + top: 2px; + left: 2px; + display: inline-block; + border: 2px solid black; + border-radius: 8px; + height: 12px; + width: 12px; + background: black; +} + +[role="switch"][aria-checked="true"] .switch span { + left: 21px; + background: green; + border-color: green; +} + +[role="switch"] .on { + display: none; +} + +[role="switch"] .off { + display: inline; +} + +[role="switch"][aria-checked="true"] .on { + display: inline; +} + +[role="switch"][aria-checked="true"] .off { + display: none; +} + +[role="switch"]:focus, +[role="switch"]:hover { + padding: 2px 2px 6px 6px; + border-width: 2px; + outline: none; + background-color: #def; + cursor: pointer; +} + +[role="switch"]:focus span.switch { + background-color: white; +} diff --git a/examples/switch/js/switch.js b/examples/switch/js/switch.js new file mode 100644 index 0000000000..42c6c4df7a --- /dev/null +++ b/examples/switch/js/switch.js @@ -0,0 +1,45 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: switch.js + * + * Desc: Switch widget that implements ARIA Authoring Practices + */ + +'use strict'; + +class Switch { + constructor(domNode) { + this.switchNode = domNode; + this.switchNode.addEventListener('click', () => this.toggleStatus()); + this.switchNode.addEventListener('keydown', (event) => + this.handleKeydown(event) + ); + } + + handleKeydown(event) { + // Only do something when space or return is pressed + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.toggleStatus(); + } + } + + // Switch state of a switch + toggleStatus() { + const currentState = + this.switchNode.getAttribute('aria-checked') === 'true'; + const newState = String(!currentState); + + this.switchNode.setAttribute('aria-checked', newState); + } +} + +// Initialize switches +window.addEventListener('load', function () { + // Initialize the Switch component on all matching DOM nodes + Array.from(document.querySelectorAll('[role^=switch]')).forEach( + (element) => new Switch(element) + ); +}); diff --git a/examples/switch/switch.html b/examples/switch/switch.html new file mode 100644 index 0000000000..f84e3ff8f5 --- /dev/null +++ b/examples/switch/switch.html @@ -0,0 +1,212 @@ + + + + + Switch Example | WAI-ARIA Authoring Practices 1.2 + + + + + + + + + + + + + + +
    +

    Switch Example

    +

    + This example illustrates implementation of the switch design pattern for a notification preferences control. + It uses a div element for the switch and CSS borders to provide graphical rendering of switch states. +

    + + +
    +
    +

    Example

    +
    + +
    +
    + Notifications + + + + + +
    +
    + +
    + +
    +

    Accessibility Features

    + +
    + +
    +

    Keyboard Support

    + + + + + + + + + + + + + + + + + +
    KeyFunction
    Tab +
      +
    • Moves keyboard focus to the switch.
    • +
    +
    Space
    Enter
    +
      +
    • Toggle switch between on and off.
    • +
    +
    +
    + +
    +

    Role, Property, State, and Tabindex Attributes

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    switchdivIdentifies the div element as a switch.
    tabindex="0"divIncludes the switch in the page Tab sequence.
    aria-checked="false"div +
      +
    • Indicates the switch is off.
    • +
    • CSS attribute selectors (e.g. [aria-checked="false"]) are used to synchronize the visual states with the value of the aria-checked attribute.
    • +
    +
    aria-checked="true"div +
      +
    • Indicates the switch is on.
    • +
    • CSS attribute selectors (e.g. [aria-checked="true"]) are used to synchronize the visual states with the value of the aria-checked attribute.
    • +
    +
    aria-hidden="true"span.on and span.off +
      +
    • Removes the strings on and off that appear to the right of the switch from the accessible name of the switch.
    • +
    • These strings are included only for enhancing visual comprehension of the state; element states are not permitted in accessible names.
    • +
    +
    +
    + +
    +

    Javascript and CSS Source Code

    + +
    + +
    +

    HTML Source Code

    + +
    + + + +
    +
    + + + diff --git a/test/tests/switch_switch.js b/test/tests/switch_switch.js new file mode 100644 index 0000000000..4bcd0e31cd --- /dev/null +++ b/test/tests/switch_switch.js @@ -0,0 +1,148 @@ +const { ariaTest } = require('..'); +const { By, Key } = require('selenium-webdriver'); +const assertAttributeValues = require('../util/assertAttributeValues'); +const assertAriaRoles = require('../util/assertAriaRoles'); + +const exampleFile = 'switch/switch.html'; + +const ex = { + switchSelector: '#ex1 [role="switch"]', + spanOnSelector: '#ex1 span.on', + spanOffSelector: '#ex1 span.off', +}; + +const waitAndCheckAriaChecked = async function (t, selector, value) { + return t.context.session + .wait( + async function () { + let s = await t.context.session.findElement(By.css(selector)); + return (await s.getAttribute('aria-checked')) === value; + }, + t.context.waitTime, + 'Timeout: aria-checked is not set to "' + value + '" for: ' + selector + ) + .catch((err) => { + return err; + }); +}; + +// Attributes + +ariaTest( + 'role="switch" elements exist', + exampleFile, + 'switch-role', + async (t) => { + await assertAriaRoles(t, 'ex1', 'switch', '1', 'div'); + + // Test that each switch has an accessible name + // In this case, the accessible name is the text within the div + let switches = await t.context.queryElements(t, ex.switchSelector); + + for (let index = 0; index < switches.length; index++) { + let text = await switches[index].getText(); + t.true( + typeof text === 'string' && text.length > 0, + 'switch div at index: ' + + index + + ' should have contain text describing the switch' + ); + } + } +); + +ariaTest( + '"aria-hidden" set to "true" on SPAN elements containing "on" and "off" ', + exampleFile, + 'aria-hidden', + async (t) => { + await assertAttributeValues(t, ex.spanOnSelector, 'aria-hidden', 'true'); + await assertAttributeValues(t, ex.spanOffSelector, 'aria-hidden', 'true'); + } +); + +ariaTest( + 'tabindex="0" for switch elements', + exampleFile, + 'switch-tabindex', + async (t) => { + await assertAttributeValues(t, ex.switchSelector, 'tabindex', '0'); + } +); + +ariaTest( + '"aria-checked" on switch element', + exampleFile, + 'switch-aria-checked', + async (t) => { + // check the aria-checked attribute is false to begin + await assertAttributeValues(t, ex.switchSelector, 'aria-checked', 'false'); + + // Click all switches to select them + let switches = await t.context.queryElements(t, ex.switchSelector); + for (let s of switches) { + await s.click(); + } + + // check the aria-checked attribute has been updated to true + await assertAttributeValues(t, ex.switchSelector, 'aria-checked', 'true'); + } +); + +ariaTest( + 'key SPACE turns switch on and off', + exampleFile, + 'key-space', + async (t) => { + // Send SPACE key to check box to select + await t.context.session + .findElement(By.css(ex.switchSelector)) + .sendKeys(' '); + + t.true( + await waitAndCheckAriaChecked(t, ex.switchSelector, 'true'), + 'aria-selected should be set after sending SPACE key to switch: ' + + ex.switchSelector + ); + + // Send SPACE key to check box to unselect + await t.context.session + .findElement(By.css(ex.switchSelector)) + .sendKeys(' '); + + t.true( + await waitAndCheckAriaChecked(t, ex.switchSelector, 'false'), + 'aria-selected should be set after sending SPACE key to switch: ' + + ex.switchSelector + ); + } +); + +ariaTest( + 'key Enter turns switch on and off', + exampleFile, + 'key-space', + async (t) => { + // Send Enter key to check box to select + await t.context.session + .findElement(By.css(ex.switchSelector)) + .sendKeys(Key.ENTER); + + t.true( + await waitAndCheckAriaChecked(t, ex.switchSelector, 'true'), + 'aria-selected should be set after sending SPACE key to switch: ' + + ex.switchSelector + ); + + // Send Enter key to check box to unselect + await t.context.session + .findElement(By.css(ex.switchSelector)) + .sendKeys(Key.ENTER); + + t.true( + await waitAndCheckAriaChecked(t, ex.switchSelector, 'false'), + 'aria-selected should be set after sending SPACE key to switch: ' + + ex.switchSelector + ); + } +);