diff --git a/aria-practices.html b/aria-practices.html index 1c980a81c..21487decc 100644 --- a/aria-practices.html +++ b/aria-practices.html @@ -2582,8 +2582,8 @@

Switch

Examples

diff --git a/examples/index.html b/examples/index.html index 5272a4ed9..d3ce45110 100644 --- a/examples/index.html +++ b/examples/index.html @@ -166,6 +166,7 @@

Examples by Role

  • Editor Menubar (HC)
  • Color Viewer Slider (HC)
  • Date Picker Spin Button
  • +
  • Switch Using HTML Button (HC)
  • File Directory Treeview Using Computed Properties
  • File Directory Treeview Using Declared Properties
  • Navigation Treeview (HC)
  • @@ -361,7 +362,12 @@

    Examples by Role

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

    Examples By Properties and States

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

    Examples By Properties and States

  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • Date Picker Spin Button
  • +
  • Switch Using HTML Button (HC)
  • Switch (HC)
  • Toolbar
  • @@ -672,6 +680,7 @@

    Examples By Properties and States

  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • Date Picker Spin Button
  • +
  • Switch Using HTML Button (HC)
  • Tabs with Automatic Activation
  • Tabs with Manual Activation
  • File Directory Treeview Using Computed Properties
  • diff --git a/examples/switch/css/switch-button.css b/examples/switch/css/switch-button.css new file mode 100644 index 000000000..b47dd87ed --- /dev/null +++ b/examples/switch/css/switch-button.css @@ -0,0 +1,80 @@ +button[role="switch"] { + display: block; + margin: 2px; + padding: 4px 4px 8px 8px; + border: 0 solid #005a9c; + border-radius: 5px; + width: 17em; + text-align: left; + background-color: white; +} + +button[role="switch"] .label { + position: relative; + top: -3px; + display: inline-block; + padding: 0; + margin: 0; + width: 10em; + vertical-align: middle; +} + +button[role="switch"] svg { + forced-color-adjust: auto; + position: relative; + top: 4px; +} + +button[role="switch"] svg rect { + fill-opacity: 0; + stroke-width: 2; + stroke: currentColor; +} + +button[role="switch"] svg rect.off { + display: block; + stroke: currentColor; + fill: currentColor; + fill-opacity: 1; +} + +button[role="switch"][aria-checked="true"] svg rect.off { + display: none; +} + +button[role="switch"] svg rect.on { + display: none; +} + +button[role="switch"][aria-checked="true"] svg rect.on { + color: green; + display: block; + stroke-color: currentColor; + fill: currentColor; + fill-opacity: 1; +} + +button[role="switch"] span.off { + display: inline; +} + +button[role="switch"] span.on { + display: none; +} + +button[role="switch"][aria-checked="true"] span.off { + display: none; +} + +button[role="switch"][aria-checked="true"] span.on { + display: inline; +} + +button[role="switch"]:focus, +button[role="switch"]:hover { + padding: 2px 2px 6px 6px; + border-width: 2px; + outline: none; + background-color: #def; + cursor: pointer; +} diff --git a/examples/switch/js/switch-button.js b/examples/switch/js/switch-button.js new file mode 100644 index 000000000..3bdb03085 --- /dev/null +++ b/examples/switch/js/switch-button.js @@ -0,0 +1,41 @@ +/* + * 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 ButtonSwitch { + constructor(domNode) { + this.switchNode = domNode; + this.switchNode.addEventListener('click', () => this.toggleStatus()); + + // Set background color for the SVG container Rect + var color = getComputedStyle(this.switchNode).getPropertyValue( + 'background-color' + ); + var containerNode = this.switchNode.querySelector('rect.container'); + containerNode.setAttribute('fill', color); + } + + // 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('button[role^=switch]')).forEach( + (element) => new ButtonSwitch(element) + ); +}); diff --git a/examples/switch/switch-button.html b/examples/switch/switch-button.html new file mode 100644 index 000000000..f6d06671c --- /dev/null +++ b/examples/switch/switch-button.html @@ -0,0 +1,248 @@ + + + + + Switch Example Using HTML Button | WAI-ARIA Authoring Practices 1.2 + + + + + + + + + + + + + + +
    +

    Switch Example Using HTML Button

    +

    + This example illustrates implementing the switch design pattern with an HTML button as a switch element and using an SVG element to provide graphical rendering of switch states. + It also demonstrates using the group role to present multiple switches in a labeled group. +

    +

    Similar examples include:

    + + +
    +
    +

    Example

    +
    + +
    +
    +

    Environmental Controls

    + + + +
    +
    + +
    + +
    +

    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
    switchbuttonIdentifies the button element as a switch.
    aria-checked="false"button +
      +
    • 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"button +
      +
    • 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.
    • +
    +
    h3Provides a grouping label for the group of switches.
    groupdivIdentifies the div element as a group container for the switches.
    aria-labelledbydivReferences the h3 element to define the accessible name for the group of switches.
    +
    + +
    +

    Javascript and CSS Source Code

    + + +
    + +
    +

    HTML Source Code

    + +
    + + + +
    +
    + + + diff --git a/examples/switch/switch.html b/examples/switch/switch.html index 9887032c6..7d8eecfac 100644 --- a/examples/switch/switch.html +++ b/examples/switch/switch.html @@ -30,13 +30,11 @@

    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.

    - - -->
    diff --git a/test/tests/switch_switch-button.js b/test/tests/switch_switch-button.js new file mode 100644 index 000000000..ce897446b --- /dev/null +++ b/test/tests/switch_switch-button.js @@ -0,0 +1,188 @@ +const { ariaTest } = require('..'); +const { By, Key } = require('selenium-webdriver'); +const assertAttributeValues = require('../util/assertAttributeValues'); +const assertAriaLabelledby = require('../util/assertAriaLabelledby'); +const assertAriaRoles = require('../util/assertAriaRoles'); +const assertTabOrder = require('../util/assertTabOrder'); + +const exampleFile = 'switch/switch-button.html'; + +const ex = { + groupSelector: '#ex1 [role="group"]', + switchSelector: '#ex1 [role="switch"]', + switches: [ + '#ex1 [role="group"] [role="switch"]:nth-of-type(1)', + '#ex1 [role="group"] [role="switch"]:nth-of-type(2)', + ], + 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('element h3 exists', exampleFile, 'h3', async (t) => { + let header = await t.context.queryElements(t, '#ex1 h3'); + + t.is( + header.length, + 1, + 'One h3 element exist within the example to label the switches' + ); + + t.truthy( + await header[0].getText(), + 'One h3 element exist with readable content within the example to label the switches' + ); +}); + +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( + 'role="group" element exists', + exampleFile, + 'group-role', + async (t) => { + await assertAriaRoles(t, 'ex1', 'group', '1', 'div'); + } +); + +ariaTest( + '"aria-labelledby" on group element', + exampleFile, + 'group-aria-labelledby', + async (t) => { + await assertAriaLabelledby(t, ex.groupSelector); + } +); + +ariaTest( + 'role="switch" elements exist', + exampleFile, + 'switch-role', + async (t) => { + await assertAriaRoles(t, 'ex1', 'switch', '2', 'button'); + + // 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-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 TAB moves focus between switches', + exampleFile, + 'key-tab', + async (t) => { + await assertTabOrder(t, ex.switches); + } +); + +ariaTest( + 'key SPACE turns switch on and off', + exampleFile, + 'key-space', + async (t) => { + for (let switchSelector of ex.switches) { + // Send SPACE key to check box to select + await t.context.session.findElement(By.css(switchSelector)).sendKeys(' '); + + t.true( + await waitAndCheckAriaChecked(t, switchSelector, 'true'), + 'aria-selected should be set after sending SPACE key to switch: ' + + switchSelector + ); + + // Send SPACE key to check box to unselect + await t.context.session.findElement(By.css(switchSelector)).sendKeys(' '); + + t.true( + await waitAndCheckAriaChecked(t, switchSelector, 'false'), + 'aria-selected should be set after sending SPACE key to switch: ' + + switchSelector + ); + } + } +); + +ariaTest( + 'key Enter turns switch on and off', + exampleFile, + 'key-space', + async (t) => { + for (let switchSelector of ex.switches) { + // Send Enter key to check box to select + await t.context.session + .findElement(By.css(switchSelector)) + .sendKeys(Key.ENTER); + + t.true( + await waitAndCheckAriaChecked(t, switchSelector, 'true'), + 'aria-selected should be set after sending ENTER key to switch: ' + + switchSelector + ); + + // Send Enter key to check box to unselect + await t.context.session + .findElement(By.css(switchSelector)) + .sendKeys(Key.ENTER); + + t.true( + await waitAndCheckAriaChecked(t, switchSelector, 'false'), + 'aria-selected should be set after sending ENTER key to switch: ' + + switchSelector + ); + } + } +);