From 533328e192a7eae9cfc8a7fe2e1c62915e88709d Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Fri, 29 Oct 2021 03:37:28 -0500 Subject: [PATCH] Add Switch Example using HTML button Element (pull #1893) Co-authored-by: Matt King --- aria-practices.html | 2 +- examples/index.html | 11 +- examples/switch/css/switch-button.css | 80 +++++++++ examples/switch/js/switch-button.js | 41 +++++ examples/switch/switch-button.html | 248 ++++++++++++++++++++++++++ examples/switch/switch.html | 6 +- test/tests/switch_switch-button.js | 188 +++++++++++++++++++ 7 files changed, 570 insertions(+), 6 deletions(-) create mode 100644 examples/switch/css/switch-button.css create mode 100644 examples/switch/js/switch-button.js create mode 100644 examples/switch/switch-button.html create mode 100644 test/tests/switch_switch-button.js diff --git a/aria-practices.html b/aria-practices.html index 1c980a81c8..21487decc7 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 5272a4ed98..d3ce45110f 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 0000000000..b47dd87edc --- /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 0000000000..3bdb030856 --- /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 0000000000..f6d06671cf --- /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:

    +
      +
    • Switch Example: A switch based on a div element that turns a notification preference on and off.
    • + +
    + +
    +
    +

    Example

    +
    + +
    +
    +

    Environmental Controls

    + + + +
    +
    + +
    + +
    +

    Accessibility Features

    +
      +
    • To help assistive technology users understand that the Environmental Controls are a group of two switches, the switches are wrapped in a group labeled by the heading that labels the set of switches.
    • +
    • + To make understanding the state of the switch easier for users with visual or cognitive disabilities, a text equivalent of the state (on or off) is displayed adjacent to the graphical state indicator. + CSS attribute selectors ensure the label displayed is synchronized with the value of the aria-checked attribute.
      + NOTE: To prevent redundant announcement of the state by screen readers, the text indicators of state are hidden from assistive technologies with aria-hidden. +
    • +
    • Spacing, stroke widths, and fill are important to ensure the graphical states will be visible and discernible to people with visual impairments, including when browser or operating system high contrast settings are enabled: +
        +
      • To make the graphical representation of the state of a switch readily perceivable, two pixel stroke width is used for the switch state container and a solid color is used to fill the rectangles indicating the on and off states.
      • +
      • To ensure users can perceive the difference between the container and the rectangles used to indicate the state of the switch, there are two pixels of space between the container border and the rectangles.
      • +
      +
    • +
    • To enhance perceivability when operating the switches, visual keyboard focus and hover are styled using the CSS :hover and :focus pseudo-classes: +
        +
      • To make it easier to perceive focus and the relationship between a label and its associated switch, focus creates a border around both the switch and the label and also changes the background color.
      • +
      • To make it easier to perceive that clicking either the label or switch will activate the switch, the hover indicator is the same as the focus indicator.
      • +
      • To help people with visual impairments identify the switch as an interactive element, the cursor is changed to a pointer when hovering over the switch.
      • +
      +
    • +
    • + To ensure the SVG graphics have sufficient contrast with the background when high contrast settings invert colors, the CSS currentColor value for the stroke and fill properties is used to synchronize the colors with text content. + If specific colors were used to specify the stroke and fill properties, the color of these elements would remain the same in high contrast mode, which could lead to insufficient contrast between them and their background or even make them invisible if their color were to match the high contrast mode background. + The fill-opacity of the container rect is set to zero for the background color of the page to provide the contrasting color to the stroke and fill colors. +
      NOTE: The SVG elements need to set the CSS forced-color-adjust property to auto for some browsers to support the currentColor value. +
    • +
    +
    + +
    +

    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 9887032c61..7d8eecfacd 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 0000000000..ce897446b7 --- /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 + ); + } + } +);