diff --git a/examples/combobox/combobox-autocomplete-both.html b/examples/combobox/combobox-autocomplete-both.html index 0a4b273c27..78a33482fe 100644 --- a/examples/combobox/combobox-autocomplete-both.html +++ b/examples/combobox/combobox-autocomplete-both.html @@ -12,10 +12,8 @@ <script type="text/javascript" src="../js/app.js"></script> <!-- js and css for this example. --> -<link rel="stylesheet" href="css/combobox.css"> -<script type="text/javascript" src="js/combobox-list.js"></script> -<script type="text/javascript" src="js/listbox.js"></script> -<script type="text/javascript" src="js/listboxOption.js"></script> +<link rel="stylesheet" href="css/combobox-autocomplete.css"> +<script type="text/javascript" src="js/combobox-autocomplete.js"></script> </head> <body> <nav aria-label="Related Links" class="feedback"> @@ -54,7 +52,13 @@ <h2 id="ex_label">Example</h2> <div class="group"> <input id="cb1-input" class="cb_edit" type="text" role="combobox" aria-autocomplete="both" aria-expanded="false" aria-controls="lb1"> - <button type="button" id="cb1-button" aria-label="Open" tabindex="-1">▼</button> + <button type="button" id="cb1-button" aria-label="Open" tabindex="-1"> + <span class="arrow"> + <svg width="18" height="16" aria-hidden="true" focusable="false"> + <polygon points="3,5 15,5 9,13"></polygon> + </svg> + </span> + </button> </div> <ul id="lb1" role="listbox" aria-label="States"> <li id="lb1-al" role="option">Alabama</li> @@ -145,6 +149,12 @@ <h3 id="kbd_label_textbox">Textbox</h3> </ul> </td> </tr> + <tr data-test-id="textbox-key-alt-down-arrow"> + <th><kbd>Alt + Down Arrow</kbd></th> + <td> + Opens the listbox without moving focus or changing selection. + </td> + </tr> <tr data-test-id="textbox-key-up-arrow"> <th><kbd>Up Arrow</kbd></th> <td> @@ -168,8 +178,8 @@ <h3 id="kbd_label_textbox">Textbox</h3> <th><kbd>Escape</kbd></th> <td> <ul> - <li>Clears the textbox.</li> <li>If the listbox is displayed, closes it.</li> + <li>If the listbox is not displayed, clears the textbox.</li> </ul> </td> </tr> @@ -213,7 +223,6 @@ <h3 id="kbd_label_listbox">Listbox Popup</h3> <th><kbd>Escape</kbd></th> <td> <ul> - <li>Clears the textbox.</li> <li>Closes the listbox.</li> <li>Sets visual focus on the textbox.</li> </ul> @@ -261,6 +270,7 @@ <h3 id="kbd_label_listbox">Listbox Popup</h3> <ul> <li>Moves visual focus to the textbox.</li> <li>Types the character in the textbox.</li> + <li>Options in the listbox are filtered based on characters in the textbox.</li> </ul> </td> </tr> @@ -326,6 +336,19 @@ <h3 id="rps_label_textbox">Textbox</h3> <td><code>input[type="text"]</code></td> <td>Indicates that the popup element <strong>is</strong> displayed.</td> </tr> + <tr data-test-id="combobox-id"> + <td></td> + <th scope="row"> + <code>id="string"</code> + </th> + <td><code>input[type="text"]</code></td> + <td> + <ul> + <li>Referenced by <code>for</code> attribute of <code>label</code> element to provide an accessible name.</li> + <li>Recommended naming method for HTML input elements because clicking label focuses input.</li> + </ul> + </td> + </tr> <tr data-test-id="combobox-aria-activedescendant"> <td></td> <th scope="row"> @@ -410,23 +433,11 @@ <h2>Javascript and CSS Source Code</h2> <ul> <li> CSS: - <a href="css/combobox.css" type="text/css">combobox.css</a> - </li> - <li> - CSS: - <a href="css/listbox.css" type="text/css">listbox.css</a> - </li> - <li> - Javascript: - <a href="js/combobox-list.js" type="text/javascript">combobox-list.js</a> - </li> - <li> - Javascript: - <a href="js/listbox.js" type="text/javascript">listbox.js</a> + <a href="css/combobox-autocomplete.css" type="text/css">combobox-autocomplete.css</a> </li> <li> Javascript: - <a href="js/listboxOption.js" type="text/javascript">listboxOption.js</a> + <a href="js/combobox-autocomplete.js" type="text/javascript">combobox-autocomplete.js</a> </li> </ul> </section> diff --git a/examples/combobox/combobox-autocomplete-list.html b/examples/combobox/combobox-autocomplete-list.html index ed807f249a..fb9b7211c9 100644 --- a/examples/combobox/combobox-autocomplete-list.html +++ b/examples/combobox/combobox-autocomplete-list.html @@ -12,10 +12,9 @@ <script src="../js/app.js"></script> <!-- js and css for this example. --> -<link href="css/combobox.css" rel="stylesheet"> -<script src="js/combobox-list.js" type="text/javascript"></script> -<script src="js/listbox.js" type="text/javascript"></script> -<script src="js/listboxOption.js" type="text/javascript"></script> +<link href="css/combobox-autocomplete.css" rel="stylesheet"> +<script src="js/combobox-autocomplete.js" type="text/javascript"></script> + </head> <body> <nav aria-label="Related Links" class="feedback"> @@ -54,7 +53,13 @@ <h2 id="ex_label">Example</h2> <div class="group"> <input id="cb1-input" class="cb_edit" type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="cb1-listbox"> - <button id="cb1-button" tabindex="-1" aria-label="Open">▼</button> + <button id="cb1-button" tabindex="-1" aria-label="Open"> + <span class="arrow"> + <svg width="18" height="16" aria-hidden="true" focusable="false"> + <polygon points="3,5 15,5 9,13"></polygon> + </svg> + </span> + </button> </div> <ul id="cb1-listbox" role="listbox" aria-label="States"> <li id="lb1-al" role="option">Alabama</li> @@ -145,6 +150,12 @@ <h3 id="kbd_label_textbox">Textbox</h3> </ul> </td> </tr> + <tr data-test-id="textbox-key-alt-down-arrow"> + <th><kbd>Alt + Down Arrow</kbd></th> + <td> + Opens the listbox without moving focus or changing selection. + </td> + </tr> <tr data-test-id="textbox-key-up-arrow"> <th><kbd>Up Arrow</kbd></th> <td> @@ -163,9 +174,9 @@ <h3 id="kbd_label_textbox">Textbox</h3> <th><kbd>Escape</kbd></th> <td> <ul> - <li>Clears the textbox.</li> <li>If the listbox is displayed, closes it.</li> - </ul> + <li>If the listbox is not displayed, clears the textbox.</li> + </ul> </td> </tr> <tr data-test-id="standard-single-line-editing-keys"> @@ -208,7 +219,6 @@ <h3 id="kbd_label_listbox">Listbox Popup</h3> <th><kbd>Escape</kbd></th> <td> <ul> - <li>Clears the textbox.</li> <li>Closes the listbox.</li> <li>Sets visual focus on the textbox.</li> </ul> @@ -256,6 +266,7 @@ <h3 id="kbd_label_listbox">Listbox Popup</h3> <ul> <li>Moves visual focus to the textbox.</li> <li>Types the character in the textbox.</li> + <li>Options in the listbox are filtered based on characters in the textbox.</li> </ul> </td> </tr> @@ -321,6 +332,19 @@ <h3 id="rps_label_textbox">Textbox</h3> <td><code>input[type="text"]</code></td> <td>Indicates that the popup element <strong>is</strong> displayed.</td> </tr> + <tr data-test-id="combobox-id"> + <td></td> + <th scope="row"> + <code>id="string"</code> + </th> + <td><code>input[type="text"]</code></td> + <td> + <ul> + <li>Referenced by <code>for</code> attribute of <code>label</code> element to provide an accessible name.</li> + <li>Recommended naming method for HTML input elements because clicking label focuses input.</li> + </ul> + </td> + </tr> <tr data-test-id="combobox-aria-activedescendant"> <td></td> <th scope="row"> @@ -405,23 +429,11 @@ <h2>Javascript and CSS Source Code</h2> <ul> <li> CSS: - <a href="css/combobox.css" type="text/css">combobox.css</a> - </li> - <li> - CSS: - <a href="css/listbox.css" type="text/css">listbox.css</a> - </li> - <li> - Javascript: - <a href="js/combobox-list.js" type="text/javascript">combobox-list.js</a> - </li> - <li> - Javascript: - <a href="js/listbox.js" type="text/javascript">listbox.js</a> + <a href="css/combobox-autocomplete.css" type="text/css">combobox-autocomplete.css</a> </li> <li> Javascript: - <a href="js/listboxOption.js" type="text/javascript">listboxOption.js</a> + <a href="js/combobox-autocomplete.js" type="text/javascript">combobox-autocomplete.js</a> </li> </ul> </section> diff --git a/examples/combobox/combobox-autocomplete-none.html b/examples/combobox/combobox-autocomplete-none.html index 05ad2d7d92..e2262a6b05 100644 --- a/examples/combobox/combobox-autocomplete-none.html +++ b/examples/combobox/combobox-autocomplete-none.html @@ -12,10 +12,8 @@ <script src="../js/app.js"></script> <!-- js and css for this example. --> -<link href="css/combobox.css" rel="stylesheet"> -<script src="js/combobox-list.js" type="text/javascript"></script> -<script src="js/listbox.js" type="text/javascript"></script> -<script src="js/listboxOption.js" type="text/javascript"></script> +<link href="css/combobox-autocomplete.css" rel="stylesheet"> +<script src="js/combobox-autocomplete.js" type="text/javascript"></script> </head> <body> <nav aria-label="Related Links" class="feedback"> @@ -34,7 +32,7 @@ <h1>Editable Combobox without Autocomplete Example</h1> The design pattern describes four types of autocomplete behavior. This example illustrates the autocomplete behavior known as <q>no autocomplete</q>. The terms that appear in the listbox popup are not related to the string that is present in the textbox. - In this implementation, The listbox popup is not automatically triggered; it is displayed only when the user opens it. + In this implementation, the listbox popup is not automatically triggered when the textbox receives focus; the list is opened when the user types a character into the textbox or through an explicit open command. </p> <p>Similar examples include: </p> <ul> @@ -55,7 +53,13 @@ <h2 id="ex_label">Example</h2> aria-autocomplete="none" aria-expanded="false" aria-controls="cb1-listbox"> - <button type="button" id="cb1-button" tabindex="-1" aria-label="Open">▼</button> + <button type="button" id="cb1-button" tabindex="-1" aria-label="Open"> + <span class="arrow"> + <svg width="18" height="16" aria-hidden="true" focusable="false"> + <polygon points="3,5 15,5 9,13"></polygon> + </svg> + </span> + </button> </div> <ul id="cb1-listbox" role="listbox" aria-label="Previous Searches"> <li id="lb1-01" role="option">weather</li> @@ -100,6 +104,12 @@ <h3 id="kbd_label_textbox">Textbox</h3> </ul> </td> </tr> + <tr data-test-id="textbox-key-alt-down-arrow"> + <th><kbd>Alt + Down Arrow</kbd></th> + <td> + Opens the listbox without moving focus or changing selection. + </td> + </tr> <tr data-test-id="textbox-key-up-arrow"> <th><kbd>Up Arrow</kbd></th> <td> @@ -158,10 +168,9 @@ <h3 id="kbd_label_listbox">Listbox Popup</h3> <th><kbd>Escape</kbd></th> <td> <ul> - <li>Clears the textbox.</li> <li>Closes the listbox.</li> <li>Sets visual focus on the textbox.</li> - </ul> + </ul> </td> </tr> <tr data-test-id="listbox-key-down-arrow"> @@ -206,6 +215,7 @@ <h3 id="kbd_label_listbox">Listbox Popup</h3> <ul> <li>Moves visual focus to the textbox.</li> <li>Types the character in the textbox.</li> + <li>Options in the listbox are <em>not</em> filtered based on the characters in the textbox.</li> </ul> </td> </tr> @@ -271,6 +281,19 @@ <h3 id="rps_label_textbox">Textbox</h3> <td><code>input[type="text"]</code></td> <td>Indicates that the popup element <strong>is</strong> displayed.</td> </tr> + <tr data-test-id="combobox-id"> + <td></td> + <th scope="row"> + <code>id="string"</code> + </th> + <td><code>input[type="text"]</code></td> + <td> + <ul> + <li>Referenced by <code>for</code> attribute of <code>label</code> element to provide an accessible name.</li> + <li>Recommended naming method for HTML input elements because clicking label focuses input.</li> + </ul> + </td> + </tr> <tr data-test-id="combobox-aria-activedescendant"> <td></td> <th scope="row"> @@ -355,23 +378,11 @@ <h2>Javascript and CSS Source Code</h2> <ul> <li> CSS: - <a href="css/combobox.css" type="text/css">combobox.css</a> - </li> - <li> - CSS: - <a href="css/listbox.css" type="text/css">listbox.css</a> - </li> - <li> - Javascript: - <a href="js/combobox-list.js" type="text/javascript">combobox-list.js</a> - </li> - <li> - Javascript: - <a href="js/listbox.js" type="text/javascript">listbox.js</a> + <a href="css/combobox-autocomplete.css" type="text/css">combobox-autocomplete.css</a> </li> <li> Javascript: - <a href="js/listboxOption.js" type="text/javascript">listboxOption.js</a> + <a href="js/combobox-autocomplete.js" type="text/javascript">combobox-autocomplete.js</a> </li> </ul> </section> diff --git a/examples/combobox/css/combobox.css b/examples/combobox/css/combobox-autocomplete.css similarity index 69% rename from examples/combobox/css/combobox.css rename to examples/combobox/css/combobox-autocomplete.css index d437c332ab..3ea6b53c0a 100644 --- a/examples/combobox/css/combobox.css +++ b/examples/combobox/css/combobox-autocomplete.css @@ -6,8 +6,8 @@ display: inline-flex; } -.combobox .group input, -.combobox .group button { +.combobox input, +.combobox button { background-color: white; color: black; box-sizing: border-box; @@ -16,9 +16,10 @@ margin: 0; vertical-align: bottom; border: 1px solid gray; + position: relative; } -.combobox .group input { +.combobox input { width: 10.75rem; border-right: none; outline: none; @@ -26,11 +27,14 @@ padding: 0.1em 0.3em; } -.combobox .group button { +.combobox button { width: 1.25rem; border-left: none; outline: none; - font-size: 70%; +} + +.combobox button.open svg { + transform: rotate(180deg) translate(0, -1px); } .combobox .group.focus { @@ -38,6 +42,22 @@ outline-offset: 1px; } +.combobox .group.focus input, +.combobox .group.focus button { + background-color: #DEF; +} + +.combobox polygon { + fill: gray; + stroke: gray; +} + +.combobox button.open polygon, +.combobox .group.focus polygon { + fill: black; + stroke: black; +} + ul[role="listbox"] { margin: 0; padding: 0; @@ -50,7 +70,7 @@ ul[role="listbox"] { box-sizing: border-box; border: 1px gray solid; border-top: none; - max-height: 12em; + max-height: 11.4em; width: 12rem; overflow: scroll; overflow-x: hidden; @@ -58,18 +78,21 @@ ul[role="listbox"] { } ul[role="listbox"] li[role="option"] { - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; display: block; + padding-left: 0.3em; + padding-top: 0.2em; + padding-bottom: 0.2em; margin: 0; - padding: 0.1em 0.3em; } /* focus and hover styling */ [role="listbox"].focus [role="option"][aria-selected="true"] { background-color: #DEF; - border-color: #8CCBF2; + padding-top: 0; + padding-bottom: 0; + border-top: 0.2em solid #8CCBF2; + border-bottom: 0.2em solid #8CCBF2; } @media (forced-colors: active), (-ms-high-contrast: active) { diff --git a/examples/combobox/js/combobox-autocomplete.js b/examples/combobox/js/combobox-autocomplete.js new file mode 100644 index 0000000000..ebf61dd2f4 --- /dev/null +++ b/examples/combobox/js/combobox-autocomplete.js @@ -0,0 +1,555 @@ +/* +* This content is licensed according to the W3C Software License at +* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document +*/ +var ComboboxAutocomplete = function (comboboxNode, buttonNode, listboxNode) { + + this.comboboxNode = comboboxNode; + this.buttonNode = buttonNode; + this.listboxNode = listboxNode; + + this.comboboxHasVisualFocus = false; + this.listboxHasVisualFocus = false; + + this.hasHover = false; + + this.isNone = false; + this.isList = false; + this.isBoth = false; + + this.allOptions = []; + + this.option = null; + this.firstOption = null; + this.lastOption = null; + + this.filteredOptions = []; + this.filter = ''; +}; + +ComboboxAutocomplete.prototype.init = function () { + + var autocomplete = this.comboboxNode.getAttribute('aria-autocomplete'); + + if (typeof autocomplete === 'string') { + autocomplete = autocomplete.toLowerCase(); + this.isNone = autocomplete === 'none'; + this.isList = autocomplete === 'list'; + this.isBoth = autocomplete === 'both'; + } + else { + // default value of autocomplete + this.isNone = true; + } + + this.comboboxNode.addEventListener('keydown', this.handleComboboxKeyDown.bind(this)); + this.comboboxNode.addEventListener('keyup', this.handleComboboxKeyUp.bind(this)); + this.comboboxNode.addEventListener('click', this.handleComboboxClick.bind(this)); + this.comboboxNode.addEventListener('focus', this.handleComboboxFocus.bind(this)); + this.comboboxNode.addEventListener('blur', this.handleComboboxBlur.bind(this)); + + // initialize pop up menu + + this.listboxNode.addEventListener('mouseover', this.handleListboxMouseover.bind(this)); + this.listboxNode.addEventListener('mouseout', this.handleListboxMouseout.bind(this)); + + // Traverse the element children of domNode: configure each with + // option role behavior and store reference in.options array. + var nodes = this.listboxNode.getElementsByTagName('LI'); + + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + this.allOptions.push(node); + + node.addEventListener('click', this.handleOptionClick.bind(this)); + node.addEventListener('mouseover', this.handleOptionMouseover.bind(this)); + node.addEventListener('mouseout', this.handleOptionMouseout.bind(this)); + + } + + this.filterOptions(); + + // Open Button + + var button = this.comboboxNode.nextElementSibling; + + if (button && button.tagName === 'BUTTON') { + button.addEventListener('click', this.handleButtonClick.bind(this)); + } + +}; + +ComboboxAutocomplete.prototype.getLowercaseContent = function (node) { + return node.textContent.toLowerCase(); +} + +ComboboxAutocomplete.prototype.setActiveDescendant = function (option) { + if (option && this.listboxHasVisualFocus) { + this.comboboxNode.setAttribute('aria-activedescendant', option.id); + } + else { + this.comboboxNode.setAttribute('aria-activedescendant', ''); + } +}; + +ComboboxAutocomplete.prototype.setValue = function (value) { + this.filter = value; + this.comboboxNode.value = this.filter; + this.comboboxNode.setSelectionRange(this.filter.length,this.filter.length); + this.filterOptions(); +}; + +ComboboxAutocomplete.prototype.setOption = function (option, flag) { + if (typeof flag !== 'boolean') { + flag = false; + } + + if (option) { + this.option = option; + this.setCurrentOptionStyle(this.option); + this.setActiveDescendant(this.option); + + if (this.isBoth) { + this.comboboxNode.value = this.option.textContent; + if (flag) { + this.comboboxNode.setSelectionRange(this.option.textContent.length,this.option.textContent.length); + } + else { + this.comboboxNode.setSelectionRange(this.filter.length,this.option.textContent.length); + } + } + } +}; + +ComboboxAutocomplete.prototype.setVisualFocusCombobox = function () { + this.listboxNode.classList.remove('focus'); + this.comboboxNode.parentNode.classList.add('focus'); // set the focus class to the parent for easier styling + this.comboboxHasVisualFocus = true; + this.listboxHasVisualFocus = false; + this.setActiveDescendant(false); +}; + +ComboboxAutocomplete.prototype.setVisualFocusListbox = function () { + this.comboboxNode.parentNode.classList.remove('focus'); + this.comboboxHasVisualFocus = false; + this.listboxHasVisualFocus = true; + this.listboxNode.classList.add('focus'); + this.setActiveDescendant(this.option); +}; + +ComboboxAutocomplete.prototype.removeVisualFocusAll = function () { + this.comboboxNode.parentNode.classList.remove('focus'); + this.comboboxHasVisualFocus = false; + this.listboxHasVisualFocus = false; + this.listboxNode.classList.remove('focus'); + this.option = null; + this.setActiveDescendant(false); +}; + +// ComboboxAutocomplete Events + +ComboboxAutocomplete.prototype.filterOptions = function () { + + // do not filter any options if autocomplete is none + if (this.isNone) { + this.filter = ''; + } + + var option = null; + var currentOption = this.option; + var filter = this.filter.toLowerCase(); + + this.filteredOptions = []; + this.listboxNode.innerHTML = ''; + + for (var i = 0; i < this.allOptions.length; i++) { + option = this.allOptions[i]; + if (filter.length === 0 || this.getLowercaseContent(option).indexOf(filter) === 0) { + this.filteredOptions.push(option); + this.listboxNode.appendChild(option); + } + } + + // Use populated options array to initialize firstOption and lastOption. + var numItems = this.filteredOptions.length; + if (numItems > 0) { + this.firstOption = this.filteredOptions[0]; + this.lastOption = this.filteredOptions[numItems - 1]; + + if (currentOption && this.filteredOptions.indexOf(currentOption) >= 0) { + option = currentOption; + } + else { + option = this.firstOption; + } + } + else { + this.firstOption = null; + option = null; + this.lastOption = null; + } + + return option; +}; + +ComboboxAutocomplete.prototype.setCurrentOptionStyle = function (option) { + + for (var i = 0; i < this.filteredOptions.length; i++) { + var opt = this.filteredOptions[i]; + if (opt === option) { + opt.setAttribute('aria-selected', 'true'); + if ((this.listboxNode.scrollTop + this.listboxNode.offsetHeight) < (opt.offsetTop + opt.offsetHeight)) { + this.listboxNode.scrollTop = opt.offsetTop + opt.offsetHeight - this.listboxNode.offsetHeight; + } + else if (this.listboxNode.scrollTop > (opt.offsetTop + 2)) { + this.listboxNode.scrollTop = opt.offsetTop; + } + } + else { + opt.removeAttribute('aria-selected'); + } + } +}; + +ComboboxAutocomplete.prototype.getPreviousOption = function (currentOption) { + if (currentOption !== this.firstOption) { + var index = this.filteredOptions.indexOf(currentOption); + return this.filteredOptions[index - 1]; + } + return this.lastOption; +}; + +ComboboxAutocomplete.prototype.getNextOption = function (currentOption) { + if (currentOption !== this.lastOption) { + var index = this.filteredOptions.indexOf(currentOption); + return this.filteredOptions[index + 1]; + } + return this.firstOption; +}; + +/* MENU DISPLAY METHODS */ + +ComboboxAutocomplete.prototype.doesOptionHaveFocus = function () { + return this.combobocNode.getAttribute('aria-activedescendant') !== ''; +}; + +ComboboxAutocomplete.prototype.isOpen = function () { + return this.listboxNode.style.display === 'block'; +}; + +ComboboxAutocomplete.prototype.isClosed = function () { + return this.listboxNode.style.display !== 'block'; +}; + +ComboboxAutocomplete.prototype.hasOptions = function () { + return this.filteredOptions.length; +}; + +ComboboxAutocomplete.prototype.open = function () { + this.listboxNode.style.display = 'block'; + this.comboboxNode.setAttribute('aria-expanded', 'true'); + this.buttonNode.classList.add('open'); +}; + +ComboboxAutocomplete.prototype.close = function (force) { + if (typeof force !== 'boolean') { + force = false; + } + + if (force || (!this.comboboxHasVisualFocus && !this.listboxHasVisualFocus && !this.hasHover)) { + this.setCurrentOptionStyle(false); + this.listboxNode.style.display = 'none'; + this.comboboxNode.setAttribute('aria-expanded', 'false'); + this.buttonNode.classList.remove('open'); + this.setActiveDescendant(false); + } +}; + +/* combobox Events */ + +ComboboxAutocomplete.prototype.handleComboboxKeyDown = function (event) { + var flag = false, + char = event.key, + altKey = event.altKey; + + if (event.ctrlKey || event.shiftKey) { + return; + } + + switch (event.key) { + + case "Enter": + if (this.listboxHasVisualFocus) { + this.setValue(this.option.textContent); + } + this.close(true); + this.setVisualFocusCombobox(); + flag = true; + break; + + case "Down": + case "ArrowDown": + if (this.filteredOptions.length > 0) { + if (altKey) { + this.open(); + } + else { + this.open(); + if (this.listboxHasVisualFocus || (this.isBoth && this.filteredOptions.length > 1)) { + this.setOption(this.getNextOption(this.option), true); + this.setVisualFocusListbox(); + } + else { + this.setOption(this.firstOption, true); + this.setVisualFocusListbox(); + } + } + } + flag = true; + break; + + case "Up": + case "ArrowUp": + + if (this.hasOptions()) { + if (this.listboxHasVisualFocus) { + this.setOption(this.getPreviousOption(this.option), true); + } + else { + this.open(); + if (!altKey) { + this.setOption(this.lastOption, true); + this.setVisualFocusListbox(); + } + } + } + flag = true; + break; + + case "Esc": + case "Escape": + if (this.isOpen()) { + this.close(true); + this.filter = this.comboboxNode.value; + this.filterOptions(); + this.setVisualFocusCombobox(); + } + else { + this.setValue(''); + this.comboboxNode.value = ''; + } + this.option = null; + flag = true; + break; + + case "Tab": + this.close(true); + if (this.listboxHasVisualFocus) { + if (this.option) { + this.setValue(this.option.textContent); + } + } + break; + + case "Home": + this.comboboxNode.setSelectionRange(0,0); + flag = true; + break; + + case "End": + var length = this.comboboxNode.value.length; + this.comboboxNode.setSelectionRange(length,length); + flag = true; + break; + + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + +}; + +ComboboxAutocomplete.prototype.isPrintableCharacter = function (str) { + return str.length === 1 && str.match(/\S/); +} + +ComboboxAutocomplete.prototype.handleComboboxKeyUp = function (event) { + var flag = false, + option = null, + char = event.key; + + if (this.isPrintableCharacter(char)) { + this.filter += char; + } + + // this is for the case when a selection in the textbox has been deleted + if (this.comboboxNode.value.length < this.filter.length) { + this.filter = this.comboboxNode.value; + this.option = null; + } + + if (event.key === "Escape" || event.key === "Esc") { + return; + } + + switch (event.key) { + + case "Backspace": + this.setVisualFocusCombobox(); + this.setCurrentOptionStyle(false); + this.option = null; + flag = true; + break; + + case "Left": + case "ArrowLeft": + case "Right": + case "ArrowRight": + case "Home": + case "End": + if (this.isBoth) { + this.filter = this.comboboxNode.value; + } + else { + this.option = null; + this.setCurrentOptionStyle(false); + } + this.setVisualFocusCombobox(); + flag = true; + break; + + default: + if (this.isPrintableCharacter(char)) { + this.setVisualFocusCombobox(); + this.setCurrentOptionStyle(false); + flag = true; + + if (this.isList || this.isBoth) { + option = this.filterOptions(); + if (option) { + if (this.isClosed() && this.comboboxNode.value.length) { + this.open(); + } + + if (this.getLowercaseContent(option).indexOf(this.comboboxNode.value.toLowerCase()) === 0) { + this.option = option; + if (this.isBoth || this.listboxHasVisualFocus) { + this.setCurrentOptionStyle(option); + if (this.isBoth) { + this.setOption(option); + } + } + } + else { + this.option = null; + this.setCurrentOptionStyle(false); + } + } + else { + this.close(); + this.option = null; + this.setActiveDescendant(false); + } + } + else if (this.comboboxNode.value.length) { + this.open(); + } + } + + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + +}; + +ComboboxAutocomplete.prototype.handleComboboxClick = function (event) { + if (this.isOpen()) { + this.close(true); + } + else { + this.open(); + } +}; + +ComboboxAutocomplete.prototype.handleComboboxFocus = function (event) { + this.filter = this.comboboxNode.value; + this.filterOptions(); + this.setVisualFocusCombobox(); + this.option = null; + this.setCurrentOptionStyle(null); +}; + +ComboboxAutocomplete.prototype.handleComboboxBlur = function (event) { + this.comboboxHasVisualFocus = false; + this.setCurrentOptionStyle(null); + this.removeVisualFocusAll(); + setTimeout(this.close.bind(this, false), 300); +}; + +ComboboxAutocomplete.prototype.handleButtonClick = function (event) { + if (this.isOpen()) { + this.close(true); + } + else { + this.open(); + } + this.comboboxNode.focus(); + this.setVisualFocusCombobox(); +}; + + +/* Listbox Events */ + +ComboboxAutocomplete.prototype.handleListboxMouseover = function (event) { + this.hasHover = true; +}; + +ComboboxAutocomplete.prototype.handleListboxMouseout = function (event) { + this.hasHover = false; + setTimeout(this.close.bind(this, false), 300); +}; + +// Listbox Option Events + +ComboboxAutocomplete.prototype.handleOptionClick = function (event) { + this.comboboxNode.value = event.target.textContent; + this.close(true); +}; + +ComboboxAutocomplete.prototype.handleOptionMouseover = function (event) { + this.hasHover = true; + this.open(); + +}; + +ComboboxAutocomplete.prototype.handleOptionMouseout = function (event) { + this.hasHover = false; + setTimeout(this.close.bind(this, false), 300); +}; + + +// Initialize comboboxes + +window.addEventListener('load', function () { + + var comboboxes = document.querySelectorAll('.combobox-list'); + + for (var i = 0; i < comboboxes.length; i++) { + var combobox = comboboxes[i]; + var comboboxNode = combobox.querySelector('input'); + var buttonNode = combobox.querySelector('button'); + var listboxNode = combobox.querySelector('[role="listbox"]'); + var cba = new ComboboxAutocomplete(comboboxNode, buttonNode, listboxNode); + cba.init(); + } + +}); diff --git a/examples/combobox/js/combobox-list.js b/examples/combobox/js/combobox-list.js deleted file mode 100644 index d5bd49a474..0000000000 --- a/examples/combobox/js/combobox-list.js +++ /dev/null @@ -1,403 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -*/ -var ComboboxList = function (domNode) { - - this.domNode = domNode; - this.listbox = false; - this.option = false; - - this.hasFocus = false; - this.hasHover = false; - this.filter = ''; - this.isNone = false; - this.isList = false; - this.isBoth = false; - - this.keyCode = Object.freeze({ - 'BACKSPACE': 8, - 'TAB': 9, - 'RETURN': 13, - 'ESC': 27, - 'SPACE': 32, - 'PAGEUP': 33, - 'PAGEDOWN': 34, - 'END': 35, - 'HOME': 36, - 'LEFT': 37, - 'UP': 38, - 'RIGHT': 39, - 'DOWN': 40 - }); -}; - -ComboboxList.prototype.init = function () { - - this.domNode.setAttribute('aria-haspopup', 'true'); - - var autocomplete = this.domNode.getAttribute('aria-autocomplete'); - - if (typeof autocomplete === 'string') { - autocomplete = autocomplete.toLowerCase(); - this.isNone = autocomplete === 'none'; - this.isList = autocomplete === 'list'; - this.isBoth = autocomplete === 'both'; - } - else { - // default value of autocomplete - this.isNone = true; - } - - this.domNode.addEventListener('keydown', this.handleKeydown.bind(this)); - this.domNode.addEventListener('keyup', this.handleKeyup.bind(this)); - this.domNode.addEventListener('click', this.handleClick.bind(this)); - this.domNode.addEventListener('focus', this.handleFocus.bind(this)); - this.domNode.addEventListener('blur', this.handleBlur.bind(this)); - - // initialize pop up menus - - var listbox = document.getElementById(this.domNode.getAttribute('aria-controls')); - - if (listbox) { - this.listbox = new Listbox(listbox, this); - this.listbox.init(); - } - - // Open Button - - var button = this.domNode.nextElementSibling; - - if (button && button.tagName === 'BUTTON') { - button.addEventListener('click', this.handleButtonClick.bind(this)); - } - -}; - -ComboboxList.prototype.setActiveDescendant = function (option) { - if (option && this.listbox.hasFocus) { - this.domNode.setAttribute('aria-activedescendant', option.domNode.id); - } - else { - this.domNode.setAttribute('aria-activedescendant', ''); - } -}; - -ComboboxList.prototype.setValue = function (value) { - this.filter = value; - this.domNode.value = this.filter; - this.domNode.setSelectionRange(this.filter.length,this.filter.length); - if (this.isList || this.isBoth) { - this.listbox.filterOptions(this.filter, this.option); - } -}; - -ComboboxList.prototype.setOption = function (option, flag) { - if (typeof flag !== 'boolean') { - flag = false; - } - - if (option) { - this.option = option; - this.listbox.setCurrentOptionStyle(this.option); - this.setActiveDescendant(this.option); - - if (this.isBoth) { - this.domNode.value = this.option.textContent; - if (flag) { - this.domNode.setSelectionRange(this.option.textContent.length,this.option.textContent.length); - } - else { - this.domNode.setSelectionRange(this.filter.length,this.option.textContent.length); - } - } - } -}; - -ComboboxList.prototype.setVisualFocusTextbox = function () { - this.listbox.domNode.classList.remove('focus'); - this.listbox.hasFocus = false; - this.domNode.parentNode.classList.add('focus'); // set the focus class to the parent for easier styling - this.hasFocus = true; - this.setActiveDescendant(false); -}; - -ComboboxList.prototype.setVisualFocusListbox = function () { - this.domNode.parentNode.classList.remove('focus'); - this.hasFocus = false; - this.listbox.domNode.classList.add('focus'); - this.listbox.hasFocus = true; - this.setActiveDescendant(this.option); -}; - -ComboboxList.prototype.removeVisualFocusAll = function () { - this.domNode.parentNode.classList.remove('focus'); - this.hasFocus = false; - this.listbox.domNode.classList.remove('focus'); - this.listbox.hasFocus = true; - this.option = false; - this.setActiveDescendant(false); -}; - -/* Event Handlers */ - -ComboboxList.prototype.handleKeydown = function (event) { - var tgt = event.currentTarget, - flag = false, - char = event.key, - shiftKey = event.shiftKey, - ctrlKey = event.ctrlKey, - altKey = event.altKey; - - switch (event.keyCode) { - - case this.keyCode.RETURN: - if ((this.listbox.hasFocus || this.isBoth) && this.option) { - this.setValue(this.option.textContent); - } - this.listbox.close(true); - flag = true; - break; - - case this.keyCode.DOWN: - - if (this.listbox.hasOptions()) { - if (this.listbox.hasFocus || (this.isBoth && this.option)) { - this.setOption(this.listbox.getNextItem(this.option), true); - } - else { - this.listbox.open(); - if (!altKey) { - this.setOption(this.listbox.getFirstItem(), true); - } - } - this.setVisualFocusListbox(); - } - flag = true; - break; - - case this.keyCode.UP: - - if (this.listbox.hasOptions()) { - if (this.listbox.hasFocus || (this.isBoth && this.option)) { - this.setOption(this.listbox.getPreviousItem(this.option), true); - } - else { - this.listbox.open(); - if (!altKey) { - this.setOption(this.listbox.getLastItem(), true); - } - } - this.setVisualFocusListbox(); - } - flag = true; - break; - - case this.keyCode.ESC: - this.listbox.close(true); - this.setVisualFocusTextbox(); - this.setValue(''); - this.option = false; - flag = true; - break; - - case this.keyCode.TAB: - this.listbox.close(true); - if (this.listbox.hasFocus) { - if (this.option) { - this.setValue(this.option.textContent); - } - } - break; - - default: - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - -}; - -ComboboxList.prototype.handleKeyup = function (event) { - var flag = false, - option = false, - char = event.key; - - function isPrintableCharacter (str) { - return str.length === 1 && str.match(/\S/); - } - - if (isPrintableCharacter(char)) { - this.filter += char; - } - - // this is for the case when a selection in the textbox has been deleted - if (this.domNode.value.length < this.filter.length) { - this.filter = this.domNode.value; - this.option = false; - } - - if (event.keyCode === this.keyCode.ESC) { - return; - } - - switch (event.keyCode) { - - case this.keyCode.BACKSPACE: - this.setValue(this.domNode.value); - this.setVisualFocusTextbox(); - this.listbox.setCurrentOptionStyle(false); - this.option = false; - flag = true; - break; - - case this.keyCode.LEFT: - case this.keyCode.RIGHT: - case this.keyCode.HOME: - case this.keyCode.END: - if (this.isBoth) { - this.filter = this.domNode.value; - } - else { - this.option = false; - this.listbox.setCurrentOptionStyle(false); - } - - this.setVisualFocusTextbox(); - flag = true; - break; - - default: - if (isPrintableCharacter(char)) { - this.setVisualFocusTextbox(); - this.listbox.setCurrentOptionStyle(false); - flag = true; - - if (this.isList || this.isBoth) { - option = this.listbox.filterOptions(this.filter, this.option); - if (option) { - if (this.listbox.isClosed() && this.domNode.value.length) { - this.listbox.open(); - } - - if (option.textComparison.indexOf(this.domNode.value.toLowerCase()) === 0) { - this.option = option; - if (this.isBoth || this.listbox.hasFocus) { - this.listbox.setCurrentOptionStyle(option); - if (this.isBoth && isPrintableCharacter(char)) { - this.setOption(option); - } - } - } - else { - this.option = false; - this.listbox.setCurrentOptionStyle(false); - } - } - else { - this.listbox.close(); - this.option = false; - this.setActiveDescendant(false); - } - } - else if (this.domNode.value.length) { - this.listbox.open(); - } - } - - break; - } - - // if (event.keyCode !== this.keyCode.RETURN) { - - // if (this.isList || this.isBoth) { - // option = this.listbox.filterOptions(this.filter, this.option); - // if (option) { - // if (this.listbox.isClosed() && this.domNode.value.length) { - // this.listbox.open(); - // } - - // if (option.textComparison.indexOf(this.domNode.value.toLowerCase()) === 0) { - // this.option = option; - // if (this.isBoth || this.listbox.hasFocus) { - // this.listbox.setCurrentOptionStyle(option); - // if (this.isBoth && isPrintableCharacter(char)) { - // this.setOption(option); - // } - // } - // } - // else { - // this.option = false; - // this.listbox.setCurrentOptionStyle(false); - // } - // } - // else { - // this.listbox.close(); - // this.option = false; - // this.setActiveDescendant(false); - // } - // } - // else if (this.domNode.value.length) { - // this.listbox.open(); - // } - - // } - - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - -}; - -ComboboxList.prototype.handleClick = function (event) { - if (this.listbox.isOpen()) { - this.listbox.close(true); - } - else { - this.listbox.open(); - } -}; - -ComboboxList.prototype.handleFocus = function (event) { - this.setVisualFocusTextbox(); - this.option = false; - this.listbox.setCurrentOptionStyle(null); -}; - -ComboboxList.prototype.handleBlur = function (event) { - this.listbox.hasFocus = false; - this.listbox.setCurrentOptionStyle(null); - this.removeVisualFocusAll(); - setTimeout(this.listbox.close.bind(this.listbox, false), 300); - -}; - -ComboboxList.prototype.handleButtonClick = function (event) { - if (this.listbox.isOpen()) { - this.listbox.close(true); - } - else { - this.listbox.open(); - } - this.domNode.focus(); - this.setVisualFocusTextbox(); -}; - - -// Initialize comboboxes - -window.addEventListener('load', function () { - - var comboboxes = document.querySelectorAll('.combobox-list [role="combobox"]'); - - for (var i = 0; i < comboboxes.length; i++) { - var combobox = new ComboboxList(comboboxes[i]); - combobox.init(); - } - -}); diff --git a/examples/combobox/js/listbox.js b/examples/combobox/js/listbox.js deleted file mode 100644 index fe9f7f55cb..0000000000 --- a/examples/combobox/js/listbox.js +++ /dev/null @@ -1,225 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -*/ -var Listbox = function (domNode, comboboxObj) { - var elementChildren, - msgPrefix = 'Listbox constructor argument domNode '; - - // Check whether domNode is a DOM element - if (!(domNode instanceof Element)) { - throw new TypeError(msgPrefix + 'is not a DOM Element.'); - } - - // Check whether domNode has child elements - if (domNode.childElementCount === 0) { - throw new Error(msgPrefix + 'has no element children.'); - } - - // Check whether domNode child elements are A elements - var childElement = domNode.firstElementChild; - while (childElement) { - var option = childElement.firstElementChild; - childElement = childElement.nextElementSibling; - } - - this.domNode = domNode; - this.combobox = comboboxObj; - - this.allOptions = []; - - this.options = []; // see PopupMenu init method - - this.firstOption = null; // see PopupMenu init method - this.lastOption = null; // see PopupMenu init method - - this.hasFocus = false; // see MenuItem handleFocus, handleBlur - this.hasHover = false; // see PopupMenu handleMouseover, handleMouseout -}; - -/* -* @method Listbox.prototype.init -* -* @desc -* Add domNode event listeners for mouseover and mouseout. Traverse -* domNode children to configure each option and populate.options -* array. Initialize firstOption and lastOption properties. -*/ -Listbox.prototype.init = function () { - var childElement, optionElement, optionElements, firstChildElement, option, textContent, numItems; - - // Configure the domNode itself - this.domNode.tabIndex = -1; - - this.domNode.setAttribute('role', 'listbox'); - - this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this)); - this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this)); - - // Traverse the element children of domNode: configure each with - // option role behavior and store reference in.options array. - optionElements = this.domNode.getElementsByTagName('LI'); - - for (var i = 0; i < optionElements.length; i++) { - - optionElement = optionElements[i]; - - if (!optionElement.firstElementChild && optionElement.getAttribute('role') != 'separator') { - option = new ListboxOption(optionElement, this); - option.init(); - this.allOptions.push(option); - } - } - - this.filterOptions(''); - -}; - -Listbox.prototype.filterOptions = function (filter, currentOption) { - - if (typeof filter !== 'string') { - filter = ''; - } - - var i, - option, - textContent, - numItems; - - filter = filter.toLowerCase(); - - this.options = []; - this.firstChars = []; - this.domNode.innerHTML = ''; - - for (i = 0; i < this.allOptions.length; i++) { - option = this.allOptions[i]; - if (filter.length === 0 || option.textComparison.indexOf(filter) === 0) { - this.options.push(option); - textContent = option.textContent.trim(); - this.firstChars.push(textContent.substring(0, 1).toLowerCase()); - this.domNode.appendChild(option.domNode); - } - } - - // Use populated.options array to initialize firstOption and lastOption. - numItems = this.options.length; - if (numItems > 0) { - this.firstOption = this.options[0]; - this.lastOption = this.options[numItems - 1]; - - if (currentOption && this.options.indexOf(currentOption) >= 0) { - option = currentOption; - } - else { - option = this.firstOption; - } - } - else { - this.firstOption = false; - option = false; - this.lastOption = false; - } - - return option; -}; - -Listbox.prototype.setCurrentOptionStyle = function (option) { - - for (var i = 0; i < this.options.length; i++) { - var opt = this.options[i]; - if (opt === option) { - opt.domNode.setAttribute('aria-selected', 'true'); - this.domNode.scrollTop = opt.domNode.offsetTop; - } - else { - opt.domNode.removeAttribute('aria-selected'); - } - } -}; - -Listbox.prototype.setOption = function (option) { - if (option) { - this.combobox.setOption(option); - this.combobox.setValue(option.textContent); - } -}; - -/* EVENT HANDLERS */ - -Listbox.prototype.handleMouseover = function (event) { - this.hasHover = true; -}; - -Listbox.prototype.handleMouseout = function (event) { - this.hasHover = false; - setTimeout(this.close.bind(this, false), 300); -}; - -/* FOCUS MANAGEMENT METHODS */ - - -Listbox.prototype.getFirstItem = function () { - return this.firstOption; -}; - -Listbox.prototype.getLastItem = function () { - return this.lastOption; -}; - -Listbox.prototype.getPreviousItem = function (currentOption) { - var index; - - if (currentOption !== this.firstOption) { - index = this.options.indexOf(currentOption); - return this.options[index - 1]; - } - return this.lastOption; -}; - -Listbox.prototype.getNextItem = function (currentOption) { - var index; - - if (currentOption !== this.lastOption) { - index = this.options.indexOf(currentOption); - return this.options[index + 1]; - } - return this.firstOption; -}; - -/* MENU DISPLAY METHODS */ - -Listbox.prototype.isOpen = function () { - return this.domNode.style.display === 'block'; -}; - -Listbox.prototype.isClosed = function () { - return this.domNode.style.display !== 'block'; -}; - -Listbox.prototype.hasOptions = function () { - return this.options.length; -}; - -Listbox.prototype.open = function () { - // set CSS properties - this.domNode.style.display = 'block'; - - // set aria-expanded attribute - this.combobox.domNode.setAttribute('aria-expanded', 'true'); -}; - -Listbox.prototype.close = function (force) { - if (typeof force !== 'boolean') { - force = false; - } - - if (force || (!this.hasFocus && !this.hasHover && !this.combobox.hasHover)) { - this.setCurrentOptionStyle(false); - this.domNode.style.display = 'none'; - this.combobox.domNode.setAttribute('aria-expanded', 'false'); - this.combobox.setActiveDescendant(false); - } -}; - - diff --git a/examples/combobox/js/listboxOption.js b/examples/combobox/js/listboxOption.js deleted file mode 100644 index 0707760d0c..0000000000 --- a/examples/combobox/js/listboxOption.js +++ /dev/null @@ -1,42 +0,0 @@ -/* -* This content is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -*/ -var ListboxOption = function (domNode, listboxObj) { - - this.domNode = domNode; - this.listbox = listboxObj; - this.textContent = domNode.textContent; - this.textComparison = domNode.textContent.toLowerCase(); - -}; - -ListboxOption.prototype.init = function () { - - if (!this.domNode.getAttribute('role')) { - this.domNode.setAttribute('role', 'option'); - } - - this.domNode.addEventListener('click', this.handleClick.bind(this)); - this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this)); - this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this)); - -}; - -/* EVENT HANDLERS */ - -ListboxOption.prototype.handleClick = function (event) { - this.listbox.setOption(this); - this.listbox.close(true); -}; - -ListboxOption.prototype.handleMouseover = function (event) { - this.listbox.hasHover = true; - this.listbox.open(); - -}; - -ListboxOption.prototype.handleMouseout = function (event) { - this.listbox.hasHover = false; - setTimeout(this.listbox.close.bind(this.listbox, false), 300); -}; diff --git a/test/tests/combobox_autocomplete-both.js b/test/tests/combobox_autocomplete-both.js index 879c30389a..f16296e16a 100644 --- a/test/tests/combobox_autocomplete-both.js +++ b/test/tests/combobox_autocomplete-both.js @@ -4,6 +4,7 @@ const { ariaTest } = require('..'); const { By, Key } = require('selenium-webdriver'); const assertAttributeValues = require('../util/assertAttributeValues'); const assertAriaLabelExists = require('../util/assertAriaLabelExists'); +const assertAttributeDNE = require('../util/assertAttributeDNE'); const assertAriaRoles = require('../util/assertAriaRoles'); const assertAriaSelectedAndActivedescendant = require('../util/assertAriaSelectedAndActivedescendant'); @@ -14,7 +15,7 @@ const ex = { listboxSelector: '#ex1 [role="listbox"]', optionsSelector: '#ex1 [role="option"]', numAOptions: 5, - numCharFirstAOption: 6 + secondAOption: 'Alaska' }; @@ -148,9 +149,29 @@ ariaTest('"aria-selected" attribute on options element', exampleFile, 'option-ar await assertAttributeValues(t, ex.optionsSelector + ':nth-of-type(1)', 'aria-selected', 'true'); }); - // Keys +ariaTest('Test alt + down key press with focus on textbox', + exampleFile, 'textbox-key-alt-down-arrow', async (t) => { + + t.plan(2); + + // Send ARROW_DOWN to the textbox + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .sendKeys(Key.ALT, Key.ARROW_DOWN); + + // Check that the listbox is displayed + t.true( + await t.context.session.findElement(By.css(ex.listboxSelector)).isDisplayed(), + 'In example the list box should display after ALT + ARROW_DOWN keypress' + ); + + await assertAttributeDNE(t, ex.optionsSelector, 'aria-selected'); + + }); + + ariaTest('Test down key press with focus on textbox', exampleFile, 'textbox-key-down-arrow', async (t) => { @@ -175,6 +196,7 @@ ariaTest('Test down key press with focus on textbox', }); + ariaTest('Test down key press with focus on list', exampleFile, 'listbox-key-down-arrow', async (t) => { @@ -310,7 +332,7 @@ ariaTest('Test enter key press with focus on listbox', .findElement(By.css(ex.textboxSelector)) .sendKeys('a', Key.ARROW_DOWN); - // Get the value of the second option in the listbox + // Get the value of the first option in the listbox const secondOption = await(await t.context.session.findElements(By.css(ex.optionsSelector)))[1] .getText(); @@ -337,17 +359,41 @@ ariaTest('Test enter key press with focus on listbox', }); -ariaTest('Test escape key press with focus on textbox', + +ariaTest('Test single escape key press with focus on textbox', exampleFile, 'textbox-key-escape', async (t) => { t.plan(2); - // Send key "a", then key ESCAPE to the textbox + // Send key "a", then key ESCAPE once to the textbox await t.context.session .findElement(By.css(ex.textboxSelector)) .sendKeys('a', Key.ESCAPE); - // Confirm the listbox is closed and the textboxed is clearedx + // Confirm the listbox is closed and the textbox is not cleared + + await assertAttributeValues(t, ex.textboxSelector, 'aria-expanded', 'false'); + t.is( + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .getAttribute('value'), + 'Alabama', + 'In key press "ESCAPE" should result in first option in textbox' + ); + + }); + +ariaTest('Test double escape key press with focus on textbox', + exampleFile, 'textbox-key-escape', async (t) => { + t.plan(2); + + // Send key "a", then key ESCAPE twice to the textbox + + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .sendKeys('a', Key.ESCAPE, Key.ESCAPE); + + // Confirm the listbox is closed and the textbox is cleared await assertAttributeValues(t, ex.textboxSelector, 'aria-expanded', 'false'); t.is( @@ -378,8 +424,8 @@ ariaTest('Test escape key press with focus on textbox', await t.context.session .findElement(By.css(ex.textboxSelector)) .getAttribute('value'), - '', - 'In listbox key press "ESCAPE" should result in first option in textbox' + ex.secondAOption, + 'In listbox key press "ESCAPE" should result in second option in textbox' ); }); @@ -389,15 +435,15 @@ ariaTest('left arrow from focus on list puts focus on listbox and moves cursor r t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "ARROW_LEFT" await textbox.sendKeys(Key.ARROW_LEFT); t.true( - await confirmCursorIndex(t, ex.textboxSelector, ex.numCharFirstAOption - 1), - 'Cursor should be at index ' + (ex.numCharFirstAOption - 1) + ' after one ARROW_LEFT key' + await confirmCursorIndex(t, ex.textboxSelector, ex.secondAOption.length - 1), + 'Cursor should be at index ' + (ex.secondAOption.length - 1) + ' after one ARROW_LEFT key' ); t.is( @@ -413,15 +459,15 @@ ariaTest('Right arrow from focus on list puts focus on listbox', t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "RIGHT_ARROW" await textbox.sendKeys(Key.ARROW_RIGHT); t.true( - await confirmCursorIndex(t, ex.textboxSelector, ex.numCharFirstAOption), - 'Cursor should be at index ' + ex.numCharFirstAOption + ' after one ARROW_RIGHT key' + await confirmCursorIndex(t, ex.textboxSelector, ex.secondAOption.length), + 'Cursor should be at index ' + ex.secondAOption.length + ' after one ARROW_RIGHT key' ); t.is( @@ -436,7 +482,7 @@ ariaTest('Home from focus on list puts focus on listbox and moves cursor', t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "ARROW_HOME" @@ -459,15 +505,15 @@ ariaTest('End from focus on list puts focus on listbox', t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "END_ARROW" await textbox.sendKeys(Key.END); t.true( - await confirmCursorIndex(t, ex.textboxSelector, ex.numCharFirstAOption), - 'Cursor should be at index ' + ex.numCharFirstAOption + ' after one ARROW_END key' + await confirmCursorIndex(t, ex.textboxSelector, ex.secondAOption.length), + 'Cursor should be at index ' + ex.secondAOption.length + ' after one ARROW_END key' ); t.is( @@ -482,7 +528,7 @@ ariaTest('Sending character keys while focus is on listbox moves focus', t.plan(2); // Send key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys(Key.ARROW_DOWN); // Send key "a" @@ -506,9 +552,17 @@ ariaTest('Sending character keys while focus is on listbox moves focus', }); -ariaTest.failing('Expected behavior for all other standard single line editing keys', +ariaTest('Expected behavior for all other standard single line editing keys', exampleFile, 'standard-single-line-editing-keys', async (t) => { t.plan(1); - t.fail(); - }); + // Send key "a" + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); + await textbox.sendKeys('a'); + + t.is( + (await t.context.session.findElements(By.css(ex.optionsSelector))).length, + ex.numAOptions, + 'Sending standard editing keys should filter results' + ); + }); diff --git a/test/tests/combobox_autocomplete-list.js b/test/tests/combobox_autocomplete-list.js index 63e3631c50..0bf230a4c6 100644 --- a/test/tests/combobox_autocomplete-list.js +++ b/test/tests/combobox_autocomplete-list.js @@ -157,6 +157,27 @@ ariaTest('"aria-selected" attribute on options element', exampleFile, 'option-ar // Keys +ariaTest('Test alt + down key press with focus on textbox', + exampleFile, 'textbox-key-alt-down-arrow', async (t) => { + + t.plan(2); + + // Send ARROW_DOWN to the textbox + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .sendKeys(Key.ALT, Key.ARROW_DOWN); + + // Check that the listbox is displayed + t.true( + await t.context.session.findElement(By.css(ex.listboxSelector)).isDisplayed(), + 'In example the list box should display after ALT + ARROW_DOWN keypress' + ); + + // aria-selected should not be on any options + await assertAttributeDNE(t, ex.optionsSelector, 'aria-selected'); + + }); + ariaTest('Test down key press with focus on textbox', exampleFile, 'textbox-key-down-arrow', async (t) => { @@ -344,17 +365,40 @@ ariaTest('Test enter key press with focus on listbox', }); -ariaTest('Test escape key press with focus on textbox', +ariaTest('Test single escape key press with focus on textbox', exampleFile, 'textbox-key-escape', async (t) => { t.plan(2); - // Send key "a", then key ESCAPE to the textbox + // Send key "a", then key ESCAPE once to the textbox await t.context.session .findElement(By.css(ex.textboxSelector)) .sendKeys('a', Key.ESCAPE); - // Confirm the listbox is closed and the textboxed is cleared + // Confirm the listbox is closed and the textbox is not cleared + + await assertAttributeValues(t, ex.textboxSelector, 'aria-expanded', 'false'); + t.is( + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .getAttribute('value'), + 'a', + 'In key press "ESCAPE" should result in first option in textbox' + ); + + }); + +ariaTest('Test double escape key press with focus on textbox', + exampleFile, 'textbox-key-escape', async (t) => { + t.plan(2); + + // Send key "a", then key ESCAPE twice to the textbox + + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .sendKeys('a', Key.ESCAPE, Key.ESCAPE); + + // Confirm the listbox is closed and the textbox is cleared await assertAttributeValues(t, ex.textboxSelector, 'aria-expanded', 'false'); t.is( @@ -385,7 +429,7 @@ ariaTest('Test escape key press with focus on textbox', await t.context.session .findElement(By.css(ex.textboxSelector)) .getAttribute('value'), - '', + 'a', 'In listbox key press "ESCAPE" should result in first option in textbox' ); @@ -396,7 +440,7 @@ ariaTest('left arrow from focus on list puts focus on listbox and moves cursor r t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "ARROW_LEFT" @@ -420,7 +464,7 @@ ariaTest('Right arrow from focus on list puts focus on listbox', t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "RIGHT_ARROW" @@ -443,7 +487,7 @@ ariaTest('Home from focus on list puts focus on listbox and moves cursor', t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "ARROW_HOME" @@ -466,7 +510,7 @@ ariaTest('End from focus on list puts focus on listbox', t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "END_ARROW" @@ -489,7 +533,7 @@ ariaTest('Sending character keys while focus is on listbox moves focus', t.plan(2); // Send key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys(Key.ARROW_DOWN); // Send key "a" @@ -510,9 +554,18 @@ ariaTest('Sending character keys while focus is on listbox moves focus', }); -ariaTest.failing('Expected behavior for all other standard single line editing keys', +ariaTest('Expected behavior for all other standard single line editing keys', exampleFile, 'standard-single-line-editing-keys', async (t) => { t.plan(1); - t.fail(); + + // Send key "a" + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); + await textbox.sendKeys('a'); + + t.is( + (await t.context.session.findElements(By.css(ex.optionsSelector))).length, + ex.numAOptions, + 'Sending standard editing keys should filter results' + ); }); diff --git a/test/tests/combobox_autocomplete-none.js b/test/tests/combobox_autocomplete-none.js index 0119089cfd..a08652bf98 100644 --- a/test/tests/combobox_autocomplete-none.js +++ b/test/tests/combobox_autocomplete-none.js @@ -157,6 +157,28 @@ ariaTest('"aria-selected" attribute on options element', exampleFile, 'option-ar // Keys +ariaTest('Test alt + down key press with focus on textbox', + exampleFile, 'textbox-key-alt-down-arrow', async (t) => { + + t.plan(2); + + // Send ARROW_DOWN to the textbox + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .sendKeys(Key.ALT, Key.ARROW_DOWN); + + // Check that the listbox is displayed + t.true( + await t.context.session.findElement(By.css(ex.listboxSelector)).isDisplayed(), + 'In example the list box should display after ALT + ARROW_DOWN keypress' + ); + + // aria-selected should not be on any options + await assertAttributeDNE(t, ex.optionsSelector, 'aria-selected'); + + }); + + ariaTest('Test down key press with focus on textbox', exampleFile, 'textbox-key-down-arrow', async (t) => { @@ -348,13 +370,37 @@ ariaTest('Test escape key press with focus on textbox', t.plan(2); // Send key "a" then key "ARROW_DOWN to put the focus on the listbox, - // then key ESCAPE to the textbox + // then key ESCAPE once to the textbox await t.context.session .findElement(By.css(ex.textboxSelector)) .sendKeys('a', Key.ARROW_DOWN, Key.ESCAPE); - // Confirm the listbox is closed and the textboxed is cleared + // Confirm the listbox is closed and the textbox is not cleared + + await assertAttributeValues(t, ex.textboxSelector, 'aria-expanded', 'false'); + t.is( + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .getAttribute('value'), + 'a', + 'In listbox key press "ESCAPE" should result in first option in textbox' + ); + + }); + +ariaTest('Test double escape key press with focus on textbox', + exampleFile, 'listbox-key-escape', async (t) => { + t.plan(2); + + // Send key "a" then key "ARROW_DOWN to put the focus on the listbox, + // then key ESCAPE twice to the textbox + + await t.context.session + .findElement(By.css(ex.textboxSelector)) + .sendKeys('a', Key.ARROW_DOWN, Key.ESCAPE, Key.ESCAPE); + + // Confirm the listbox is closed and the textbox is cleared await assertAttributeValues(t, ex.textboxSelector, 'aria-expanded', 'false'); t.is( @@ -372,7 +418,7 @@ ariaTest('left arrow from focus on list puts focus on listbox and moves cursor r t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "ARROW_LEFT" @@ -396,7 +442,7 @@ ariaTest('Right arrow from focus on list puts focus on listbox', t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "RIGHT_ARROW" @@ -419,7 +465,7 @@ ariaTest('Home from focus on list puts focus on listbox and moves cursor', t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "ARROW_HOME" @@ -442,7 +488,7 @@ ariaTest('End from focus on list puts focus on listbox', t.plan(2); // Send key "a" then key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys('a', Key.ARROW_DOWN); // Send key "END_ARROW" @@ -466,7 +512,7 @@ ariaTest('Sending character keys while focus is on listbox moves focus', t.plan(2); // Send key "ARROW_DOWN" to put the focus on the listbox - const textbox = t.context.session.findElement(By.css(ex.textboxSelector)); + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); await textbox.sendKeys(Key.ARROW_DOWN); // Send key "a" @@ -487,9 +533,20 @@ ariaTest('Sending character keys while focus is on listbox moves focus', }); -ariaTest.failing('Expected behavior for all other standard single line editing keys', +ariaTest('Expected behavior for all other standard single line editing keys', exampleFile, 'standard-single-line-editing-keys', async (t) => { t.plan(1); - t.fail(); + + let numOptions = (await t.context.session.findElements(By.css(ex.optionsSelector))).length; + + // Send key "w" + const textbox = await t.context.session.findElement(By.css(ex.textboxSelector)); + await textbox.sendKeys('w'); + + t.is( + (await t.context.session.findElements(By.css(ex.optionsSelector))).length, + numOptions, + 'Sending standard editing keys should NOT filter results' + ); });