From 827c0d868f9bc70c057ab0e3ebf2a72a027d024c Mon Sep 17 00:00:00 2001 From: Isabel Del Castillo Date: Tue, 26 Apr 2022 14:47:30 -0500 Subject: [PATCH 1/8] update timestamped example directory and references.csv --- .../combobox-select-only/data/references.csv | 4 +- .../combobox-select-only.html | 438 +++++++++ .../css/combobox-autocomplete.css | 99 ++ .../css/combobox-datepicker.css | 249 +++++ .../2022-4-26_144616/css/grid-combo.css | 88 ++ .../2022-4-26_144616/css/select-only.css | 103 +++ .../js/combobox-autocomplete.js | 600 ++++++++++++ .../js/combobox-datepicker.js | 859 ++++++++++++++++++ .../2022-4-26_144616/js/grid-combo-example.js | 101 ++ .../2022-4-26_144616/js/grid-combo.js | 318 +++++++ .../2022-4-26_144616/js/select-only.js | 417 +++++++++ 11 files changed, 3274 insertions(+), 2 deletions(-) create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-autocomplete.css create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-datepicker.css create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/css/grid-combo.css create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-autocomplete.js create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-datepicker.js create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo-example.js create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo.js create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js diff --git a/tests/combobox-select-only/data/references.csv b/tests/combobox-select-only/data/references.csv index f06df4bea..df07f4101 100644 --- a/tests/combobox-select-only/data/references.csv +++ b/tests/combobox-select-only/data/references.csv @@ -1,8 +1,8 @@ refId,value author,James Scholes authorEmail,james@pac.bz -title,Select Only Combobox Example -reference,reference/combobox-select-only.html +title,Select Only Combobox +reference,reference/2022-4-26_144616/combobox-s2. Update tests/combobox-select-only/data/reference.csv with the reference reference/2022-4-26_144616/combobox-s example,https://w3c.github.io/aria-practices/examples/combobox/combobox-select-only.html combobox,https://w3c.github.io/aria-practices/#combobox listbox,https://w3c.github.io/aria/#listbox diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html new file mode 100644 index 000000000..0868ba26c --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html @@ -0,0 +1,438 @@ + + + + +Select-Only Combobox Example | WAI-ARIA Authoring Practices 1.2 + + + + + + + + + + + + + + +
+

Select-Only Combobox Example

+

+ The following example implementation of the ARIA design pattern for combobox + demonstrates a single-select combobox widget that is functionally similar to an HTML select element. + Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. + However, like an HTML <select>, users can type characters to select matching options. +

+

Similar examples include:

+ + +
+
+

Example

+
+ +
+ +
+ +
+ + +
+
+
+ +
+ +
+

Accessibility Features

+

While the functionality and user experience of this example are nearly equivalent to an HTML select element with the attribute size="1", the following differences in behavior are implemented to improve both accessibility and general usability.

+
    +
  1. + If the combobox is collapsed and the user types printable characters, the listbox is displayed and receives accessibility focus via aria-activedescendant. + This enables users to perceive the presence of the options, and enables assistive technology users to comprehend the size of the list of options. +
  2. +
  3. + Navigating the list of options does not set the value of the input. + This gives screen reader users, who need to navigate among the options to perceive them, the ability to explore options without losing the current value of the input. + The value is set when users press Space, Enter, or Tab, or when focus moves out of the combobox. + The current value is retained if the listbox is closed with Escape or if the user collapses the list by clicking the input. +
  4. +
  5. Browsers do not manage visibility of elements referenced by aria-activedescendant like they do for elements with focus. When a keyboard event changes the active option in the listbox, the JavaScript scrolls the option referenced by aria-activedescendant into view. Managing aria-activedescendant visibility is essential to accessibility for people who use a browser's zoom feature to increase the size of content.
  6. +
+
+ +
+

Keyboard Support

+

+ The example combobox on this page implements the following keyboard interface. + Other variations and options for the keyboard interface are described in the + Keyboard Interaction section of the combobox design pattern. +

+

Closed Combobox

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyFunction
Down Arrow +
    +
  • Opens the listbox if it is not already displayed without moving focus or changing selection.
  • +
  • DOM focus remains on the combobox.
  • +
+
Alt + Down Arrow + Opens the listbox without moving focus or changing selection. +
Up Arrow +
    +
  • First opens the listbox if it is not already displayed and then moves visual focus to the first option.
  • +
  • DOM focus remains on the combobox.
  • +
+
Enter + Opens the listbox without moving focus or changing selection. +
Space + Opens the listbox without moving focus or changing selection. +
Home + Opens the listbox and moves visual focus to the first option. +
End + Opens the listbox and moves visual focus to the last option. +
Printable Characters +
    +
  • First opens the listbox if it is not already displayed and then moves visual focus to the first option that matches the typed character.
  • +
  • If multiple keys are typed in quick succession, visual focus moves to the first option that matches the full string.
  • +
  • If the same character is typed in succession, visual focus cycles among the options starting with that character
  • +
+
+

Listbox Popup

+

+ NOTE: When visual focus is in the listbox, DOM focus remains on the combobox and the value of aria-activedescendant on the combobox is set to a value that refers to the listbox option that is visually indicated as focused. + Where the following descriptions of keyboard commands mention focus, they are referring to the visual focus indicator. + For more information about this focus management technique, see + Using aria-activedescendant to Manage Focus. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyFunction
Enter +
    +
  • Sets the value to the content of the focused option in the listbox.
  • +
  • Closes the listbox.
  • +
  • Sets visual focus on the combobox.
  • +
+
Space +
    +
  • Sets the value to the content of the focused option in the listbox.
  • +
  • Closes the listbox.
  • +
  • Sets visual focus on the combobox.
  • +
+
Tab +
    +
  • Sets the value to the content of the focused option in the listbox.
  • +
  • Closes the listbox.
  • +
  • Performs the default action, moving focus to the next focusable element. + Note: the native <select> element closes the listbox but does not move focus on tab. + This pattern matches the behavior of the other comboboxes rather than the native element in this case.
  • +
+
Escape +
    +
  • Closes the listbox.
  • +
  • Sets visual focus on the combobox.
  • +
+
Down Arrow +
    +
  • Moves visual focus to the next option.
  • +
  • If visual focus is on the last option, visual focus does not move.
  • +
+
Up Arrow +
    +
  • Moves visual focus to the previous option.
  • +
  • If visual focus is on the first option, visual focus does not move.
  • +
+
Alt + Up Arrow +
    +
  • Sets the value to the content of the focused option in the listbox.
  • +
  • Closes the listbox.
  • +
  • Sets visual focus on the combobox.
  • +
+
HomeMoves visual focus to the first option.
EndMoves visual focus to the last option.
PageUpJumps visual focus up 10 options (or to first option).
PageDownJumps visual focus down 10 options (or to last option).
Printable Characters +
    +
  • First opens the listbox if it is not already displayed and then moves visual focus to the first option that matches the typed character.
  • +
  • If multiple keys are typed in quick succession, visual focus moves to the first option that matches the full string.
  • +
  • If the same character is typed in succession, visual focus cycles among the options starting with that character
  • +
+
+
+ +
+

Role, Property, State, and Tabindex Attributes

+

+ The example combobox on this page implements the following ARIA roles, states, and properties. + Information about other ways of applying ARIA roles, states, and properties is available in the + Roles, States, and Properties section of the combobox design pattern. +

+

Combobox

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RoleAttributeElementUsage
+ combobox + divIdentifies the input as a combobox.
+ aria-labelledby="#IDREF" + divIdentifies the element that labels the combobox.
+ aria-controls="#IDREF" + divIdentifies the element that serves as the popup.
+ aria-expanded="false" + divIndicates that the popup element is not displayed.
+ aria-expanded="true" + divIndicates that the popup element is displayed.
+ aria-activedescendant="IDREF" + div +
    +
  • When an option in the listbox is visually indicated as having keyboard focus, refers to that option.
  • +
  • When navigation keys, such as Down Arrow, are pressed, the JavaScript changes the value.
  • +
  • Enables assistive technologies to know which element the application regards as focused while DOM focus remains on the input element.
  • +
  • + For more information about this focus management technique, see + Using aria-activedescendant to Manage Focus. +
  • +
+
+

Listbox Popup

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RoleAttributeElementUsage
+ listbox + + div + Identifies the element as a listbox.
+ option + div +
    +
  • Identifies the element as a listbox option.
  • +
  • The text content of the element provides the accessible name of the option.
  • +
+
+ aria-selected="true" + li +
    +
  • Specified on an option in the listbox when it is visually highlighted as selected.
  • +
  • Occurs only when an option in the list is referenced by aria-activedescendant.
  • +
+
+
+ +
+

Javascript and CSS Source Code

+ +
+ +
+

HTML Source Code

