Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select-only combobox #1396

Merged
merged 12 commits into from
Jul 1, 2020
1 change: 1 addition & 0 deletions aria-practices.html
Original file line number Diff line number Diff line change
@@ -790,6 +790,7 @@ <h3>Combobox</h3>
<section class="notoc">
<h4>Examples</h4>
<ul>
<li><a href="examples/combobox/combobox-select-only.html">Select-Only Combobox</a>: A single-select combobox with no text input that is functionally similar to an HTML <code>select</code> element.</li>
<li><a href="examples/combobox/combobox-autocomplete-both.html">Editable Combobox with Both List and Inline Autocomplete</a>: An editable combobox that demonstrates the autocomplete behavior known as <q>list with inline autocomplete</q>.</li>
<li><a href="examples/combobox/combobox-autocomplete-list.html">Editable Combobox with List Autocomplete</a>: An editable combobox that demonstrates the autocomplete behavior known as <q>list with manual selection</q>.</li>
<li><a href="examples/combobox/combobox-autocomplete-none.html">Editable Combobox Without Autocomplete</a>: An editable combobox that demonstrates the behavior associated with <code>aria-autocomplete=none</code>.</li>
1 change: 1 addition & 0 deletions examples/combobox/combobox-autocomplete-both.html
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ <h1>Editable Combobox With Both List and Inline Autocomplete Example</h1>
</p>
<p>Similar examples include:</p>
<ul>
<li><a href="examples/combobox/combobox-select-only.html">Select-Only Combobox</a>: A single-select combobox with no text input that is functionally similar to an HTML <code>select</code> element.</li>
<li><a href="combobox-autocomplete-list.html">Editable Combobox with List Autocomplete</a>: An editable combobox that demonstrates the autocomplete behavior known as <q>list with manual selection</q>.</li>
<li><a href="combobox-autocomplete-none.html">Editable Combobox Without Autocomplete</a>: An editable combobox that demonstrates the behavior associated with <code>aria-autocomplete=none</code>.</li>
<li><a href="grid-combo.html">Editable Combobox with Grid Popup</a>: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.</li>
1 change: 1 addition & 0 deletions examples/combobox/combobox-autocomplete-list.html
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ <h1>Editable Combobox With List Autocomplete Example</h1>
</p>
<p>Similar examples include:</p>
<ul>
<li><a href="examples/combobox/combobox-select-only.html">Select-Only Combobox</a>: A single-select combobox with no text input that is functionally similar to an HTML <code>select</code> element.</li>
<li><a href="combobox-autocomplete-both.html">Editable Combobox with Both List and Inline Autocomplete</a>: An editable combobox that demonstrates the autocomplete behavior known as <q>list with inline autocomplete</q>.</li>
<li><a href="combobox-autocomplete-none.html">Editable Combobox Without Autocomplete</a>: An editable combobox that demonstrates the behavior associated with <code>aria-autocomplete=none</code>.</li>
<li><a href="grid-combo.html">Editable Combobox with Grid Popup</a>: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.</li>
1 change: 1 addition & 0 deletions examples/combobox/combobox-autocomplete-none.html
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@ <h1>Editable Combobox without Autocomplete Example</h1>
</p>
<p>Similar examples include: </p>
<ul>
<li><a href="examples/combobox/combobox-select-only.html">Select-Only Combobox</a>: A single-select combobox with no text input that is functionally similar to an HTML <code>select</code> element.</li>
<li><a href="combobox-autocomplete-both.html">Editable Combobox with Both List and Inline Autocomplete</a>: An editable combobox that demonstrates the autocomplete behavior known as <q>list with inline autocomplete</q>.</li>
<li><a href="combobox-autocomplete-list.html">Editable Combobox with List Autocomplete</a>: An editable combobox that demonstrates the autocomplete behavior known as <q>list with manual selection</q>.</li>
<li><a href="grid-combo.html">Editable Combobox with Grid Popup</a>: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.</li>
417 changes: 417 additions & 0 deletions examples/combobox/combobox-select-only.html

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions examples/combobox/css/select-only.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
.combo *,
.combo *::before,
.combo *::after {
box-sizing: border-box;
}

.combo {
display: block;
margin-bottom: 1.5em;
max-width: 400px;
position: relative;
}

.combo::after {
border-bottom: 2px solid rgba(0, 0, 0, 0.75);
border-right: 2px solid rgba(0, 0, 0, 0.75);
content: '';
display: block;
height: 12px;
pointer-events: none;
position: absolute;
right: 16px;
top: 50%;
transform: translate(0, -65%) rotate(45deg);
width: 12px;
}

