The following example implementation of the ARIA design pattern for combobox
+ demonstrates a single-select combobox widget that is functionally similar to an HTML select element.
+ Contrary to other combobox examples, this combobox has no <input> element, and does not take freeform user input. Like a <select> a user can still type characters to auto-select matching options, however.
+
Editable Combobox with Grid Popup: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.
First opens the listbox if it is not already displayed and then moves visual focus to the first option.
+
DOM focus remains on the textbox.
+
+
+
+
+
Alt + Down Arrow
+
+ Opens the listbox without moving focus or changing selection.
+
+
+
+
Up Arrow
+
+
+
First opens the listbox if it is not already displayed and then moves visual focus to the last option.
+
DOM focus remains on the textbox.
+
+
+
+
+
Enter
+
+
+
Sets the textbox value to the content of the selected option.
+
Closes the listbox if it is displayed.
+
+
+
+
+
Standard single line text editing keys
+
+
+
Keys used for cursor movement and text manipulation, such as Delete and Shift + Right Arrow.
+
An HTML input with type="text" is used for the textbox so the browser will provide platform-specific editing keys.
+
+
+
+
+
+
Listbox Popup
+
+ NOTE: When visual focus is in the listbox, DOM focus remains on the textbox and the value of aria-activedescendant on the textbox is set to a value that refers to the listbox option that is visually indicated as focused.
+ Where the following descriptions of keyboard commands mention focus, they are referring to the visual focus indicator.
+ For more information about this focus management technique, see
+ Using aria-activedescendant to Manage Focus.
+
+
+
+
+
Key
+
Function
+
+
+
+
+
Enter
+
+
+
Sets the textbox value to the content of the focused option in the listbox.
+
Closes the listbox.
+
Sets visual focus on the textbox.
+
+
+
+
+
Escape
+
+
+
Closes the listbox.
+
Sets visual focus on the textbox.
+
+
+
+
+
Down Arrow
+
+
+
Moves visual focus to the next option.
+
If visual focus is on the last option, moves visual focus to the first option.
+
Note: This wrapping behavior is useful when Home and End move the editing cursor as described below.
+
+
+
+
+
Up Arrow
+
+
+
Moves visual focus to the previous option.
+
If visual focus is on the first option, moves visual focus to the last option.
+
Note: This wrapping behavior is useful when Home and End move the editing cursor as described below.
+
+
+
+
+
Right Arrow
+
Moves visual focus to the textbox and moves the editing cursor one character to the right.
+
+
+
Left Arrow
+
Moves visual focus to the textbox and moves the editing cursor one character to the left.
+
+
+
Home
+
Moves visual focus to the textbox and places the editing cursor at the beginning of the field.
+
+
+
End
+
Moves visual focus to the textbox and places the editing cursor at the end of the field.
+
+
+
Printable Characters
+
+
+
Moves visual focus to the textbox.
+
Types the character in the textbox.
+
Options in the listbox are not filtered based on the characters in the textbox.
+
+
+
+
+
+
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.
+
The text content of the element provides the accessible name of the option.
+
+
+
+
+
+
+ aria-selected="true"
+
+
li
+
+
+
Specified on an option in the listbox when it is visually highlighted as selected.
+
Occurs only when an option in the list is referenced by aria-activedescendant.
+
+
+
+
+
+
Button
+
+
+
+
Role
+
Attribute
+
Element
+
Usage
+
+
+
+
+
+
+ tabindex="-1"
+
+
button
+
Removes 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"
+
+
button
+
Provides a label for the button.
+
+
+
+
+ aria-controls="#IDREF"
+
+
button
+
Identifies the element that serves as the popup.
+
+
+
+
+ aria-expanded="false"
+
+
button
+
Indicates that the popup element is not displayed.
First opens the listbox if it is not already displayed and then moves visual focus to the last option.
+
First opens the listbox if it is not already displayed and then moves visual focus to the first option.
DOM focus remains on the textbox.
-
-
Enter
-
-
-
Sets the textbox value to the content of the selected option.
-
Closes the listbox if it is displayed.
-
-
-
-
Standard single line text editing keys
+
Printable Characters
-
Keys used for cursor movement and text manipulation, such as Delete and Shift + Right Arrow.
-
An HTML input with type="text" is used for the textbox so the browser will provide platform-specific editing keys.
+
First opens the listbox if it is not already displayed and then moves visual focus to the first option that matches the typed character.
+
If multiple keys are typed in quick succession, visual focus moves to the first option that matches the full string.
+
If the same character is typed in succession, visual focus cycles among the options starting with that character
@@ -145,9 +136,9 @@
Listbox Popup
Enter
-
Sets the textbox value to the content of the focused option in the listbox.
+
Sets the value to the content of the focused option in the listbox.
Closes the listbox.
-
Sets visual focus on the textbox.
+
Sets visual focus on the combobox.
@@ -156,7 +147,7 @@
Listbox Popup
Closes the listbox.
-
Sets visual focus on the textbox.
+
Sets visual focus on the combobox.
@@ -165,8 +156,7 @@
Listbox Popup
Moves visual focus to the next option.
-
If visual focus is on the last option, moves visual focus to the first option.
-
Note: This wrapping behavior is useful when Home and End move the editing cursor as described below.
+
If visual focus is on the last option, visual focus does not move.
@@ -175,43 +165,30 @@
Listbox Popup
Moves visual focus to the previous option.
-
If visual focus is on the first option, moves visual focus to the last option.
-
Note: This wrapping behavior is useful when Home and End move the editing cursor as described below.
+
If visual focus is on the first option, visual focus does not move.
-
-
Right Arrow
-
Moves visual focus to the textbox and moves the editing cursor one character to the right.
-
-
-
Left Arrow
-
Moves visual focus to the textbox and moves the editing cursor one character to the left.
-
Home
-
Moves visual focus to the textbox and places the editing cursor at the beginning of the field.
+
Moves visual focus to the first option.
End
-
Moves visual focus to the textbox and places the editing cursor at the end of the field.
+
Moves visual focus to the last option.
Printable Characters
-
Moves visual focus to the textbox.
-
Types the character in the textbox.
-
Options in the listbox are not filtered based on the characters in the textbox.
+
First opens the listbox if it is not already displayed and then moves visual focus to the first option that matches the typed character.
+
If multiple keys are typed in quick succession, visual focus moves to the first option that matches the full string.
+
If the same character is typed in succession, visual focus cycles among the options starting with that character
-
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.
-
Indicates that the suggestions in the combobox popup are not values that complete the current textbox input.
-
aria-controls="#IDREF"
-
input[type="text"]
+
div
Identifies the element that serves as the popup.
@@ -261,7 +230,7 @@
Textbox
aria-expanded="false"
-
input[type="text"]
+
div
Indicates that the popup element is not displayed.
@@ -269,28 +238,15 @@
Textbox
aria-expanded="true"
-
input[type="text"]
+
div
Indicates that the popup element is displayed.
-
-
-
- id="string"
-
-
input[type="text"]
-
-
-
Referenced by for attribute of label element to provide an accessible name.
-
Recommended naming method for HTML input elements because clicking label focuses input.
-
-
-
aria-activedescendant="IDREF"
-
input[type="text"]
+
div
When an option in the listbox is visually indicated as having keyboard focus, refers to that option.
@@ -322,24 +278,16 @@
Listbox Popup
- ul
+ div
-
Identifies the ul element as a listbox.
-
-
-
-
- aria-label="Previous Searches"
-
-
ul
-
Provides a label for the listbox.
+
Identifies the element as a listbox.
option
-
li
+
div
Identifies the element as a listboxoption.
@@ -362,59 +310,6 @@
Listbox Popup
-
Button
-
-
-
-
Role
-
Attribute
-
Element
-
Usage
-
-
-
-
-
-
- tabindex="-1"
-
-
button
-
Removes 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"
-
-
button
-
Provides a label for the button.
-
-
-
-
- aria-controls="#IDREF"
-
-
button
-
Identifies the element that serves as the popup.
-
-
-
-
- aria-expanded="false"
-
-
button
-
Indicates that the popup element is not displayed.
Editable Combobox with Grid Popup: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.
Editable Combobox with Grid Popup: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.
Editable Combobox with Grid Popup: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.
First opens the listbox if it is not already displayed and then moves visual focus to the first option.
-
DOM focus remains on the textbox.
+
Opens the listbox if it is not already displayed without moving focus or changing selection.
+
DOM focus remains on the combobox.
-
+
Alt + Down Arrow
Opens the listbox without moving focus or changing selection.
-
+
Up Arrow
First opens the listbox if it is not already displayed and then moves visual focus to the first option.
-
DOM focus remains on the textbox.
+
DOM focus remains on the combobox.
+
+
Enter
+
+ Opens the listbox without moving focus or changing selection.
+
+
+
+
Space
+
+ Opens the listbox without moving focus or changing selection.
+
+
+
+
Home
+
+ Opens the listbox and moves visual focus to the first option.
+
+
+
+
End
+
+ Opens the listbox and moves visual focus to the last option.
+
+
Printable Characters
@@ -118,7 +142,7 @@
Closed Combobox
Listbox Popup
- NOTE: When visual focus is in the listbox, DOM focus remains on the textbox and the value of aria-activedescendant on the textbox is set to a value that refers to the listbox option that is visually indicated as focused.
+ NOTE: When visual focus is in the listbox, DOM focus remains on the combobox and the value of aria-activedescendant on the combobox is set to a value that refers to the listbox option that is visually indicated as focused.
Where the following descriptions of keyboard commands mention focus, they are referring to the visual focus indicator.
For more information about this focus management technique, see
Using aria-activedescendant to Manage Focus.
@@ -141,6 +165,26 @@
Listbox Popup
+
+
Space
+
+
+
Sets the value to the content of the focused option in the listbox.
+
Closes the listbox.
+
Sets visual focus on the combobox.
+
+
+
+
+
Tab
+
+
+
Sets the value to the content of the focused option in the listbox.
+
Closes the listbox.
+
Performs the default action, moving focus to the next focusable element.
+
+
+
Escape
@@ -168,6 +212,16 @@
Listbox Popup
+
+
Alt + Up Arrow
+
+
+
Sets the value to the content of the focused option in the listbox.
+
Closes the listbox.
+
Sets visual focus on the combobox.
+
+
+
Home
Moves visual focus to the first option.
@@ -176,6 +230,14 @@
Listbox Popup
End
Moves visual focus to the last option.
+
+
PageUp
+
Jumps visual focus up 10 options (or to first option).
+
+
+
PageDown
+
Jumps visual focus down 10 options (or to last option).
diff --git a/examples/combobox/js/select-only.js b/examples/combobox/js/select-only.js
index 8a5ed69c0e..7fae7dd145 100644
--- a/examples/combobox/js/select-only.js
+++ b/examples/combobox/js/select-only.js
@@ -174,7 +174,7 @@ Select.prototype.init = function() {
// add event listeners
this.comboEl.addEventListener('blur', this.onComboBlur.bind(this));
- this.comboEl.addEventListener('click', () => this.updateMenuState(true));
+ this.comboEl.addEventListener('click', this.onComboClick.bind(this));
this.comboEl.addEventListener('keydown', this.onComboKeyDown.bind(this));
// create options
@@ -231,6 +231,10 @@ Select.prototype.onComboBlur = function() {
}
}
+Select.prototype.onComboClick = function() {
+ this.updateMenuState(!this.open, false);
+}
+
Select.prototype.onComboKeyDown = function(event) {
const { key } = event;
const max = this.options.length - 1;
@@ -356,6 +360,6 @@ window.addEventListener('load', function () {
const selectEls = document.querySelectorAll('.js-select');
selectEls.forEach((el) => {
- const selectComponent = new Select(el, options);
+ new Select(el, options);
});
});
\ No newline at end of file
diff --git a/test/tests/combobox_select-only.js b/test/tests/combobox_select-only.js
new file mode 100644
index 0000000000..1fbcd15e19
--- /dev/null
+++ b/test/tests/combobox_select-only.js
@@ -0,0 +1,508 @@
+'use strict';
+
+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 exampleFile = 'combobox/combobox-select-only.html';
+
+const ex = {
+ comboSelector: '#combo1',
+ listboxSelector: '#listbox1'
+};
+
+// Attributes
+ariaTest('role="combobox"', exampleFile, 'combobox-role', async (t) => {
+ await assertAriaRoles(t, 'ex1', 'combobox', '1', 'div');
+});
+
+ariaTest('role="listbox"', exampleFile, 'listbox-role', async (t) => {
+ await assertAriaRoles(t, 'ex1', 'listbox', '1', 'div');
+});
+
+ariaTest('role "option"', exampleFile, 'option-role', async (t) => {
+ // open combo
+ await t.context.session.findElement(By.css(ex.comboSelector)).click();
+
+ // query listbox children
+ const options = await t.context.queryElements(t, `${ex.listboxSelector} > div`);
+ await Promise.all(options.map(async (option) => {
+ const role = await option.getAttribute('role');
+ t.is(role, 'option', 'Immediate descendents of the listbox should have role="option"');
+ }));
+});
+
+ariaTest('aria-labelledby on combobox', exampleFile, 'combobox-aria-labelledby', async (t) => {
+ await assertAriaLabelledby(t, ex.comboSelector);
+});
+
+ariaTest('aria-controls on combobox', exampleFile, 'combobox-aria-controls', async (t) => {
+ const controlledId = await t.context.session
+ .findElement(By.css(ex.comboSelector))
+ .getAttribute('aria-controls');
+
+ t.truthy(controlledId, '"aria-controls" should exist on the combobox');
+
+ const controlledRole = await t.context.session.findElement(By.id(controlledId)).getAttribute('role');
+
+ t.is(controlledRole, 'listbox', 'The combobox\'s aria-controls attribute should point to a listbox');
+});
+
+ariaTest('aria-expanded="false" when closed', exampleFile, 'combobox-aria-expanded', async (t) => {
+ const expanded = await t.context.session.findElement(By.css(ex.comboSelector)).getAttribute('aria-expanded');
+
+ t.is(expanded, 'false', 'aria-expanded should be false by default');
+});
+
+ariaTest('click opens combobox and sets aria-expanded="true"', exampleFile, 'combobox-aria-expanded', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+
+ await combobox.click();
+ const expanded = await combobox.getAttribute('aria-expanded');
+ const popupDisplayed = await t.context.session.findElement(By.css(ex.listboxSelector)).isDisplayed();
+
+ t.is(expanded, 'true', 'aria-expanded should be true when opened');
+ t.true(popupDisplayed, 'listbox should be present after click');
+});
+
+ariaTest('"aria-activedescendant" on combobox element', exampleFile, 'combobox-aria-activedescendant', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const firstOption = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]`));
+ const optionId = await firstOption.getAttribute('id');
+
+ await combobox.click();
+
+ await assertAttributeValues(t, ex.comboSelector, 'aria-activedescendant', optionId);
+});
+
+ariaTest('"aria-selected" attribute on first option', exampleFile, 'option-aria-selected', async (t) => {
+ const firstOption = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]`));
+ const secondOption = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:nth-child(2)`));
+
+ t.is(await firstOption.getAttribute('aria-selected'), 'true', 'the first option is selected by default');
+ t.is(await secondOption.getAttribute('aria-selected'), 'false', 'other options have aria-selected set to false');
+});
+
+// Behavior
+
+// Open listbox
+ariaTest('Alt + down arrow opens listbox', exampleFile, 'combobox-key-alt-down-arrow', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const firstOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]`)).getAttribute('id');
+
+ // listbox starts collapsed
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden on load');
+
+ // Send ALT + ARROW_DOWN to the combo
+ await combobox.sendKeys(Key.ALT, Key.ARROW_DOWN);
+
+ // Check that the listbox is displayed
+ t.true(await listbox.isDisplayed(), 'alt + down should show the listbox');
+ t.is(await combobox.getAttribute('aria-expanded'), 'true', 'aria-expanded should be true when opened');
+
+ // the first option should be selected
+ t.is(await combobox.getAttribute('aria-activedescendant'), firstOptionId, 'Alt + Down should highlight the first option');
+});
+
+ariaTest('Down arrow opens listbox', exampleFile, 'combobox-key-down-arrow', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const firstOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]`)).getAttribute('id');
+
+ // listbox starts collapsed
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden on load');
+
+ // Send ARROW_DOWN to the combo
+ await combobox.sendKeys(Key.ARROW_DOWN);
+
+ // Check that the listbox is displayed
+ t.true(await listbox.isDisplayed(), 'alt + down should show the listbox');
+ t.is(await combobox.getAttribute('aria-expanded'), 'true', 'aria-expanded should be true when opened');
+
+ // the first option should be selected
+ t.is(await combobox.getAttribute('aria-activedescendant'), firstOptionId, 'Down arrow should highlight the first option');
+});
+
+ariaTest('Enter opens listbox', exampleFile, 'combobox-key-enter', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const firstOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]`)).getAttribute('id');
+
+ // listbox starts collapsed
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden on load');
+
+ // Send ENTER to the combo
+ await combobox.sendKeys(Key.ENTER);
+
+ // Check that the listbox is displayed
+ t.true(await listbox.isDisplayed(), 'enter should show the listbox');
+
+ // the first option should be selected
+ t.is(await combobox.getAttribute('aria-activedescendant'), firstOptionId, 'enter should highlight the first option');
+});
+
+ariaTest('Space opens listbox', exampleFile, 'combobox-key-space', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const firstOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]`)).getAttribute('id');
+
+ // listbox starts collapsed
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden on load');
+
+ // Send space to the combo
+ await combobox.sendKeys(' ');
+
+ // Check that the listbox is displayed
+ t.true(await listbox.isDisplayed(), 'space should show the listbox');
+
+ // the first option should be selected
+ t.is(await combobox.getAttribute('aria-activedescendant'), firstOptionId, 'space should highlight the first option');
+});
+
+ariaTest('combobox opens on last highlighted option', exampleFile, 'combobox-key-down-arrow', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const secondOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:nth-child(2)`)).getAttribute('id');
+
+ // Open, select second option, close
+ await combobox.sendKeys(' ');
+ await combobox.sendKeys(Key.ARROW_DOWN);
+ await combobox.sendKeys(Key.ESCAPE);
+
+ // Open again
+ await combobox.sendKeys(' ');
+
+ // Check that the listbox is displayed and second option is highlighted
+ t.true(await listbox.isDisplayed(), 'space should show the listbox');
+ t.is(await combobox.getAttribute('aria-activedescendant'), secondOptionId, 'second option should be highlighted');
+});
+
+ariaTest('Home opens listbox to first option', exampleFile, 'combobox-key-home', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const firstOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]`)).getAttribute('id');
+
+ // Open, select second option, close
+ await combobox.sendKeys(' ');
+ await combobox.sendKeys(Key.ARROW_DOWN);
+ await combobox.sendKeys(Key.ESCAPE);
+
+ // listbox is collapsed
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden');
+
+ // Send home key to the combo
+ await combobox.sendKeys(Key.HOME);
+
+ // Check that the listbox is displayed and first option is highlighted
+ t.true(await listbox.isDisplayed(), 'home should show the listbox');
+ t.is(await combobox.getAttribute('aria-activedescendant'), firstOptionId, 'home should always highlight the first option');
+});
+
+ariaTest('End opens listbox to last option', exampleFile, 'combobox-key-end', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const options = await t.context.queryElements(t, `${ex.listboxSelector} [role=option]`);
+ const lastOptionId = await options[options.length - 1].getAttribute('id');
+
+ // Send end key to the combo
+ await combobox.sendKeys(Key.END);
+
+ // Check that the listbox is displayed and first option is highlighted
+ t.true(await listbox.isDisplayed(), 'end should show the listbox');
+ t.is(await combobox.getAttribute('aria-activedescendant'), lastOptionId, 'end should always highlight the last option');
+});
+
+ariaTest('character keys open listbox to matching option', exampleFile, 'listbox-key-char', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const secondOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:nth-child(2)`)).getAttribute('id');
+
+ // type "a"
+ await combobox.sendKeys('a');
+
+ // Check that the listbox is displayed and the second option is highlighted
+ // bit of hard-coding here; we know that the second option begins with "a", since the first is a placeholder
+ t.true(await listbox.isDisplayed(), 'character key should show the listbox');
+ t.is(await combobox.getAttribute('aria-activedescendant'), secondOptionId, 'typing "a" should highlight the first option beginning with "a"');
+});
+
+// Close listbox
+ariaTest('click opens and closes listbox', exampleFile, 'test-additional-behavior', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+
+ await combobox.click();
+ t.true(await listbox.isDisplayed(), 'listbox should be present after click');
+
+ await combobox.click();
+ t.false(await listbox.isDisplayed(), 'second click should close listbox');
+ t.is(await combobox.getAttribute('aria-expanded'), 'false', 'aria-expanded should be set to false after second click');
+});
+
+ariaTest('clicking an option selects and closes', exampleFile, 'test-additional-behavior', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const fourthOption = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:nth-child(4)`));
+
+ await combobox.click();
+ const fourthOptionText = await fourthOption.getText();
+ t.true(await listbox.isDisplayed(), 'listbox should be present after click');
+
+ await fourthOption.click();
+ t.false(await listbox.isDisplayed(), 'option click should close listbox');
+ t.is(await combobox.getText(), fourthOptionText, 'Combobox inner text should match the clicked option');
+ t.is(await fourthOption.getAttribute('aria-selected'), 'true', 'Clicked option has aria-selected set to true');
+});
+
+ariaTest('Enter closes listbox and selects option', exampleFile, 'listbox-key-enter', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const thirdOption = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:nth-child(3)`));
+
+ // Open, move to third option, hit enter
+ await combobox.sendKeys(Key.ENTER, Key.ARROW_DOWN, Key.ARROW_DOWN);
+ const thirdOptionText = await thirdOption.getText();
+ await combobox.sendKeys(Key.ENTER);
+
+
+ // listbox is collapsed and the value is set to the third option
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden');
+ t.is(await combobox.getAttribute('aria-expanded'), 'false', 'test aria-expanded on combo');
+ t.is(await combobox.getText(), thirdOptionText, 'Combobox inner text should match the third option');
+ t.is(await thirdOption.getAttribute('aria-selected'), 'true', 'Third option has aria-selected set to true');
+});
+
+ariaTest('Space closes listbox and selects option', exampleFile, 'listbox-key-space', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const secondOption = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:nth-child(2)`));
+
+ // Open, move to third option, hit space
+ await combobox.sendKeys(Key.ENTER, Key.ARROW_DOWN);
+ const secondOptionText = await secondOption.getText();
+ await combobox.sendKeys(' ');
+
+ // listbox is collapsed and the value is set to the third option
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden');
+ t.is(await combobox.getText(), secondOptionText, 'Combobox inner text should match the second option');
+ t.is(await secondOption.getAttribute('aria-selected'), 'true', 'Second option has aria-selected set to true');
+});
+
+ariaTest('Escape closes listbox without selecting option', exampleFile, 'listbox-key-escape', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+
+ // Open, move to third option, hit enter
+ await combobox.sendKeys(Key.ENTER, Key.ARROW_DOWN, Key.ARROW_DOWN);
+ const firstOptionText = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]`)).getText();
+ await combobox.sendKeys(Key.ESCAPE);
+
+ // listbox is collapsed and the value is still set to the first option
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden');
+ t.is(await combobox.getText(), firstOptionText, 'Combobox inner text should match the first option');
+});
+
+ariaTest('Tab closes listbox and selects option', exampleFile, 'listbox-key-tab', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const fourthOption = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:nth-child(4)`));
+
+ // Open, move to fourth option, hit tab
+ await combobox.sendKeys(Key.ENTER, Key.ARROW_DOWN, Key.ARROW_DOWN, Key.ARROW_DOWN);
+ const fourthOptionText = await fourthOption.getText();
+ await combobox.sendKeys(Key.TAB);
+
+ // listbox is collapsed and the value is set to the fourth option
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden');
+ t.is(await combobox.getText(), fourthOptionText, 'Combobox inner text should match the second option');
+ t.is(await fourthOption.getAttribute('aria-selected'), 'true', 'Fourth option has aria-selected set to true');
+});
+
+// Changing options
+ariaTest('Down arrow moves to next option', exampleFile, 'listbox-key-down-arrow', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const thirdOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:nth-child(3)`)).getAttribute('id');
+
+ // Open, press down arrow
+ await combobox.click();
+ await combobox.sendKeys(Key.ARROW_DOWN, Key.ARROW_DOWN);
+
+ // Second option is highlighted
+ t.is(await combobox.getAttribute('aria-activedescendant'), thirdOptionId, 'aria-activedescendant points to the third option after two down arrows');
+});
+
+ariaTest('Down arrow does not wrap after last option', exampleFile, 'listbox-key-down-arrow', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const lastOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:last-child`)).getAttribute('id');
+
+ // Open, press end, press down arrow
+ await combobox.click();
+ await combobox.sendKeys(Key.END, Key.ARROW_DOWN);
+
+ // last option is highlighted
+ t.is(await combobox.getAttribute('aria-activedescendant'), lastOptionId, 'aria-activedescendant points to the last option after end + down arrow');
+});
+
+ariaTest('Up arrow does not wrap from first option', exampleFile, 'listbox-key-up-arrow', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const firstOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:first-child`)).getAttribute('id');
+
+ // Open, press up arrow
+ await combobox.click();
+ await combobox.sendKeys(Key.ARROW_UP);
+
+ // first option is highlighted
+ t.is(await combobox.getAttribute('aria-activedescendant'), firstOptionId, 'aria-activedescendant points to the first option after up arrow');
+});
+
+ariaTest('Up arrow moves to previous option', exampleFile, 'listbox-key-up-arrow', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const options = await t.context.queryElements(t, `${ex.listboxSelector} [role=option]`);
+ const optionId = await options[options.length - 2].getAttribute('id');
+
+ // Open, press end + up arrow
+ await combobox.click();
+ await combobox.sendKeys(Key.END, Key.ARROW_UP);
+
+ // second to last option is highlighted
+ t.is(await combobox.getAttribute('aria-activedescendant'), optionId, 'aria-activedescendant points to the second-to-last option after end + up arrow');
+});
+
+ariaTest('End moves to last option', exampleFile, 'listbox-key-end', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const lastOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:last-child`)).getAttribute('id');
+
+ // Open, press end
+ await combobox.click();
+ await combobox.sendKeys(Key.END);
+
+ // last option is highlighted
+ t.is(await combobox.getAttribute('aria-activedescendant'), lastOptionId, 'aria-activedescendant points to the last option after end');
+});
+
+ariaTest('End scrolls last option into view', exampleFile, 'test-additional-behavior', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const options = await t.context.queryElements(t, `${ex.listboxSelector} [role=option]`);
+
+ await combobox.click();
+
+ let listboxBounds = await listbox.getRect();
+ let optionBounds = await options[options.length - 1].getRect();
+
+ t.true(listboxBounds.y + listboxBounds.height - optionBounds.y < 0, 'last option is not initially displayed');
+
+ await combobox.sendKeys(Key.END);
+ listboxBounds = await listbox.getRect();
+ optionBounds = await options[options.length - 1].getRect();
+
+ t.true(listboxBounds.y + listboxBounds.height - optionBounds.y >= 0, 'last option is in view after end key');
+});
+
+ariaTest('Home moves to first option', exampleFile, 'listbox-key-home', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const firstOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:first-child`)).getAttribute('id');
+
+ // Open, press down a couple times, then home
+ await combobox.click();
+ await combobox.sendKeys(Key.ARROW_DOWN, Key.ARROW_DOWN, Key.HOME);
+
+ // first option is highlighted
+ t.is(await combobox.getAttribute('aria-activedescendant'), firstOptionId, 'aria-activedescendant points to the first option after home');
+});
+
+ariaTest('PageDown moves 10 options, and does not wrap', exampleFile, 'listbox-key-pagedown', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const options = await t.context.queryElements(t, `${ex.listboxSelector} [role=option]`);
+
+ // Open, press page down
+ await combobox.click();
+ await combobox.sendKeys(Key.PAGE_DOWN);
+
+ // 11th option is highlighted
+ let optionId = await options[10].getAttribute('id');
+ t.is(await combobox.getAttribute('aria-activedescendant'), optionId, 'aria-activedescendant points to the 10th option after pagedown');
+
+ // last option is highlighted
+ await combobox.sendKeys(Key.PAGE_DOWN);
+ optionId = await options[options.length - 1].getAttribute('id');
+ t.is(await combobox.getAttribute('aria-activedescendant'), optionId, 'aria-activedescendant points to the last option after second pagedown');
+});
+
+ariaTest('PageUp moves up 10 options, and does not wrap', exampleFile, 'listbox-key-pageup', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const options = await t.context.queryElements(t, `${ex.listboxSelector} [role=option]`);
+
+ // Open, press end then page up
+ await combobox.click();
+ await combobox.sendKeys(Key.END, Key.PAGE_UP);
+
+ // 11th-from-last option is highlighted
+ let optionId = await options[options.length - 11].getAttribute('id');
+ t.is(await combobox.getAttribute('aria-activedescendant'), optionId, 'aria-activedescendant points to the 10th-from-last option after end + pageup');
+
+ // first option is highlighted
+ await combobox.sendKeys(Key.PAGE_UP);
+ optionId = await options[0].getAttribute('id');
+ t.is(await combobox.getAttribute('aria-activedescendant'), optionId, 'aria-activedescendant points to the first option after second pageup');
+});
+
+ariaTest('Multiple single-character presses cycle through options', exampleFile, 'listbox-key-char', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const options = await t.context.queryElements(t, `${ex.listboxSelector} [role=option]`);
+
+ // Open, then type "a"
+ await combobox.click();
+
+ // get indices of matching options
+ const optionNames = await Promise.all(options.map(async (option) => {
+ return await option.getText();
+ }));
+ const matchingOps = optionNames
+ .filter((name) => name[0].toLowerCase() === 'b')
+ .map((name) => optionNames.indexOf(name));
+
+
+ // type b, check first matching op is highlighted
+ await combobox.sendKeys('b');
+ let matchingId = await options[matchingOps[0]].getAttribute('id');
+ t.is(await combobox.getAttribute('aria-activedescendant'), `${matchingId}`, 'aria-activedescendant points to the first option beginning with "b"');
+
+ // type b again, second matching option is highlighted
+ await combobox.sendKeys('b');
+ matchingId = await options[matchingOps[1]].getAttribute('id');
+ t.is(await combobox.getAttribute('aria-activedescendant'), `${matchingId}`, 'aria-activedescendant points to the second option beginning with "b"');
+
+ // type "b" as many times as there are matching options
+ // focus should wrap and end up on second option again
+ const keys = matchingOps.map((op) => 'b');
+ await combobox.sendKeys(...keys);
+ matchingId = await options[matchingOps[1]].getAttribute('id');
+ t.is(await combobox.getAttribute('aria-activedescendant'), `${matchingId}`, 'aria-activedescendant points to the second option beginning with "b"');
+});
+
+ariaTest('Typing multiple characters refines search', exampleFile, 'listbox-key-char', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const options = await t.context.queryElements(t, `${ex.listboxSelector} [role=option]`);
+
+ await combobox.click();
+
+ // get info about the fourth option
+ const fourthName = await options[3].getText();
+ const fourthId = await options[3].getAttribute('id');
+
+ // type first letter
+ await combobox.sendKeys(fourthName[0]);
+
+ // fourth op should not be hightlighted after only first letter
+ t.not(await combobox.getAttribute('aria-activedescendant'), fourthId, 'The fourth option is not highlighted after typing only the first letter');
+
+ // type more letters
+ await combobox.sendKeys(...fourthName.slice(1, 4).split(''));
+
+ // now fourth option should be highlighted
+ t.is(await combobox.getAttribute('aria-activedescendant'), fourthId, 'The fourth option is highlighted after typing multiple letters');
+});
\ No newline at end of file
diff --git a/test/util/assertAriaControls.js b/test/util/assertAriaControls.js
index 243ae5bca2..899d128109 100644
--- a/test/util/assertAriaControls.js
+++ b/test/util/assertAriaControls.js
@@ -12,11 +12,6 @@ const assert = require('assert');
module.exports = async function assertAriaControls (t, elementSelector) {
const elements = await t.context.queryElements(t, elementSelector);
- assert.ok(
- elements.length,
- 'CSS elector returned no results: ' + elementSelector
- );
-
for (let element of elements) {
const ariaControlsExists = await t.context.session.executeScript(async function () {
const selector = arguments[0];
diff --git a/test/util/assertAriaDescribedby.js b/test/util/assertAriaDescribedby.js
index a9600f576a..f679c2c287 100644
--- a/test/util/assertAriaDescribedby.js
+++ b/test/util/assertAriaDescribedby.js
@@ -13,11 +13,6 @@ const assert = require('assert');
module.exports = async function assertAriaDescribedby (t, elementSelector) {
const elements = await t.context.queryElements(t, elementSelector);
- assert.ok(
- elements.length,
- 'CSS elector returned no results: ' + elementSelector
- );
-
for (let index = 0; index < elements.length; index++) {
let ariaDescribedbyExists = await t.context.session.executeScript(async function () {
diff --git a/test/util/assertAriaLabelExists.js b/test/util/assertAriaLabelExists.js
index e62cac4dcd..fd2519d90e 100644
--- a/test/util/assertAriaLabelExists.js
+++ b/test/util/assertAriaLabelExists.js
@@ -10,14 +10,8 @@ const assert = require('assert');
*/
module.exports = async function assertAriaLabel (t, elementSelector) {
-
const elements = await t.context.queryElements(t, elementSelector);
- assert.ok(
- elements.length,
- 'CSS elector returned no results: ' + elementSelector
- );
-
for (let index = 0; index < elements.length; index++) {
let ariaLabelExists = await t.context.session.executeScript(async function () {
diff --git a/test/util/assertAriaLabelledby.js b/test/util/assertAriaLabelledby.js
index fa2753812a..1d4bf2dfc3 100644
--- a/test/util/assertAriaLabelledby.js
+++ b/test/util/assertAriaLabelledby.js
@@ -12,11 +12,6 @@ const assert = require('assert');
module.exports = async function assertAriaLabelledby (t, elementSelector) {
const elements = await t.context.queryElements(t, elementSelector);
- assert.ok(
- elements.length,
- 'CSS elector returned no results: ' + elementSelector
- );
-
for (let index = 0; index < elements.length; index++) {
const ariaLabelledbyExists = await t.context.session.executeScript(async function () {
const [selector, index] = arguments;
diff --git a/test/util/assertAttributeDNE.js b/test/util/assertAttributeDNE.js
index 4073bef921..741ad12b87 100644
--- a/test/util/assertAttributeDNE.js
+++ b/test/util/assertAttributeDNE.js
@@ -10,14 +10,8 @@ const assert = require('assert');
* @param {String} attribute - attribute that should not exist
*/
module.exports = async function assertAttributeDNE (t, selector, attribute) {
-
const numElements = (await t.context.queryElements(t, selector)).length;
- assert.ok(
- numElements,
- 'CSS elector returned no results: ' + selector
- );
-
for (let index = 0; index < numElements; index++) {
const attributeExists = await t.context.session.executeScript(function () {
let [selector, index, attribute] = arguments;
diff --git a/test/util/assertAttributeValues.js b/test/util/assertAttributeValues.js
index 724d5fe978..05d7662ca6 100644
--- a/test/util/assertAttributeValues.js
+++ b/test/util/assertAttributeValues.js
@@ -13,11 +13,6 @@ const assert = require('assert');
module.exports = async function assertAttributeValues (t, elementSelector, attribute, value) {
let elements = await t.context.queryElements(t, elementSelector);
- assert.ok(
- elements.length,
- 'CSS elector returned no results: ' + elementSelector
- );
-
for (let element of elements) {
assert.strictEqual(
await element.getAttribute(attribute),
diff --git a/test/util/assertRovingTabindex.js b/test/util/assertRovingTabindex.js
index a87d008a2c..20cf4ab42b 100644
--- a/test/util/assertRovingTabindex.js
+++ b/test/util/assertRovingTabindex.js
@@ -15,11 +15,6 @@ module.exports = async function assertRovingTabindex (t, elementsSelector, key)
// tabindex='0' is expected on the first element
let elements = await t.context.queryElements(t, elementsSelector);
- assert.ok(
- elements.length,
- 'CSS elector returned no results: ' + elementsSelector
- );
-
// test only one element has tabindex="0"
for (let tabableEl = 0; tabableEl < elements.length; tabableEl++) {
for (let el = 0; el < elements.length; el++) {
From cd19697b8a1786212f6b2ad2982c84d4ec424a32 Mon Sep 17 00:00:00 2001
From: Sarah Higley
Date: Tue, 9 Jun 2020 12:28:34 -0700
Subject: [PATCH 08/12] add keyboard note about tab behavior on native select
elements
---
examples/combobox/combobox-select-only.html | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/examples/combobox/combobox-select-only.html b/examples/combobox/combobox-select-only.html
index 1cc7e62063..2404d04145 100644
--- a/examples/combobox/combobox-select-only.html
+++ b/examples/combobox/combobox-select-only.html
@@ -181,7 +181,9 @@
Listbox Popup
Sets the value to the content of the focused option in the listbox.
Closes the listbox.
-
Performs the default action, moving focus to the next focusable element.
+
Performs the default action, moving focus to the next focusable element.
+ Note: the native <select> element closes the listbox but does not move focus on tab.
+ This pattern matches the behavior of the other comboboxes rather than the native element in this case.
Opens the listbox and moves visual focus to the last option.
-
+
Printable Characters
@@ -240,7 +240,7 @@
Listbox Popup
PageDown
Jumps visual focus down 10 options (or to last option).
-
+
Printable Characters
diff --git a/test/tests/combobox_select-only.js b/test/tests/combobox_select-only.js
index 1fbcd15e19..4bbaf076a4 100644
--- a/test/tests/combobox_select-only.js
+++ b/test/tests/combobox_select-only.js
@@ -97,7 +97,7 @@ ariaTest('Alt + down arrow opens listbox', exampleFile, 'combobox-key-alt-down-a
t.false(await listbox.isDisplayed(), 'Listbox should be hidden on load');
// Send ALT + ARROW_DOWN to the combo
- await combobox.sendKeys(Key.ALT, Key.ARROW_DOWN);
+ await combobox.sendKeys(Key.chord(Key.ALT, Key.ARROW_DOWN));
// Check that the listbox is displayed
t.true(await listbox.isDisplayed(), 'alt + down should show the listbox');
@@ -107,7 +107,27 @@ ariaTest('Alt + down arrow opens listbox', exampleFile, 'combobox-key-alt-down-a
t.is(await combobox.getAttribute('aria-activedescendant'), firstOptionId, 'Alt + Down should highlight the first option');
});
-ariaTest('Down arrow opens listbox', exampleFile, 'combobox-key-down-arrow', async (t) => {
+ariaTest('Up arrow opens listbox', exampleFile, 'combobox-key-up-arrow', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const firstOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]`)).getAttribute('id');
+
+ // listbox starts collapsed
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden on load');
+
+ // Send ARROW_UP to the combo
+ await combobox.sendKeys(Key.ARROW_UP);
+
+ // Check that the listbox is displayed
+ t.true(await listbox.isDisplayed(), 'arrow up should show the listbox');
+ t.is(await combobox.getAttribute('aria-expanded'), 'true', 'aria-expanded should be true when opened');
+
+ // the first option should be selected
+ t.is(await combobox.getAttribute('aria-activedescendant'), firstOptionId, 'arrow up should highlight the first option');
+});
+
+
+ariaTest(' arrow opens listbox', exampleFile, 'combobox-key-down-arrow', async (t) => {
const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
const firstOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]`)).getAttribute('id');
@@ -215,7 +235,7 @@ ariaTest('End opens listbox to last option', exampleFile, 'combobox-key-end', as
t.is(await combobox.getAttribute('aria-activedescendant'), lastOptionId, 'end should always highlight the last option');
});
-ariaTest('character keys open listbox to matching option', exampleFile, 'listbox-key-char', async (t) => {
+ariaTest('character keys open listbox to matching option', exampleFile, 'printable-chars', async (t) => {
const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
const secondOptionId = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:nth-child(2)`)).getAttribute('id');
@@ -291,6 +311,23 @@ ariaTest('Space closes listbox and selects option', exampleFile, 'listbox-key-sp
t.is(await secondOption.getAttribute('aria-selected'), 'true', 'Second option has aria-selected set to true');
});
+ariaTest('Space closes listbox and selects option', exampleFile, 'listbox-key-alt-up-arrow', async (t) => {
+ const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
+ const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
+ const secondOption = await t.context.session.findElement(By.css(`${ex.listboxSelector} [role=option]:nth-child(2)`));
+
+ // Open, move to third option, send ALT+UP ARROW
+ await combobox.sendKeys(Key.ENTER, Key.ARROW_DOWN);
+ const secondOptionText = await secondOption.getText();
+ await combobox.sendKeys(Key.chord(Key.ALT, Key.ARROW_UP));
+
+ // listbox is collapsed and the value is set to the third option
+ t.false(await listbox.isDisplayed(), 'Listbox should be hidden');
+ t.is(await combobox.getText(), secondOptionText, 'Combobox inner text should match the second option');
+ t.is(await secondOption.getAttribute('aria-selected'), 'true', 'Second option has aria-selected set to true');
+});
+
+
ariaTest('Escape closes listbox without selecting option', exampleFile, 'listbox-key-escape', async (t) => {
const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
@@ -450,7 +487,7 @@ ariaTest('PageUp moves up 10 options, and does not wrap', exampleFile, 'listbox-
t.is(await combobox.getAttribute('aria-activedescendant'), optionId, 'aria-activedescendant points to the first option after second pageup');
});
-ariaTest('Multiple single-character presses cycle through options', exampleFile, 'listbox-key-char', async (t) => {
+ariaTest('Multiple single-character presses cycle through options', exampleFile, 'printable-chars', async (t) => {
const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
const options = await t.context.queryElements(t, `${ex.listboxSelector} [role=option]`);
@@ -484,7 +521,7 @@ ariaTest('Multiple single-character presses cycle through options', exampleFile,
t.is(await combobox.getAttribute('aria-activedescendant'), `${matchingId}`, 'aria-activedescendant points to the second option beginning with "b"');
});
-ariaTest('Typing multiple characters refines search', exampleFile, 'listbox-key-char', async (t) => {
+ariaTest('Typing multiple characters refines search', exampleFile, 'printable-chars', async (t) => {
const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
const options = await t.context.queryElements(t, `${ex.listboxSelector} [role=option]`);
@@ -505,4 +542,4 @@ ariaTest('Typing multiple characters refines search', exampleFile, 'listbox-key-
// now fourth option should be highlighted
t.is(await combobox.getAttribute('aria-activedescendant'), fourthId, 'The fourth option is highlighted after typing multiple letters');
-});
\ No newline at end of file
+});
From a4728bb2198cf667a73d1e2e32ce92fb4e0fb27b Mon Sep 17 00:00:00 2001
From: Matt King
Date: Tue, 30 Jun 2020 12:31:31 -0700
Subject: [PATCH 10/12] Minor editorial revisions to introduction
---
examples/combobox/combobox-select-only.html | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/examples/combobox/combobox-select-only.html b/examples/combobox/combobox-select-only.html
index b7bc3703c4..e06cbf1af7 100644
--- a/examples/combobox/combobox-select-only.html
+++ b/examples/combobox/combobox-select-only.html
@@ -26,9 +26,11 @@
Select-Only Combobox Example
-
The following example implementation of the ARIA design pattern for combobox
- demonstrates a single-select combobox widget that is functionally similar to an HTML select element.
- Contrary to other combobox examples, this combobox has no <input> element, and does not take freeform user input. Like a <select> a user can still type characters to auto-select matching options, however.
+
+ The following example implementation of the ARIA design pattern for combobox
+ demonstrates a single-select combobox widget that is functionally similar to an HTML select element.
+ Unlike the editable combobox examples, this select-only combobox is not made with a <input> element, and it does not accept freeform user input.
+ However, like an HTML <select>, users can type characters to select matching options.
Similar examples include:
From 00d409d0b3b70e732580f13b454053d0d8e83c6e Mon Sep 17 00:00:00 2001
From: Matt King
Date: Tue, 30 Jun 2020 13:46:30 -0700
Subject: [PATCH 11/12] Add use strict and license statement
---
examples/combobox/js/select-only.js | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/examples/combobox/js/select-only.js b/examples/combobox/js/select-only.js
index 7fae7dd145..2c18abed80 100644
--- a/examples/combobox/js/select-only.js
+++ b/examples/combobox/js/select-only.js
@@ -1,3 +1,10 @@
+/*
+* This content is licensed according to the W3C Software License at
+* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
+*/
+
+'use strict';
+
// Save a list of named combobox actions, for future readability
const SelectActions = {
Close: 0,
From 6695e84da007b966f6491902e7942ab46216b6f6 Mon Sep 17 00:00:00 2001
From: Matt King
Date: Tue, 30 Jun 2020 13:57:55 -0700
Subject: [PATCH 12/12] Accept change from Carolyn "a" > "an"
Co-authored-by: Carolyn MacLeod
---
examples/combobox/combobox-select-only.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/combobox/combobox-select-only.html b/examples/combobox/combobox-select-only.html
index e06cbf1af7..64fe2b9337 100644
--- a/examples/combobox/combobox-select-only.html
+++ b/examples/combobox/combobox-select-only.html
@@ -29,7 +29,7 @@
Select-Only Combobox Example
The following example implementation of the ARIA design pattern for combobox
demonstrates a single-select combobox widget that is functionally similar to an HTML select element.
- Unlike the editable combobox examples, this select-only combobox is not made with a <input> element, and it does not accept freeform user input.
+ Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input.
However, like an HTML <select>, users can type characters to select matching options.