+ +
+ + +
+
+ + + diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-autocomplete.css b/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-autocomplete.css new file mode 100644 index 000000000..5bf2d8b9b --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-autocomplete.css @@ -0,0 +1,99 @@ +.combobox-list { + position: relative; +} + +.combobox .group { + display: inline-flex; + padding: 4px; + cursor: pointer; +} + +.combobox input, +.combobox button { + background-color: white; + color: black; + box-sizing: border-box; + height: 24px; + padding: 0; + margin: 0; + vertical-align: bottom; + border: 1px solid gray; + position: relative; + cursor: pointer; +} + +.combobox input { + width: 150px; + border-right: none; + outline: none; + font-size: 87.5%; + padding: 1px 3px; +} + +.combobox button { + width: 19px; + border-left: none; + outline: none; + color: rgb(0 90 156); +} + +.combobox button[aria-expanded="true"] svg { + transform: rotate(180deg) translate(0, -3px); +} + +ul[role="listbox"] { + margin: 0; + padding: 0; + position: absolute; + left: 4px; + top: 28px; + list-style: none; + background-color: white; + display: none; + box-sizing: border-box; + border: 2px currentColor solid; + max-height: 250px; + width: 168px; + overflow: scroll; + overflow-x: hidden; + font-size: 87.5%; + cursor: pointer; +} + +ul[role="listbox"] li[role="option"] { + margin: 0; + display: block; + padding-left: 3px; + padding-top: 2px; + padding-bottom: 2px; +} + +/* focus and hover styling */ + +.combobox .group.focus, +.combobox .group:hover { + padding: 2px; + border: 2px solid currentColor; + border-radius: 4px; +} + +.combobox .group.focus polygon, +.combobox .group:hover polygon { + fill-opacity: 1; +} + +.combobox .group.focus input, +.combobox .group.focus button, +.combobox .group input:hover, +.combobox .group button:hover { + background-color: #def; +} + +[role="listbox"].focus [role="option"][aria-selected="true"], +[role="listbox"] [role="option"]:hover { + background-color: #def; + padding-top: 0; + padding-bottom: 0; + border-top: 2px solid currentColor; + border-bottom: 2px solid currentColor; +} diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-datepicker.css b/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-datepicker.css new file mode 100644 index 000000000..c4161d02e --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-datepicker.css @@ -0,0 +1,249 @@ +.combobox-datepicker { + margin-top: 1em; + position: relative; +} + +.combobox-datepicker .group { + display: inline-flex; + position: relative; + width: 12.125rem; +} + +.combobox-datepicker label { + display: block; +} + +.combobox-datepicker .group input, +.combobox-datepicker .group button { + background-color: white; + color: black; + box-sizing: border-box; + height: 1.75rem; + padding: 0; + margin: 0; + vertical-align: bottom; + border: 1px solid gray; + position: relative; +} + +.combobox-datepicker .group input { + width: 10.75rem; + border-right: none; + outline: none; + font-size: 87.5%; + padding: 0.1em 0.3em; +} + +.combobox-datepicker .group button { + position: absolute; + left: 10.75rem; + padding-right: 0.125rem; + border-left: none; + outline: none; +} + +.combobox-datepicker .group .desc { + position: absolute; + top: 2em; + left: 0; + margin-top: 0.1em; + font-size: 90%; + font-style: italic; + letter-spacing: 0.025em; +} + +.combobox-datepicker .group.focus { + outline: 2px solid black; + outline-offset: 2px; +} + +.combobox-datepicker .dialog button { + border-style: none; + background: transparent; +} + +.combobox-datepicker .dialog-ok-cancel-group button { + padding: 6px; + margin-left: 1em; + width: 5em; + background-color: hsl(216deg 80% 92%); + font-size: 0.85em; + color: black; + outline: none; + border-radius: 5px; +} + +.combobox-datepicker .dialog button::-moz-focus-inner { + border: 0; +} + +.combobox-datepicker .group.focus input, +.combobox-datepicker .group.focus button { + background-color: #def; +} + +.combobox-datepicker .group polygon { + fill: gray; + stroke: transparent; +} + +.combobox-datepicker .group.focus polygon, +.combobox-datepicker .group button[aria-expanded="true"] polygon { + fill: black; + stroke: white; +} + +.combobox-datepicker .group button.open svg { + transform: rotate(180deg) translate(0, -1px); +} + +.combobox-datepicker .dialog { + position: absolute; + width: 320px; + clear: both; + border: 3px solid hsl(216deg 80% 51%); + margin-top: 1em; + border-radius: 5px; + padding: 0; + background-color: #fff; +} + +.combobox-datepicker .header { + cursor: default; + background-color: hsl(216deg 80% 51%); + padding: 7px; + font-weight: bold; + text-transform: uppercase; + color: white; + display: flex; + justify-content: space-around; +} + +.combobox-datepicker .dialog h2 { + margin: 0; + padding: 0; + display: inline-block; + font-size: 1em; + color: white; + text-transform: none; + font-weight: bold; +} + +.combobox-datepicker .dates { + width: 320px; + padding-left: 1em; + padding-right: 1em; + padding-top: 1em; +} + +.combobox-datepicker .prev-year, +.combobox-datepicker .prev-month, +.combobox-datepicker .next-month, +.combobox-datepicker .next-year { + padding: 4px; + width: 24px; + height: 24px; + color: white; +} + +.combobox-datepicker .prev-year:focus, +.combobox-datepicker .prev-month:focus, +.combobox-datepicker .next-month:focus, +.combobox-datepicker .next-year:focus { + padding: 2px; + border: 2px solid white; + border-radius: 4px; + outline: 0; +} + +.combobox-datepicker .prev-year:hover, +.combobox-datepicker .prev-month:hover, +.combobox-datepicker .next-month:hover, +.combobox-datepicker .next-year:hover { + padding: 3px; + border: 1px solid white; + border-radius: 4px; + outline: 0; +} + +.combobox-datepicker .dialog-ok-cancel-group { + text-align: right; + margin-top: 1em; + margin-bottom: 1em; + margin-right: 1em; +} + +.combobox-datepicker .dialog-button:focus { + padding: 4px; + border: 2px solid black; +} + +.combobox-datepicker .dialog-button:hover { + padding: 5px; + border: 1px solid black; +} + +.combobox-datepicker .fa-calendar-alt { + color: hsl(216deg 89% 51%); +} + +.combobox-datepicker .month-year { + display: inline-block; + width: 12em; + text-align: center; +} + +.combobox-datepicker .dates th, +.combobox-datepicker .dates td { + text-align: center; +} + +.combobox-datepicker .dates tr { + border: 1px solid black; +} + +.combobox-datepicker .dates td { + padding: 3px; + margin: 0; + line-height: inherit; + height: 40px; + width: 40px; + border-radius: 5px; + font-size: 15px; + background: #eee; +} + +.combobox-datepicker .dates td[aria-selected] { + padding: 1px; + border: 2px dotted black; + background-color: hsl(216deg 80% 96%); +} + +.combobox-datepicker .dates td[tabindex="0"] { + background-color: hsl(216deg 80% 51%); + color: white; +} + +.combobox-datepicker .dates td:hover { + padding: 0; + background-color: hsl(216deg 80% 92%); +} + +.combobox-datepicker .dates td:focus { + padding: 1px; + border: 2px solid rgb(100 100 100); + outline: 0; +} + +.combobox-datepicker .dates td:not(.disabled):hover { + padding: 2px; + border: 1px solid rgb(100 100 100); +} + +.combobox-datepicker .dialog-message { + padding-top: 0.25em; + padding-left: 1em; + height: 1.75em; + background: hsl(216deg 80% 51%); + color: white; +} diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/css/grid-combo.css b/tests/combobox-select-only/reference/2022-4-26_144616/css/grid-combo.css new file mode 100644 index 000000000..5484f8f7b --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/css/grid-combo.css @@ -0,0 +1,88 @@ +.hidden { + display: none; +} + +.combobox-wrapper { + display: inline-block; + position: relative; + font-size: 16px; +} + +.combobox-label { + font-size: 14px; + font-weight: bold; + margin-right: 5px; +} + +.listbox, +.grid { + min-width: 230px; + background: white; + border: 1px solid #ccc; + list-style: none; + margin: 0; + padding: 0; + position: absolute; + top: 1.7em; + z-index: 1; +} + +.listbox .result { + cursor: default; + margin: 0; +} + +.grid .result-row { + padding: 2px; + cursor: default; + margin: 0; +} + +.listbox .result:hover, +.grid .result-row:hover { + background: rgb(139 189 225); +} + +.listbox .focused, +.grid .focused { + background: rgb(139 189 225); +} + +.grid .focused-cell { + outline-style: dotted; + outline-color: green; +} + +.combobox-wrapper input { + font-size: inherit; + border: 1px solid #aaa; + border-radius: 2px; + line-height: 1.5em; + padding-right: 30px; + width: 200px; +} + +.combobox-dropdown { + position: absolute; + right: 0; + top: 0; + padding: 0 0 2px; + height: 1.5em; + border-radius: 0 2px 2px 0; + border: 1px solid #aaa; +} + +.grid .result-cell { + display: inline-block; + cursor: default; + margin: 0; + padding: 0 5px; +} + +.grid .result-cell:last-child { + float: right; + font-size: 12px; + font-weight: 200; + color: #333; + line-height: 24px; +} diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css b/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css new file mode 100644 index 000000000..2f67d370e --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css @@ -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 rgb(0 0 0 / 75%); + border-right: 2px solid rgb(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 rgb(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 rgb(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: rgb(0 0 0 / 10%); +} + +.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; +} diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-autocomplete.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-autocomplete.js new file mode 100644 index 000000000..98cb12a65 --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-autocomplete.js @@ -0,0 +1,600 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + */ + +'use strict'; + +class ComboboxAutocomplete { + constructor(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 = ''; + + 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.onComboboxKeyDown.bind(this) + ); + this.comboboxNode.addEventListener( + 'keyup', + this.onComboboxKeyUp.bind(this) + ); + this.comboboxNode.addEventListener( + 'click', + this.onComboboxClick.bind(this) + ); + this.comboboxNode.addEventListener( + 'focus', + this.onComboboxFocus.bind(this) + ); + this.comboboxNode.addEventListener('blur', this.onComboboxBlur.bind(this)); + + document.body.addEventListener( + 'pointerup', + this.onBackgroundPointerUp.bind(this), + true + ); + + // initialize pop up menu + + this.listboxNode.addEventListener( + 'pointerover', + this.onListboxPointerover.bind(this) + ); + this.listboxNode.addEventListener( + 'pointerout', + this.onListboxPointerout.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.onOptionClick.bind(this)); + node.addEventListener('pointerover', this.onOptionPointerover.bind(this)); + node.addEventListener('pointerout', this.onOptionPointerout.bind(this)); + } + + this.filterOptions(); + + // Open Button + + var button = this.comboboxNode.nextElementSibling; + + if (button && button.tagName === 'BUTTON') { + button.addEventListener('click', this.onButtonClick.bind(this)); + } + } + + getLowercaseContent(node) { + return node.textContent.toLowerCase(); + } + + isOptionInView(option) { + var bounding = option.getBoundingClientRect(); + return ( + bounding.top >= 0 && + bounding.left >= 0 && + bounding.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + bounding.right <= + (window.innerWidth || document.documentElement.clientWidth) + ); + } + + setActiveDescendant(option) { + if (option && this.listboxHasVisualFocus) { + this.comboboxNode.setAttribute('aria-activedescendant', option.id); + if (!this.isOptionInView(option)) { + option.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + } else { + this.comboboxNode.setAttribute('aria-activedescendant', ''); + } + } + + setValue(value) { + this.filter = value; + this.comboboxNode.value = this.filter; + this.comboboxNode.setSelectionRange(this.filter.length, this.filter.length); + this.filterOptions(); + } + + setOption(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 + ); + } + } + } + } + + setVisualFocusCombobox() { + 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); + } + + setVisualFocusListbox() { + this.comboboxNode.parentNode.classList.remove('focus'); + this.comboboxHasVisualFocus = false; + this.listboxHasVisualFocus = true; + this.listboxNode.classList.add('focus'); + this.setActiveDescendant(this.option); + } + + removeVisualFocusAll() { + 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 + + filterOptions() { + // 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; + } + + setCurrentOptionStyle(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'); + } + } + } + + getPreviousOption(currentOption) { + if (currentOption !== this.firstOption) { + var index = this.filteredOptions.indexOf(currentOption); + return this.filteredOptions[index - 1]; + } + return this.lastOption; + } + + getNextOption(currentOption) { + if (currentOption !== this.lastOption) { + var index = this.filteredOptions.indexOf(currentOption); + return this.filteredOptions[index + 1]; + } + return this.firstOption; + } + + /* MENU DISPLAY METHODS */ + + doesOptionHaveFocus() { + return this.comboboxNode.getAttribute('aria-activedescendant') !== ''; + } + + isOpen() { + return this.listboxNode.style.display === 'block'; + } + + isClosed() { + return this.listboxNode.style.display !== 'block'; + } + + hasOptions() { + return this.filteredOptions.length; + } + + open() { + this.listboxNode.style.display = 'block'; + this.comboboxNode.setAttribute('aria-expanded', 'true'); + this.buttonNode.setAttribute('aria-expanded', 'true'); + } + + close(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.setAttribute('aria-expanded', 'false'); + this.setActiveDescendant(false); + this.comboboxNode.parentNode.classList.add('focus'); + } + } + + /* combobox Events */ + + onComboboxKeyDown(event) { + var flag = false, + 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(); + } + } + + isPrintableCharacter(str) { + return str.length === 1 && str.match(/\S| /); + } + + onComboboxKeyUp(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; + this.filterOptions(); + } + + if (event.key === 'Escape' || event.key === 'Esc') { + return; + } + + switch (event.key) { + case 'Backspace': + this.setVisualFocusCombobox(); + this.setCurrentOptionStyle(false); + this.filter = this.comboboxNode.value; + this.option = null; + this.filterOptions(); + 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(); + } + } + + onComboboxClick() { + if (this.isOpen()) { + this.close(true); + } else { + this.open(); + } + } + + onComboboxFocus() { + this.filter = this.comboboxNode.value; + this.filterOptions(); + this.setVisualFocusCombobox(); + this.option = null; + this.setCurrentOptionStyle(null); + } + + onComboboxBlur() { + this.removeVisualFocusAll(); + } + + onBackgroundPointerUp(event) { + if ( + !this.comboboxNode.contains(event.target) && + !this.listboxNode.contains(event.target) && + !this.buttonNode.contains(event.target) + ) { + this.comboboxHasVisualFocus = false; + this.setCurrentOptionStyle(null); + this.removeVisualFocusAll(); + setTimeout(this.close.bind(this, true), 300); + } + } + + onButtonClick() { + if (this.isOpen()) { + this.close(true); + } else { + this.open(); + } + this.comboboxNode.focus(); + this.setVisualFocusCombobox(); + } + + /* Listbox Events */ + + onListboxPointerover() { + this.hasHover = true; + } + + onListboxPointerout() { + this.hasHover = false; + setTimeout(this.close.bind(this, false), 300); + } + + // Listbox Option Events + + onOptionClick(event) { + this.comboboxNode.value = event.target.textContent; + this.close(true); + } + + onOptionPointerover() { + this.hasHover = true; + this.open(); + } + + onOptionPointerout() { + 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"]'); + new ComboboxAutocomplete(comboboxNode, buttonNode, listboxNode); + } +}); diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-datepicker.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-datepicker.js new file mode 100644 index 000000000..607e824ba --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-datepicker.js @@ -0,0 +1,859 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: ComboboxDatePicker.js + */ + +'use strict'; + +class ComboboxDatePicker { + constructor(cdp) { + this.buttonLabel = 'Date'; + this.monthLabels = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + + this.messageCursorKeys = 'Cursor keys can navigate dates'; + this.lastMessage = ''; + + this.comboboxNode = cdp.querySelector('input[type="text"]'); + this.buttonNode = cdp.querySelector('.group button'); + this.dialogNode = cdp.querySelector('[role="dialog"]'); + this.messageNode = this.dialogNode.querySelector('.dialog-message'); + + this.monthYearNode = this.dialogNode.querySelector('.month-year'); + + this.prevYearNode = this.dialogNode.querySelector('.prev-year'); + this.prevMonthNode = this.dialogNode.querySelector('.prev-month'); + this.nextMonthNode = this.dialogNode.querySelector('.next-month'); + this.nextYearNode = this.dialogNode.querySelector('.next-year'); + + this.okButtonNode = this.dialogNode.querySelector('button[value="ok"]'); + this.cancelButtonNode = this.dialogNode.querySelector( + 'button[value="cancel"]' + ); + + this.tbodyNode = this.dialogNode.querySelector('table.dates tbody'); + + this.lastRowNode = null; + + this.days = []; + + this.focusDay = new Date(); + this.selectedDay = new Date(0, 0, 1); + + this.isMouseDownOnBackground = false; + + this.comboboxNode.addEventListener( + 'keydown', + this.onComboboxKeyDown.bind(this) + ); + this.comboboxNode.addEventListener( + 'click', + this.onComboboxClick.bind(this) + ); + this.comboboxNode.addEventListener( + 'focus', + this.onComboboxFocus.bind(this) + ); + this.comboboxNode.addEventListener('blur', this.onComboboxBlur.bind(this)); + + this.buttonNode.addEventListener( + 'keydown', + this.onButtonKeyDown.bind(this) + ); + this.buttonNode.addEventListener('click', this.onButtonClick.bind(this)); + + this.okButtonNode.addEventListener('click', this.onOkButton.bind(this)); + this.okButtonNode.addEventListener('keydown', this.onOkButton.bind(this)); + + this.cancelButtonNode.addEventListener( + 'click', + this.onCancelButton.bind(this) + ); + this.cancelButtonNode.addEventListener( + 'keydown', + this.onCancelButton.bind(this) + ); + + this.prevMonthNode.addEventListener( + 'click', + this.onPreviousMonthButton.bind(this) + ); + this.nextMonthNode.addEventListener( + 'click', + this.onNextMonthButton.bind(this) + ); + this.prevYearNode.addEventListener( + 'click', + this.onPreviousYearButton.bind(this) + ); + this.nextYearNode.addEventListener( + 'click', + this.onNextYearButton.bind(this) + ); + + this.prevMonthNode.addEventListener( + 'keydown', + this.onPreviousMonthButton.bind(this) + ); + this.nextMonthNode.addEventListener( + 'keydown', + this.onNextMonthButton.bind(this) + ); + this.prevYearNode.addEventListener( + 'keydown', + this.onPreviousYearButton.bind(this) + ); + this.nextYearNode.addEventListener( + 'keydown', + this.onNextYearButton.bind(this) + ); + + document.body.addEventListener( + 'mouseup', + this.onBackgroundMouseUp.bind(this), + true + ); + + // Create Grid of Dates + + this.tbodyNode.innerHTML = ''; + for (var i = 0; i < 6; i++) { + var row = this.tbodyNode.insertRow(i); + this.lastRowNode = row; + for (var j = 0; j < 7; j++) { + var cell = document.createElement('td'); + + cell.setAttribute('tabindex', '-1'); + cell.addEventListener('click', this.onDayClick.bind(this)); + cell.addEventListener('keydown', this.onDayKeyDown.bind(this)); + cell.addEventListener('focus', this.onDayFocus.bind(this)); + + cell.textContent = '-1'; + + row.appendChild(cell); + this.days.push(cell); + } + } + + this.updateGrid(); + this.close(false); + } + + isSameDay(day1, day2) { + return ( + day1.getFullYear() == day2.getFullYear() && + day1.getMonth() == day2.getMonth() && + day1.getDate() == day2.getDate() + ); + } + + isNotSameMonth(day1, day2) { + return ( + day1.getFullYear() != day2.getFullYear() || + day1.getMonth() != day2.getMonth() + ); + } + + updateGrid() { + var i, flag; + var fd = this.focusDay; + + this.monthYearNode.textContent = + this.monthLabels[fd.getMonth()] + ' ' + fd.getFullYear(); + + var firstDayOfMonth = new Date(fd.getFullYear(), fd.getMonth(), 1); + var dayOfWeek = firstDayOfMonth.getDay(); + + firstDayOfMonth.setDate(firstDayOfMonth.getDate() - dayOfWeek); + + var d = new Date(firstDayOfMonth); + + for (i = 0; i < this.days.length; i++) { + flag = d.getMonth() != fd.getMonth(); + this.updateDate( + this.days[i], + flag, + d, + this.isSameDay(d, this.selectedDay) + ); + d.setDate(d.getDate() + 1); + + // Hide last row if all disabled dates + if (i === 35) { + if (flag) { + this.lastRowNode.style.visibility = 'hidden'; + } else { + this.lastRowNode.style.visibility = 'visible'; + } + } + } + } + + setFocusDay(flag) { + if (typeof flag !== 'boolean') { + flag = true; + } + + var fd = this.focusDay; + var getDayFromDataDateAttribute = this.getDayFromDataDateAttribute; + + function checkDay(domNode) { + var d = getDayFromDataDateAttribute(domNode); + + domNode.setAttribute('tabindex', '-1'); + if (this.isSameDay(d, fd)) { + domNode.setAttribute('tabindex', '0'); + if (flag) { + domNode.focus(); + } + } + } + + this.days.forEach(checkDay.bind(this)); + } + + open() { + this.dialogNode.style.display = 'block'; + this.dialogNode.style.zIndex = 2; + + this.comboboxNode.setAttribute('aria-expanded', 'true'); + this.buttonNode.classList.add('open'); + this.getDateFromCombobox(); + this.updateGrid(); + } + + isOpen() { + return window.getComputedStyle(this.dialogNode).display !== 'none'; + } + + close(flag) { + if (typeof flag !== 'boolean') { + // Default is to move focus to combobox + flag = true; + } + + this.setMessage(''); + this.dialogNode.style.display = 'none'; + this.comboboxNode.setAttribute('aria-expanded', 'false'); + this.buttonNode.classList.remove('open'); + + if (flag) { + this.comboboxNode.focus(); + } + } + + onOkButton(event) { + var flag = false; + + switch (event.type) { + case 'keydown': + switch (event.key) { + case 'Tab': + if (!event.shiftKey) { + this.prevYearNode.focus(); + flag = true; + } + break; + + case 'Esc': + case 'Escape': + this.close(); + flag = true; + break; + + default: + break; + } + break; + + case 'click': + this.setComboboxDate(); + this.close(); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onCancelButton(event) { + var flag = false; + + switch (event.type) { + case 'keydown': + switch (event.key) { + case 'Esc': + case 'Escape': + this.close(); + flag = true; + break; + + default: + break; + } + break; + + case 'click': + this.close(); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onNextYearButton(event) { + var flag = false; + + switch (event.type) { + case 'keydown': + switch (event.key) { + case 'Esc': + case 'Escape': + this.close(); + flag = true; + break; + + case 'Enter': + this.moveToNextYear(); + this.setFocusDay(false); + flag = true; + break; + } + + break; + + case 'click': + this.moveToNextYear(); + this.setFocusDay(false); + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onPreviousYearButton(event) { + var flag = false; + + switch (event.type) { + case 'keydown': + switch (event.key) { + case 'Enter': + this.moveToPreviousYear(); + this.setFocusDay(false); + flag = true; + break; + + case 'Tab': + if (event.shiftKey) { + this.okButtonNode.focus(); + flag = true; + } + break; + + case 'Esc': + case 'Escape': + this.close(); + flag = true; + break; + + default: + break; + } + + break; + + case 'click': + this.moveToPreviousYear(); + this.setFocusDay(false); + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onNextMonthButton(event) { + var flag = false; + + switch (event.type) { + case 'keydown': + switch (event.key) { + case 'Esc': + case 'Escape': + this.close(); + flag = true; + break; + + case 'Enter': + this.moveToNextMonth(); + this.setFocusDay(false); + flag = true; + break; + } + + break; + + case 'click': + this.moveToNextMonth(); + this.setFocusDay(false); + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onPreviousMonthButton(event) { + var flag = false; + + switch (event.type) { + case 'keydown': + switch (event.key) { + case 'Esc': + case 'Escape': + this.close(); + flag = true; + break; + + case 'Enter': + this.moveToPreviousMonth(); + this.setFocusDay(false); + flag = true; + break; + } + + break; + + case 'click': + this.moveToPreviousMonth(); + this.setFocusDay(false); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + moveFocusToDay(day) { + var d = this.focusDay; + + this.focusDay = day; + + if ( + d.getMonth() != this.focusDay.getMonth() || + d.getYear() != this.focusDay.getYear() + ) { + this.updateGrid(); + } + this.setFocusDay(); + } + + moveToNextYear() { + this.focusDay.setFullYear(this.focusDay.getFullYear() + 1); + this.updateGrid(); + } + + moveToPreviousYear() { + this.focusDay.setFullYear(this.focusDay.getFullYear() - 1); + this.updateGrid(); + } + + moveToNextMonth() { + this.focusDay.setMonth(this.focusDay.getMonth() + 1); + this.updateGrid(); + } + + moveToPreviousMonth() { + this.focusDay.setMonth(this.focusDay.getMonth() - 1); + this.updateGrid(); + } + + moveFocusToNextDay() { + var d = new Date(this.focusDay); + d.setDate(d.getDate() + 1); + this.moveFocusToDay(d); + } + + moveFocusToNextWeek() { + var d = new Date(this.focusDay); + d.setDate(d.getDate() + 7); + this.moveFocusToDay(d); + } + + moveFocusToPreviousDay() { + var d = new Date(this.focusDay); + d.setDate(d.getDate() - 1); + this.moveFocusToDay(d); + } + + moveFocusToPreviousWeek() { + var d = new Date(this.focusDay); + d.setDate(d.getDate() - 7); + this.moveFocusToDay(d); + } + + moveFocusToFirstDayOfWeek() { + var d = new Date(this.focusDay); + d.setDate(d.getDate() - d.getDay()); + this.moveFocusToDay(d); + } + + moveFocusToLastDayOfWeek() { + var d = new Date(this.focusDay); + d.setDate(d.getDate() + (6 - d.getDay())); + this.moveFocusToDay(d); + } + + // Day methods + + isDayDisabled(domNode) { + return domNode.classList.contains('disabled'); + } + + getDayFromDataDateAttribute(domNode) { + var parts = domNode.getAttribute('data-date').split('-'); + return new Date(parts[0], parseInt(parts[1]) - 1, parts[2]); + } + + updateDate(domNode, disable, day, selected) { + var d = day.getDate().toString(); + if (day.getDate() <= 9) { + d = '0' + d; + } + + var m = day.getMonth() + 1; + if (day.getMonth() < 9) { + m = '0' + m; + } + + domNode.setAttribute('tabindex', '-1'); + domNode.removeAttribute('aria-selected'); + domNode.setAttribute('data-date', day.getFullYear() + '-' + m + '-' + d); + + if (disable) { + domNode.classList.add('disabled'); + domNode.textContent = ''; + } else { + domNode.classList.remove('disabled'); + domNode.textContent = day.getDate(); + if (selected) { + domNode.setAttribute('aria-selected', 'true'); + domNode.setAttribute('tabindex', '0'); + } + } + } + + updateSelected(domNode) { + for (var i = 0; i < this.days.length; i++) { + var day = this.days[i]; + if (day === domNode) { + day.setAttribute('aria-selected', 'true'); + } else { + day.removeAttribute('aria-selected'); + } + } + } + + onDayKeyDown(event) { + var flag = false; + + switch (event.key) { + case 'Esc': + case 'Escape': + this.close(); + break; + + case ' ': + this.updateSelected(event.currentTarget); + this.setComboboxDate(event.currentTarget); + flag = true; + break; + + case 'Enter': + this.setComboboxDate(event.currentTarget); + this.close(); + break; + + case 'Tab': + this.cancelButtonNode.focus(); + if (event.shiftKey) { + this.nextYearNode.focus(); + } + this.setMessage(''); + flag = true; + break; + + case 'Right': + case 'ArrowRight': + this.moveFocusToNextDay(); + flag = true; + break; + + case 'Left': + case 'ArrowLeft': + this.moveFocusToPreviousDay(); + flag = true; + break; + + case 'Down': + case 'ArrowDown': + this.moveFocusToNextWeek(); + flag = true; + break; + + case 'Up': + case 'ArrowUp': + this.moveFocusToPreviousWeek(); + flag = true; + break; + + case 'PageUp': + if (event.shiftKey) { + this.moveToPreviousYear(); + } else { + this.moveToPreviousMonth(); + } + this.setFocusDay(); + flag = true; + break; + + case 'PageDown': + if (event.shiftKey) { + this.moveToNextYear(); + } else { + this.moveToNextMonth(); + } + this.setFocusDay(); + flag = true; + break; + + case 'Home': + this.moveFocusToFirstDayOfWeek(); + flag = true; + break; + + case 'End': + this.moveFocusToLastDayOfWeek(); + flag = true; + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onDayClick(event) { + if (!this.isDayDisabled(event.currentTarget)) { + this.setComboboxDate(event.currentTarget); + this.close(); + } + + event.stopPropagation(); + event.preventDefault(); + } + + onDayFocus() { + this.setMessage(this.messageCursorKeys); + } + + // Combobox methods + + setComboboxDate(domNode) { + var d = this.focusDay; + + if (domNode) { + d = this.getDayFromDataDateAttribute(domNode); + } + + this.comboboxNode.value = + d.getMonth() + 1 + '/' + d.getDate() + '/' + d.getFullYear(); + } + + getDateFromCombobox() { + var parts = this.comboboxNode.value.split('/'); + + if ( + parts.length === 3 && + Number.isInteger(parseInt(parts[0])) && + Number.isInteger(parseInt(parts[1])) && + Number.isInteger(parseInt(parts[2])) + ) { + this.focusDay = new Date( + parseInt(parts[2]), + parseInt(parts[0]) - 1, + parseInt(parts[1]) + ); + this.selectedDay = new Date(this.focusDay); + } else { + // If not a valid date (MM/DD/YY) initialize with todays date + this.focusDay = new Date(); + this.selectedDay = new Date(0, 0, 1); + } + } + + setMessage(str) { + function setMessageDelayed() { + this.messageNode.textContent = str; + } + + if (str !== this.lastMessage) { + setTimeout(setMessageDelayed.bind(this), 200); + this.lastMessage = str; + } + } + + onComboboxKeyDown(event) { + var flag = false; + + if (event.ctrlKey || event.shiftKey) { + return; + } + + switch (event.key) { + case 'Down': + case 'ArrowDown': + this.open(); + this.setFocusDay(); + flag = true; + break; + + case 'Esc': + case 'Escape': + if (this.isOpen()) { + this.close(false); + } else { + this.comboboxNode.value = ''; + } + this.option = null; + flag = true; + break; + + case 'Tab': + this.close(false); + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onComboboxClick(event) { + if (this.isOpen()) { + this.close(false); + } else { + this.open(); + } + + event.stopPropagation(); + event.preventDefault(); + } + + onComboboxFocus(event) { + event.currentTarget.parentNode.classList.add('focus'); + } + + onComboboxBlur(event) { + event.currentTarget.parentNode.classList.remove('focus'); + } + + onButtonKeyDown(event) { + if (event.key === 'Enter' || event.key === ' ') { + this.open(); + this.setFocusDay(); + + event.stopPropagation(); + event.preventDefault(); + } + } + + onButtonClick(event) { + if (this.isOpen()) { + this.close(); + } else { + this.open(); + this.setFocusDay(); + } + + event.stopPropagation(); + event.preventDefault(); + } + + onBackgroundMouseUp(event) { + if ( + !this.comboboxNode.contains(event.target) && + !this.buttonNode.contains(event.target) && + !this.dialogNode.contains(event.target) + ) { + if (this.isOpen()) { + this.close(false); + event.stopPropagation(); + event.preventDefault(); + } + } + } +} + +// Initialize menu button date picker + +window.addEventListener('load', function () { + var comboboxDatePickers = document.querySelectorAll('.combobox-datepicker'); + comboboxDatePickers.forEach(function (dp) { + new ComboboxDatePicker(dp); + }); +}); diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo-example.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo-example.js new file mode 100644 index 000000000..4a47e32df --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo-example.js @@ -0,0 +1,101 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * ARIA Combobox Examples + */ + +'use strict'; + +var aria = aria || {}; + +var FRUITS_AND_VEGGIES = [ + ['Apple', 'Fruit'], + ['Artichoke', 'Vegetable'], + ['Asparagus', 'Vegetable'], + ['Banana', 'Fruit'], + ['Beets', 'Vegetable'], + ['Bell pepper', 'Vegetable'], + ['Broccoli', 'Vegetable'], + ['Brussels sprout', 'Vegetable'], + ['Cabbage', 'Vegetable'], + ['Carrot', 'Vegetable'], + ['Cauliflower', 'Vegetable'], + ['Celery', 'Vegetable'], + ['Chard', 'Vegetable'], + ['Chicory', 'Vegetable'], + ['Corn', 'Vegetable'], + ['Cucumber', 'Vegetable'], + ['Daikon', 'Vegetable'], + ['Date', 'Fruit'], + ['Edamame', 'Vegetable'], + ['Eggplant', 'Vegetable'], + ['Elderberry', 'Fruit'], + ['Fennel', 'Vegetable'], + ['Fig', 'Fruit'], + ['Garlic', 'Vegetable'], + ['Grape', 'Fruit'], + ['Honeydew melon', 'Fruit'], + ['Iceberg lettuce', 'Vegetable'], + ['Jerusalem artichoke', 'Vegetable'], + ['Kale', 'Vegetable'], + ['Kiwi', 'Fruit'], + ['Leek', 'Vegetable'], + ['Lemon', 'Fruit'], + ['Mango', 'Fruit'], + ['Mangosteen', 'Fruit'], + ['Melon', 'Fruit'], + ['Mushroom', 'Fungus'], + ['Nectarine', 'Fruit'], + ['Okra', 'Vegetable'], + ['Olive', 'Vegetable'], + ['Onion', 'Vegetable'], + ['Orange', 'Fruit'], + ['Parsnip', 'Vegetable'], + ['Pea', 'Vegetable'], + ['Pear', 'Fruit'], + ['Pineapple', 'Fruit'], + ['Potato', 'Vegetable'], + ['Pumpkin', 'Fruit'], + ['Quince', 'Fruit'], + ['Radish', 'Vegetable'], + ['Rhubarb', 'Vegetable'], + ['Shallot', 'Vegetable'], + ['Spinach', 'Vegetable'], + ['Squash', 'Vegetable'], + ['Strawberry', 'Fruit'], + ['Sweet potato', 'Vegetable'], + ['Tomato', 'Fruit'], + ['Turnip', 'Vegetable'], + ['Ugli fruit', 'Fruit'], + ['Victoria plum', 'Fruit'], + ['Watercress', 'Vegetable'], + ['Watermelon', 'Fruit'], + ['Yam', 'Vegetable'], + ['Zucchini', 'Vegetable'], +]; + +function searchVeggies(searchString) { + var results = []; + + for (var i = 0; i < FRUITS_AND_VEGGIES.length; i++) { + var veggie = FRUITS_AND_VEGGIES[i][0].toLowerCase(); + if (veggie.indexOf(searchString.toLowerCase()) === 0) { + results.push(FRUITS_AND_VEGGIES[i]); + } + } + + return results; +} + +/** + * @function onload + * @description Initialize the combobox examples once the page has loaded + */ +window.addEventListener('load', function () { + new aria.GridCombobox( + document.getElementById('ex1-input'), + document.getElementById('ex1-grid'), + searchVeggies + ); +}); diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo.js new file mode 100644 index 000000000..04d2c902b --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo.js @@ -0,0 +1,318 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + */ + +'use strict'; + +var aria = aria || {}; + +/** + * @class + * @description + * Combobox object representing the state and interactions for a combobox + * widget + * @param input + * The input node + * @param grid + * The grid node to load results in + * @param searchFn + * The search function. The function accepts a search string and returns an + * array of results. + */ +aria.GridCombobox = function (input, grid, searchFn) { + this.input = input; + this.grid = grid; + this.searchFn = searchFn; + this.activeRowIndex = -1; + this.activeColIndex = 0; + this.rowsCount = 0; + this.colsCount = 0; + this.gridFocused = false; + this.shown = false; + this.selectionCol = 0; + + this.setupEvents(); +}; + +aria.GridCombobox.prototype.setupEvents = function () { + document.body.addEventListener('click', this.handleBodyClick.bind(this)); + this.input.addEventListener('keyup', this.handleInputKeyUp.bind(this)); + this.input.addEventListener('keydown', this.handleInputKeyDown.bind(this)); + this.input.addEventListener('focus', this.handleInputFocus.bind(this)); + this.grid.addEventListener('click', this.handleGridClick.bind(this)); +}; + +aria.GridCombobox.prototype.handleBodyClick = function (evt) { + if (evt.target === this.input || this.grid.contains(evt.target)) { + return; + } + this.hideResults(); +}; + +aria.GridCombobox.prototype.handleInputKeyUp = function (evt) { + var key = evt.which || evt.keyCode; + + switch (key) { + case aria.KeyCode.UP: + case aria.KeyCode.DOWN: + case aria.KeyCode.ESC: + case aria.KeyCode.RETURN: + evt.preventDefault(); + return; + case aria.KeyCode.LEFT: + case aria.KeyCode.RIGHT: + if (this.gridFocused) { + evt.preventDefault(); + return; + } + break; + default: + this.updateResults(); + } +}; + +aria.GridCombobox.prototype.handleInputKeyDown = function (evt) { + var key = evt.which || evt.keyCode; + var activeRowIndex = this.activeRowIndex; + var activeColIndex = this.activeColIndex; + + if (key === aria.KeyCode.ESC) { + if (this.gridFocused) { + this.gridFocused = false; + this.removeFocusCell(this.activeRowIndex, this.activeColIndex); + this.activeRowIndex = -1; + this.activeColIndex = 0; + this.input.setAttribute('aria-activedescendant', ''); + } else { + if (!this.shown) { + setTimeout( + function () { + // On Firefox, input does not get cleared here unless wrapped in + // a setTimeout + this.input.value = ''; + }.bind(this), + 1 + ); + } + } + if (this.shown) { + this.hideResults(); + } + return; + } + + if (this.rowsCount < 1) { + return; + } + + var prevActive = this.getItemAt(activeRowIndex, this.selectionCol); + var activeItem; + + switch (key) { + case aria.KeyCode.UP: + this.gridFocused = true; + activeRowIndex = this.getRowIndex(key); + evt.preventDefault(); + break; + case aria.KeyCode.DOWN: + this.gridFocused = true; + activeRowIndex = this.getRowIndex(key); + evt.preventDefault(); + break; + case aria.KeyCode.LEFT: + if (activeColIndex <= 0) { + activeColIndex = this.colsCount - 1; + activeRowIndex = this.getRowIndex(key); + } else { + activeColIndex--; + } + if (this.gridFocused) { + evt.preventDefault(); + } + break; + case aria.KeyCode.RIGHT: + if (activeColIndex === -1 || activeColIndex >= this.colsCount - 1) { + activeColIndex = 0; + activeRowIndex = this.getRowIndex(key); + } else { + activeColIndex++; + } + if (this.gridFocused) { + evt.preventDefault(); + } + break; + case aria.KeyCode.RETURN: + activeItem = this.getItemAt(activeRowIndex, this.selectionCol); + this.selectItem(activeItem); + this.gridFocused = false; + return; + case aria.KeyCode.TAB: + this.hideResults(); + return; + default: + return; + } + + if (prevActive) { + this.removeFocusCell(this.activeRowIndex, this.activeColIndex); + prevActive.setAttribute('aria-selected', 'false'); + } + + activeItem = this.getItemAt(activeRowIndex, activeColIndex); + this.activeRowIndex = activeRowIndex; + this.activeColIndex = activeColIndex; + + if (activeItem) { + this.input.setAttribute( + 'aria-activedescendant', + 'result-item-' + activeRowIndex + 'x' + activeColIndex + ); + this.focusCell(activeRowIndex, activeColIndex); + var selectedItem = this.getItemAt(activeRowIndex, this.selectionCol); + selectedItem.setAttribute('aria-selected', 'true'); + } else { + this.input.setAttribute('aria-activedescendant', ''); + } +}; + +aria.GridCombobox.prototype.handleInputFocus = function () { + this.updateResults(); +}; + +aria.GridCombobox.prototype.handleGridClick = function (evt) { + if (!evt.target) { + return; + } + + var row; + if (evt.target.getAttribute('role') === 'row') { + row = evt.target; + } else if (evt.target.getAttribute('role') === 'gridcell') { + row = evt.target.parentNode; + } else { + return; + } + + var selectItem = row.querySelector('.result-cell'); + this.selectItem(selectItem); +}; + +aria.GridCombobox.prototype.isElementInView = function (element) { + var bounding = element.getBoundingClientRect(); + + return ( + bounding.top >= 0 && + bounding.left >= 0 && + bounding.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + bounding.right <= + (window.innerWidth || document.documentElement.clientWidth) + ); +}; + +aria.GridCombobox.prototype.updateResults = function () { + var searchString = this.input.value; + var results = this.searchFn(searchString); + + this.hideResults(); + + if (!searchString) { + results = []; + } + + if (results.length) { + for (var row = 0; row < results.length; row++) { + var resultRow = document.createElement('div'); + resultRow.className = 'result-row'; + resultRow.setAttribute('role', 'row'); + resultRow.setAttribute('id', 'result-row-' + row); + for (var col = 0; col < results[row].length; col++) { + var resultCell = document.createElement('div'); + resultCell.className = 'result-cell'; + resultCell.setAttribute('role', 'gridcell'); + resultCell.setAttribute('id', 'result-item-' + row + 'x' + col); + resultCell.innerText = results[row][col]; + resultRow.appendChild(resultCell); + } + this.grid.appendChild(resultRow); + } + aria.Utils.removeClass(this.grid, 'hidden'); + this.input.setAttribute('aria-expanded', 'true'); + this.rowsCount = results.length; + this.colsCount = results.length ? results[0].length : 0; + this.shown = true; + } +}; + +aria.GridCombobox.prototype.getRowIndex = function (key) { + var activeRowIndex = this.activeRowIndex; + + switch (key) { + case aria.KeyCode.UP: + case aria.KeyCode.LEFT: + if (activeRowIndex <= 0) { + activeRowIndex = this.rowsCount - 1; + } else { + activeRowIndex--; + } + break; + case aria.KeyCode.DOWN: + case aria.KeyCode.RIGHT: + if (activeRowIndex === -1 || activeRowIndex >= this.rowsCount - 1) { + activeRowIndex = 0; + } else { + activeRowIndex++; + } + } + + return activeRowIndex; +}; + +aria.GridCombobox.prototype.getItemAt = function (rowIndex, colIndex) { + return document.getElementById('result-item-' + rowIndex + 'x' + colIndex); +}; + +aria.GridCombobox.prototype.selectItem = function (item) { + if (item) { + this.input.value = item.innerText; + this.hideResults(); + } +}; + +aria.GridCombobox.prototype.hideResults = function () { + this.gridFocused = false; + this.shown = false; + this.activeRowIndex = -1; + this.activeColIndex = 0; + this.grid.innerHTML = ''; + aria.Utils.addClass(this.grid, 'hidden'); + this.input.setAttribute('aria-expanded', 'false'); + this.rowsCount = 0; + this.colsCount = 0; + this.input.setAttribute('aria-activedescendant', ''); + + // ensure the input is in view + if (!this.isElementInView(this.input)) { + this.input.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } +}; + +aria.GridCombobox.prototype.removeFocusCell = function (rowIndex, colIndex) { + var row = document.getElementById('result-row-' + rowIndex); + aria.Utils.removeClass(row, 'focused'); + var cell = this.getItemAt(rowIndex, colIndex); + aria.Utils.removeClass(cell, 'focused-cell'); +}; + +aria.GridCombobox.prototype.focusCell = function (rowIndex, colIndex) { + var row = document.getElementById('result-row-' + rowIndex); + aria.Utils.addClass(row, 'focused'); + var cell = this.getItemAt(rowIndex, colIndex); + aria.Utils.addClass(cell, 'focused-cell'); + + // ensure the cell is in view + if (!this.isElementInView(cell)) { + cell.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } +}; diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js new file mode 100644 index 000000000..fdf5874a8 --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js @@ -0,0 +1,417 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + */ + +'use strict'; + +// Save a list of named combobox actions, for future readability +const SelectActions = { + Close: 0, + 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 element is visible in browser view port +function isElementInView(element) { + var bounding = element.getBoundingClientRect(); + + return ( + bounding.top >= 0 && + bounding.left >= 0 && + bounding.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + bounding.right <= + (window.innerWidth || document.documentElement.clientWidth) + ); +} + +// 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); + } + + // ensure the new option is visible on screen + // ensure the new option is in view + if (!isElementInView(options[index])) { + options[index].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } +}; + +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); + + if (activeID === '' && !isElementInView(this.comboEl)) { + this.comboEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + + // 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); + }); +}); From 6f06454bfc6f8ade117d3d9061bbbe862c06fa49 Mon Sep 17 00:00:00 2001 From: Isabel Del Castillo Date: Tue, 26 Apr 2022 14:53:01 -0500 Subject: [PATCH 2/8] Example: remove unneeded CSS/JS references and update page title --- .../combobox-select-only.html | 2 +- .../2022-4-26_144616/css/select-only.css | 10 +- .../2022-4-26_144616/js/select-only.js | 31 +- .../reference/css/select-only.css | 103 ----- .../reference/js/select-only.js | 390 ------------------ 5 files changed, 8 insertions(+), 528 deletions(-) delete mode 100644 tests/combobox-select-only/reference/css/select-only.css delete mode 100644 tests/combobox-select-only/reference/js/select-only.js diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html index 0868ba26c..33992af8a 100644 --- a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html +++ b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html @@ -2,7 +2,7 @@ -Select-Only Combobox Example | WAI-ARIA Authoring Practices 1.2 +Select-Only Combobox Example diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css b/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css index 2f67d370e..1d8beb3a7 100644 --- a/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css +++ b/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css @@ -12,8 +12,8 @@ } .combo::after { - border-bottom: 2px solid rgb(0 0 0 / 75%); - border-right: 2px solid rgb(0 0 0 / 75%); + 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; @@ -27,7 +27,7 @@ .combo-input { background-color: #f5f5f5; - border: 2px solid rgb(0 0 0 / 75%); + border: 2px solid rgba(0, 0, 0, 0.75); border-radius: 4px; display: block; font-size: 1em; @@ -56,7 +56,7 @@ .combo-menu { background-color: #f5f5f5; - border: 1px solid rgb(0 0 0 / 75%); + border: 1px solid rgba(0, 0, 0, 0.75); border-radius: 0 0 4px 4px; display: none; max-height: 300px; @@ -77,7 +77,7 @@ } .combo-option:hover { - background-color: rgb(0 0 0 / 10%); + background-color: rgba(0, 0, 0, 0.1); } .combo-option.option-current { diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js index fdf5874a8..2398e1b9d 100644 --- a/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js +++ b/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js @@ -128,20 +128,6 @@ function getUpdatedIndex(currentIndex, maxIndex, action) { } } -// check if element is visible in browser view port -function isElementInView(element) { - var bounding = element.getBoundingClientRect(); - - return ( - bounding.top >= 0 && - bounding.left >= 0 && - bounding.bottom <= - (window.innerHeight || document.documentElement.clientHeight) && - bounding.right <= - (window.innerWidth || document.documentElement.clientWidth) - ); -} - // check if an element is currently scrollable function isScrollable(element) { return element && element.clientHeight < element.scrollHeight; @@ -332,12 +318,6 @@ Select.prototype.onOptionChange = function (index) { if (isScrollable(this.listboxEl)) { maintainScrollVisibility(options[index], this.listboxEl); } - - // ensure the new option is visible on screen - // ensure the new option is in view - if (!isElementInView(options[index])) { - options[index].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } }; Select.prototype.onOptionClick = function (index) { @@ -384,10 +364,6 @@ Select.prototype.updateMenuState = function (open, callFocus = true) { const activeID = open ? `${this.idBase}-${this.activeIndex}` : ''; this.comboEl.setAttribute('aria-activedescendant', activeID); - if (activeID === '' && !isElementInView(this.comboEl)) { - this.comboEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } - // move focus back to the combobox, if needed callFocus && this.comboEl.focus(); }; @@ -409,9 +385,6 @@ window.addEventListener('load', function () { 'Guava', 'Huckleberry', ]; - const selectEls = document.querySelectorAll('.js-select'); - - selectEls.forEach((el) => { - new Select(el, options); - }); + const selectEl = document.querySelector('.js-select'); + document.defaultView.selectController = new Select(selectEl, options); }); diff --git a/tests/combobox-select-only/reference/css/select-only.css b/tests/combobox-select-only/reference/css/select-only.css deleted file mode 100644 index 1d8beb3a7..000000000 --- a/tests/combobox-select-only/reference/css/select-only.css +++ /dev/null @@ -1,103 +0,0 @@ -.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; -} diff --git a/tests/combobox-select-only/reference/js/select-only.js b/tests/combobox-select-only/reference/js/select-only.js deleted file mode 100644 index 2398e1b9d..000000000 --- a/tests/combobox-select-only/reference/js/select-only.js +++ /dev/null @@ -1,390 +0,0 @@ -/* - * This content is licensed according to the W3C Software License at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - */ - -'use strict'; - -// Save a list of named combobox actions, for future readability -const SelectActions = { - Close: 0, - 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 selectEl = document.querySelector('.js-select'); - document.defaultView.selectController = new Select(selectEl, options); -}); From 9514609a586910f933bd42242247e262b6092431 Mon Sep 17 00:00:00 2001 From: Isabel Del Castillo Date: Tue, 26 Apr 2022 14:57:04 -0500 Subject: [PATCH 3/8] Example: remove 'Related Links' nav and similar example links, as well as 'Accessibility Features', 'Keyboard Support', 'Role, Property, State, and Tabindex Attributes' and source code sections and footer nav --- .../combobox-select-only.html | 393 +----------------- 1 file changed, 3 insertions(+), 390 deletions(-) diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html index 33992af8a..7d08c8cb7 100644 --- a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html +++ b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.html @@ -4,48 +4,25 @@ Select-Only Combobox Example - - - - - - - -

Select-Only Combobox Example

- The following example implementation of the ARIA design pattern for combobox + The following example implementation of the ARIA design pattern for combobox demonstrates a single-select combobox widget that is functionally similar to an HTML select element. Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. However, like an HTML <select>, users can type characters to select matching options.

-

Similar examples include:

- -

Example

+ Navigate forwards from here
@@ -66,373 +43,9 @@

Example

+ Navigate backwards from here
- -
-

Accessibility Features

-

While the functionality and user experience of this example are nearly equivalent to an HTML select element with the attribute size="1", the following differences in behavior are implemented to improve both accessibility and general usability.

-
    -
  1. - If the combobox is collapsed and the user types printable characters, the listbox is displayed and receives accessibility focus via aria-activedescendant. - This enables users to perceive the presence of the options, and enables assistive technology users to comprehend the size of the list of options. -
  2. -
  3. - Navigating the list of options does not set the value of the input. - This gives screen reader users, who need to navigate among the options to perceive them, the ability to explore options without losing the current value of the input. - The value is set when users press Space, Enter, or Tab, or when focus moves out of the combobox. - The current value is retained if the listbox is closed with Escape or if the user collapses the list by clicking the input. -
  4. -
  5. Browsers do not manage visibility of elements referenced by aria-activedescendant like they do for elements with focus. When a keyboard event changes the active option in the listbox, the JavaScript scrolls the option referenced by aria-activedescendant into view. Managing aria-activedescendant visibility is essential to accessibility for people who use a browser's zoom feature to increase the size of content.
  6. -
-
- -
-

Keyboard Support

-

- The example combobox on this page implements the following keyboard interface. - Other variations and options for the keyboard interface are described in the - Keyboard Interaction section of the combobox design pattern. -

-

Closed Combobox

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyFunction
Down Arrow -
    -
  • Opens the listbox if it is not already displayed without moving focus or changing selection.
  • -
  • DOM focus remains on the combobox.
  • -
-
Alt + Down Arrow - Opens the listbox without moving focus or changing selection. -
Up Arrow -
    -
  • First opens the listbox if it is not already displayed and then moves visual focus to the first option.
  • -
  • DOM focus remains on the combobox.
  • -
-
Enter - Opens the listbox without moving focus or changing selection. -
Space - Opens the listbox without moving focus or changing selection. -
Home - Opens the listbox and moves visual focus to the first option. -
End - Opens the listbox and moves visual focus to the last option. -
Printable Characters -
    -
  • First opens the listbox if it is not already displayed and then moves visual focus to the first option that matches the typed character.
  • -
  • If multiple keys are typed in quick succession, visual focus moves to the first option that matches the full string.
  • -
  • If the same character is typed in succession, visual focus cycles among the options starting with that character
  • -
-
-

Listbox Popup

-

- NOTE: When visual focus is in the listbox, DOM focus remains on the combobox and the value of aria-activedescendant on the combobox is set to a value that refers to the listbox option that is visually indicated as focused. - Where the following descriptions of keyboard commands mention focus, they are referring to the visual focus indicator. - For more information about this focus management technique, see - Using aria-activedescendant to Manage Focus. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyFunction
Enter -
    -
  • Sets the value to the content of the focused option in the listbox.
  • -
  • Closes the listbox.
  • -
  • Sets visual focus on the combobox.
  • -
-
Space -
    -
  • Sets the value to the content of the focused option in the listbox.
  • -
  • Closes the listbox.
  • -
  • Sets visual focus on the combobox.
  • -
-
Tab -
    -
  • Sets the value to the content of the focused option in the listbox.
  • -
  • Closes the listbox.
  • -
  • Performs the default action, moving focus to the next focusable element. - Note: the native <select> element closes the listbox but does not move focus on tab. - This pattern matches the behavior of the other comboboxes rather than the native element in this case.
  • -
-
Escape -
    -
  • Closes the listbox.
  • -
  • Sets visual focus on the combobox.
  • -
-
Down Arrow -
    -
  • Moves visual focus to the next option.
  • -
  • If visual focus is on the last option, visual focus does not move.
  • -
-
Up Arrow -
    -
  • Moves visual focus to the previous option.
  • -
  • If visual focus is on the first option, visual focus does not move.
  • -
-
Alt + Up Arrow -
    -
  • Sets the value to the content of the focused option in the listbox.
  • -
  • Closes the listbox.
  • -
  • Sets visual focus on the combobox.
  • -
-
HomeMoves visual focus to the first option.
EndMoves visual focus to the last option.
PageUpJumps visual focus up 10 options (or to first option).
PageDownJumps visual focus down 10 options (or to last option).
Printable Characters -
    -
  • First opens the listbox if it is not already displayed and then moves visual focus to the first option that matches the typed character.
  • -
  • If multiple keys are typed in quick succession, visual focus moves to the first option that matches the full string.
  • -
  • If the same character is typed in succession, visual focus cycles among the options starting with that character
  • -
-
-
- -
-

Role, Property, State, and Tabindex Attributes

-

- The example combobox on this page implements the following ARIA roles, states, and properties. - Information about other ways of applying ARIA roles, states, and properties is available in the - Roles, States, and Properties section of the combobox design pattern. -

-

Combobox

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RoleAttributeElementUsage
- combobox - divIdentifies the input as a combobox.
- aria-labelledby="#IDREF" - divIdentifies the element that labels the combobox.
- aria-controls="#IDREF" - divIdentifies the element that serves as the popup.
- aria-expanded="false" - divIndicates that the popup element is not displayed.
- aria-expanded="true" - divIndicates that the popup element is displayed.
- aria-activedescendant="IDREF" - div -
    -
  • When an option in the listbox is visually indicated as having keyboard focus, refers to that option.
  • -
  • When navigation keys, such as Down Arrow, are pressed, the JavaScript changes the value.
  • -
  • Enables assistive technologies to know which element the application regards as focused while DOM focus remains on the input element.
  • -
  • - For more information about this focus management technique, see - Using aria-activedescendant to Manage Focus. -
  • -
-
-

Listbox Popup

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RoleAttributeElementUsage
- listbox - - div - Identifies the element as a listbox.
- option - div -
    -
  • Identifies the element as a listbox option.
  • -
  • The text content of the element provides the accessible name of the option.
  • -
-
- aria-selected="true" - li -
    -
  • Specified on an option in the listbox when it is visually highlighted as selected.
  • -
  • Occurs only when an option in the list is referenced by aria-activedescendant.
  • -
-
-
- -
-

Javascript and CSS Source Code

- -
- -
-

HTML Source Code

- -
- - -
- From 5ef200f1bca0d2fa1ca2101b455ba3fab6275dd5 Mon Sep 17 00:00:00 2001 From: Isabel Del Castillo Date: Tue, 26 Apr 2022 14:59:15 -0500 Subject: [PATCH 4/8] Remove previous APG example --- .../reference/combobox-select-only.html | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 tests/combobox-select-only/reference/combobox-select-only.html diff --git a/tests/combobox-select-only/reference/combobox-select-only.html b/tests/combobox-select-only/reference/combobox-select-only.html deleted file mode 100644 index c67aa4049..000000000 --- a/tests/combobox-select-only/reference/combobox-select-only.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - -Select-Only Combobox Example - - - - - - -
-

Select-Only Combobox Example

-

- The following example implementation of the ARIA design pattern for combobox - demonstrates a single-select combobox widget that is functionally similar to an HTML select element. - Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. - However, like an HTML <select>, users can type characters to select matching options. -

-
-
-

Example

-
- - Navigate forwards from here -
- -
- -
- - -
-
-
- Navigate backwards from here - -
-
- - From 165d815aad6d715380593571ffedf7719215bcb9 Mon Sep 17 00:00:00 2001 From: Isabel Del Castillo Date: Tue, 26 Apr 2022 15:01:43 -0500 Subject: [PATCH 5/8] Remove previous setupScripts --- ...ox-select-only.moveFocusAfterCombobox.html | 74 ------------------ ...x-select-only.moveFocusBeforeCombobox.html | 74 ------------------ ...bobox-select-only.moveFocusToCombobox.html | 74 ------------------ .../combobox-select-only.openListbox.html | 75 ------------------ ...mbobox-select-only.openListboxToApple.html | 76 ------------------- ...mbobox-select-only.openListboxToGuava.html | 76 ------------------- ...-select-only.openListboxToHuckleberry.html | 76 ------------------- 7 files changed, 525 deletions(-) delete mode 100644 tests/combobox-select-only/reference/combobox-select-only.moveFocusAfterCombobox.html delete mode 100644 tests/combobox-select-only/reference/combobox-select-only.moveFocusBeforeCombobox.html delete mode 100644 tests/combobox-select-only/reference/combobox-select-only.moveFocusToCombobox.html delete mode 100644 tests/combobox-select-only/reference/combobox-select-only.openListbox.html delete mode 100644 tests/combobox-select-only/reference/combobox-select-only.openListboxToApple.html delete mode 100644 tests/combobox-select-only/reference/combobox-select-only.openListboxToGuava.html delete mode 100644 tests/combobox-select-only/reference/combobox-select-only.openListboxToHuckleberry.html diff --git a/tests/combobox-select-only/reference/combobox-select-only.moveFocusAfterCombobox.html b/tests/combobox-select-only/reference/combobox-select-only.moveFocusAfterCombobox.html deleted file mode 100644 index 86466a6c2..000000000 --- a/tests/combobox-select-only/reference/combobox-select-only.moveFocusAfterCombobox.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - -Select-Only Combobox Example - - - - - - - - - -
-

Select-Only Combobox Example

-

- The following example implementation of the ARIA design pattern for combobox - demonstrates a single-select combobox widget that is functionally similar to an HTML select element. - Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. - However, like an HTML <select>, users can type characters to select matching options. -

-
-
-

Example

-
- - Navigate forwards from here -
- -
- -
- - -
-
-
- Navigate backwards from here - -
- - -
- -
-
- - diff --git a/tests/combobox-select-only/reference/combobox-select-only.moveFocusBeforeCombobox.html b/tests/combobox-select-only/reference/combobox-select-only.moveFocusBeforeCombobox.html deleted file mode 100644 index 18a3b6110..000000000 --- a/tests/combobox-select-only/reference/combobox-select-only.moveFocusBeforeCombobox.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - -Select-Only Combobox Example - - - - - - - - - -
-

Select-Only Combobox Example

-

- The following example implementation of the ARIA design pattern for combobox - demonstrates a single-select combobox widget that is functionally similar to an HTML select element. - Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. - However, like an HTML <select>, users can type characters to select matching options. -

-
-
-

Example

-
- - Navigate forwards from here -
- -
- -
- - -
-
-
- Navigate backwards from here - -
- - -
- -
-
- - diff --git a/tests/combobox-select-only/reference/combobox-select-only.moveFocusToCombobox.html b/tests/combobox-select-only/reference/combobox-select-only.moveFocusToCombobox.html deleted file mode 100644 index 433d3c6e9..000000000 --- a/tests/combobox-select-only/reference/combobox-select-only.moveFocusToCombobox.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - -Select-Only Combobox Example - - - - - - - - - -
-

Select-Only Combobox Example

-

- The following example implementation of the ARIA design pattern for combobox - demonstrates a single-select combobox widget that is functionally similar to an HTML select element. - Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. - However, like an HTML <select>, users can type characters to select matching options. -

-
-
-

Example

-
- - Navigate forwards from here -
- -
- -
- - -
-
-
- Navigate backwards from here - -
- - -
- -
-
- - diff --git a/tests/combobox-select-only/reference/combobox-select-only.openListbox.html b/tests/combobox-select-only/reference/combobox-select-only.openListbox.html deleted file mode 100644 index 1533a43fc..000000000 --- a/tests/combobox-select-only/reference/combobox-select-only.openListbox.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - -Select-Only Combobox Example - - - - - - - - - -
-

Select-Only Combobox Example

-

- The following example implementation of the ARIA design pattern for combobox - demonstrates a single-select combobox widget that is functionally similar to an HTML select element. - Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. - However, like an HTML <select>, users can type characters to select matching options. -

-
-
-

Example

-
- - Navigate forwards from here -
- -
- -
- - -
-
-
- Navigate backwards from here - -
- - -
- -
-
- - diff --git a/tests/combobox-select-only/reference/combobox-select-only.openListboxToApple.html b/tests/combobox-select-only/reference/combobox-select-only.openListboxToApple.html deleted file mode 100644 index 0198e9d23..000000000 --- a/tests/combobox-select-only/reference/combobox-select-only.openListboxToApple.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - -Select-Only Combobox Example - - - - - - - - - -
-

Select-Only Combobox Example

-

- The following example implementation of the ARIA design pattern for combobox - demonstrates a single-select combobox widget that is functionally similar to an HTML select element. - Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. - However, like an HTML <select>, users can type characters to select matching options. -

-
-
-

Example

-
- - Navigate forwards from here -
- -
- -
- - -
-
-
- Navigate backwards from here - -
- - -
- -
-
- - diff --git a/tests/combobox-select-only/reference/combobox-select-only.openListboxToGuava.html b/tests/combobox-select-only/reference/combobox-select-only.openListboxToGuava.html deleted file mode 100644 index 0505d5804..000000000 --- a/tests/combobox-select-only/reference/combobox-select-only.openListboxToGuava.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - -Select-Only Combobox Example - - - - - - - - - -
-

Select-Only Combobox Example

-

- The following example implementation of the ARIA design pattern for combobox - demonstrates a single-select combobox widget that is functionally similar to an HTML select element. - Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. - However, like an HTML <select>, users can type characters to select matching options. -

-
-
-

Example

-
- - Navigate forwards from here -
- -
- -
- - -
-
-
- Navigate backwards from here - -
- - -
- -
-
- - diff --git a/tests/combobox-select-only/reference/combobox-select-only.openListboxToHuckleberry.html b/tests/combobox-select-only/reference/combobox-select-only.openListboxToHuckleberry.html deleted file mode 100644 index 09661b898..000000000 --- a/tests/combobox-select-only/reference/combobox-select-only.openListboxToHuckleberry.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - -Select-Only Combobox Example - - - - - - - - - -
-

Select-Only Combobox Example

-

- The following example implementation of the ARIA design pattern for combobox - demonstrates a single-select combobox widget that is functionally similar to an HTML select element. - Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. - However, like an HTML <select>, users can type characters to select matching options. -

-
-
-

Example

-
- - Navigate forwards from here -
- -
- -
- - -
-
-
- Navigate backwards from here - -
- - -
- -
-
- - From 46a152fc7482e535cf3cfdcf024b6b68386f61f6 Mon Sep 17 00:00:00 2001 From: Isabel Del Castillo Date: Tue, 26 Apr 2022 15:27:40 -0500 Subject: [PATCH 6/8] Fix typo on references.csv --- tests/combobox-select-only/data/references.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/combobox-select-only/data/references.csv b/tests/combobox-select-only/data/references.csv index df07f4101..972d77191 100644 --- a/tests/combobox-select-only/data/references.csv +++ b/tests/combobox-select-only/data/references.csv @@ -2,7 +2,7 @@ refId,value author,James Scholes authorEmail,james@pac.bz title,Select Only Combobox -reference,reference/2022-4-26_144616/combobox-s2. Update tests/combobox-select-only/data/reference.csv with the reference reference/2022-4-26_144616/combobox-s +reference,reference/2022-4-26_144616/combobox-select-only.html example,https://w3c.github.io/aria-practices/examples/combobox/combobox-select-only.html combobox,https://w3c.github.io/aria-practices/#combobox listbox,https://w3c.github.io/aria/#listbox From 31176280623852d78391c0bd005b1df5a3f2b0ed Mon Sep 17 00:00:00 2001 From: IsaDC Date: Tue, 26 Apr 2022 20:28:24 +0000 Subject: [PATCH 7/8] Generate .html source files with scripts automatically --- ...ox-select-only.moveFocusAfterCombobox.html | 74 ++++++++++++++++++ ...x-select-only.moveFocusBeforeCombobox.html | 74 ++++++++++++++++++ ...bobox-select-only.moveFocusToCombobox.html | 74 ++++++++++++++++++ .../combobox-select-only.openListbox.html | 75 ++++++++++++++++++ ...mbobox-select-only.openListboxToApple.html | 76 +++++++++++++++++++ ...mbobox-select-only.openListboxToGuava.html | 76 +++++++++++++++++++ ...-select-only.openListboxToHuckleberry.html | 76 +++++++++++++++++++ 7 files changed, 525 insertions(+) create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusAfterCombobox.html create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusBeforeCombobox.html create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusToCombobox.html create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListbox.html create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToApple.html create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToGuava.html create mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToHuckleberry.html diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusAfterCombobox.html b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusAfterCombobox.html new file mode 100644 index 000000000..95ef8f07a --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusAfterCombobox.html @@ -0,0 +1,74 @@ + + + + +Select-Only Combobox Example + + + + + + + + + +
+

Select-Only Combobox Example

+

+ The following example implementation of the ARIA design pattern for combobox + demonstrates a single-select combobox widget that is functionally similar to an HTML select element. + Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. + However, like an HTML <select>, users can type characters to select matching options. +

+
+
+

Example

+
+ + Navigate forwards from here +
+ +
+ +
+ + +
+
+
+ Navigate backwards from here + +
+ + +
+ +
+
+ + diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusBeforeCombobox.html b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusBeforeCombobox.html new file mode 100644 index 000000000..a35043fda --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusBeforeCombobox.html @@ -0,0 +1,74 @@ + + + + +Select-Only Combobox Example + + + + + + + + + +
+

Select-Only Combobox Example

+

+ The following example implementation of the ARIA design pattern for combobox + demonstrates a single-select combobox widget that is functionally similar to an HTML select element. + Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. + However, like an HTML <select>, users can type characters to select matching options. +

+
+
+

Example

+
+ + Navigate forwards from here +
+ +
+ +
+ + +
+
+
+ Navigate backwards from here + +
+ + +
+ +
+
+ + diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusToCombobox.html b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusToCombobox.html new file mode 100644 index 000000000..23fce7c1d --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.moveFocusToCombobox.html @@ -0,0 +1,74 @@ + + + + +Select-Only Combobox Example + + + + + + + + + +
+

Select-Only Combobox Example

+

+ The following example implementation of the ARIA design pattern for combobox + demonstrates a single-select combobox widget that is functionally similar to an HTML select element. + Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. + However, like an HTML <select>, users can type characters to select matching options. +

+
+
+

Example

+
+ + Navigate forwards from here +
+ +
+ +
+ + +
+
+
+ Navigate backwards from here + +
+ + +
+ +
+
+ + diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListbox.html b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListbox.html new file mode 100644 index 000000000..8b89994be --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListbox.html @@ -0,0 +1,75 @@ + + + + +Select-Only Combobox Example + + + + + + + + + +
+

Select-Only Combobox Example

+

+ The following example implementation of the ARIA design pattern for combobox + demonstrates a single-select combobox widget that is functionally similar to an HTML select element. + Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. + However, like an HTML <select>, users can type characters to select matching options. +

+
+
+

Example

+
+ + Navigate forwards from here +
+ +
+ +
+ + +
+
+
+ Navigate backwards from here + +
+ + +
+ +
+
+ + diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToApple.html b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToApple.html new file mode 100644 index 000000000..eef1d6c63 --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToApple.html @@ -0,0 +1,76 @@ + + + + +Select-Only Combobox Example + + + + + + + + + +
+

Select-Only Combobox Example

+

+ The following example implementation of the ARIA design pattern for combobox + demonstrates a single-select combobox widget that is functionally similar to an HTML select element. + Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. + However, like an HTML <select>, users can type characters to select matching options. +

+
+
+

Example

+
+ + Navigate forwards from here +
+ +
+ +
+ + +
+
+
+ Navigate backwards from here + +
+ + +
+ +
+
+ + diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToGuava.html b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToGuava.html new file mode 100644 index 000000000..303e7bc9e --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToGuava.html @@ -0,0 +1,76 @@ + + + + +Select-Only Combobox Example + + + + + + + + + +
+

Select-Only Combobox Example

+

+ The following example implementation of the ARIA design pattern for combobox + demonstrates a single-select combobox widget that is functionally similar to an HTML select element. + Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. + However, like an HTML <select>, users can type characters to select matching options. +

+
+
+

Example

+
+ + Navigate forwards from here +
+ +
+ +
+ + +
+
+
+ Navigate backwards from here + +
+ + +
+ +
+
+ + diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToHuckleberry.html b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToHuckleberry.html new file mode 100644 index 000000000..07353c262 --- /dev/null +++ b/tests/combobox-select-only/reference/2022-4-26_144616/combobox-select-only.openListboxToHuckleberry.html @@ -0,0 +1,76 @@ + + + + +Select-Only Combobox Example + + + + + + + + + +
+

Select-Only Combobox Example

+

+ The following example implementation of the ARIA design pattern for combobox + demonstrates a single-select combobox widget that is functionally similar to an HTML select element. + Unlike the editable combobox examples, this select-only combobox is not made with an <input> element, and it does not accept freeform user input. + However, like an HTML <select>, users can type characters to select matching options. +

+
+
+

Example

+
+ + Navigate forwards from here +
+ +
+ +
+ + +
+
+
+ Navigate backwards from here + +
+ + +
+ +
+
+ + From 533f8f34db3ed4b1fe17dde5ae8178e0513bc403 Mon Sep 17 00:00:00 2001 From: Isabel Del Castillo Date: Tue, 26 Apr 2022 15:37:14 -0500 Subject: [PATCH 8/8] Restore JS file to original --- .../css/combobox-autocomplete.css | 99 -- .../css/combobox-datepicker.css | 249 ----- .../2022-4-26_144616/css/grid-combo.css | 88 -- .../2022-4-26_144616/css/select-only.css | 10 +- .../js/combobox-autocomplete.js | 600 ------------ .../js/combobox-datepicker.js | 859 ------------------ .../2022-4-26_144616/js/grid-combo-example.js | 101 -- .../2022-4-26_144616/js/grid-combo.js | 318 ------- .../2022-4-26_144616/js/select-only.js | 31 +- 9 files changed, 34 insertions(+), 2321 deletions(-) delete mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-autocomplete.css delete mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-datepicker.css delete mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/css/grid-combo.css delete mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-autocomplete.js delete mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-datepicker.js delete mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo-example.js delete mode 100644 tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo.js diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-autocomplete.css b/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-autocomplete.css deleted file mode 100644 index 5bf2d8b9b..000000000 --- a/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-autocomplete.css +++ /dev/null @@ -1,99 +0,0 @@ -.combobox-list { - position: relative; -} - -.combobox .group { - display: inline-flex; - padding: 4px; - cursor: pointer; -} - -.combobox input, -.combobox button { - background-color: white; - color: black; - box-sizing: border-box; - height: 24px; - padding: 0; - margin: 0; - vertical-align: bottom; - border: 1px solid gray; - position: relative; - cursor: pointer; -} - -.combobox input { - width: 150px; - border-right: none; - outline: none; - font-size: 87.5%; - padding: 1px 3px; -} - -.combobox button { - width: 19px; - border-left: none; - outline: none; - color: rgb(0 90 156); -} - -.combobox button[aria-expanded="true"] svg { - transform: rotate(180deg) translate(0, -3px); -} - -ul[role="listbox"] { - margin: 0; - padding: 0; - position: absolute; - left: 4px; - top: 28px; - list-style: none; - background-color: white; - display: none; - box-sizing: border-box; - border: 2px currentColor solid; - max-height: 250px; - width: 168px; - overflow: scroll; - overflow-x: hidden; - font-size: 87.5%; - cursor: pointer; -} - -ul[role="listbox"] li[role="option"] { - margin: 0; - display: block; - padding-left: 3px; - padding-top: 2px; - padding-bottom: 2px; -} - -/* focus and hover styling */ - -.combobox .group.focus, -.combobox .group:hover { - padding: 2px; - border: 2px solid currentColor; - border-radius: 4px; -} - -.combobox .group.focus polygon, -.combobox .group:hover polygon { - fill-opacity: 1; -} - -.combobox .group.focus input, -.combobox .group.focus button, -.combobox .group input:hover, -.combobox .group button:hover { - background-color: #def; -} - -[role="listbox"].focus [role="option"][aria-selected="true"], -[role="listbox"] [role="option"]:hover { - background-color: #def; - padding-top: 0; - padding-bottom: 0; - border-top: 2px solid currentColor; - border-bottom: 2px solid currentColor; -} diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-datepicker.css b/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-datepicker.css deleted file mode 100644 index c4161d02e..000000000 --- a/tests/combobox-select-only/reference/2022-4-26_144616/css/combobox-datepicker.css +++ /dev/null @@ -1,249 +0,0 @@ -.combobox-datepicker { - margin-top: 1em; - position: relative; -} - -.combobox-datepicker .group { - display: inline-flex; - position: relative; - width: 12.125rem; -} - -.combobox-datepicker label { - display: block; -} - -.combobox-datepicker .group input, -.combobox-datepicker .group button { - background-color: white; - color: black; - box-sizing: border-box; - height: 1.75rem; - padding: 0; - margin: 0; - vertical-align: bottom; - border: 1px solid gray; - position: relative; -} - -.combobox-datepicker .group input { - width: 10.75rem; - border-right: none; - outline: none; - font-size: 87.5%; - padding: 0.1em 0.3em; -} - -.combobox-datepicker .group button { - position: absolute; - left: 10.75rem; - padding-right: 0.125rem; - border-left: none; - outline: none; -} - -.combobox-datepicker .group .desc { - position: absolute; - top: 2em; - left: 0; - margin-top: 0.1em; - font-size: 90%; - font-style: italic; - letter-spacing: 0.025em; -} - -.combobox-datepicker .group.focus { - outline: 2px solid black; - outline-offset: 2px; -} - -.combobox-datepicker .dialog button { - border-style: none; - background: transparent; -} - -.combobox-datepicker .dialog-ok-cancel-group button { - padding: 6px; - margin-left: 1em; - width: 5em; - background-color: hsl(216deg 80% 92%); - font-size: 0.85em; - color: black; - outline: none; - border-radius: 5px; -} - -.combobox-datepicker .dialog button::-moz-focus-inner { - border: 0; -} - -.combobox-datepicker .group.focus input, -.combobox-datepicker .group.focus button { - background-color: #def; -} - -.combobox-datepicker .group polygon { - fill: gray; - stroke: transparent; -} - -.combobox-datepicker .group.focus polygon, -.combobox-datepicker .group button[aria-expanded="true"] polygon { - fill: black; - stroke: white; -} - -.combobox-datepicker .group button.open svg { - transform: rotate(180deg) translate(0, -1px); -} - -.combobox-datepicker .dialog { - position: absolute; - width: 320px; - clear: both; - border: 3px solid hsl(216deg 80% 51%); - margin-top: 1em; - border-radius: 5px; - padding: 0; - background-color: #fff; -} - -.combobox-datepicker .header { - cursor: default; - background-color: hsl(216deg 80% 51%); - padding: 7px; - font-weight: bold; - text-transform: uppercase; - color: white; - display: flex; - justify-content: space-around; -} - -.combobox-datepicker .dialog h2 { - margin: 0; - padding: 0; - display: inline-block; - font-size: 1em; - color: white; - text-transform: none; - font-weight: bold; -} - -.combobox-datepicker .dates { - width: 320px; - padding-left: 1em; - padding-right: 1em; - padding-top: 1em; -} - -.combobox-datepicker .prev-year, -.combobox-datepicker .prev-month, -.combobox-datepicker .next-month, -.combobox-datepicker .next-year { - padding: 4px; - width: 24px; - height: 24px; - color: white; -} - -.combobox-datepicker .prev-year:focus, -.combobox-datepicker .prev-month:focus, -.combobox-datepicker .next-month:focus, -.combobox-datepicker .next-year:focus { - padding: 2px; - border: 2px solid white; - border-radius: 4px; - outline: 0; -} - -.combobox-datepicker .prev-year:hover, -.combobox-datepicker .prev-month:hover, -.combobox-datepicker .next-month:hover, -.combobox-datepicker .next-year:hover { - padding: 3px; - border: 1px solid white; - border-radius: 4px; - outline: 0; -} - -.combobox-datepicker .dialog-ok-cancel-group { - text-align: right; - margin-top: 1em; - margin-bottom: 1em; - margin-right: 1em; -} - -.combobox-datepicker .dialog-button:focus { - padding: 4px; - border: 2px solid black; -} - -.combobox-datepicker .dialog-button:hover { - padding: 5px; - border: 1px solid black; -} - -.combobox-datepicker .fa-calendar-alt { - color: hsl(216deg 89% 51%); -} - -.combobox-datepicker .month-year { - display: inline-block; - width: 12em; - text-align: center; -} - -.combobox-datepicker .dates th, -.combobox-datepicker .dates td { - text-align: center; -} - -.combobox-datepicker .dates tr { - border: 1px solid black; -} - -.combobox-datepicker .dates td { - padding: 3px; - margin: 0; - line-height: inherit; - height: 40px; - width: 40px; - border-radius: 5px; - font-size: 15px; - background: #eee; -} - -.combobox-datepicker .dates td[aria-selected] { - padding: 1px; - border: 2px dotted black; - background-color: hsl(216deg 80% 96%); -} - -.combobox-datepicker .dates td[tabindex="0"] { - background-color: hsl(216deg 80% 51%); - color: white; -} - -.combobox-datepicker .dates td:hover { - padding: 0; - background-color: hsl(216deg 80% 92%); -} - -.combobox-datepicker .dates td:focus { - padding: 1px; - border: 2px solid rgb(100 100 100); - outline: 0; -} - -.combobox-datepicker .dates td:not(.disabled):hover { - padding: 2px; - border: 1px solid rgb(100 100 100); -} - -.combobox-datepicker .dialog-message { - padding-top: 0.25em; - padding-left: 1em; - height: 1.75em; - background: hsl(216deg 80% 51%); - color: white; -} diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/css/grid-combo.css b/tests/combobox-select-only/reference/2022-4-26_144616/css/grid-combo.css deleted file mode 100644 index 5484f8f7b..000000000 --- a/tests/combobox-select-only/reference/2022-4-26_144616/css/grid-combo.css +++ /dev/null @@ -1,88 +0,0 @@ -.hidden { - display: none; -} - -.combobox-wrapper { - display: inline-block; - position: relative; - font-size: 16px; -} - -.combobox-label { - font-size: 14px; - font-weight: bold; - margin-right: 5px; -} - -.listbox, -.grid { - min-width: 230px; - background: white; - border: 1px solid #ccc; - list-style: none; - margin: 0; - padding: 0; - position: absolute; - top: 1.7em; - z-index: 1; -} - -.listbox .result { - cursor: default; - margin: 0; -} - -.grid .result-row { - padding: 2px; - cursor: default; - margin: 0; -} - -.listbox .result:hover, -.grid .result-row:hover { - background: rgb(139 189 225); -} - -.listbox .focused, -.grid .focused { - background: rgb(139 189 225); -} - -.grid .focused-cell { - outline-style: dotted; - outline-color: green; -} - -.combobox-wrapper input { - font-size: inherit; - border: 1px solid #aaa; - border-radius: 2px; - line-height: 1.5em; - padding-right: 30px; - width: 200px; -} - -.combobox-dropdown { - position: absolute; - right: 0; - top: 0; - padding: 0 0 2px; - height: 1.5em; - border-radius: 0 2px 2px 0; - border: 1px solid #aaa; -} - -.grid .result-cell { - display: inline-block; - cursor: default; - margin: 0; - padding: 0 5px; -} - -.grid .result-cell:last-child { - float: right; - font-size: 12px; - font-weight: 200; - color: #333; - line-height: 24px; -} diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css b/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css index 1d8beb3a7..2f67d370e 100644 --- a/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css +++ b/tests/combobox-select-only/reference/2022-4-26_144616/css/select-only.css @@ -12,8 +12,8 @@ } .combo::after { - border-bottom: 2px solid rgba(0, 0, 0, 0.75); - border-right: 2px solid rgba(0, 0, 0, 0.75); + border-bottom: 2px solid rgb(0 0 0 / 75%); + border-right: 2px solid rgb(0 0 0 / 75%); content: ""; display: block; height: 12px; @@ -27,7 +27,7 @@ .combo-input { background-color: #f5f5f5; - border: 2px solid rgba(0, 0, 0, 0.75); + border: 2px solid rgb(0 0 0 / 75%); border-radius: 4px; display: block; font-size: 1em; @@ -56,7 +56,7 @@ .combo-menu { background-color: #f5f5f5; - border: 1px solid rgba(0, 0, 0, 0.75); + border: 1px solid rgb(0 0 0 / 75%); border-radius: 0 0 4px 4px; display: none; max-height: 300px; @@ -77,7 +77,7 @@ } .combo-option:hover { - background-color: rgba(0, 0, 0, 0.1); + background-color: rgb(0 0 0 / 10%); } .combo-option.option-current { diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-autocomplete.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-autocomplete.js deleted file mode 100644 index 98cb12a65..000000000 --- a/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-autocomplete.js +++ /dev/null @@ -1,600 +0,0 @@ -/* - * This content is licensed according to the W3C Software License at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - */ - -'use strict'; - -class ComboboxAutocomplete { - constructor(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 = ''; - - 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.onComboboxKeyDown.bind(this) - ); - this.comboboxNode.addEventListener( - 'keyup', - this.onComboboxKeyUp.bind(this) - ); - this.comboboxNode.addEventListener( - 'click', - this.onComboboxClick.bind(this) - ); - this.comboboxNode.addEventListener( - 'focus', - this.onComboboxFocus.bind(this) - ); - this.comboboxNode.addEventListener('blur', this.onComboboxBlur.bind(this)); - - document.body.addEventListener( - 'pointerup', - this.onBackgroundPointerUp.bind(this), - true - ); - - // initialize pop up menu - - this.listboxNode.addEventListener( - 'pointerover', - this.onListboxPointerover.bind(this) - ); - this.listboxNode.addEventListener( - 'pointerout', - this.onListboxPointerout.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.onOptionClick.bind(this)); - node.addEventListener('pointerover', this.onOptionPointerover.bind(this)); - node.addEventListener('pointerout', this.onOptionPointerout.bind(this)); - } - - this.filterOptions(); - - // Open Button - - var button = this.comboboxNode.nextElementSibling; - - if (button && button.tagName === 'BUTTON') { - button.addEventListener('click', this.onButtonClick.bind(this)); - } - } - - getLowercaseContent(node) { - return node.textContent.toLowerCase(); - } - - isOptionInView(option) { - var bounding = option.getBoundingClientRect(); - return ( - bounding.top >= 0 && - bounding.left >= 0 && - bounding.bottom <= - (window.innerHeight || document.documentElement.clientHeight) && - bounding.right <= - (window.innerWidth || document.documentElement.clientWidth) - ); - } - - setActiveDescendant(option) { - if (option && this.listboxHasVisualFocus) { - this.comboboxNode.setAttribute('aria-activedescendant', option.id); - if (!this.isOptionInView(option)) { - option.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } - } else { - this.comboboxNode.setAttribute('aria-activedescendant', ''); - } - } - - setValue(value) { - this.filter = value; - this.comboboxNode.value = this.filter; - this.comboboxNode.setSelectionRange(this.filter.length, this.filter.length); - this.filterOptions(); - } - - setOption(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 - ); - } - } - } - } - - setVisualFocusCombobox() { - 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); - } - - setVisualFocusListbox() { - this.comboboxNode.parentNode.classList.remove('focus'); - this.comboboxHasVisualFocus = false; - this.listboxHasVisualFocus = true; - this.listboxNode.classList.add('focus'); - this.setActiveDescendant(this.option); - } - - removeVisualFocusAll() { - 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 - - filterOptions() { - // 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; - } - - setCurrentOptionStyle(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'); - } - } - } - - getPreviousOption(currentOption) { - if (currentOption !== this.firstOption) { - var index = this.filteredOptions.indexOf(currentOption); - return this.filteredOptions[index - 1]; - } - return this.lastOption; - } - - getNextOption(currentOption) { - if (currentOption !== this.lastOption) { - var index = this.filteredOptions.indexOf(currentOption); - return this.filteredOptions[index + 1]; - } - return this.firstOption; - } - - /* MENU DISPLAY METHODS */ - - doesOptionHaveFocus() { - return this.comboboxNode.getAttribute('aria-activedescendant') !== ''; - } - - isOpen() { - return this.listboxNode.style.display === 'block'; - } - - isClosed() { - return this.listboxNode.style.display !== 'block'; - } - - hasOptions() { - return this.filteredOptions.length; - } - - open() { - this.listboxNode.style.display = 'block'; - this.comboboxNode.setAttribute('aria-expanded', 'true'); - this.buttonNode.setAttribute('aria-expanded', 'true'); - } - - close(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.setAttribute('aria-expanded', 'false'); - this.setActiveDescendant(false); - this.comboboxNode.parentNode.classList.add('focus'); - } - } - - /* combobox Events */ - - onComboboxKeyDown(event) { - var flag = false, - 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(); - } - } - - isPrintableCharacter(str) { - return str.length === 1 && str.match(/\S| /); - } - - onComboboxKeyUp(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; - this.filterOptions(); - } - - if (event.key === 'Escape' || event.key === 'Esc') { - return; - } - - switch (event.key) { - case 'Backspace': - this.setVisualFocusCombobox(); - this.setCurrentOptionStyle(false); - this.filter = this.comboboxNode.value; - this.option = null; - this.filterOptions(); - 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(); - } - } - - onComboboxClick() { - if (this.isOpen()) { - this.close(true); - } else { - this.open(); - } - } - - onComboboxFocus() { - this.filter = this.comboboxNode.value; - this.filterOptions(); - this.setVisualFocusCombobox(); - this.option = null; - this.setCurrentOptionStyle(null); - } - - onComboboxBlur() { - this.removeVisualFocusAll(); - } - - onBackgroundPointerUp(event) { - if ( - !this.comboboxNode.contains(event.target) && - !this.listboxNode.contains(event.target) && - !this.buttonNode.contains(event.target) - ) { - this.comboboxHasVisualFocus = false; - this.setCurrentOptionStyle(null); - this.removeVisualFocusAll(); - setTimeout(this.close.bind(this, true), 300); - } - } - - onButtonClick() { - if (this.isOpen()) { - this.close(true); - } else { - this.open(); - } - this.comboboxNode.focus(); - this.setVisualFocusCombobox(); - } - - /* Listbox Events */ - - onListboxPointerover() { - this.hasHover = true; - } - - onListboxPointerout() { - this.hasHover = false; - setTimeout(this.close.bind(this, false), 300); - } - - // Listbox Option Events - - onOptionClick(event) { - this.comboboxNode.value = event.target.textContent; - this.close(true); - } - - onOptionPointerover() { - this.hasHover = true; - this.open(); - } - - onOptionPointerout() { - 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"]'); - new ComboboxAutocomplete(comboboxNode, buttonNode, listboxNode); - } -}); diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-datepicker.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-datepicker.js deleted file mode 100644 index 607e824ba..000000000 --- a/tests/combobox-select-only/reference/2022-4-26_144616/js/combobox-datepicker.js +++ /dev/null @@ -1,859 +0,0 @@ -/* - * This content is licensed according to the W3C Software License at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - * - * File: ComboboxDatePicker.js - */ - -'use strict'; - -class ComboboxDatePicker { - constructor(cdp) { - this.buttonLabel = 'Date'; - this.monthLabels = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', - ]; - - this.messageCursorKeys = 'Cursor keys can navigate dates'; - this.lastMessage = ''; - - this.comboboxNode = cdp.querySelector('input[type="text"]'); - this.buttonNode = cdp.querySelector('.group button'); - this.dialogNode = cdp.querySelector('[role="dialog"]'); - this.messageNode = this.dialogNode.querySelector('.dialog-message'); - - this.monthYearNode = this.dialogNode.querySelector('.month-year'); - - this.prevYearNode = this.dialogNode.querySelector('.prev-year'); - this.prevMonthNode = this.dialogNode.querySelector('.prev-month'); - this.nextMonthNode = this.dialogNode.querySelector('.next-month'); - this.nextYearNode = this.dialogNode.querySelector('.next-year'); - - this.okButtonNode = this.dialogNode.querySelector('button[value="ok"]'); - this.cancelButtonNode = this.dialogNode.querySelector( - 'button[value="cancel"]' - ); - - this.tbodyNode = this.dialogNode.querySelector('table.dates tbody'); - - this.lastRowNode = null; - - this.days = []; - - this.focusDay = new Date(); - this.selectedDay = new Date(0, 0, 1); - - this.isMouseDownOnBackground = false; - - this.comboboxNode.addEventListener( - 'keydown', - this.onComboboxKeyDown.bind(this) - ); - this.comboboxNode.addEventListener( - 'click', - this.onComboboxClick.bind(this) - ); - this.comboboxNode.addEventListener( - 'focus', - this.onComboboxFocus.bind(this) - ); - this.comboboxNode.addEventListener('blur', this.onComboboxBlur.bind(this)); - - this.buttonNode.addEventListener( - 'keydown', - this.onButtonKeyDown.bind(this) - ); - this.buttonNode.addEventListener('click', this.onButtonClick.bind(this)); - - this.okButtonNode.addEventListener('click', this.onOkButton.bind(this)); - this.okButtonNode.addEventListener('keydown', this.onOkButton.bind(this)); - - this.cancelButtonNode.addEventListener( - 'click', - this.onCancelButton.bind(this) - ); - this.cancelButtonNode.addEventListener( - 'keydown', - this.onCancelButton.bind(this) - ); - - this.prevMonthNode.addEventListener( - 'click', - this.onPreviousMonthButton.bind(this) - ); - this.nextMonthNode.addEventListener( - 'click', - this.onNextMonthButton.bind(this) - ); - this.prevYearNode.addEventListener( - 'click', - this.onPreviousYearButton.bind(this) - ); - this.nextYearNode.addEventListener( - 'click', - this.onNextYearButton.bind(this) - ); - - this.prevMonthNode.addEventListener( - 'keydown', - this.onPreviousMonthButton.bind(this) - ); - this.nextMonthNode.addEventListener( - 'keydown', - this.onNextMonthButton.bind(this) - ); - this.prevYearNode.addEventListener( - 'keydown', - this.onPreviousYearButton.bind(this) - ); - this.nextYearNode.addEventListener( - 'keydown', - this.onNextYearButton.bind(this) - ); - - document.body.addEventListener( - 'mouseup', - this.onBackgroundMouseUp.bind(this), - true - ); - - // Create Grid of Dates - - this.tbodyNode.innerHTML = ''; - for (var i = 0; i < 6; i++) { - var row = this.tbodyNode.insertRow(i); - this.lastRowNode = row; - for (var j = 0; j < 7; j++) { - var cell = document.createElement('td'); - - cell.setAttribute('tabindex', '-1'); - cell.addEventListener('click', this.onDayClick.bind(this)); - cell.addEventListener('keydown', this.onDayKeyDown.bind(this)); - cell.addEventListener('focus', this.onDayFocus.bind(this)); - - cell.textContent = '-1'; - - row.appendChild(cell); - this.days.push(cell); - } - } - - this.updateGrid(); - this.close(false); - } - - isSameDay(day1, day2) { - return ( - day1.getFullYear() == day2.getFullYear() && - day1.getMonth() == day2.getMonth() && - day1.getDate() == day2.getDate() - ); - } - - isNotSameMonth(day1, day2) { - return ( - day1.getFullYear() != day2.getFullYear() || - day1.getMonth() != day2.getMonth() - ); - } - - updateGrid() { - var i, flag; - var fd = this.focusDay; - - this.monthYearNode.textContent = - this.monthLabels[fd.getMonth()] + ' ' + fd.getFullYear(); - - var firstDayOfMonth = new Date(fd.getFullYear(), fd.getMonth(), 1); - var dayOfWeek = firstDayOfMonth.getDay(); - - firstDayOfMonth.setDate(firstDayOfMonth.getDate() - dayOfWeek); - - var d = new Date(firstDayOfMonth); - - for (i = 0; i < this.days.length; i++) { - flag = d.getMonth() != fd.getMonth(); - this.updateDate( - this.days[i], - flag, - d, - this.isSameDay(d, this.selectedDay) - ); - d.setDate(d.getDate() + 1); - - // Hide last row if all disabled dates - if (i === 35) { - if (flag) { - this.lastRowNode.style.visibility = 'hidden'; - } else { - this.lastRowNode.style.visibility = 'visible'; - } - } - } - } - - setFocusDay(flag) { - if (typeof flag !== 'boolean') { - flag = true; - } - - var fd = this.focusDay; - var getDayFromDataDateAttribute = this.getDayFromDataDateAttribute; - - function checkDay(domNode) { - var d = getDayFromDataDateAttribute(domNode); - - domNode.setAttribute('tabindex', '-1'); - if (this.isSameDay(d, fd)) { - domNode.setAttribute('tabindex', '0'); - if (flag) { - domNode.focus(); - } - } - } - - this.days.forEach(checkDay.bind(this)); - } - - open() { - this.dialogNode.style.display = 'block'; - this.dialogNode.style.zIndex = 2; - - this.comboboxNode.setAttribute('aria-expanded', 'true'); - this.buttonNode.classList.add('open'); - this.getDateFromCombobox(); - this.updateGrid(); - } - - isOpen() { - return window.getComputedStyle(this.dialogNode).display !== 'none'; - } - - close(flag) { - if (typeof flag !== 'boolean') { - // Default is to move focus to combobox - flag = true; - } - - this.setMessage(''); - this.dialogNode.style.display = 'none'; - this.comboboxNode.setAttribute('aria-expanded', 'false'); - this.buttonNode.classList.remove('open'); - - if (flag) { - this.comboboxNode.focus(); - } - } - - onOkButton(event) { - var flag = false; - - switch (event.type) { - case 'keydown': - switch (event.key) { - case 'Tab': - if (!event.shiftKey) { - this.prevYearNode.focus(); - flag = true; - } - break; - - case 'Esc': - case 'Escape': - this.close(); - flag = true; - break; - - default: - break; - } - break; - - case 'click': - this.setComboboxDate(); - this.close(); - flag = true; - break; - - default: - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - } - - onCancelButton(event) { - var flag = false; - - switch (event.type) { - case 'keydown': - switch (event.key) { - case 'Esc': - case 'Escape': - this.close(); - flag = true; - break; - - default: - break; - } - break; - - case 'click': - this.close(); - flag = true; - break; - - default: - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - } - - onNextYearButton(event) { - var flag = false; - - switch (event.type) { - case 'keydown': - switch (event.key) { - case 'Esc': - case 'Escape': - this.close(); - flag = true; - break; - - case 'Enter': - this.moveToNextYear(); - this.setFocusDay(false); - flag = true; - break; - } - - break; - - case 'click': - this.moveToNextYear(); - this.setFocusDay(false); - break; - - default: - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - } - - onPreviousYearButton(event) { - var flag = false; - - switch (event.type) { - case 'keydown': - switch (event.key) { - case 'Enter': - this.moveToPreviousYear(); - this.setFocusDay(false); - flag = true; - break; - - case 'Tab': - if (event.shiftKey) { - this.okButtonNode.focus(); - flag = true; - } - break; - - case 'Esc': - case 'Escape': - this.close(); - flag = true; - break; - - default: - break; - } - - break; - - case 'click': - this.moveToPreviousYear(); - this.setFocusDay(false); - break; - - default: - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - } - - onNextMonthButton(event) { - var flag = false; - - switch (event.type) { - case 'keydown': - switch (event.key) { - case 'Esc': - case 'Escape': - this.close(); - flag = true; - break; - - case 'Enter': - this.moveToNextMonth(); - this.setFocusDay(false); - flag = true; - break; - } - - break; - - case 'click': - this.moveToNextMonth(); - this.setFocusDay(false); - break; - - default: - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - } - - onPreviousMonthButton(event) { - var flag = false; - - switch (event.type) { - case 'keydown': - switch (event.key) { - case 'Esc': - case 'Escape': - this.close(); - flag = true; - break; - - case 'Enter': - this.moveToPreviousMonth(); - this.setFocusDay(false); - flag = true; - break; - } - - break; - - case 'click': - this.moveToPreviousMonth(); - this.setFocusDay(false); - flag = true; - break; - - default: - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - } - - moveFocusToDay(day) { - var d = this.focusDay; - - this.focusDay = day; - - if ( - d.getMonth() != this.focusDay.getMonth() || - d.getYear() != this.focusDay.getYear() - ) { - this.updateGrid(); - } - this.setFocusDay(); - } - - moveToNextYear() { - this.focusDay.setFullYear(this.focusDay.getFullYear() + 1); - this.updateGrid(); - } - - moveToPreviousYear() { - this.focusDay.setFullYear(this.focusDay.getFullYear() - 1); - this.updateGrid(); - } - - moveToNextMonth() { - this.focusDay.setMonth(this.focusDay.getMonth() + 1); - this.updateGrid(); - } - - moveToPreviousMonth() { - this.focusDay.setMonth(this.focusDay.getMonth() - 1); - this.updateGrid(); - } - - moveFocusToNextDay() { - var d = new Date(this.focusDay); - d.setDate(d.getDate() + 1); - this.moveFocusToDay(d); - } - - moveFocusToNextWeek() { - var d = new Date(this.focusDay); - d.setDate(d.getDate() + 7); - this.moveFocusToDay(d); - } - - moveFocusToPreviousDay() { - var d = new Date(this.focusDay); - d.setDate(d.getDate() - 1); - this.moveFocusToDay(d); - } - - moveFocusToPreviousWeek() { - var d = new Date(this.focusDay); - d.setDate(d.getDate() - 7); - this.moveFocusToDay(d); - } - - moveFocusToFirstDayOfWeek() { - var d = new Date(this.focusDay); - d.setDate(d.getDate() - d.getDay()); - this.moveFocusToDay(d); - } - - moveFocusToLastDayOfWeek() { - var d = new Date(this.focusDay); - d.setDate(d.getDate() + (6 - d.getDay())); - this.moveFocusToDay(d); - } - - // Day methods - - isDayDisabled(domNode) { - return domNode.classList.contains('disabled'); - } - - getDayFromDataDateAttribute(domNode) { - var parts = domNode.getAttribute('data-date').split('-'); - return new Date(parts[0], parseInt(parts[1]) - 1, parts[2]); - } - - updateDate(domNode, disable, day, selected) { - var d = day.getDate().toString(); - if (day.getDate() <= 9) { - d = '0' + d; - } - - var m = day.getMonth() + 1; - if (day.getMonth() < 9) { - m = '0' + m; - } - - domNode.setAttribute('tabindex', '-1'); - domNode.removeAttribute('aria-selected'); - domNode.setAttribute('data-date', day.getFullYear() + '-' + m + '-' + d); - - if (disable) { - domNode.classList.add('disabled'); - domNode.textContent = ''; - } else { - domNode.classList.remove('disabled'); - domNode.textContent = day.getDate(); - if (selected) { - domNode.setAttribute('aria-selected', 'true'); - domNode.setAttribute('tabindex', '0'); - } - } - } - - updateSelected(domNode) { - for (var i = 0; i < this.days.length; i++) { - var day = this.days[i]; - if (day === domNode) { - day.setAttribute('aria-selected', 'true'); - } else { - day.removeAttribute('aria-selected'); - } - } - } - - onDayKeyDown(event) { - var flag = false; - - switch (event.key) { - case 'Esc': - case 'Escape': - this.close(); - break; - - case ' ': - this.updateSelected(event.currentTarget); - this.setComboboxDate(event.currentTarget); - flag = true; - break; - - case 'Enter': - this.setComboboxDate(event.currentTarget); - this.close(); - break; - - case 'Tab': - this.cancelButtonNode.focus(); - if (event.shiftKey) { - this.nextYearNode.focus(); - } - this.setMessage(''); - flag = true; - break; - - case 'Right': - case 'ArrowRight': - this.moveFocusToNextDay(); - flag = true; - break; - - case 'Left': - case 'ArrowLeft': - this.moveFocusToPreviousDay(); - flag = true; - break; - - case 'Down': - case 'ArrowDown': - this.moveFocusToNextWeek(); - flag = true; - break; - - case 'Up': - case 'ArrowUp': - this.moveFocusToPreviousWeek(); - flag = true; - break; - - case 'PageUp': - if (event.shiftKey) { - this.moveToPreviousYear(); - } else { - this.moveToPreviousMonth(); - } - this.setFocusDay(); - flag = true; - break; - - case 'PageDown': - if (event.shiftKey) { - this.moveToNextYear(); - } else { - this.moveToNextMonth(); - } - this.setFocusDay(); - flag = true; - break; - - case 'Home': - this.moveFocusToFirstDayOfWeek(); - flag = true; - break; - - case 'End': - this.moveFocusToLastDayOfWeek(); - flag = true; - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - } - - onDayClick(event) { - if (!this.isDayDisabled(event.currentTarget)) { - this.setComboboxDate(event.currentTarget); - this.close(); - } - - event.stopPropagation(); - event.preventDefault(); - } - - onDayFocus() { - this.setMessage(this.messageCursorKeys); - } - - // Combobox methods - - setComboboxDate(domNode) { - var d = this.focusDay; - - if (domNode) { - d = this.getDayFromDataDateAttribute(domNode); - } - - this.comboboxNode.value = - d.getMonth() + 1 + '/' + d.getDate() + '/' + d.getFullYear(); - } - - getDateFromCombobox() { - var parts = this.comboboxNode.value.split('/'); - - if ( - parts.length === 3 && - Number.isInteger(parseInt(parts[0])) && - Number.isInteger(parseInt(parts[1])) && - Number.isInteger(parseInt(parts[2])) - ) { - this.focusDay = new Date( - parseInt(parts[2]), - parseInt(parts[0]) - 1, - parseInt(parts[1]) - ); - this.selectedDay = new Date(this.focusDay); - } else { - // If not a valid date (MM/DD/YY) initialize with todays date - this.focusDay = new Date(); - this.selectedDay = new Date(0, 0, 1); - } - } - - setMessage(str) { - function setMessageDelayed() { - this.messageNode.textContent = str; - } - - if (str !== this.lastMessage) { - setTimeout(setMessageDelayed.bind(this), 200); - this.lastMessage = str; - } - } - - onComboboxKeyDown(event) { - var flag = false; - - if (event.ctrlKey || event.shiftKey) { - return; - } - - switch (event.key) { - case 'Down': - case 'ArrowDown': - this.open(); - this.setFocusDay(); - flag = true; - break; - - case 'Esc': - case 'Escape': - if (this.isOpen()) { - this.close(false); - } else { - this.comboboxNode.value = ''; - } - this.option = null; - flag = true; - break; - - case 'Tab': - this.close(false); - break; - - default: - break; - } - - if (flag) { - event.stopPropagation(); - event.preventDefault(); - } - } - - onComboboxClick(event) { - if (this.isOpen()) { - this.close(false); - } else { - this.open(); - } - - event.stopPropagation(); - event.preventDefault(); - } - - onComboboxFocus(event) { - event.currentTarget.parentNode.classList.add('focus'); - } - - onComboboxBlur(event) { - event.currentTarget.parentNode.classList.remove('focus'); - } - - onButtonKeyDown(event) { - if (event.key === 'Enter' || event.key === ' ') { - this.open(); - this.setFocusDay(); - - event.stopPropagation(); - event.preventDefault(); - } - } - - onButtonClick(event) { - if (this.isOpen()) { - this.close(); - } else { - this.open(); - this.setFocusDay(); - } - - event.stopPropagation(); - event.preventDefault(); - } - - onBackgroundMouseUp(event) { - if ( - !this.comboboxNode.contains(event.target) && - !this.buttonNode.contains(event.target) && - !this.dialogNode.contains(event.target) - ) { - if (this.isOpen()) { - this.close(false); - event.stopPropagation(); - event.preventDefault(); - } - } - } -} - -// Initialize menu button date picker - -window.addEventListener('load', function () { - var comboboxDatePickers = document.querySelectorAll('.combobox-datepicker'); - comboboxDatePickers.forEach(function (dp) { - new ComboboxDatePicker(dp); - }); -}); diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo-example.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo-example.js deleted file mode 100644 index 4a47e32df..000000000 --- a/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo-example.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * This content is licensed according to the W3C Software License at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - * - * ARIA Combobox Examples - */ - -'use strict'; - -var aria = aria || {}; - -var FRUITS_AND_VEGGIES = [ - ['Apple', 'Fruit'], - ['Artichoke', 'Vegetable'], - ['Asparagus', 'Vegetable'], - ['Banana', 'Fruit'], - ['Beets', 'Vegetable'], - ['Bell pepper', 'Vegetable'], - ['Broccoli', 'Vegetable'], - ['Brussels sprout', 'Vegetable'], - ['Cabbage', 'Vegetable'], - ['Carrot', 'Vegetable'], - ['Cauliflower', 'Vegetable'], - ['Celery', 'Vegetable'], - ['Chard', 'Vegetable'], - ['Chicory', 'Vegetable'], - ['Corn', 'Vegetable'], - ['Cucumber', 'Vegetable'], - ['Daikon', 'Vegetable'], - ['Date', 'Fruit'], - ['Edamame', 'Vegetable'], - ['Eggplant', 'Vegetable'], - ['Elderberry', 'Fruit'], - ['Fennel', 'Vegetable'], - ['Fig', 'Fruit'], - ['Garlic', 'Vegetable'], - ['Grape', 'Fruit'], - ['Honeydew melon', 'Fruit'], - ['Iceberg lettuce', 'Vegetable'], - ['Jerusalem artichoke', 'Vegetable'], - ['Kale', 'Vegetable'], - ['Kiwi', 'Fruit'], - ['Leek', 'Vegetable'], - ['Lemon', 'Fruit'], - ['Mango', 'Fruit'], - ['Mangosteen', 'Fruit'], - ['Melon', 'Fruit'], - ['Mushroom', 'Fungus'], - ['Nectarine', 'Fruit'], - ['Okra', 'Vegetable'], - ['Olive', 'Vegetable'], - ['Onion', 'Vegetable'], - ['Orange', 'Fruit'], - ['Parsnip', 'Vegetable'], - ['Pea', 'Vegetable'], - ['Pear', 'Fruit'], - ['Pineapple', 'Fruit'], - ['Potato', 'Vegetable'], - ['Pumpkin', 'Fruit'], - ['Quince', 'Fruit'], - ['Radish', 'Vegetable'], - ['Rhubarb', 'Vegetable'], - ['Shallot', 'Vegetable'], - ['Spinach', 'Vegetable'], - ['Squash', 'Vegetable'], - ['Strawberry', 'Fruit'], - ['Sweet potato', 'Vegetable'], - ['Tomato', 'Fruit'], - ['Turnip', 'Vegetable'], - ['Ugli fruit', 'Fruit'], - ['Victoria plum', 'Fruit'], - ['Watercress', 'Vegetable'], - ['Watermelon', 'Fruit'], - ['Yam', 'Vegetable'], - ['Zucchini', 'Vegetable'], -]; - -function searchVeggies(searchString) { - var results = []; - - for (var i = 0; i < FRUITS_AND_VEGGIES.length; i++) { - var veggie = FRUITS_AND_VEGGIES[i][0].toLowerCase(); - if (veggie.indexOf(searchString.toLowerCase()) === 0) { - results.push(FRUITS_AND_VEGGIES[i]); - } - } - - return results; -} - -/** - * @function onload - * @description Initialize the combobox examples once the page has loaded - */ -window.addEventListener('load', function () { - new aria.GridCombobox( - document.getElementById('ex1-input'), - document.getElementById('ex1-grid'), - searchVeggies - ); -}); diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo.js deleted file mode 100644 index 04d2c902b..000000000 --- a/tests/combobox-select-only/reference/2022-4-26_144616/js/grid-combo.js +++ /dev/null @@ -1,318 +0,0 @@ -/* - * This content is licensed according to the W3C Software License at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - */ - -'use strict'; - -var aria = aria || {}; - -/** - * @class - * @description - * Combobox object representing the state and interactions for a combobox - * widget - * @param input - * The input node - * @param grid - * The grid node to load results in - * @param searchFn - * The search function. The function accepts a search string and returns an - * array of results. - */ -aria.GridCombobox = function (input, grid, searchFn) { - this.input = input; - this.grid = grid; - this.searchFn = searchFn; - this.activeRowIndex = -1; - this.activeColIndex = 0; - this.rowsCount = 0; - this.colsCount = 0; - this.gridFocused = false; - this.shown = false; - this.selectionCol = 0; - - this.setupEvents(); -}; - -aria.GridCombobox.prototype.setupEvents = function () { - document.body.addEventListener('click', this.handleBodyClick.bind(this)); - this.input.addEventListener('keyup', this.handleInputKeyUp.bind(this)); - this.input.addEventListener('keydown', this.handleInputKeyDown.bind(this)); - this.input.addEventListener('focus', this.handleInputFocus.bind(this)); - this.grid.addEventListener('click', this.handleGridClick.bind(this)); -}; - -aria.GridCombobox.prototype.handleBodyClick = function (evt) { - if (evt.target === this.input || this.grid.contains(evt.target)) { - return; - } - this.hideResults(); -}; - -aria.GridCombobox.prototype.handleInputKeyUp = function (evt) { - var key = evt.which || evt.keyCode; - - switch (key) { - case aria.KeyCode.UP: - case aria.KeyCode.DOWN: - case aria.KeyCode.ESC: - case aria.KeyCode.RETURN: - evt.preventDefault(); - return; - case aria.KeyCode.LEFT: - case aria.KeyCode.RIGHT: - if (this.gridFocused) { - evt.preventDefault(); - return; - } - break; - default: - this.updateResults(); - } -}; - -aria.GridCombobox.prototype.handleInputKeyDown = function (evt) { - var key = evt.which || evt.keyCode; - var activeRowIndex = this.activeRowIndex; - var activeColIndex = this.activeColIndex; - - if (key === aria.KeyCode.ESC) { - if (this.gridFocused) { - this.gridFocused = false; - this.removeFocusCell(this.activeRowIndex, this.activeColIndex); - this.activeRowIndex = -1; - this.activeColIndex = 0; - this.input.setAttribute('aria-activedescendant', ''); - } else { - if (!this.shown) { - setTimeout( - function () { - // On Firefox, input does not get cleared here unless wrapped in - // a setTimeout - this.input.value = ''; - }.bind(this), - 1 - ); - } - } - if (this.shown) { - this.hideResults(); - } - return; - } - - if (this.rowsCount < 1) { - return; - } - - var prevActive = this.getItemAt(activeRowIndex, this.selectionCol); - var activeItem; - - switch (key) { - case aria.KeyCode.UP: - this.gridFocused = true; - activeRowIndex = this.getRowIndex(key); - evt.preventDefault(); - break; - case aria.KeyCode.DOWN: - this.gridFocused = true; - activeRowIndex = this.getRowIndex(key); - evt.preventDefault(); - break; - case aria.KeyCode.LEFT: - if (activeColIndex <= 0) { - activeColIndex = this.colsCount - 1; - activeRowIndex = this.getRowIndex(key); - } else { - activeColIndex--; - } - if (this.gridFocused) { - evt.preventDefault(); - } - break; - case aria.KeyCode.RIGHT: - if (activeColIndex === -1 || activeColIndex >= this.colsCount - 1) { - activeColIndex = 0; - activeRowIndex = this.getRowIndex(key); - } else { - activeColIndex++; - } - if (this.gridFocused) { - evt.preventDefault(); - } - break; - case aria.KeyCode.RETURN: - activeItem = this.getItemAt(activeRowIndex, this.selectionCol); - this.selectItem(activeItem); - this.gridFocused = false; - return; - case aria.KeyCode.TAB: - this.hideResults(); - return; - default: - return; - } - - if (prevActive) { - this.removeFocusCell(this.activeRowIndex, this.activeColIndex); - prevActive.setAttribute('aria-selected', 'false'); - } - - activeItem = this.getItemAt(activeRowIndex, activeColIndex); - this.activeRowIndex = activeRowIndex; - this.activeColIndex = activeColIndex; - - if (activeItem) { - this.input.setAttribute( - 'aria-activedescendant', - 'result-item-' + activeRowIndex + 'x' + activeColIndex - ); - this.focusCell(activeRowIndex, activeColIndex); - var selectedItem = this.getItemAt(activeRowIndex, this.selectionCol); - selectedItem.setAttribute('aria-selected', 'true'); - } else { - this.input.setAttribute('aria-activedescendant', ''); - } -}; - -aria.GridCombobox.prototype.handleInputFocus = function () { - this.updateResults(); -}; - -aria.GridCombobox.prototype.handleGridClick = function (evt) { - if (!evt.target) { - return; - } - - var row; - if (evt.target.getAttribute('role') === 'row') { - row = evt.target; - } else if (evt.target.getAttribute('role') === 'gridcell') { - row = evt.target.parentNode; - } else { - return; - } - - var selectItem = row.querySelector('.result-cell'); - this.selectItem(selectItem); -}; - -aria.GridCombobox.prototype.isElementInView = function (element) { - var bounding = element.getBoundingClientRect(); - - return ( - bounding.top >= 0 && - bounding.left >= 0 && - bounding.bottom <= - (window.innerHeight || document.documentElement.clientHeight) && - bounding.right <= - (window.innerWidth || document.documentElement.clientWidth) - ); -}; - -aria.GridCombobox.prototype.updateResults = function () { - var searchString = this.input.value; - var results = this.searchFn(searchString); - - this.hideResults(); - - if (!searchString) { - results = []; - } - - if (results.length) { - for (var row = 0; row < results.length; row++) { - var resultRow = document.createElement('div'); - resultRow.className = 'result-row'; - resultRow.setAttribute('role', 'row'); - resultRow.setAttribute('id', 'result-row-' + row); - for (var col = 0; col < results[row].length; col++) { - var resultCell = document.createElement('div'); - resultCell.className = 'result-cell'; - resultCell.setAttribute('role', 'gridcell'); - resultCell.setAttribute('id', 'result-item-' + row + 'x' + col); - resultCell.innerText = results[row][col]; - resultRow.appendChild(resultCell); - } - this.grid.appendChild(resultRow); - } - aria.Utils.removeClass(this.grid, 'hidden'); - this.input.setAttribute('aria-expanded', 'true'); - this.rowsCount = results.length; - this.colsCount = results.length ? results[0].length : 0; - this.shown = true; - } -}; - -aria.GridCombobox.prototype.getRowIndex = function (key) { - var activeRowIndex = this.activeRowIndex; - - switch (key) { - case aria.KeyCode.UP: - case aria.KeyCode.LEFT: - if (activeRowIndex <= 0) { - activeRowIndex = this.rowsCount - 1; - } else { - activeRowIndex--; - } - break; - case aria.KeyCode.DOWN: - case aria.KeyCode.RIGHT: - if (activeRowIndex === -1 || activeRowIndex >= this.rowsCount - 1) { - activeRowIndex = 0; - } else { - activeRowIndex++; - } - } - - return activeRowIndex; -}; - -aria.GridCombobox.prototype.getItemAt = function (rowIndex, colIndex) { - return document.getElementById('result-item-' + rowIndex + 'x' + colIndex); -}; - -aria.GridCombobox.prototype.selectItem = function (item) { - if (item) { - this.input.value = item.innerText; - this.hideResults(); - } -}; - -aria.GridCombobox.prototype.hideResults = function () { - this.gridFocused = false; - this.shown = false; - this.activeRowIndex = -1; - this.activeColIndex = 0; - this.grid.innerHTML = ''; - aria.Utils.addClass(this.grid, 'hidden'); - this.input.setAttribute('aria-expanded', 'false'); - this.rowsCount = 0; - this.colsCount = 0; - this.input.setAttribute('aria-activedescendant', ''); - - // ensure the input is in view - if (!this.isElementInView(this.input)) { - this.input.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } -}; - -aria.GridCombobox.prototype.removeFocusCell = function (rowIndex, colIndex) { - var row = document.getElementById('result-row-' + rowIndex); - aria.Utils.removeClass(row, 'focused'); - var cell = this.getItemAt(rowIndex, colIndex); - aria.Utils.removeClass(cell, 'focused-cell'); -}; - -aria.GridCombobox.prototype.focusCell = function (rowIndex, colIndex) { - var row = document.getElementById('result-row-' + rowIndex); - aria.Utils.addClass(row, 'focused'); - var cell = this.getItemAt(rowIndex, colIndex); - aria.Utils.addClass(cell, 'focused-cell'); - - // ensure the cell is in view - if (!this.isElementInView(cell)) { - cell.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } -}; diff --git a/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js b/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js index 2398e1b9d..fdf5874a8 100644 --- a/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js +++ b/tests/combobox-select-only/reference/2022-4-26_144616/js/select-only.js @@ -128,6 +128,20 @@ function getUpdatedIndex(currentIndex, maxIndex, action) { } } +// check if element is visible in browser view port +function isElementInView(element) { + var bounding = element.getBoundingClientRect(); + + return ( + bounding.top >= 0 && + bounding.left >= 0 && + bounding.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + bounding.right <= + (window.innerWidth || document.documentElement.clientWidth) + ); +} + // check if an element is currently scrollable function isScrollable(element) { return element && element.clientHeight < element.scrollHeight; @@ -318,6 +332,12 @@ Select.prototype.onOptionChange = function (index) { if (isScrollable(this.listboxEl)) { maintainScrollVisibility(options[index], this.listboxEl); } + + // ensure the new option is visible on screen + // ensure the new option is in view + if (!isElementInView(options[index])) { + options[index].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } }; Select.prototype.onOptionClick = function (index) { @@ -364,6 +384,10 @@ Select.prototype.updateMenuState = function (open, callFocus = true) { const activeID = open ? `${this.idBase}-${this.activeIndex}` : ''; this.comboEl.setAttribute('aria-activedescendant', activeID); + if (activeID === '' && !isElementInView(this.comboEl)) { + this.comboEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + // move focus back to the combobox, if needed callFocus && this.comboEl.focus(); }; @@ -385,6 +409,9 @@ window.addEventListener('load', function () { 'Guava', 'Huckleberry', ]; - const selectEl = document.querySelector('.js-select'); - document.defaultView.selectController = new Select(selectEl, options); + const selectEls = document.querySelectorAll('.js-select'); + + selectEls.forEach((el) => { + new Select(el, options); + }); });