.combo-input {
background-color: #f5f5f5;
border: 2px solid rgba(0, 0, 0, 0.75);
border-radius: 4px;
display: block;
font-size: 1em;
min-height: calc(1.4em + 26px);
padding: 12px 16px 14px;
text-align: left;
width: 100%;
}

.open .combo-input {
border-radius: 4px 4px 0 0;
}

.combo-input:focus {
border-color: #0067b8;
box-shadow: 0 0 4px 2px #0067b8;
outline: 4px solid transparent;
}

.combo-label {
display: block;
font-size: 20px;
font-weight: 100;
margin-bottom: 0.25em;
}

.combo-menu {
background-color: #f5f5f5;
border: 1px solid rgba(0, 0, 0, 0.75);
border-radius: 0 0 4px 4px;
display: none;
max-height: 300px;
overflow-y:scroll;
left: 0;
position: absolute;
top: 100%;
width: 100%;
z-index: 100;
}

.open .combo-menu {
display: block;
}

.combo-option {
padding: 10px 12px 12px;
}

.combo-option:hover {
background-color: rgba(0, 0, 0, 0.1);
}

.combo-option.option-current {
outline: 3px solid #0067b8;
outline-offset: -3px;
}

.combo-option[aria-selected="true"] {
padding-right: 30px;
position: relative;
}

.combo-option[aria-selected="true"]::after {
border-bottom: 2px solid #000;
border-right: 2px solid #000;
content: '';
height: 16px;
position: absolute;
right: 15px;
top: 50%;
transform: translate(0, -50%) rotate(45deg);
width: 8px;
}
1 change: 1 addition & 0 deletions examples/combobox/grid-combo.html
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@ <h1>Editable Combobox with Grid Popup Example</h1>
</p>
<p>Similar examples include: </p>
<ul>
<li><a href="examples/combobox/combobox-select-only.html">Select-Only Combobox</a>: A single-select combobox with no text input that is functionally similar to an HTML <code>select</code> element.</li>
<li><a href="combobox-autocomplete-both.html">Editable Combobox with Both List and Inline Autocomplete</a>: An editable combobox that demonstrates the autocomplete behavior known as <q>list with inline autocomplete</q>.</li>
<li><a href="combobox-autocomplete-list.html">Editable Combobox with List Autocomplete</a>: An editable combobox that demonstrates the autocomplete behavior known as <q>list with manual selection</q>.</li>
<li><a href="combobox-autocomplete-none.html">Editable Combobox Without Autocomplete</a>: An editable combobox that demonstrates the behavior associated with <code>aria-autocomplete=none</code>.</li>
365 changes: 365 additions & 0 deletions examples/combobox/js/select-only.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
// Save a list of named combobox actions, for future readability
const SelectActions = {
Close: 0,
CloseSelect: 1,
First: 2,
Last: 3,
Next: 4,
Open: 5,
PageDown: 6,
PageUp: 7,
Previous: 8,
Select: 9,
Type: 10
}

/*
* Helper functions
*/

// filter an array of options against an input string
// returns an array of options that begin with the filter string, case-independent
function filterOptions(options = [], filter, exclude = []) {
return options.filter((option) => {
const matches = option.toLowerCase().indexOf(filter.toLowerCase()) === 0;
return matches && exclude.indexOf(option) < 0;
});
}

// map a key press to an action
function getActionFromKey(event, menuOpen) {
const { key, altKey, ctrlKey, metaKey } = event;
const openKeys = ['ArrowDown', 'ArrowUp', 'Enter', ' ']; // all keys that will do the default open action
// handle opening when closed
if (!menuOpen && openKeys.includes(key)) {
return SelectActions.Open;
}

// home and end move the selected option when open or closed
if (key === 'Home') {
return SelectActions.First;
}
if (key === 'End') {
return SelectActions.Last;
}

// handle typing characters when open or closed
if (key === 'Backspace' || key === 'Clear' || (key.length === 1 && key !== ' ' && !altKey && !ctrlKey && !metaKey)) {
return SelectActions.Type;
}

// handle keys when open
if (menuOpen) {
if (key === 'ArrowUp' && altKey) {
return SelectActions.CloseSelect;
}
else if (key === 'ArrowDown' && !altKey) {
return SelectActions.Next;
}
else if (key === 'ArrowUp') {
return SelectActions.Previous;
}
else if (key === 'PageUp') {
return SelectActions.PageUp;
}
else if (key === 'PageDown') {
return SelectActions.PageDown;
}
else if (key === 'Escape') {
return SelectActions.Close;
}
else if (key === 'Enter' || key === ' ') {
return SelectActions.CloseSelect;
}
}
}

