From 6190a9063efa158d1c2302edf12eb19aa67bed70 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Sun, 8 Mar 2020 21:29:16 -0500 Subject: [PATCH] Combobox examples with listbox popup: Add expanded and controls to popup control button and make visible in high contrast (pull #1336) Fixes issues #1333 and #1331: * added new name, aria-expanded, and aria-controls to popup control button. * Added regression test for new aria properties on popup control button. * Added documentation for attributes on popup control button. * updated css to support Windows high contrast mode. Co-authored-by: Matt King Co-authored-by: Simon Pieters --- .../combobox/combobox-autocomplete-both.html | 68 +++++++++++++- .../combobox/combobox-autocomplete-list.html | 63 ++++++++++++- .../combobox/combobox-autocomplete-none.html | 62 ++++++++++++- .../combobox/css/combobox-autocomplete.css | 31 ++++--- examples/combobox/js/combobox-autocomplete.js | 4 +- test/tests/combobox_autocomplete-both.js | 89 +++++++++++++++++++ test/tests/combobox_autocomplete-list.js | 89 +++++++++++++++++++ test/tests/combobox_autocomplete-none.js | 88 +++++++++++++++++- 8 files changed, 473 insertions(+), 21 deletions(-) diff --git a/examples/combobox/combobox-autocomplete-both.html b/examples/combobox/combobox-autocomplete-both.html index 78a33482fe..c3153415ef 100644 --- a/examples/combobox/combobox-autocomplete-both.html +++ b/examples/combobox/combobox-autocomplete-both.html @@ -51,16 +51,17 @@

Example

-
-
    +
    • Alabama
    • Alaska
    • American Samoa
    • @@ -276,6 +277,11 @@

      Listbox Popup

      +

      Button

      +

      + The button has been removed from the tab sequence of the page, but is still important to assistive technologies for mobile devices that use touch events to open the list of options. +

      +
      @@ -426,6 +432,60 @@

      Listbox Popup

      + +

      Button

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      RoleAttributeElementUsage
      + tabindex="-1" + buttonRemoves the button from the tab sequence of the page because its function is redundant with the keyboard operation of the combobox.
      + aria-label="States" + buttonProvides a label for the button.
      + aria-controls="#IDREF" + buttonIdentifies the element that serves as the popup.
      + aria-expanded="false" + buttonIndicates that the popup element is not displayed.
      + aria-expanded="true" + buttonIndicates that the popup element is displayed.
      diff --git a/examples/combobox/combobox-autocomplete-list.html b/examples/combobox/combobox-autocomplete-list.html index fb9b7211c9..3be610cd02 100644 --- a/examples/combobox/combobox-autocomplete-list.html +++ b/examples/combobox/combobox-autocomplete-list.html @@ -53,10 +53,11 @@

      Example

      - @@ -272,6 +273,10 @@

      Listbox Popup

      +

      Button

      +

      + The button has been removed from the tab sequence of the page, but is still important to assistive technologies for mobile devices that use touch events to open the list of options. +

      @@ -422,6 +427,60 @@

      Listbox Popup

      + +

      Button

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      RoleAttributeElementUsage
      + tabindex="-1" + buttonRemoves the button from the tab sequence of the page because its function is redundant with the keyboard operation of the combobox.
      + aria-label="States" + buttonProvides a label for the button.
      + aria-controls="#IDREF" + buttonIdentifies the element that serves as the popup.
      + aria-expanded="false" + buttonIndicates that the popup element is not displayed.
      + aria-expanded="true" + buttonIndicates that the popup element is displayed.
      diff --git a/examples/combobox/combobox-autocomplete-none.html b/examples/combobox/combobox-autocomplete-none.html index e2262a6b05..35d05c1420 100644 --- a/examples/combobox/combobox-autocomplete-none.html +++ b/examples/combobox/combobox-autocomplete-none.html @@ -53,10 +53,11 @@

      Example

      aria-autocomplete="none" aria-expanded="false" aria-controls="cb1-listbox"> - @@ -221,6 +222,10 @@

      Listbox Popup

      +

      Button

      +

      + The button has been removed from the tab sequence of the page, but is still important to assistive technologies for mobile devices that use touch events to open the list of options. +

      @@ -371,6 +376,59 @@

      Listbox Popup

      +

      Button

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      RoleAttributeElementUsage
      + tabindex="-1" + buttonRemoves the button from the tab sequence of the page, since it's keyboard function is redundant with the keyboard operation of the textbox to open the listbox.
      + aria-label="Previous Searches" + buttonProvides a label for the button.
      + aria-controls="#IDREF" + buttonIdentifies the element that serves as the popup.
      + aria-expanded="false" + buttonIndicates that the popup element is not displayed.
      + aria-expanded="true" + buttonIndicates that the popup element is displayed.
      diff --git a/examples/combobox/css/combobox-autocomplete.css b/examples/combobox/css/combobox-autocomplete.css index 3ea6b53c0a..bd15ab978c 100644 --- a/examples/combobox/css/combobox-autocomplete.css +++ b/examples/combobox/css/combobox-autocomplete.css @@ -33,7 +33,7 @@ outline: none; } -.combobox button.open svg { +.combobox button[aria-expanded="true"] svg { transform: rotate(180deg) translate(0, -1px); } @@ -44,16 +44,27 @@ .combobox .group.focus input, .combobox .group.focus button { - background-color: #DEF; + background-color: #def; } -.combobox polygon { +.combobox button polygon.outline { + stroke: transparent; + fill: transparent; +} + +.combobox button[aria-expanded="true"] polygon.outline, +.combobox .group.focus polygon.outline { + stroke: white; + fill: white; +} + +.combobox polygon.arrow { fill: gray; stroke: gray; } -.combobox button.open polygon, -.combobox .group.focus polygon { +.combobox button[aria-expanded="true"] polygon.arrow, +.combobox .group.focus polygon.arrow { fill: black; stroke: black; } @@ -88,11 +99,11 @@ ul[role="listbox"] li[role="option"] { /* focus and hover styling */ [role="listbox"].focus [role="option"][aria-selected="true"] { - background-color: #DEF; + background-color: #def; padding-top: 0; padding-bottom: 0; - border-top: 0.2em solid #8CCBF2; - border-bottom: 0.2em solid #8CCBF2; + border-top: 0.2em solid #8ccbf2; + border-bottom: 0.2em solid #8ccbf2; } @media (forced-colors: active), (-ms-high-contrast: active) { @@ -100,10 +111,10 @@ ul[role="listbox"] li[role="option"] { -ms-high-contrast-adjust: none; /* disable the backgrounds that Edge puts behind text */ background-color: highlight; color: highlighttext; - border-color: currentcolor; + border-color: currentColor; } } [role="listbox"] li[role="option"]:hover { - background-color: #DEF; + background-color: #def; } diff --git a/examples/combobox/js/combobox-autocomplete.js b/examples/combobox/js/combobox-autocomplete.js index 8bae873d66..6f39ccf466 100644 --- a/examples/combobox/js/combobox-autocomplete.js +++ b/examples/combobox/js/combobox-autocomplete.js @@ -248,7 +248,7 @@ ComboboxAutocomplete.prototype.hasOptions = function () { ComboboxAutocomplete.prototype.open = function () { this.listboxNode.style.display = 'block'; this.comboboxNode.setAttribute('aria-expanded', 'true'); - this.buttonNode.classList.add('open'); + this.buttonNode.setAttribute('aria-expanded', 'true'); }; ComboboxAutocomplete.prototype.close = function (force) { @@ -260,7 +260,7 @@ ComboboxAutocomplete.prototype.close = function (force) { this.setCurrentOptionStyle(false); this.listboxNode.style.display = 'none'; this.comboboxNode.setAttribute('aria-expanded', 'false'); - this.buttonNode.classList.remove('open'); + this.buttonNode.setAttribute('aria-expanded', 'false'); this.setActiveDescendant(false); } }; diff --git a/test/tests/combobox_autocomplete-both.js b/test/tests/combobox_autocomplete-both.js index f16296e16a..7699fcbc3b 100644 --- a/test/tests/combobox_autocomplete-both.js +++ b/test/tests/combobox_autocomplete-both.js @@ -14,6 +14,7 @@ const ex = { textboxSelector: '#ex1 input[type="text"]', listboxSelector: '#ex1 [role="listbox"]', optionsSelector: '#ex1 [role="option"]', + buttonSelector: '#ex1 button', numAOptions: 5, secondAOption: 'Alaska' @@ -149,6 +150,94 @@ ariaTest('"aria-selected" attribute on options element', exampleFile, 'option-ar await assertAttributeValues(t, ex.optionsSelector + ':nth-of-type(1)', 'aria-selected', 'true'); }); +ariaTest('Button should have tabindex="-1"', exampleFile, 'button-tabindex', async (t) => { + t.plan(1); + + const button = await t.context.session.findElement(By.css(ex.buttonSelector)) + + t.is( + await button.getAttribute('tabindex'), + '-1', + 'tabindex should be set to "-1" on button' + ); +}); + +ariaTest('"aria-label" attribute on button element', exampleFile, 'button-aria-label', async (t) => { + t.plan(1); + await assertAriaLabelExists(t, ex.buttonSelector); +}); + + +ariaTest('"aria-controls" attribute on button element', exampleFile, 'button-aria-controls', async (t) => { + t.plan(2); + + const popupId = await t.context.session + .findElement(By.css(ex.buttonSelector)) + .getAttribute('aria-controls'); + + t.truthy( + popupId, + '"aria-controls" attribute should exist on: ' + ex.buttonSelector + ); + + const popupElements = await t.context.session + .findElement(By.id('ex1')) + .findElements(By.id(popupId)); + + t.is( + popupElements.length, + 1, + 'There should be a element with id "' + popupId + '" as referenced by the aria-controls attribute' + ); +}); + +ariaTest('"aria-expanded" on button element', exampleFile, 'button-aria-expanded', async (t) => { + t.plan(4); + + const button = await t.context.session.findElement(By.css(ex.buttonSelector)); + + // Check that aria-expanded is false and the listbox is not visible before interacting + + t.is( + await button.getAttribute('aria-expanded'), + 'false', + 'button element should have attribute "aria-expanded" set to false by default.' + ); + + const popupId = await t.context.session + .findElement(By.css(ex.textboxSelector)) + .getAttribute('aria-controls'); + + const popupElement = await t.context.session + .findElement(By.id('ex1')) + .findElement(By.id(popupId)); + + t.false( + await popupElement.isDisplayed(), + 'Popup element should not be displayed when \'aria-expanded\' is \'false\'' + ); + + // Send key "a" to textbox + + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .sendKeys('a'); + + // Check that aria-expanded is true and the listbox is visible + + t.is( + await button.getAttribute('aria-expanded'), + 'true', + 'button element should have attribute "aria-expand" set to true after typing.' + ); + + t.true( + await popupElement.isDisplayed(), + 'Popup element should be displayed when \'aria-expanded\' is \'true\'' + ); + +}); + // Keys ariaTest('Test alt + down key press with focus on textbox', diff --git a/test/tests/combobox_autocomplete-list.js b/test/tests/combobox_autocomplete-list.js index 0bf230a4c6..ba991e5641 100644 --- a/test/tests/combobox_autocomplete-list.js +++ b/test/tests/combobox_autocomplete-list.js @@ -14,6 +14,7 @@ const ex = { textboxSelector: '#ex1 input[type="text"]', listboxSelector: '#ex1 [role="listbox"]', optionsSelector: '#ex1 [role="option"]', + buttonSelector: '#ex1 button', numAOptions: 5 }; @@ -154,6 +155,94 @@ ariaTest('"aria-selected" attribute on options element', exampleFile, 'option-ar await assertAttributeValues(t, ex.optionsSelector + ':nth-of-type(1)', 'aria-selected', 'true'); }); +ariaTest('Button should have tabindex="-1"', exampleFile, 'button-tabindex', async (t) => { + t.plan(1); + + const button = await t.context.session.findElement(By.css(ex.buttonSelector)) + + t.is( + await button.getAttribute('tabindex'), + '-1', + 'tabindex should be set to "-1" on button' + ); +}); + +ariaTest('"aria-label" attribute on button element', exampleFile, 'button-aria-label', async (t) => { + t.plan(1); + await assertAriaLabelExists(t, ex.buttonSelector); +}); + +ariaTest('"aria-controls" attribute on button element', exampleFile, 'button-aria-controls', async (t) => { + t.plan(2); + + const popupId = await t.context.session + .findElement(By.css(ex.buttonSelector)) + .getAttribute('aria-controls'); + + t.truthy( + popupId, + '"aria-controls" attribute should exist on: ' + ex.buttonSelector + ); + + const popupElements = await t.context.session + .findElement(By.id('ex1')) + .findElements(By.id(popupId)); + + t.is( + popupElements.length, + 1, + 'There should be a element with id "' + popupId + '" as referenced by the aria-controls attribute' + ); +}); + + +ariaTest('"aria-expanded" on button element', exampleFile, 'button-aria-expanded', async (t) => { + t.plan(4); + + const button = await t.context.session.findElement(By.css(ex.buttonSelector)); + + // Check that aria-expanded is false and the listbox is not visible before interacting + + t.is( + await button.getAttribute('aria-expanded'), + 'false', + 'button element should have attribute "aria-expanded" set to false by default.' + ); + + const popupId = await t.context.session + .findElement(By.css(ex.textboxSelector)) + .getAttribute('aria-controls'); + + const popupElement = await t.context.session + .findElement(By.id('ex1')) + .findElement(By.id(popupId)); + + t.false( + await popupElement.isDisplayed(), + 'Popup element should not be displayed when \'aria-expanded\' is false\'' + ); + + // Send key "a" to textbox + + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .sendKeys('a'); + + // Check that aria-expanded is true and the listbox is visible + + t.is( + await button.getAttribute('aria-expanded'), + 'true', + 'button element should have attribute "aria-expand" set to true after typing.' + ); + + t.true( + await popupElement.isDisplayed(), + 'Popup element should be displayed when \'aria-expanded\' is true\'' + ); + +}); + // Keys diff --git a/test/tests/combobox_autocomplete-none.js b/test/tests/combobox_autocomplete-none.js index a08652bf98..b87c11885c 100644 --- a/test/tests/combobox_autocomplete-none.js +++ b/test/tests/combobox_autocomplete-none.js @@ -14,6 +14,7 @@ const ex = { textboxSelector: '#ex1 input[type="text"]', listboxSelector: '#ex1 [role="listbox"]', optionsSelector: '#ex1 [role="option"]', + buttonSelector: '#ex1 button', numOptions: 11 }; @@ -154,6 +155,92 @@ ariaTest('"aria-selected" attribute on options element', exampleFile, 'option-ar await assertAttributeValues(t, ex.optionsSelector + ':nth-of-type(1)', 'aria-selected', 'true'); }); +ariaTest('Button should have tabindex="-1"', exampleFile, 'button-tabindex', async (t) => { + t.plan(1); + + const button = await t.context.session.findElement(By.css(ex.buttonSelector)) + + t.is( + await button.getAttribute('tabindex'), + '-1', + 'tabindex should be set to "-1" on button' + ); +}); + +ariaTest('"aria-label" attribute on button element', exampleFile, 'button-aria-label', async (t) => { + t.plan(1); + await assertAriaLabelExists(t, ex.buttonSelector); +}); + +ariaTest('"aria-controls" attribute on button element', exampleFile, 'button-aria-controls', async (t) => { + t.plan(2); + + const popupId = await t.context.session + .findElement(By.css(ex.buttonSelector)) + .getAttribute('aria-controls'); + + t.truthy( + popupId, + '"aria-controls" attribute should exist on: ' + ex.buttonSelector + ); + + const popupElements = await t.context.session + .findElement(By.id('ex1')) + .findElements(By.id(popupId)); + + t.is( + popupElements.length, + 1, + 'There should be a element with id "' + popupId + '" as referenced by the aria-controls attribute' + ); +}); + +ariaTest('"aria-expanded" on button element', exampleFile, 'button-aria-expanded', async (t) => { + t.plan(4); + + const button = await t.context.session.findElement(By.css(ex.buttonSelector)); + + // Check that aria-expanded is false and the listbox is not visible before interacting + + t.is( + await button.getAttribute('aria-expanded'), + 'false', + 'button element should have attribute "aria-expanded" set to false by default.' + ); + + const popupId = await t.context.session + .findElement(By.css(ex.textboxSelector)) + .getAttribute('aria-controls'); + + const popupElement = await t.context.session + .findElement(By.id('ex1')) + .findElement(By.id(popupId)); + + t.false( + await popupElement.isDisplayed(), + 'Popup element should not be displayed when \'aria-expanded\' is \'false\'' + ); + + // Send key "a" to textbox + + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .sendKeys('a'); + + // Check that aria-expanded is true and the listbox is visible + + t.is( + await button.getAttribute('aria-expanded'), + 'true', + 'button element should have attribute "aria-expand" set to true after typing.' + ); + + t.true( + await popupElement.isDisplayed(), + 'Popup element should be displayed when \'aria-expanded\' is \'true\'' + ); + +}); // Keys @@ -549,4 +636,3 @@ ariaTest('Expected behavior for all other standard single line editing keys', 'Sending standard editing keys should NOT filter results' ); }); -