diff --git a/aria-practices.html b/aria-practices.html index 7a6b53098a..8fa54c512b 100644 --- a/aria-practices.html +++ b/aria-practices.html @@ -795,6 +795,7 @@

Examples

  • Editable Combobox with List Autocomplete: An editable combobox that demonstrates the autocomplete behavior known as list with manual selection.
  • Editable Combobox Without Autocomplete: An editable combobox that demonstrates the behavior associated with aria-autocomplete=none.
  • Editable Combobox with Grid Popup: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.
  • +
  • Date Picker Combobox: An editable date input combobox that opens a dialog containing a calendar grid and buttons for navigating by month and year.
  • diff --git a/cspell.json b/cspell.json index 5404f893cf..9290c0804f 100644 --- a/cspell.json +++ b/cspell.json @@ -7,6 +7,7 @@ ], "words": [ "activedescendant", + "affordance", "ahederson", "alertdialog", "amet", @@ -79,6 +80,7 @@ "Garaventa", "Geppy", "gridcell", + "gridcells", "GUIs", "Gunderson", "haspopup", @@ -136,6 +138,7 @@ "Nemeth", "Nihonium", "nofollow", + "norotate", "Nurthen", "NVDA", "Obel", diff --git a/examples/combobox/combobox-autocomplete-both.html b/examples/combobox/combobox-autocomplete-both.html index fba31e0ea8..ecd3b4ba20 100644 --- a/examples/combobox/combobox-autocomplete-both.html +++ b/examples/combobox/combobox-autocomplete-both.html @@ -43,6 +43,7 @@

    Editable Combobox With Both List and Inline Autocomplete Example

  • Editable Combobox with List Autocomplete: An editable combobox that demonstrates the autocomplete behavior known as list with manual selection.
  • Editable Combobox Without Autocomplete: An editable combobox that demonstrates the behavior associated with aria-autocomplete=none.
  • Editable Combobox with Grid Popup: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.
  • +
  • Date Picker Combobox: An editable date input combobox that opens a dialog containing a calendar grid and buttons for navigating by month and year.
  • Example

    diff --git a/examples/combobox/combobox-autocomplete-list.html b/examples/combobox/combobox-autocomplete-list.html index 4e3414e623..712508f492 100644 --- a/examples/combobox/combobox-autocomplete-list.html +++ b/examples/combobox/combobox-autocomplete-list.html @@ -43,6 +43,7 @@

    Editable Combobox With List Autocomplete Example

  • Editable Combobox with Both List and Inline Autocomplete: An editable combobox that demonstrates the autocomplete behavior known as list with inline autocomplete.
  • Editable Combobox Without Autocomplete: An editable combobox that demonstrates the behavior associated with aria-autocomplete=none.
  • Editable Combobox with Grid Popup: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.
  • +
  • Date Picker Combobox: An editable date input combobox that opens a dialog containing a calendar grid and buttons for navigating by month and year.
  • diff --git a/examples/combobox/combobox-autocomplete-none.html b/examples/combobox/combobox-autocomplete-none.html index 1eb5c29d36..8eeaac5f7c 100644 --- a/examples/combobox/combobox-autocomplete-none.html +++ b/examples/combobox/combobox-autocomplete-none.html @@ -40,6 +40,7 @@

    Editable Combobox without Autocomplete Example

  • Editable Combobox with Both List and Inline Autocomplete: An editable combobox that demonstrates the autocomplete behavior known as list with inline autocomplete.
  • Editable Combobox with List Autocomplete: An editable combobox that demonstrates the autocomplete behavior known as list with manual selection.
  • Editable Combobox with Grid Popup: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.
  • +
  • Date Picker Combobox: An editable date input combobox that opens a dialog containing a calendar grid and buttons for navigating by month and year.
  • diff --git a/examples/combobox/combobox-datepicker.html b/examples/combobox/combobox-datepicker.html new file mode 100644 index 0000000000..9572d5c468 --- /dev/null +++ b/examples/combobox/combobox-datepicker.html @@ -0,0 +1,746 @@ + + + + +Date Picker Combobox Example | WAI-ARIA Authoring Practices 1.2 + + + + + + + + + + + + + + + +
    +

    Date Picker Combobox Example

    +

    + The below date picker demonstrates an implementation of the combobox design pattern that opens a dialog. + The date picker dialog is opened by activating the choose date button or by moving keyboard focus to the combobox and pressing Down Arrow or Alt + Down Arrow. + The dialog contains an implementation of the grid pattern for displaying a calendar and enabling selection of a date. + Additional buttons in the dialog are available for changing the month and year shown in the grid. +

    +

    Similar examples include:

    + + +
    +

    Example

    + + +
    + +
    + +
    + +
    + +
    + + + (date format: mm/dd/yyyy) + +
    + + +
    +
    + +
    + +
    +

    Accessibility Features

    +
      +
    • The description of the date format is associated with the combobox via aria-describedby, making it available to assistive technologies as an accessible description.
    • +
    • + While the down arrow icon is excluded from the Tab sequence as specified in the combobox design pattern, it is made accessible to assistive technologies as the Choose Date button. + This enables assistive technology users who might not have a keyboard, e.g., someone using a touch-based screen reader, to open the date picker dialog. +
    • +
    • In the dialog, shortcut keys are assigned to the additional buttons for changing the month and year displayed in the calendar.
    • +
    • Keyboard help is displayed at the bottom of the dialog. A live region is used to announce it to screen reader users when focus moves into the grid.
    • +
    • The calendar heading displaying the month and year is marked up as a live region so screen reader users get feedback from the buttons and keyboard commands that change the month and year.
    • +
    • + To facilitate compact visual design in the calendar, the day names in the column headers are abbreviated to two characters. + However, this makes it more difficult for screen reader users to understand the day names. + So, full day names are provided to assistive technologies in the HTML abbr attribute on the column headers, enabling screen readers to announce the full names when users navigate the grid. +
    • +
    • Focus and hover styling of the controls support operating system high contrast settings via the CSS border property: +
        +
      • When a button or date cell receives focus a border is added.
      • +
      • When hovering over a button or date cell with a pointing device a border is added.
      • +
      • By default, buttons and date cells do not have a border; padding provides a placeholder space for focus and hover styling.
      • +
      +
    • +
    +
    + +
    +

    Keyboard Support

    + +
    +

    Combobox

    + + + + + + + + + + + + + +
    KeyFunction
    Down Arrow,
    ALT + Down Arrow
    +
      +
    • Open the date picker dialog.
    • +
    • + If the combobox contains a valid date, moves focus to that date in the calendar grid. + Otherwise, moves focus to current date, i.e., today's date. +
    • +
    +
    +
    + +
    +

    Date Picker Dialog

    + + + + + + + + + + + + + + + + + + + + + +
    KeyFunction
    ESCCloses the dialog and moves focus to the combobox.
    TAB +
      +
    • Moves focus to next element in the dialog Tab sequence.
    • +
    • Note that, as specified in the grid design pattern, only one element in the calendar grid is in the Tab sequence.
    • +
    • If focus is on the last button (i.e., "OK"), moves focus to the first button (i.e. "Previous Year").
    • +
    +
    Shift + TAB +
      +
    • Moves focus to previous element in the dialog Tab sequence.
    • +
    • Note that, as specified in the grid design pattern, only one element in the calendar grid is in the Tab sequence.
    • +
    • If focus is on the first button (i.e., "Previous Year"), moves focus to the last button (i.e. "OK").
    • +
    +
    +
    + +
    +

    Date Picker Dialog: Calendar Buttons

    + + + + + + + + + + + + + +
    KeyFunction
    Space,
    Enter
    + Change the month and/or year displayed in the calendar grid. +
    +
    + +
    +

    Date Picker Dialog: Date Grid

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyFunction
    Space +
      +
    • Selects the date.
    • +
    • Updates the value of the combobox with the selected date.
    • +
    +
    Enter +
      +
    • Selects the date.
    • +
    • Updates the value of the combobox with the selected date.
    • +
    • Closes the dialog and moves focus to the combobox.
    • +
    +
    Up ArrowMoves focus to the same day of the previous week.
    Down ArrowMoves focus to the same day of the next week.
    Right ArrowMoves focus to the next day.
    Left ArrowMoves focus to the previous day.
    HomeMoves focus to the first day (e.g. Sunday) of the current week.
    EndMoves focus to the last day (e.g. Saturday) of the current week.
    PageUp +
      +
    • Changes the grid of dates to the previous month.
    • +
    • Moves focus to the same day of the same week. If that day does not exist, moves focus to the same day of the previous or next week.
    • +
    +
    Shift+
    PageUp
    +
      +
    • Changes the grid of dates to the previous year.
    • +
    • Moves focus to the same day of the same week in the previous year. If that day does not exist, moves focus to the same day of the previous or next week.
    • +
    +
    PageDown +
      +
    • Changes the grid of dates to the next month.
    • +
    • Moves focus to the same day of the same week. If that day does not exist, moves focus to the same day of previous or next week.
    • +
    +
    Shift+
    PageDown
    +
      +
    • Changes the grid of dates to the next year.
    • +
    • Moves focus to the same day of the same week in the next year. If that day does not exist, moves focus to the same day of previous or next week.
    • +
    +
    +
    + +
    +

    Date Picker Dialog: OK and Cancel Buttons

    + + + + + + + + + + + + + +
    KeyFunction
    Space,
    Enter
    + Activates the button: +
      +
    • "Cancel": Closes the dialog, moves focus to combobox, does not update combobox value.
    • +
    • "OK": Closes the dialog, moves focus to combobox, updates date in combobox.
    • +
    +
    +
    +
    + +
    +

    Role, Property, State, and Tabindex Attributes

    + +
    +

    Combobox

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    role="combobox"input + Identifies the input element as a combobox. +
    aria-haspopup="dialog"input + Indicates that the combobox opens a dialog. +
    aria-expanded="false"input + Indicates that the combobox is collapsed, i.e., the "Choose Date" dialog is not displayed. +
    aria-expanded="true"input + Indicates that the combobox is expanded, i.e., the "Choose Date" dialog is open. +
    aria-autocomplete="none"input + Indicates the combobox does not support autocomplete. +
    aria-controls="IDREF"input + Identifies the element controled by the combobox. +
    aria-describedby="IDREF"input + Identifies the element that provides an accessible description for the combobox, enableing assistive technologies to associate the date format description with the input. +
    +
    + +
    +

    Choose Date Button

    + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    tabindex="-1"button + Excludes the button (i.e., the down arrow icon) from the Tab sequence as specified by the combobox design pattern. +
    aria-label="string"buttonDefines the accessible name as "Choose Date", which matches the title of the dialog opened by activating the button.
    +
    + +
    +

    Date Picker Dialog

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    dialog + div + Identifies the element as a dialog.
    aria-modal="true"divIndicates the dialog is modal.
    aria-label="Choose Date"divDefines the accessible name for the dialog.
    aria-live="polite"h2 +
      +
    • Indicates that screen readers should automatically announce the element containing the currently displayed month and year when they change.
    • +
    • The polite value indicates that other announcements should not be interrupted.
    • +
    +
    aria-live="polite"div +
      +
    • Indicates the element that displays information about keyboard commands for navigating the grid should be automatically announced by screen readers.
    • +
    • The script slightly delays display of the information, so screen readers are more likely to announce it after announcing focus change events.
    • +
    • The polite value indicates that other announcements should not be interrupted.
    • +
    +
    +
    + +
    +

    Date Picker Dialog: Calendar Navigation Buttons

    + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    aria-label="String"button + Defines the accessible name of the button (e.g. "Next Year"). +
    +
    + +
    +

    Date Picker Dialog: Date Grid

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    grid + table + +
      +
    • Identifies the table element as a grid widget.
    • +
    • Since the grid role is applied to a table element, the row, columnheader, and gridcell roles do not need to be specified because they are implied by tr, th, and td tags.
    • +
    +
    aria-labelledby="IDREF"table + Identifies the element that provides the accessible name for the grid, which is the h2 that shows the month and year of the dates displayed in the grid. +
    + tabindex="0" + + td + +
      +
    • Makes the gridcell focusable and includes it in the dialog Tab sequence.
    • +
    • Set dynamically by the JavaScript when the element is to be included in the dialog Tab sequence.
    • +
    • At any given time, only one gridcell within the grid is in the dialog Tab sequence.
    • +
    • This approach to managing focus is described in the section on roving tabindex.
    • +
    +
    + tabindex="-1" + + td + +
      +
    • Makes the gridcell focusable and excludes it from the dialog Tab sequence.
    • +
    • Changed dynamically to 0 by the JavaScript when the gridcell is to be included in the dialog Tab sequence.
    • +
    • At any given time, only one gridcell within the grid is in the dialog Tab sequence.
    • +
    • This approach to managing focus is described in the section on roving tabindex.
    • +
    +
    aria-selected="true"td +
      +
    • Indicates the cell is selected.
    • +
    • Set on the gridcell representing the current value of the combobox; no other gridcells have aria-selected specified.
    • +
    +
    +

    + Note: Since the names of the days of the week in the column headers are abbreviated to two characters, they may be difficult to understand when announced by a screen reader. + An alternative column header name can be provided to screen readers by applying the abbr attribute to the th elements. + So, each th element includes an abbr attribute containing the full spelling of the name of the day for that column. +

    +
    +
    + +
    +

    Javascript and CSS Source Code

    + +
    + +
    +

    HTML Source Code

    + +
    + + +
    +
    + + + diff --git a/examples/combobox/combobox-select-only.html b/examples/combobox/combobox-select-only.html index 64fe2b9337..c470f4d8b5 100644 --- a/examples/combobox/combobox-select-only.html +++ b/examples/combobox/combobox-select-only.html @@ -38,6 +38,7 @@

    Select-Only Combobox Example

  • Editable Combobox with List Autocomplete: An editable combobox that demonstrates the autocomplete behavior known as list with manual selection.
  • Editable Combobox Without Autocomplete: An editable combobox that demonstrates the behavior associated with aria-autocomplete=none.
  • Editable Combobox with Grid Popup: An editable combobox that presents suggestions in a grid, enabling users to navigate descriptive information about each suggestion.
  • +
  • Date Picker Combobox: An editable date input combobox that opens a dialog containing a calendar grid and buttons for navigating by month and year.
  • diff --git a/examples/combobox/css/combobox-autocomplete.css b/examples/combobox/css/combobox-autocomplete.css index bd15ab978c..297f818b58 100644 --- a/examples/combobox/css/combobox-autocomplete.css +++ b/examples/combobox/css/combobox-autocomplete.css @@ -63,12 +63,6 @@ stroke: gray; } -.combobox button[aria-expanded="true"] polygon.arrow, -.combobox .group.focus polygon.arrow { - fill: black; - stroke: black; -} - ul[role="listbox"] { margin: 0; padding: 0; diff --git a/examples/combobox/css/combobox-datepicker.css b/examples/combobox/css/combobox-datepicker.css new file mode 100644 index 0000000000..833cf69de5 --- /dev/null +++ b/examples/combobox/css/combobox-datepicker.css @@ -0,0 +1,257 @@ +.combobox-datepicker { + margin-top: 1em; + position: relative; +} + +.combobox-datepicker .group { + display: inline-flex; +} + +.combobox-datepicker label { + display: block; +} + +.combobox-datepicker .group { + position: relative; + width: 12.125rem; +} + +.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 .group.focus input, +.combobox-datepicker .group.focus button { + background-color: #DEF; +} + +.combobox-datepicker .group polygon { + fill: gray; + stroke: transparent; +} + +.combobox-datepicker .group button[aria-expanded="true"] polygon, +.combobox-datepicker .group.focus 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(216, 80%, 51%); + margin-top: 1em; + border-radius: 5px; + padding: 0; + background-color: #fff; +} + +.combobox-datepicker .header { + cursor: default; + background-color: hsl(216, 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 .dialog button { + border-style: none; + background: transparent; +} + +.combobox-datepicker .dialog button::-moz-focus-inner { + border: 0; +} + +.combobox-datepicker .dates { + width: 320px; +} + +.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-ok-cancel-group button { + padding: 6px; + margin-left: 1em; + width: 5em; + background-color: hsl(216, 80%, 92%); + font-size: 0.85em; + color: black; + outline: none; + border-radius: 5px; +} + +.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(216, 89%, 51%); +} + +.combobox-datepicker .month-year { + display: inline-block; + width: 12em; + text-align: center; +} + +.combobox-datepicker .dates { + padding-left: 1em; + padding-right: 1em; + padding-top: 1em; +} + +.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(216, 80%, 96%); +} + +.combobox-datepicker .dates td[tabindex="0"] { + background-color: hsl(216, 80%, 92%); +} + +.combobox-datepicker .dates td:focus, +.combobox-datepicker .dates td:hover { + padding: 0; + background-color: hsl(216, 80%, 92%); +} + +.combobox-datepicker .dates td:not(.disabled):hover { + padding: 2px; + border: 1px solid rgb(100, 100, 100); +} + +.combobox-datepicker .dates td:focus { + padding: 1px; + border: 2px solid rgb(100, 100, 100); + outline: 0; +} + +.combobox-datepicker .dialog-message { + padding-top: 0.25em; + padding-left: 1em; + height: 1.75em; + background: hsl(216, 80%, 51%); + color: white; +} diff --git a/examples/combobox/grid-combo.html b/examples/combobox/grid-combo.html index 6329a0168b..35a0f35868 100644 --- a/examples/combobox/grid-combo.html +++ b/examples/combobox/grid-combo.html @@ -48,6 +48,7 @@

    Editable Combobox with Grid Popup Example

  • Editable Combobox with Both List and Inline Autocomplete: An editable combobox that demonstrates the autocomplete behavior known as list with inline autocomplete.
  • Editable Combobox with List Autocomplete: An editable combobox that demonstrates the autocomplete behavior known as list with manual selection.
  • Editable Combobox Without Autocomplete: An editable combobox that demonstrates the behavior associated with aria-autocomplete=none.
  • +
  • Date Picker Combobox: An editable date input combobox that opens a dialog containing a calendar grid and buttons for navigating by month and year.
  • diff --git a/examples/combobox/js/combobox-datepicker.js b/examples/combobox/js/combobox-datepicker.js new file mode 100644 index 0000000000..ab6cb9fade --- /dev/null +++ b/examples/combobox/js/combobox-datepicker.js @@ -0,0 +1,830 @@ +/* +* 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'; + +var ComboboxDatePicker = function (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; + +}; + +ComboboxDatePicker.prototype.init = function () { + + this.comboboxNode.addEventListener('keydown', this.handleComboboxKeyDown.bind(this)); + this.comboboxNode.addEventListener('click', this.handleComboboxClick.bind(this)); + this.comboboxNode.addEventListener('focus', this.handleComboboxFocus.bind(this)); + this.comboboxNode.addEventListener('blur', this.handleComboboxBlur.bind(this)); + + this.buttonNode.addEventListener('keydown', this.handleButtonKeyDown.bind(this)); + this.buttonNode.addEventListener('click', this.handleButtonClick.bind(this)); + + this.okButtonNode.addEventListener('click', this.handleOkButton.bind(this)); + this.okButtonNode.addEventListener('keydown', this.handleOkButton.bind(this)); + + this.cancelButtonNode.addEventListener('click', this.handleCancelButton.bind(this)); + this.cancelButtonNode.addEventListener('keydown', this.handleCancelButton.bind(this)); + + this.prevMonthNode.addEventListener('click', this.handlePreviousMonthButton.bind(this)); + this.nextMonthNode.addEventListener('click', this.handleNextMonthButton.bind(this)); + this.prevYearNode.addEventListener('click', this.handlePreviousYearButton.bind(this)); + this.nextYearNode.addEventListener('click', this.handleNextYearButton.bind(this)); + + this.prevMonthNode.addEventListener('keydown', this.handlePreviousMonthButton.bind(this)); + this.nextMonthNode.addEventListener('keydown', this.handleNextMonthButton.bind(this)); + this.prevYearNode.addEventListener('keydown', this.handlePreviousYearButton.bind(this)); + this.nextYearNode.addEventListener('keydown', this.handleNextYearButton.bind(this)); + + document.body.addEventListener('mouseup', this.handleBackgroundMouseUp.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.handleDayClick.bind(this)); + cell.addEventListener('keydown', this.handleDayKeyDown.bind(this)); + cell.addEventListener('focus', this.handleDayFocus.bind(this)); + + cell.innerHTML = '-1'; + + row.appendChild(cell); + this.days.push(cell); + } + } + + this.updateGrid(); + this.close(false); +}; + +ComboboxDatePicker.prototype.isSameDay = function (day1, day2) { + return (day1.getFullYear() == day2.getFullYear()) && + (day1.getMonth() == day2.getMonth()) && + (day1.getDate() == day2.getDate()); +}; + +ComboboxDatePicker.prototype.isNotSameMonth = function (day1, day2) { + return (day1.getFullYear() != day2.getFullYear()) || + (day1.getMonth() != day2.getMonth()); +}; + +ComboboxDatePicker.prototype.updateGrid = function () { + + var i, flag; + var fd = this.focusDay; + + this.monthYearNode.innerHTML = 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'; + } + } + } +}; + +ComboboxDatePicker.prototype.setFocusDay = function (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)); + +}; + +ComboboxDatePicker.prototype.open = function () { + 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(); +}; + +ComboboxDatePicker.prototype.isOpen = function () { + return window.getComputedStyle(this.dialogNode).display !== 'none'; +}; + +ComboboxDatePicker.prototype.close = function (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(); + } +}; + +ComboboxDatePicker.prototype.handleOkButton = function (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(); + } +}; + +ComboboxDatePicker.prototype.handleCancelButton = function (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(); + } +}; + +ComboboxDatePicker.prototype.handleNextYearButton = function (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(); + } +}; + +ComboboxDatePicker.prototype.handlePreviousYearButton = function (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(); + } +}; + +ComboboxDatePicker.prototype.handleNextMonthButton = function (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(); + } +}; + +ComboboxDatePicker.prototype.handlePreviousMonthButton = function (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(); + } +}; + +ComboboxDatePicker.prototype.moveFocusToDay = function (day) { + var d = this.focusDay; + + this.focusDay = day; + + if ((d.getMonth() != this.focusDay.getMonth()) || + (d.getYear() != this.focusDay.getYear())) { + this.updateGrid(); + } + this.setFocusDay(); +}; + + +ComboboxDatePicker.prototype.moveToNextYear = function () { + this.focusDay.setFullYear(this.focusDay.getFullYear() + 1); + this.updateGrid(); +}; + +ComboboxDatePicker.prototype.moveToPreviousYear = function () { + this.focusDay.setFullYear(this.focusDay.getFullYear() - 1); + this.updateGrid(); +}; + +ComboboxDatePicker.prototype.moveToNextMonth = function () { + this.focusDay.setMonth(this.focusDay.getMonth() + 1); + this.updateGrid(); +}; + +ComboboxDatePicker.prototype.moveToPreviousMonth = function () { + this.focusDay.setMonth(this.focusDay.getMonth() - 1); + this.updateGrid(); +}; + +ComboboxDatePicker.prototype.moveFocusToNextDay = function () { + var d = new Date(this.focusDay); + d.setDate(d.getDate() + 1); + this.moveFocusToDay(d); +}; + +ComboboxDatePicker.prototype.moveFocusToNextWeek = function () { + var d = new Date(this.focusDay); + d.setDate(d.getDate() + 7); + this.moveFocusToDay(d); +}; + +ComboboxDatePicker.prototype.moveFocusToPreviousDay = function () { + var d = new Date(this.focusDay); + d.setDate(d.getDate() - 1); + this.moveFocusToDay(d); +}; + +ComboboxDatePicker.prototype.moveFocusToPreviousWeek = function () { + var d = new Date(this.focusDay); + d.setDate(d.getDate() - 7); + this.moveFocusToDay(d); +}; + +ComboboxDatePicker.prototype.moveFocusToFirstDayOfWeek = function () { + var d = new Date(this.focusDay); + d.setDate(d.getDate() - d.getDay()); + this.moveFocusToDay(d); +}; + +ComboboxDatePicker.prototype.moveFocusToLastDayOfWeek = function () { + var d = new Date(this.focusDay); + d.setDate(d.getDate() + (6 - d.getDay())); + this.moveFocusToDay(d); +}; + +// Day methods + +ComboboxDatePicker.prototype.isDayDisabled = function (domNode) { + return domNode.classList.contains('disabled'); +}; + +ComboboxDatePicker.prototype.getDayFromDataDateAttribute = function (domNode) { + var parts = domNode.getAttribute('data-date').split('-'); + return new Date(parts[0], parseInt(parts[1])-1, parts[2]); +}; + +ComboboxDatePicker.prototype.updateDate = function (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.innerHTML = ''; + } + else { + domNode.classList.remove('disabled'); + domNode.innerHTML = day.getDate(); + if (selected) { + domNode.setAttribute('aria-selected', 'true'); + domNode.setAttribute('tabindex', '0'); + } + } + +}; + +ComboboxDatePicker.prototype.updateSelected = function (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'); + } + } +}; + + +ComboboxDatePicker.prototype.handleDayKeyDown = function (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(); + } +}; + +ComboboxDatePicker.prototype.handleDayClick = function (event) { + + if (!this.isDayDisabled(event.currentTarget)) { + this.setComboboxDate(event.currentTarget); + this.close(); + } + + event.stopPropagation(); + event.preventDefault(); + +}; + +ComboboxDatePicker.prototype.handleDayFocus = function () { + this.setMessage(this.messageCursorKeys); +}; + +// Combobox methods + +ComboboxDatePicker.prototype.setComboboxDate = function (domNode) { + + var d = this.focusDay; + + if (domNode) { + d = this.getDayFromDataDateAttribute(domNode); + } + + this.comboboxNode.value = (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear(); + +}; + +ComboboxDatePicker.prototype.getDateFromCombobox = function () { + + 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); + } + +}; + +ComboboxDatePicker.prototype.setMessage = function (str) { + + function setMessageDelayed () { + this.messageNode.textContent = str; + } + + if (str !== this.lastMessage) { + setTimeout(setMessageDelayed.bind(this), 200); + this.lastMessage = str; + } +}; + +ComboboxDatePicker.prototype.handleComboboxKeyDown = function (event) { + var flag = false, + char = event.key, + altKey = event.altKey; + + 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(); + } + +}; + +ComboboxDatePicker.prototype.handleComboboxClick = function (event) { + if (this.isOpen()) { + this.close(false); + } + else { + this.open(); + } + + event.stopPropagation(); + event.preventDefault(); +}; + +ComboboxDatePicker.prototype.handleComboboxFocus = function (event) { + event.currentTarget.parentNode.classList.add('focus'); +}; + +ComboboxDatePicker.prototype.handleComboboxBlur = function (event) { + event.currentTarget.parentNode.classList.remove('focus'); +}; + +ComboboxDatePicker.prototype.handleButtonKeyDown = function (event) { + + if (event.key === "Enter" || event.key === " ") { + this.open(); + this.setFocusDay(); + + event.stopPropagation(); + event.preventDefault(); + } +}; + +ComboboxDatePicker.prototype.handleButtonClick = function (event) { + if (this.isOpen()) { + this.close(); + } + else { + this.open(); + this.setFocusDay(); + } + + event.stopPropagation(); + event.preventDefault(); +}; + +ComboboxDatePicker.prototype.handleBackgroundMouseUp = function (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) { + var datePicker = new ComboboxDatePicker(dp); + datePicker.init(); + }); + +}); diff --git a/examples/dialog-modal/datepicker-dialog.html b/examples/dialog-modal/datepicker-dialog.html index 631e07373c..4ea5abf432 100644 --- a/examples/dialog-modal/datepicker-dialog.html +++ b/examples/dialog-modal/datepicker-dialog.html @@ -40,6 +40,7 @@

    Date Picker Dialog Example

    • Alert Dialog Example: A confirmation prompt that demonstrates an alert dialog.
    • Modal Dialog Example: An example demonstrating multiple layers of modal dialogs with both small and large amounts of content.
    • +
    • Date Picker Combobox: An editable date input combobox that opens a dialog containing a calendar grid and buttons for navigating by month and year.

    Example

    diff --git a/test/tests/combobox_datepicker.js b/test/tests/combobox_datepicker.js new file mode 100644 index 0000000000..22b570bdcd --- /dev/null +++ b/test/tests/combobox_datepicker.js @@ -0,0 +1,422 @@ +'use strict'; + +const { ariaTest } = require('..'); +const { By, Key } = require('selenium-webdriver'); +const assertAttributeValues = require('../util/assertAttributeValues'); +const assertAttributeDNE = require('../util/assertAttributeDNE'); +const assertAriaControls = require('../util/assertAriaControls'); +const assertAriaLabelledby = require('../util/assertAriaLabelledby'); +const assertAriaDescribedby = require('../util/assertAriaDescribedby'); +const assertAriaLabelExists = require('../util/assertAriaLabelExists'); +const assertAriaRoles = require('../util/assertAriaRoles'); +const assertRovingTabindex = require('../util/assertRovingTabindex'); +const assertTabOrder = require('../util/assertTabOrder'); + +const exampleFile = 'combobox/combobox-datepicker.html'; + +let today = new Date(); +let todayDataDate = today.toISOString().split('T')[0]; + +const ex = { + comboboxSelector: '#ex1 .group input', + buttonSelector: '#ex1 .group button', + calendarNavigationButtonSelector: '#ex1 [role="dialog"] .header button', + dialogSelector: '#ex1 [role="dialog"]', + cancelSelector: '#ex1 [role="dialog"] button[value="cancel"]', + dialogMessageSelector: '#ex1 .dialog-message', + gridSelector: '#ex1 [role="grid"]', + controlButtons: '#ex1 [role="dialog"] .header button', + currentMonthDateButtons: '#ex1 [role="dialog"] .dates td:not(.disabled)', + allDates: '#ex1 [role="dialog"] .dates td', + jan12019Day: '#ex1 [role="dialog"] .dates td[data-date="2019-01-01"]', + jan22019Day: '#ex1 [role="dialog"] .dates td[data-date="2019-01-02"]', + todayButton: `#ex1 [role="dialog"] .dates [data-date="${todayDataDate}"]`, + monthYear: '#cb-grid-label', + prevYear: '#ex1 [role="dialog"] button.prev-year', + prevMonth: '#ex1 [role="dialog"] button.prev-month', + nextMonth: '#ex1 [role="dialog"] button.next-month', + nextYear: '#ex1 [role="dialog"] button.next-year' +}; + +ex.allFocusableElementsInDialog = [ + `#ex1 [role="dialog"] td[data-date="${todayDataDate}"]`, + '#ex1 [role="dialog"] button[value="cancel"]', + '#ex1 [role="dialog"] button[value="ok"]', + ex.prevYear, + ex.prevMonth, + ex.nextMonth, + ex.nextYear +] + +const clickFirstOfMonth = async function (t) { + let today = new Date(); + today.setUTCHours(0,0,0,0); + + let firstOfMonth = new Date(today); + firstOfMonth.setDate(1); + let firstOfMonthString = today.toISOString().split('T')[0]; + + return (await t.context.queryElements(t, `[data-date=${firstOfMonthString}]`))[0].click(); +}; + +const clickToday = async function (t) { + let today = new Date(); + today.setUTCHours(0,0,0,0); + let todayString = today.toISOString().split('T')[0]; + return (await t.context.queryElements(t, `[data-date=${todayString}]`))[0].click(); +}; + +const setDateToJanFirst2019 = async function (t) { + await (await t.context.queryElements(t, ex.comboboxSelector))[0].click(); + return t.context.session.executeScript(function () { + const inputSelector = arguments[0]; + document.querySelector(inputSelector).value = '1/1/2019'; + }, ex.comboboxSelector); +}; + +const focusMatchesElement = async function (t, selector) { + return t.context.session.wait(async function () { + return t.context.session.executeScript(function () { + selector = arguments[0]; + return document.activeElement === document.querySelector(selector); + }, selector); + }, t.context.WaitTime); +}; + +// Attributes + +ariaTest('Combobox: has role', exampleFile, 'textbox-role', async (t) => { + await assertAriaRoles(t, 'ex1', 'combobox', 1, 'input'); +}); + +ariaTest('Combobox: has aria-haspopup set to "dialog"', exampleFile, 'textbox-aria-haspopup', async (t) => { + await assertAttributeValues(t, ex.comboboxSelector, 'aria-haspopup', 'dialog'); +}); + +ariaTest('Combobox: has aria-contorls set to "id-dialog-1"', exampleFile, 'textbox-aria-controls', async (t) => { + await assertAttributeValues(t, ex.comboboxSelector, 'aria-controls', 'cb-dialog-1'); +}); + +ariaTest('Combobox: has aria-contorls set to "id-descrption-1"', exampleFile, 'textbox-aria-describedby', async (t) => { + await assertAttributeValues(t, ex.comboboxSelector, 'aria-describedby', 'cb-description-1'); +}); + + +ariaTest('Combobox: Initially aria-expanded set to "false"', exampleFile, 'textbox-aria-expanded-false', async (t) => { + await assertAttributeValues(t, ex.comboboxSelector, 'aria-expanded', 'false'); +}); + +ariaTest('Combobox: aria-expanded set to "true" when dialog is open', exampleFile, 'textbox-aria-expanded-true', async (t) => { + // Open dialog box + await (await t.context.queryElements(t, ex.comboboxSelector))[0].sendKeys(Key.ARROW_DOWN); + await assertAttributeValues(t, ex.comboboxSelector, 'aria-expanded', 'true'); +}); + + + +// Button Tests + +ariaTest('Button: "aria-label" attribute', exampleFile, 'calendar-button-aria-label', async (t) => { + await assertAriaLabelExists(t, ex.buttonSelector); +}); + +ariaTest('Button: "tabindex" is set to -1', exampleFile, 'calendar-button-tabindex', async (t) => { + await assertAttributeValues(t, ex.buttonSelector, 'tabindex', '-1'); +}); + + +// Dialog Tests + +ariaTest('role="dialog" attribute on div', exampleFile, 'dialog-role', async (t) => { + await assertAriaRoles(t, 'ex1', 'dialog', 1, 'div'); +}); + +ariaTest('aria-modal="true" on modal', exampleFile, 'dialog-aria-modal', async (t) => { + await assertAttributeValues(t, ex.dialogSelector, 'aria-modal', 'true'); +}); + +ariaTest('aria-label exist on dialog', exampleFile, 'dialog-aria-label', async (t) => { + await assertAriaLabelExists(t, ex.dialogSelector); +}); + +ariaTest('aria-live="polite" on keyboard support message', exampleFile, 'dialog-aria-live', async (t) => { + await assertAttributeValues(t, ex.dialogMessageSelector, 'aria-live', 'polite'); +}); + +ariaTest('"aria-label" exists on control buttons', exampleFile, 'calendar-navigation-button-aria-label', async (t) => { + await assertAriaLabelExists(t, ex.calendarNavigationButtonSelector); +}); + +ariaTest('aria-live="polite" on dialog header', exampleFile, 'calendar-navigation-aria-live', async (t) => { + await assertAttributeValues(t, `${ex.dialogSelector} h2`, 'aria-live', 'polite'); +}); + +ariaTest('grid role on table element', exampleFile, 'grid-role', async (t) => { + await assertAriaRoles(t, 'ex1', 'grid', 1, 'table'); +}); + +ariaTest('aria-labelledby on grid element', exampleFile, 'grid-aria-labelledby', async (t) => { + await assertAriaLabelledby(t, ex.gridSelector); +}); + + +ariaTest('Roving tab index on dates in gridcell', exampleFile, 'gridcell-tabindex', async (t) => { + let button = (await t.context.queryElements(t, ex.buttonSelector))[0]; + await setDateToJanFirst2019(t); + + await button.sendKeys(Key.ENTER); + + let focusableButtons = await t.context.queryElements(t, ex.currentMonthDateButtons); + let allButtons = await t.context.queryElements(t, ex.allDates); + + // test only one element has tabindex="0" + for (let tabableEl = 0; tabableEl < focusableButtons.length; tabableEl++) { + let dateSelected = await focusableButtons[tabableEl].getText(); + + for (let el = 0; el < allButtons.length; el++) { + let date = await allButtons[el].getText(); + let disabled = (await allButtons[el].getAttribute('class')).includes('disabled'); + let tabindex = dateSelected === date && !disabled ? '0' : '-1'; + t.log('Tabindex: ' + tabindex + ' DS: ' + dateSelected + ' D: ' + date + ' Disabled: ' + disabled); + + t.is( + await allButtons[el].getAttribute('tabindex'), + tabindex, + 'focus is on day ' + (tabableEl + 1) + ' therefore the button number ' + + el + ' should have tab index set to: ' + tabindex + ); + } + + // Send the tabindex="0" element the appropriate key to switch focus to the next element + await focusableButtons[tabableEl].sendKeys(Key.ARROW_RIGHT); + } +}); + +ariaTest('aria-selected on selected date', exampleFile, 'gridcell-aria-selected', async (t) => { + let button = (await t.context.queryElements(t, ex.buttonSelector))[0]; + + await button.click(); + await assertAttributeDNE(t, ex.allDates, 'aria-selected'); + + await setDateToJanFirst2019(t); + await button.click(); + await assertAttributeValues(t, ex.jan12019Day, 'aria-selected', 'true'); + + let selectedButtons = await t.context.queryElements(t, `${ex.allDates}[aria-selected="true"]`); + + t.is( + selectedButtons.length, + 1, + 'after setting date in box, only one button should have aria-selected' + ); + + await (await t.context.queryElements(t, ex.jan22019Day))[0].click(); + await button.click(); + await assertAttributeValues(t, ex.jan22019Day, 'aria-selected', 'true'); + + selectedButtons = await t.context.queryElements(t, `${ex.allDates}[aria-selected="true"]`); + + t.is( + selectedButtons.length, + 1, + 'after clicking a date and re-opening datepicker, only one button should have aria-selected' + ); + +}); + +// Keyboard + + +ariaTest('DOWN ARROW, ALT plus DOWN ARROW and ENTER to open datepicker', exampleFile, 'combobox-down-arrow', async (t) => { + let combobox = (await t.context.queryElements(t, ex.comboboxSelector))[0]; + let dialog = (await t.context.queryElements(t, ex.dialogSelector))[0]; + let cancel = (await t.context.queryElements(t, ex.cancelSelector))[0]; + + // Test DOWN ARROW key + await combobox.sendKeys(Key.ARROW_DOWN); + + t.not( + await dialog.getCssValue('display'), + 'none', + 'After sending DOWN ARROW to the combobox, the calendar dialog should open' + ); + + // Close dialog + await cancel.sendKeys(Key.ENTER); + + t.not( + await dialog.getCssValue('display'), + 'block', + 'After sending ESCAPE to the dialog, the calendar dialog should close' + ); + + // Test ALT + DOWN ARROW key + await combobox.sendKeys(Key.ALT, Key.ARROW_DOWN); + + t.not( + await dialog.getCssValue('display'), + 'none', + 'After sending DOWN ARROW to the combobox, the calendar dialog should open' + ); + + // Close dialog + await cancel.sendKeys(Key.ENTER); + + t.not( + await dialog.getCssValue('display'), + 'block', + 'After sending ESCAPE to the dialog, the calendar dialog should close' + ); + +}); + +ariaTest('Sending key ESC when focus is in dialog closes dialog', exampleFile, 'dialog-esc', async (t) => { + let chooseDateButton = (await t.context.queryElements(t, ex.buttonSelector))[0]; + + for (let i = 0; i < ex.allFocusableElementsInDialog.length; i++) { + + await chooseDateButton.sendKeys(Key.ENTER); + let el = (await t.context.queryElements(t, ex.allFocusableElementsInDialog[i]))[0]; + await el.sendKeys(Key.ESCAPE); + + t.is( + await (await t.context.queryElements(t, ex.dialogSelector))[0].getCssValue('display'), + 'none', + 'After sending ESC to element "' + ex.allFocusableElementsInDialog[i] + '" in the dialog, the calendar dialog should close' + ); + + t.is( + await (await t.context.queryElements(t, ex.comboboxSelector))[0].getAttribute('value'), + '', + 'After sending ESC to element "' + ex.allFocusableElementsInDialog[i] + '" in the dialog, no date should be selected' + ); + } +}); + +ariaTest('ENTER on previous year or month and SPACE on next year or month changes the year or month', exampleFile, 'month-year-button-space-return', async (t) => { + await (await t.context.queryElements(t, ex.buttonSelector))[0].sendKeys(Key.ENTER); + + let monthYear = (await t.context.queryElements(t, ex.monthYear))[0]; + let originalMonthYear = await monthYear.getText(); + + for (let yearOrMonth of ['Year', 'Month']) { + let yearOrMonthLower = yearOrMonth.toLowerCase(); + + // enter on previous year or month should change the monthYear text + await (await t.context.queryElements(t, ex[`prev${yearOrMonth}`]))[0].sendKeys(Key.ENTER); + + t.not( + await monthYear.getText(), + originalMonthYear, + `After sending ENTER on the "previous ${yearOrMonthLower}" button, the month and year text should be not be ${originalMonthYear}` + ); + + // space on next year or month should change it back to the original + await (await t.context.queryElements(t, ex[`next${yearOrMonth}`]))[0].sendKeys(' '); + + t.is( + await monthYear.getText(), + originalMonthYear, + `After sending SPACE on the "next ${yearOrMonthLower}" button, the month and year text should be ${originalMonthYear}` + ); + } +}); + +ariaTest('Tab should go through all tabbable items, then repear', exampleFile, 'dialog-tab', async (t) => { + await (await t.context.queryElements(t, ex.buttonSelector))[0].sendKeys(Key.ENTER); + + for (let itemSelector of ex.allFocusableElementsInDialog) { + t.true( + await focusMatchesElement(t, itemSelector), + 'Focus should be on: ' + itemSelector + ); + + await (await t.context.queryElements(t, itemSelector))[0].sendKeys(Key.TAB); + } + + t.true( + await focusMatchesElement(t, ex.allFocusableElementsInDialog[0]), + 'After tabbing through all items, focus should return to: ' + ex.allFocusableElementsInDialog[0] + ); +}); + +ariaTest('Shift-tab should move focus backwards', exampleFile, 'dialog-shift-tab', async (t) => { + t.plan(7); + + await (await t.context.queryElements(t, ex.buttonSelector))[0].sendKeys(Key.ENTER); + + await (await t.context.queryElements(t, ex.allFocusableElementsInDialog[0]))[0] + .sendKeys(Key.chord(Key.SHIFT, Key.TAB)); + + let lastIndex = ex.allFocusableElementsInDialog.length - 1; + for (let i = lastIndex; i >= 0; i--) { + t.true( + await focusMatchesElement(t, ex.allFocusableElementsInDialog[i]), + 'Focus should be on: ' + ex.allFocusableElementsInDialog[i] + ); + + await (await t.context.queryElements(t, ex.allFocusableElementsInDialog[i]))[0] + .sendKeys(Key.chord(Key.SHIFT, Key.TAB)); + } +}); + +// TODO(zcorpan): Missing tests. Either mark as "test-not-required" or write the test. +ariaTest.failing(`Test not implemented: grid-space`, exampleFile, 'grid-space', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-return`, exampleFile, 'grid-return', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-up-arrow`, exampleFile, 'grid-up-arrow', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-down-arrow`, exampleFile, 'grid-down-arrow', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-right-arrow`, exampleFile, 'grid-right-arrow', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-left-arrow`, exampleFile, 'grid-left-arrow', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-home`, exampleFile, 'grid-home', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-end`, exampleFile, 'grid-end', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-pageup`, exampleFile, 'grid-pageup', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-shift-pageup`, exampleFile, 'grid-shift-pageup', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-pagedown`, exampleFile, 'grid-pagedown', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: grid-shift-pagedown`, exampleFile, 'grid-shift-pagedown', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: okay-cancel-button-space-return`, exampleFile, 'okay-cancel-button-space-return', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: textbox-aria-autocomplete`, exampleFile, 'textbox-aria-autocomplete', async (t) => { + t.fail(); +}); + +ariaTest.failing(`Test not implemented: textbox-aria-live`, exampleFile, 'textbox-aria-live', async (t) => { + t.fail(); +});