// return the index of an option from an array of options, based on a search string
// if the filter is multiple iterations of the same letter (e.g "aaa"), then cycle through first-letter matches
function getIndexByLetter(options, filter, startIndex = 0) {
const orderedOptions = [...options.slice(startIndex), ...options.slice(0, startIndex)];
const firstMatch = filterOptions(orderedOptions, filter)[0];
const allSameLetter = (array) => array.every((letter) => letter === array[0]);

// first check if there is an exact match for the typed string
if (firstMatch) {
return options.indexOf(firstMatch);
}

// if the same letter is being repeated, cycle through first-letter matches
else if (allSameLetter(filter.split(''))) {
const matches = filterOptions(orderedOptions, filter[0]);
return options.indexOf(matches[0]);
}

// if no matches, return -1
else {
return -1;
}
}

// get an updated option index after performing an action
function getUpdatedIndex(currentIndex, maxIndex, action) {
const pageSize = 10; // used for pageup/pagedown

switch(action) {
case SelectActions.First:
return 0;
case SelectActions.Last:
return maxIndex;
case SelectActions.Previous:
return Math.max(0, currentIndex - 1);
case SelectActions.Next:
return Math.min(maxIndex, currentIndex + 1);
case SelectActions.PageUp:
return Math.max(0, currentIndex - pageSize);
case SelectActions.PageDown:
return Math.min(maxIndex, currentIndex + pageSize);
default:
return currentIndex;
}
}

// check if an element is currently scrollable
function isScrollable(element) {
return element && element.clientHeight < element.scrollHeight;
}

// ensure a given child element is within the parent's visible scroll area
// if the child is not visible, scroll the parent
function maintainScrollVisibility(activeElement, scrollParent) {
const { offsetHeight, offsetTop } = activeElement;
const { offsetHeight: parentOffsetHeight, scrollTop } = scrollParent;

const isAbove = offsetTop < scrollTop;
const isBelow = (offsetTop + offsetHeight) > (scrollTop + parentOffsetHeight);

if (isAbove) {
scrollParent.scrollTo(0, offsetTop);
}
else if (isBelow) {
scrollParent.scrollTo(0, offsetTop - parentOffsetHeight + offsetHeight);
}
}

/*
* Select Component
* Accepts a combobox element and an array of string options
*/
const Select = function(el, options = []) {
// element refs
this.el = el;
this.comboEl = el.querySelector('[role=combobox]');
this.listboxEl = el.querySelector('[role=listbox]');

// data
this.idBase = this.comboEl.id || 'combo';
this.options = options;

// state
this.activeIndex = 0;
this.open = false;
this.searchString = '';
this.searchTimeout = null;

// init
if (el && this.comboEl && this.listboxEl) {
this.init();
}
}

Select.prototype.init = function() {
// select first option by default
this.comboEl.innerHTML = this.options[0];

// add event listeners
this.comboEl.addEventListener('blur', this.onComboBlur.bind(this));
this.comboEl.addEventListener('click', this.onComboClick.bind(this));
this.comboEl.addEventListener('keydown', this.onComboKeyDown.bind(this));

// create options
this.options.map((option, index) => {
const optionEl = this.createOption(option, index);
this.listboxEl.appendChild(optionEl);
});
}

Select.prototype.createOption = function(optionText, index) {
const optionEl = document.createElement('div');
optionEl.setAttribute('role', 'option');
optionEl.id = `${this.idBase}-${index}`;
optionEl.className = index === 0 ? 'combo-option option-current' : 'combo-option';
optionEl.setAttribute('aria-selected', `${index === 0}`);
optionEl.innerText = optionText;

optionEl.addEventListener('click', (event) => {
event.stopPropagation();
this.onOptionClick(index);
});
optionEl.addEventListener('mousedown', this.onOptionMouseDown.bind(this));

return optionEl;
}

Select.prototype.getSearchString = function(char) {
// reset typing timeout and start new timeout
// this allows us to make multiple-letter matches, like a native select
if (typeof this.searchTimeout === 'number') {
window.clearTimeout(this.searchTimeout);
}

this.searchTimeout = window.setTimeout(() => {
this.searchString = '';
}, 500);

// add most recent letter to saved search string
this.searchString += char;
return this.searchString;
}

Select.prototype.onComboBlur = function() {
// do not do blur action if ignoreBlur flag has been set
if (this.ignoreBlur) {
this.ignoreBlur = false;
return;
}

// select current option and close
if (this.open) {
this.selectOption(this.activeIndex);
this.updateMenuState(false, false);
}
}

Select.prototype.onComboClick = function() {
this.updateMenuState(!this.open, false);
}

Select.prototype.onComboKeyDown = function(event) {
const { key } = event;
const max = this.options.length - 1;

const action = getActionFromKey(event, this.open);

switch(action) {
case SelectActions.Last:
case SelectActions.First:
this.updateMenuState(true);
// intentional fallthrough
case SelectActions.Next:
case SelectActions.Previous:
case SelectActions.PageUp:
case SelectActions.PageDown:
event.preventDefault();
return this.onOptionChange(getUpdatedIndex(this.activeIndex, max, action));
case SelectActions.CloseSelect:
event.preventDefault();
this.selectOption(this.activeIndex);
// intentional fallthrough
case SelectActions.Close:
event.preventDefault();
return this.updateMenuState(false);
case SelectActions.Type:
return this.onComboType(key);
case SelectActions.Open:
event.preventDefault();
return this.updateMenuState(true);
}
}

Select.prototype.onComboType = function(letter) {
// open the listbox if it is closed
this.updateMenuState(true);

// find the index of the first matching option
const searchString = this.getSearchString(letter);
const searchIndex = getIndexByLetter(this.options, searchString, this.activeIndex + 1);

// if a match was found, go to it
if (searchIndex >= 0) {
this.onOptionChange(searchIndex);
}
// if no matches, clear the timeout and search string
else {
window.clearTimeout(this.searchTimeout);
this.searchString = '';
}
}

Select.prototype.onOptionChange = function(index) {
// update state
this.activeIndex = index;

// update aria-activedescendant
this.comboEl.setAttribute('aria-activedescendant', `${this.idBase}-${index}`);

// update active option styles
const options = this.el.querySelectorAll('[role=option]');
[...options].forEach((optionEl) => {
optionEl.classList.remove('option-current');
});
options[index].classList.add('option-current');

// ensure the new option is in view
if (isScrollable(this.listboxEl)) {
maintainScrollVisibility(options[index], this.listboxEl);
}
}

Select.prototype.onOptionClick = function(index) {
this.onOptionChange(index);
this.selectOption(index);
this.updateMenuState(false);
}

Select.prototype.onOptionMouseDown = function() {
// Clicking an option will cause a blur event,
// but we don't want to perform the default keyboard blur action
this.ignoreBlur = true;
}

Select.prototype.selectOption = function(index) {
// update state
this.activeIndex = index;

// update displayed value
const selected = this.options[index];
this.comboEl.innerHTML = selected;

// update aria-selected
const options = this.el.querySelectorAll('[role=option]');
[...options].forEach((optionEl) => {
optionEl.setAttribute('aria-selected', 'false');
});
options[index].setAttribute('aria-selected', 'true');
}

Select.prototype.updateMenuState = function(open, callFocus = true) {
if (this.open === open) {
return;
}

// update state
this.open = open;

// update aria-expanded and styles
this.comboEl.setAttribute('aria-expanded', `${open}`);
open ? this.el.classList.add('open') : this.el.classList.remove('open');

// update activedescendant
const activeID = open ? `${this.idBase}-${this.activeIndex}` : '';
this.comboEl.setAttribute('aria-activedescendant', activeID);

// move focus back to the combobox, if needed
callFocus && this.comboEl.focus();
}

// init select
window.addEventListener('load', function () {
const options = ['Choose a Fruit', 'Apple', 'Banana', 'Blueberry', 'Boysenberry', 'Cherry', 'Cranberry', 'Durian', 'Eggplant', 'Fig', 'Grape', 'Guava', 'Huckleberry'];
const selectEls = document.querySelectorAll('.js-select');

selectEls.forEach((el) => {
new Select(el, options);
});
});
20 changes: 19 additions & 1 deletion examples/index.html
Original file line number Diff line number Diff line change
@@ -80,6 +80,7 @@ <h2 id="examples_by_role_label">Examples by Role</h2>
<li><a href="combobox/combobox-autocomplete-both.html">Editable Combobox With Both List and Inline Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-list.html">Editable Combobox With List Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-none.html">Editable Combobox without Autocomplete</a></li>
<li><a href="combobox/combobox-select-only.html">Select-Only Combobox</a></li>
<li><a href="combobox/grid-combo.html">Editable Combobox with Grid Popup</a></li>
</ul>
</td>
@@ -148,6 +149,7 @@ <h2 id="examples_by_role_label">Examples by Role</h2>
<li><a href="combobox/combobox-autocomplete-both.html">Editable Combobox With Both List and Inline Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-list.html">Editable Combobox With List Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-none.html">Editable Combobox without Autocomplete</a></li>
<li><a href="combobox/combobox-select-only.html">Select-Only Combobox</a></li>
<li><a href="listbox/listbox-collapsible.html">Collapsible Dropdown Listbox</a></li>
<li><a href="listbox/listbox-grouped.html">Listbox with Grouped Options</a></li>
<li><a href="listbox/listbox-rearrangeable.html">Listboxes with Rearrangeable Options</a></li>
@@ -225,6 +227,7 @@ <h2 id="examples_by_role_label">Examples by Role</h2>
<li><a href="combobox/combobox-autocomplete-both.html">Editable Combobox With Both List and Inline Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-list.html">Editable Combobox With List Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-none.html">Editable Combobox without Autocomplete</a></li>
<li><a href="combobox/combobox-select-only.html">Select-Only Combobox</a></li>
</ul>
</td>
</tr>
@@ -298,6 +301,7 @@ <h2 id="examples_by_role_label">Examples by Role</h2>
<td><code>tab</code></td>
<td>
<ul>
<li><a href="carousel/carousel-2-tablist.html">Auto-Rotating Image Carousel with a Tablist</a></li>
<li><a href="tabs/tabs-1/tabs.html">Tabs with Automatic Activation</a></li>
<li><a href="tabs/tabs-2/tabs.html">Tabs with Manual Activation</a></li>
</ul>
@@ -311,6 +315,7 @@ <h2 id="examples_by_role_label">Examples by Role</h2>
<td><code>tablist</code></td>
<td>
<ul>
<li><a href="carousel/carousel-2-tablist.html">Auto-Rotating Image Carousel with a Tablist</a></li>
<li><a href="tabs/tabs-1/tabs.html">Tabs with Automatic Activation</a></li>
<li><a href="tabs/tabs-2/tabs.html">Tabs with Manual Activation</a></li>
</ul>
@@ -320,6 +325,7 @@ <h2 id="examples_by_role_label">Examples by Role</h2>
<td><code>tabpanel</code></td>
<td>
<ul>
<li><a href="carousel/carousel-2-tablist.html">Auto-Rotating Image Carousel with a Tablist</a></li>
<li><a href="tabs/tabs-1/tabs.html">Tabs with Automatic Activation</a></li>
<li><a href="tabs/tabs-2/tabs.html">Tabs with Manual Activation</a></li>
</ul>
@@ -374,6 +380,7 @@ <h2 id="examples_by_props_label">Examples By Properties and States</h2>
<li><a href="combobox/combobox-autocomplete-both.html">Editable Combobox With Both List and Inline Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-list.html">Editable Combobox With List Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-none.html">Editable Combobox without Autocomplete</a></li>
<li><a href="combobox/combobox-select-only.html">Select-Only Combobox</a></li>
<li><a href="combobox/grid-combo.html">Editable Combobox with Grid Popup</a></li>
<li><a href="listbox/listbox-collapsible.html">Collapsible Dropdown Listbox</a></li>
<li><a href="listbox/listbox-grouped.html">Listbox with Grouped Options</a></li>
@@ -421,10 +428,12 @@ <h2 id="examples_by_props_label">Examples By Properties and States</h2>
<td>
<ul>
<li><a href="accordion/accordion.html">Accordion</a></li>
<li><a href="carousel/carousel-2-tablist.html">Auto-Rotating Image Carousel with a Tablist</a></li>
<li><a href="checkbox/checkbox-2/checkbox-2.html">Checkbox (Mixed-State)</a></li>
<li><a href="combobox/combobox-autocomplete-both.html">Editable Combobox With Both List and Inline Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-list.html">Editable Combobox With List Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-none.html">Editable Combobox without Autocomplete</a></li>
<li><a href="combobox/combobox-select-only.html">Select-Only Combobox</a></li>
<li><a href="combobox/grid-combo.html">Editable Combobox with Grid Popup</a></li>
<li><a href="disclosure/disclosure-faq.html">Disclosure (Show/Hide) for Answers to Frequently Asked Questions</a></li>
<li><a href="disclosure/disclosure-img-long-description.html">Disclosure (Show/Hide) for Image Description</a></li>
@@ -471,6 +480,7 @@ <h2 id="examples_by_props_label">Examples By Properties and States</h2>
<li><a href="combobox/combobox-autocomplete-both.html">Editable Combobox With Both List and Inline Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-list.html">Editable Combobox With List Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-none.html">Editable Combobox without Autocomplete</a></li>
<li><a href="combobox/combobox-select-only.html">Select-Only Combobox</a></li>
<li><a href="combobox/grid-combo.html">Editable Combobox with Grid Popup</a></li>
<li><a href="disclosure/disclosure-faq.html">Disclosure (Show/Hide) for Answers to Frequently Asked Questions</a></li>
<li><a href="disclosure/disclosure-img-long-description.html">Disclosure (Show/Hide) for Image Description</a></li>
@@ -520,6 +530,7 @@ <h2 id="examples_by_props_label">Examples By Properties and States</h2>
<td>
<ul>
<li><a href="carousel/carousel-1.html">Auto-Rotating Image Carousel</a></li>
<li><a href="carousel/carousel-2-tablist.html">Auto-Rotating Image Carousel with a Tablist</a></li>
<li><a href="checkbox/checkbox-1/checkbox-1.html">Checkbox (Two State)</a></li>
<li><a href="combobox/combobox-autocomplete-both.html">Editable Combobox With Both List and Inline Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-list.html">Editable Combobox With List Autocomplete</a></li>
@@ -609,6 +620,7 @@ <h2 id="examples_by_props_label">Examples By Properties and States</h2>
<ul>
<li><a href="alert/alert.html">Alert</a></li>
<li><a href="carousel/carousel-1.html">Auto-Rotating Image Carousel</a></li>
<li><a href="carousel/carousel-2-tablist.html">Auto-Rotating Image Carousel with a Tablist</a></li>
<li><a href="dialog-modal/datepicker-dialog.html">Date Picker Dialog</a></li>
</ul>
</td>
@@ -654,7 +666,12 @@ <h2 id="examples_by_props_label">Examples By Properties and States</h2>
</tr>
<tr>
<td><code>aria-roledescription</code></td>
<td><a href="carousel/carousel-1.html">Auto-Rotating Image Carousel</a></td>
<td>
<ul>
<li><a href="carousel/carousel-1.html">Auto-Rotating Image Carousel</a></li>
<li><a href="carousel/carousel-2-tablist.html">Auto-Rotating Image Carousel with a Tablist</a></li>
</ul>
</td>
</tr>
<tr>
<td><code>aria-rowcount</code></td>
@@ -678,6 +695,7 @@ <h2 id="examples_by_props_label">Examples By Properties and States</h2>
<td><code>aria-selected</code></td>
<td>
<ul>
<li><a href="carousel/carousel-2-tablist.html">Auto-Rotating Image Carousel with a Tablist</a></li>
<li><a href="combobox/combobox-autocomplete-both.html">Editable Combobox With Both List and Inline Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-list.html">Editable Combobox With List Autocomplete</a></li>
<li><a href="combobox/combobox-autocomplete-none.html">Editable Combobox without Autocomplete</a></li>
545 changes: 545 additions & 0 deletions test/tests/combobox_select-only.js

Large diffs are not rendered by default.

5 changes: 0 additions & 5 deletions test/util/assertAriaControls.js
Original file line number Diff line number Diff line change
@@ -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];
5 changes: 0 additions & 5 deletions test/util/assertAriaDescribedby.js
Original file line number Diff line number Diff line change
@@ -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 () {
6 changes: 0 additions & 6 deletions test/util/assertAriaLabelExists.js
Original file line number Diff line number Diff line change
@@ -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 () {
5 changes: 0 additions & 5 deletions test/util/assertAriaLabelledby.js
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 0 additions & 6 deletions test/util/assertAttributeDNE.js
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 0 additions & 5 deletions test/util/assertAttributeValues.js
Original file line number Diff line number Diff line change
@@ -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),
5 changes: 0 additions & 5 deletions test/util/assertRovingTabindex.js
Original file line number Diff line number Diff line change
@@ -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++) {