From 04f356c6bbd209e30bc13d436617f539eb442cfd Mon Sep 17 00:00:00 2001 From: Adam Page Date: Mon, 4 Aug 2025 16:56:48 -0700 Subject: [PATCH 01/25] initial commit --- .../examples/css/quantity-spinbutton.css | 130 ++++++ .../examples/js/quantity-spinbutton.js | 95 ++++ .../examples/quantity-spinbutton.html | 416 ++++++++++++++++++ .../spinbutton/spinbutton-pattern.html | 1 + 4 files changed, 642 insertions(+) create mode 100644 content/patterns/spinbutton/examples/css/quantity-spinbutton.css create mode 100644 content/patterns/spinbutton/examples/js/quantity-spinbutton.js create mode 100644 content/patterns/spinbutton/examples/quantity-spinbutton.html diff --git a/content/patterns/spinbutton/examples/css/quantity-spinbutton.css b/content/patterns/spinbutton/examples/css/quantity-spinbutton.css new file mode 100644 index 0000000000..d09b86474f --- /dev/null +++ b/content/patterns/spinbutton/examples/css/quantity-spinbutton.css @@ -0,0 +1,130 @@ +.spinners { + --length-s: 0.25rem; + --length-m: 0.5rem; + --color-field-background: white; + --color-button-background-idle: color-mix(in srgb, ghostwhite, darkblue 10%); + --color-button-background-hover: color-mix(in srgb, ghostwhite, darkblue 20%); + --color-interactive-focus: var(--wai-green, #005a6a); + --transition-duration-snappy: 0; + --transition-duration-leisurely: 0; + + @media (prefers-reduced-motion: no-preference) and (forced-colors: none) { + --transition-duration-snappy: 0.15s; + --transition-duration-leisurely: 0.5s; + } + + @media (forced-colors: active) { + --color-interactive-focus: Highlight; + } + + display: inline-flex; + font-family: system-ui, sans-serif; + line-height: 1.4; + padding: 1rem; + background-color: color-mix(in srgb, ghostwhite, darkblue 1%); + border: 1px solid color-mix(in srgb, ghostwhite, darkblue 10%); + border-radius: 0.5rem; + + *, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + .visually-hidden { + border: 0; + clip: rect(0 0 0 0); + height: auto; + margin: 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; + } + + fieldset { + padding: 0.5rem; + border: 1px solid transparent; + } + + legend { + font-size: 1.2rem; + font-weight: bold; + margin-block-end: 1rem; + } + + .fields { + display: flex; + flex-wrap: wrap; + gap: 2ch; + } + + .field { + display: flex; + flex-direction: column; + gap: 0.5rem; + + label { + font-size: 1.2rem; + } + } + + .spinner { + display: flex; + flex-wrap: wrap; + gap: 0; + max-inline-size: calc(100vw - 6.4rem); + font-size: 1.4rem; + border: 1px solid color-mix(in srgb, ghostwhite, darkblue 60%); + border-radius: 0.25rem; + padding: 0.125em; + background-color: var(--color-field-background); + outline: 0 solid transparent; + outline-offset: 0; + transition: + outline-offset var(--transition-duration-snappy) ease, + outline-width var(--transition-duration-snappy) ease, + outline-color var(--transition-duration-snappy) ease, + border-color var(--transition-duration-snappy) ease; + + &:focus-within { + outline: var(--length-s) solid var(--color-interactive-focus); + outline-offset: var(--length-s); + } + + input, button { + font: inherit; + font-weight: bold; + color: inherit; + border: none; + background: transparent; + padding: 0.25em 0.5em; + margin: 0; + outline: none; + } + + [role="spinbutton"] { + text-align: center; + min-inline-size: 4ch; + max-inline-size: fit-content; + field-sizing: content; + font-variant-numeric: tabular-nums; + } + + button { + min-inline-size: 3ch; + background-color: var(--color-button-background-idle); + + &:hover { + background-color: var(--color-button-background-hover); + } + + &[aria-disabled="true"] { + opacity: 0.25; + background-color: transparent; + cursor: not-allowed; + } + } + } +} diff --git a/content/patterns/spinbutton/examples/js/quantity-spinbutton.js b/content/patterns/spinbutton/examples/js/quantity-spinbutton.js new file mode 100644 index 0000000000..9159f2b387 --- /dev/null +++ b/content/patterns/spinbutton/examples/js/quantity-spinbutton.js @@ -0,0 +1,95 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: quantity-spinbutton.js + */ + +'use strict'; + +class SpinButton { + constructor(el) { + this.el = el; + this.id = el.id; + this.controls = Array.from(document.querySelectorAll(`button[aria-controls="${this.id}"]`)); + this.output = document.querySelector(`output[for="${this.id}"]`); + this.timer = null; + this.setBounds(); + el.addEventListener('input', () => this.setValue(el.value)); + el.addEventListener('blur', () => this.setValue(el.value, true)); + el.addEventListener('keydown', e => this.handleKey(e)); + this.controls.forEach(btn => btn.addEventListener('click', () => this.handleClick(btn))); + this.setValue(el.value); + } + + clamp(n) { + return Math.min(Math.max(n, this.min), this.max); + } + + parseValue(raw) { + const s = String(raw).trim(); + if (!s) return null; + const n = parseInt(s.replace(/[^\d-]/g, ''), 10); + return isNaN(n) ? null : n; + } + + setBounds() { + const el = this.el; + this.hasMin = el.hasAttribute('aria-valuemin'); + this.hasMax = el.hasAttribute('aria-valuemax'); + this.min = this.hasMin ? +el.getAttribute('aria-valuemin') : Number.MIN_SAFE_INTEGER; + this.max = this.hasMax ? +el.getAttribute('aria-valuemax') : Number.MAX_SAFE_INTEGER; + } + + setValue(raw, onBlur = false) { + let val = typeof raw === 'number' ? raw : this.parseValue(raw); + val = (val === null) ? ((onBlur && this.hasMin) ? this.min : '') : this.clamp(val); + this.el.value = val; + this.el.setAttribute('aria-valuenow', val); + this.updateButtonStates(); + } + + updateButtonStates() { + const val = +this.el.value; + this.controls.forEach(btn => { + const op = btn.getAttribute('data-spinbutton-operation'); + btn.setAttribute('aria-disabled', + (op === 'decrement' ? val <= this.min : val >= this.max) ? 'true' : 'false' + ); + }); + } + + announce() { + if (!this.output) return; + this.output.textContent = this.el.value; + clearTimeout(this.timer); + this.timer = setTimeout(() => { + this.output.textContent = ''; + this.timer = null; + }, 3000); + } + + handleKey(e) { + let v = +this.el.value || 0; + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault(); + this.setValue(v + (e.key === 'ArrowUp' ? 1 : -1)); + } else if (e.key === 'Home') { + e.preventDefault(); + this.setValue(this.min); + } else if (e.key === 'End') { + e.preventDefault(); + this.setValue(this.max); + } + } + + handleClick(btn) { + const dir = btn.getAttribute('data-spinbutton-operation') === 'decrement' ? -1 : 1; + this.setValue((+this.el.value || 0) + dir); + this.announce(); + } +} + +window.addEventListener('load', () => + document.querySelectorAll('[role="spinbutton"]').forEach(el => new SpinButton(el)) +); diff --git a/content/patterns/spinbutton/examples/quantity-spinbutton.html b/content/patterns/spinbutton/examples/quantity-spinbutton.html new file mode 100644 index 0000000000..47f10945d5 --- /dev/null +++ b/content/patterns/spinbutton/examples/quantity-spinbutton.html @@ -0,0 +1,416 @@ + + + + + + Quantity Spin Button Example + + + + + + + + + + + + + + + + +
+

Quantity Spin Button Example

+ +
+

About This Example

+ +

+ The following example uses the Spin Button Pattern to implement three quantity inputs. +

+

Similar examples include:

+
    +
  • Toolbar Example: A toolbar that contains a spin button for setting font size.
  • +
+
+ +
+
+

Example

+
+ +
+
+
+ Guests +
+
+ +
+ + + +
+ + +
+
+ +
+ + + +
+ +
+
+ +
+ + + +
+ +
+
+
+
+
+ +
+ +
+

Accessibility Features

+
    +
  • + The element with role spinbutton allows text input, for + users who prefer to enter a value directly. +
  • +
  • + The spin button input and its adjacent buttons are visually + presented as a singular form field containing an editable value, an + increase operation, and a decrease operation. +
  • +
  • + When either the spin button input or its adjacent buttons have + received focus, a single visual focus indicator encompasses all + three, reinforcing the relationship between then. +
  • +
  • + For users who have not set a preference for reduced motion, the + focus indicator appears with subtle animation to draw attention. +
  • +
  • + The increase and decrease buttons: +
      +
    • + Are generously sized for ease of use. +
    • +
    • + Are adjacent to the spin button input so they can be accessed by + users of touch-based and voice-based assistive technologies. +
    • +
    • + Are labeled with the title attribute, providing a + human-friendly representation of the plus and minus characters + for users of voice control and touch screen. The + title attribute also presents a tooltip on hover, + clarifying the meaning of each button’s icon. +
    • +
    • + Use an invisible live region to announce the updated value to + when pressed. The live region empties its contents after 3 + seconds to avoid leaving stale content in the document. +
    • +
    • + Are excluded from the page Tab sequence with + tabindex="-1" because they are redundant with the + arrow key support provided to keyboard users. +
    • +
    • + Can be activated with voice control by speaking a command such + as Click add adult. +
    • +
    +
  • +
  • + When forced colors are enabled, system + color keywords are used to ensure visibility and sufficient + contrast for the spin button’s content and interactive states. +
  • +
+
+ +
+

Keyboard Support

+

The spin buttons provide the following keyboard support described in the Spin Button Pattern.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
KeyFunction
Down ArrowDecreases value by 1.
Up ArrowIncreases value by 1.
HomeDecreases to minimum value.
EndIncreases to maximum value.
+
+ +
+

Role, Property, State, and Tabindex Attributes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RoleAttributeElementUsage
spinbuttoninput[type="text"]Identifies the input[type="text"] element as a spin button.
aria-valuenow="NUMBER"input[type="text"] +
    +
  • Indicates the current numeric value of the spin button.
  • +
  • Updated by JavaScript as users change the value of the spin button.
  • +
+
aria-valuemin="NUMBER"input[type="text"]Indicates the minimum allowed value for the spin button.
aria-valuemax="NUMBER"input[type="text"] +
    +
  • Indicates the maximum allowed value for the spin button.
  • +
  • For the Day spin button, this property is updated based on the value of the Month spin button.
  • +
+
title="NAME_STRING"buttonDefines the accessible name for each increase and decrease button (Remove adult, Add adult, Remove kid, Add kid, Remove animal, and Add animal).
+ aria-controls="ID_REF" + buttonIdentifies the element whose value will be modified when the button is activated.
tabindex="-1"buttonRemoves the decrease and increase buttons from the page Tab sequence while keeping them focusable so they can be accessed with touch-based and voice-based assistive technologies.
aria-disabled="true"buttonSet when the minimum or maximum value has been reached to inform assistive technologies that the button has been disabled.
aria-disabled="false"buttonSet when the value is greater than the minimum value or lesser than the maximum value.
aria-hidden="true"spanFor assistive technology users, hides the visible minus and plus characters in the decrease and increase buttons since they are symbolic of the superior button labels provided by the title attribute.
logoutputIdentifies the invisible output element as a log.
+ aria-live="polite" + + output + +
    +
  • Triggers a screen reader announcement of the output element’s updated content at the next graceful opportunity.
  • +
  • When either the increase or decrease button is pressed, the current value of the spin button is injected into the output element.
  • +
  • In keeping with the log role of the output, its contents are emptied 3 seconds after injection.
  • +
+
+
+ +
+

JavaScript and CSS Source Code

+ +
+ +
+

HTML Source Code

+

To copy the following HTML code, please open it in CodePen.

+ +
+ + +
+
+ + diff --git a/content/patterns/spinbutton/spinbutton-pattern.html b/content/patterns/spinbutton/spinbutton-pattern.html index 58f1f3e052..adee9c8a6d 100644 --- a/content/patterns/spinbutton/spinbutton-pattern.html +++ b/content/patterns/spinbutton/spinbutton-pattern.html @@ -36,6 +36,7 @@

About This Pattern

Example

+

Quantity Spin Button Example: A set of three spin buttons to collect the quantities of adults, children, and animals for a hotel reservation.

Date Picker Spin Button Example: Illustrates a date picker made from three spin buttons for day, month, and year.

From d1e838850f856fedbaac764fb003ec693d47feba Mon Sep 17 00:00:00 2001 From: Adam Page Date: Mon, 4 Aug 2025 16:57:34 -0700 Subject: [PATCH 02/25] coverage report and reference tables --- .../coverage-and-quality-report.html | 50 ++++++++++++++++--- .../coverage-and-quality/prop-coverage.csv | 14 +++--- .../coverage-and-quality/role-coverage.csv | 4 +- content/index/index.html | 12 +++++ 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/content/about/coverage-and-quality/coverage-and-quality-report.html b/content/about/coverage-and-quality/coverage-and-quality-report.html index dd4c7ecf84..d490f6bd98 100644 --- a/content/about/coverage-and-quality/coverage-and-quality-report.html +++ b/content/about/coverage-and-quality/coverage-and-quality-report.html @@ -53,8 +53,8 @@

About These Reports

@@ -1243,6 +1249,7 @@

Properties and States with More than One
  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • +
  • Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -1259,6 +1266,7 @@

    Properties and States with More than One
  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • +
  • Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -1272,6 +1280,7 @@

    Properties and States with More than One
  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • +
  • Date Picker Spin Button
  • Toolbar
  • @@ -1287,7 +1296,7 @@

    Coding Summary

    Total Examples - 62 + 63 High Contrast Documentation @@ -1295,7 +1304,7 @@

    Coding Summary

    Uses SVG - 35 + 36 Uses forced-colors media query @@ -1313,7 +1322,7 @@

    Coding Summary

    --> Uses event.KeyCode - 15 + 16 Uses event.which @@ -1325,7 +1334,7 @@

    Coding Summary

    Use Prototype - 21 + 22 Mouse Events @@ -1967,6 +1976,19 @@

    Coding Practices

    7 + + Date Picker Spin Button + prototype + Yes + + + example + 2 + 2 + 7 + 7 + + Quantity Spin Button class @@ -2465,6 +2487,14 @@

    SVG and High Contrast Techniques

    Yes + + Date Picker Spin Button + Yes + Yes + + + + Quantity Spin Button diff --git a/content/about/coverage-and-quality/prop-coverage.csv b/content/about/coverage-and-quality/prop-coverage.csv index 796218971a..6de4fdfb79 100644 --- a/content/about/coverage-and-quality/prop-coverage.csv +++ b/content/about/coverage-and-quality/prop-coverage.csv @@ -18,11 +18,11 @@ "aria-flowto","0","0" "aria-grabbed","0","0" "aria-haspopup","0","9","Example: Date Picker Combobox","Example: Editable Combobox with Grid Popup","Example: (Deprecated) Collapsible Dropdown Listbox","Example: Actions Menu Button Using aria-activedescendant","Example: Actions Menu Button Using element.focus()","Example: Navigation Menu Button","Example: Editor Menubar","Example: Navigation Menubar","Example: Toolbar" -"aria-hidden","0","16","Example: Button (IDL Version)","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Editor Menubar","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Quantity Spin Button","Example: Switch Using HTML Button","Example: Switch Using HTML Checkbox Input","Example: Switch","Example: Sortable Table","Example: Toolbar" +"aria-hidden","0","17","Example: Button (IDL Version)","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Editor Menubar","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Quantity Spin Button","Example: Switch Using HTML Button","Example: Switch Using HTML Checkbox Input","Example: Switch","Example: Sortable Table","Example: Toolbar" "aria-invalid","0","0" "aria-keyshortcuts","0","0" -"aria-label","1","17","Guidance: Naming with a String Attribute Via aria-label","Example: Breadcrumb","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Editable Combobox With Both List and Inline Autocomplete","Example: Editable Combobox With List Autocomplete","Example: Editable Combobox without Autocomplete","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Link","Example: Editor Menubar","Example: Navigation Menubar","Example: Rating Radio Group","Example: Horizontal Multi-Thumb Slider","Example: Table","Example: Toolbar","Example: Treegrid Email Inbox","Example: Navigation Treeview" -"aria-labelledby","1","40","Guidance: Naming with Referenced Content Via aria-labelledby","Example: Accordion","Example: Alert Dialog","Example: Checkbox (Two State)","Example: Date Picker Combobox","Example: Select-Only Combobox","Example: Editable Combobox with Grid Popup","Example: Date Picker Dialog","Example: Modal Dialog","Example: Infinite Scrolling Feed","Example: Data Grid","Example: Layout Grid","Example: (Deprecated) Collapsible Dropdown Listbox","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Actions Menu Button Using aria-activedescendant","Example: Actions Menu Button Using element.focus()","Example: Navigation Menu Button","Example: Navigation Menubar","Example: Meter","Example: Radio Group Using aria-activedescendant","Example: Rating Radio Group","Example: Radio Group Using Roving tabindex","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Switch Using HTML Button","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties","Example: Navigation Treeview","Example: Complementary Landmark","Example: Form Landmark","Example: Main Landmark","Example: Navigation Landmark","Example: Region Landmark","Example: Search Landmark" +"aria-label","1","18","Guidance: Naming with a String Attribute Via aria-label","Example: Breadcrumb","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Editable Combobox With Both List and Inline Autocomplete","Example: Editable Combobox With List Autocomplete","Example: Editable Combobox without Autocomplete","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Link","Example: Editor Menubar","Example: Navigation Menubar","Example: Rating Radio Group","Example: Horizontal Multi-Thumb Slider","Example: Date Picker Spin Button","Example: Table","Example: Toolbar","Example: Treegrid Email Inbox","Example: Navigation Treeview" +"aria-labelledby","1","41","Guidance: Naming with Referenced Content Via aria-labelledby","Example: Accordion","Example: Alert Dialog","Example: Checkbox (Two State)","Example: Date Picker Combobox","Example: Select-Only Combobox","Example: Editable Combobox with Grid Popup","Example: Date Picker Dialog","Example: Modal Dialog","Example: Infinite Scrolling Feed","Example: Data Grid","Example: Layout Grid","Example: (Deprecated) Collapsible Dropdown Listbox","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Actions Menu Button Using aria-activedescendant","Example: Actions Menu Button Using element.focus()","Example: Navigation Menu Button","Example: Navigation Menubar","Example: Meter","Example: Radio Group Using aria-activedescendant","Example: Rating Radio Group","Example: Radio Group Using Roving tabindex","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Switch Using HTML Button","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties","Example: Navigation Treeview","Example: Complementary Landmark","Example: Form Landmark","Example: Main Landmark","Example: Navigation Landmark","Example: Region Landmark","Example: Search Landmark" "aria-level","0","2","Example: Treegrid Email Inbox","Example: File Directory Treeview Using Declared Properties" "aria-live","0","6","Example: Alert","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Quantity Spin Button" "aria-modal","0","4","Example: Alert Dialog","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Modal Dialog" @@ -43,7 +43,7 @@ "aria-selected","0","17","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Editable Combobox With Both List and Inline Autocomplete","Example: Editable Combobox With List Autocomplete","Example: Editable Combobox without Autocomplete","Example: Date Picker Combobox","Example: Select-Only Combobox","Example: Editable Combobox with Grid Popup","Example: Date Picker Dialog","Example: (Deprecated) Collapsible Dropdown Listbox","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties" "aria-setsize","0","3","Example: Infinite Scrolling Feed","Example: Treegrid Email Inbox","Example: File Directory Treeview Using Declared Properties" "aria-sort","1","2","Guidance: Indicating sort order with aria-sort","Example: Data Grid","Example: Sortable Table" -"aria-valuemax","1","8","Guidance: Using aria-valuemin, aria-valuemax and aria-valuenow","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Quantity Spin Button","Example: Toolbar" -"aria-valuemin","0","8","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Quantity Spin Button","Example: Toolbar" -"aria-valuenow","1","8","Guidance: Using aria-valuemin, aria-valuemax and aria-valuenow","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Quantity Spin Button","Example: Toolbar" -"aria-valuetext","1","4","Guidance: Using aria-valuetext","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Toolbar" +"aria-valuemax","1","9","Guidance: Using aria-valuemin, aria-valuemax and aria-valuenow","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" +"aria-valuemin","0","9","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" +"aria-valuenow","1","9","Guidance: Using aria-valuemin, aria-valuemax and aria-valuenow","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" +"aria-valuetext","1","5","Guidance: Using aria-valuetext","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Toolbar" diff --git a/content/about/coverage-and-quality/role-coverage.csv b/content/about/coverage-and-quality/role-coverage.csv index e3b4213d86..bb97e24eeb 100644 --- a/content/about/coverage-and-quality/role-coverage.csv +++ b/content/about/coverage-and-quality/role-coverage.csv @@ -25,7 +25,7 @@ "generic","0","0" "grid","3","5","Guidance: Grid Popup Keyboard Interaction","Guidance: Grid (Interactive Tabular Data and Layout Containers) Pattern","Guidance: Grid and Table Properties","Example: Date Picker Combobox","Example: Editable Combobox with Grid Popup","Example: Date Picker Dialog","Example: Data Grid","Example: Layout Grid" "gridcell","0","3","Example: Editable Combobox with Grid Popup","Example: Layout Grid","Example: Treegrid Email Inbox" -"group","2","9","Guidance: Radio Group Pattern","Guidance: For Radio Group Contained in a Toolbar","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Checkbox (Two State)","Example: Listbox with Grouped Options","Example: Editor Menubar","Example: Color Viewer Slider","Example: Switch Using HTML Button","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties","Example: Navigation Treeview" +"group","2","10","Guidance: Radio Group Pattern","Guidance: For Radio Group Contained in a Toolbar","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Checkbox (Two State)","Example: Listbox with Grouped Options","Example: Editor Menubar","Example: Color Viewer Slider","Example: Date Picker Spin Button","Example: Switch Using HTML Button","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties","Example: Navigation Treeview" "heading","0","0" "img","0","0" "input","0","0" @@ -62,7 +62,7 @@ "searchbox","0","0" "separator","0","1","Example: Editor Menubar" "slider","2","5","Guidance: Slider (Multi-Thumb) Pattern","Guidance: Slider Pattern","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider" -"spinbutton","1","2","Guidance: Spinbutton Pattern","Example: Quantity Spin Button","Example: Toolbar" +"spinbutton","1","3","Guidance: Spinbutton Pattern","Example: Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" "status","0","0" "switch","1","3","Guidance: Switch Pattern","Example: Switch Using HTML Button","Example: Switch Using HTML Checkbox Input","Example: Switch" "tab","1","4","Guidance: Keyboard Navigation Between Components (The Tab Sequence)","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation" diff --git a/content/index/index.html b/content/index/index.html index 225e1a357a..877b9221f9 100644 --- a/content/index/index.html +++ b/content/index/index.html @@ -168,6 +168,7 @@

    Examples by Role

  • Listbox with Grouped Options
  • Editor Menubar (HC)
  • Color Viewer Slider (HC)
  • +
  • Date Picker Spin Button
  • Switch Using HTML Button (HC)
  • File Directory Treeview Using Computed Properties
  • File Directory Treeview Using Declared Properties
  • @@ -366,6 +367,7 @@

    Examples by Role

    spinbutton @@ -643,6 +645,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • +
  • Date Picker Spin Button
  • Quantity Spin Button
  • Switch Using HTML Button (HC)
  • Switch Using HTML Checkbox Input (HC)
  • @@ -669,6 +672,7 @@

    Examples By Properties and States

  • Navigation Menubar (HC)
  • Rating Radio Group (HC)
  • Horizontal Multi-Thumb Slider (HC)
  • +
  • Date Picker Spin Button
  • Table
  • Toolbar
  • Treegrid Email Inbox
  • @@ -707,6 +711,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • +
  • Date Picker Spin Button
  • Switch Using HTML Button (HC)
  • Tabs with Automatic Activation (HC)
  • Tabs with Manual Activation (HC)
  • @@ -866,6 +871,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • +
  • Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -881,6 +887,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • +
  • Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -896,6 +903,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • +
  • Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -908,6 +916,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • +
  • Date Picker Spin Button
  • Toolbar
  • diff --git a/content/patterns/spinbutton/examples/css/datepicker-spinbuttons.css b/content/patterns/spinbutton/examples/css/datepicker-spinbuttons.css new file mode 100644 index 0000000000..4a6929c1dd --- /dev/null +++ b/content/patterns/spinbutton/examples/css/datepicker-spinbuttons.css @@ -0,0 +1,91 @@ +.datepicker-spinbuttons { + margin-top: 1em; +} + +.datepicker-spinbuttons .day { + width: 2em; +} + +.datepicker-spinbuttons .month { + width: 6em; +} + +.datepicker-spinbuttons .year { + width: 3em; +} + +.datepicker-spinbuttons .spinbutton { + float: left; + text-align: center; +} + +.datepicker-spinbuttons .spinbutton:first-child { + border-left: 4px; +} + +.datepicker-spinbuttons .spinbutton:last-child { + border-right: 4px; +} + +.datepicker-spinbuttons .spinbutton .previous, +.datepicker-spinbuttons .spinbutton .next { + color: #666; +} + +.datepicker-spinbuttons .spinbutton.focus { + outline: 2px solid #005a9c; +} + +.datepicker-spinbuttons .spinbutton.focus, +.datepicker-spinbuttons .spinbutton:hover { + color: #444; + background-color: #eee; +} + +.datepicker-spinbuttons .spinbutton.focus [role="spinbutton"], +.datepicker-spinbuttons .spinbutton:hover [role="spinbutton"] { + background-color: #fff; + color: black; +} + +.datepicker-spinbuttons .spinbutton .previous { + border-bottom: 1px solid black; +} + +.datepicker-spinbuttons .spinbutton .next { + border-top: 1px solid black; +} + +.datepicker-spinbuttons .spinbutton button { + padding: 0; + margin: 0; + border: none; + background-color: transparent; +} + +.datepicker-spinbuttons .spinbutton .decrease svg polygon, +.datepicker-spinbuttons .spinbutton .increase svg polygon { + fill: #333; + stroke-width: 3px; + stroke: transparent; +} + +.datepicker-spinbuttons .spinbutton .decrease { + position: relative; + top: 4px; +} + +.datepicker-spinbuttons .spinbutton.focus svg polygon { + fill: #005a9c; + stroke: #005a9c; +} + +.datepicker-spinbuttons .spinbutton .decrease:hover svg polygon, +.datepicker-spinbuttons .spinbutton .increase:hover svg polygon { + fill: #005a9c; + stroke: #005a9c; +} + +div[role="separator"] { + clear: both; +} diff --git a/content/patterns/spinbutton/examples/datepicker-spinbuttons.html b/content/patterns/spinbutton/examples/datepicker-spinbuttons.html new file mode 100644 index 0000000000..906b15aed4 --- /dev/null +++ b/content/patterns/spinbutton/examples/datepicker-spinbuttons.html @@ -0,0 +1,339 @@ + + + + + + Date Picker Spin Button Example + + + + + + + + + + + + + + + + + +
    +

    Date Picker Spin Button Example

    + +
    +

    About This Example

    + +

    + The following example uses the Spin Button Pattern to implement a date picker. + It includes three spin buttons: one for setting the day, a second for month, and a third for year. +

    +

    Similar examples include:

    + +
    + +
    +
    +

    Example

    +
    + +
    +
    +
    Choose a Date
    + + +
    + + +
    1
    + + +
    + +
    + + +
    June
    + + +
    + +
    + + +
    2019
    + + +
    +
    +
    + +
    + +
    +

    Accessibility Features

    +
      +
    • + The three spin buttons are wrapped in a group to help assistive technology users understand that all three elements together represent a date. + The accessible name for the group includes a hidden div that contains a date string that reflects the current values of the three spin buttons. + This enables screen reader users to easily perceive the date represented by the three buttons without having to navigate to all three buttons and remember the value of each one; it is the screen reader user equivalent to seeing the date at a glance. +
    • +
    • The day spin button uses aria-valuetext to properly pronounce the day, e.g. first, second, third ...
    • +
    • The month spin button uses aria-valuetext to properly pronounce the month instead of the numeric value, e.g. January, February, ...
    • +
    • On hover, the up and down arrow images enlarge slightly, enlarging the effective target area for increasing or decreasing the spin button value.
    • +
    • Focusing a spin button enlarges the increase and decrease buttons to make perceiving keyboard focus easier.
    • +
    • + The increase and decrease buttons are not contained within the div with role spinbutton so they can be separately focusable by users of touch screen readers. + However, they are excluded from the page Tab sequence with tabindex="-1" because they are redundant with the arrow key support provided to keyboard users. +
    • +
    +
    + +
    +

    Keyboard Support

    +

    The spin buttons provide the following keyboard support described in the Spin Button Pattern.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyFunction
    Down Arrow +
      +
    • Decreases value 1 step.
    • +
    • When focus is on the Day spin button and the value is the first day of the month, changes value to the last day of the month.
    • +
    • When focus is on the Month spin button and the value is January, changes value to December.
    • +
    +
    Up Arrow +
      +
    • Increases the value 1 step.
    • +
    • When focus is on the Day spin button and the value is the last day of the month, changes value to the first day of the month.
    • +
    • When focus is on the Month spin button and the value is December, changes value to January.
    • +
    +
    Page Down +
      +
    • Decreases the value 5 steps.
    • +
    • When focus is on the Day spin button and the value is the fifth day of the month or less, changes value to the last day of the month.
    • +
    • When focus is on the Month spin button and the value is the fifth month of the year or less, changes value to December.
    • +
    +
    Page Up +
      +
    • Increases the value 5 steps.
    • +
    • When focus is on the Day spin button and the value is any of the last five days of the month, changes value to the first day of the month.
    • +
    • When focus is on the Month spin button and the value is any of the last five months of the year, changes value to January.
    • +
    +
    HomeDecreases to minimum value.
    EndIncreases to maximum value.
    +
    + +
    +

    Role, Property, State, and Tabindex Attributes

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    groupdiv +
      +
    • Identifies the div as a group.
    • +
    • The group provides a means to inform assistive technology users that the three spin buttons are all related to the single purpose of choosing a date.
    • +
    +
    aria-labelledby="IDREFs"div +
      +
    • Contains two IDs that reference the div elements that provide the accessible name for the group.
    • +
    • One ID refers to the visible label Choose a Date.
    • +
    • The second ID refers to a hidden div that contains a string specifying the current date represented by the values of the three spin buttons, which is updated by JavaScript as users change the values of the spin buttons.
    • +
    +
    spinbuttondivIdentifies the div element as a spinbutton.
    aria-label="NAME_STRING"divDefines the accessible name for each spin button (e.g. day, month and year).
    aria-valuenow="NUMBER"div +
      +
    • Indicates the current numeric value of the spin button.
    • +
    • Updated by JavaScript as users change the value of the spin button.
    • +
    +
    + aria-valuetext="DAY_NUMBER_STRING" or
    + aria-valuetext="MONTH_STRING" +
    div +
      +
    • For the Day spin button, provides screen reader users with a friendlier pronunciation of the number of the day of the month (i.e. first instead of one).
    • +
    • For the Month spin button, provides screen reader users with a friendlier announcement of the month (i.e., January instead of one).
    • +
    +
    aria-valuemin="NUMBER"divIndicates the minimum allowed value for the spin button.
    aria-valuemax="NUMBER"div +
      +
    • Indicates the maximum allowed value for the spin button.
    • +
    • For the Day spin button, this property is updated based on the value of the Month spin button.
    • +
    +
    aria-hidden="true" +
      +
    • div.next
    • +
    • div.previous
    • +
    +
    For assistive technology users, hides the text showing the next and previous values that is displayed adjacent to the up and down arrow icons since it would otherwise add superfluous elements to the screen reader reading order that are likely to be more confusing than helpful.
    aria-label="NAME_STRING"buttonDefines the accessible name for each increase and decrease button (Next Day, Previous Day, Next Month, Previous Month, Next year, and Previous Year).
    tabindex="-1"buttonRemoves the decrease and increase buttons from the page Tab sequence while keeping them focusable so they can be accessed with touch-based assistive technologies.
    +
    + +
    +

    JavaScript and CSS Source Code

    + +
    + +
    +

    HTML Source Code

    +

    To copy the following HTML code, please open it in CodePen.

    + +
    + + +
    +
    + + diff --git a/content/patterns/spinbutton/examples/js/datepicker-spinbuttons.js b/content/patterns/spinbutton/examples/js/datepicker-spinbuttons.js new file mode 100644 index 0000000000..94ea9f1b33 --- /dev/null +++ b/content/patterns/spinbutton/examples/js/datepicker-spinbuttons.js @@ -0,0 +1,205 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: datepicker-spinbuttons.js + */ + +'use strict'; + +/* global SpinButtonDate */ + +var DatePickerSpinButtons = function (domNode) { + this.domNode = domNode; + this.monthNode = domNode.querySelector('.spinbutton.month'); + this.dayNode = domNode.querySelector('.spinbutton.day'); + this.yearNode = domNode.querySelector('.spinbutton.year'); + this.dateNode = domNode.querySelector('.date'); + + this.valuesWeek = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + ]; + this.valuesDay = [ + '', + 'first', + 'second', + 'third', + 'fourth', + 'fifth', + 'sixth', + 'seventh', + 'eighth', + 'ninth', + 'tenth', + 'eleventh', + 'twelfth', + 'thirteenth', + 'fourteenth', + 'fifteenth', + 'sixteenth', + 'seventeenth', + 'eighteenth', + 'nineteenth', + 'twentieth', + 'twenty first', + 'twenty second', + 'twenty third', + 'twenty fourth', + 'twenty fifth', + 'twenty sixth', + 'twenty seventh', + 'twenty eighth', + 'twenty ninth', + 'thirtieth', + 'thirty first', + ]; + this.valuesMonth = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; +}; + +// Initialize slider +DatePickerSpinButtons.prototype.init = function () { + this.spinbuttonDay = new SpinButtonDate( + this.dayNode, + null, + this.updateDay.bind(this) + ); + this.spinbuttonDay.init(); + + this.spinbuttonMonth = new SpinButtonDate( + this.monthNode, + this.valuesMonth, + this.updateMonth.bind(this) + ); + this.spinbuttonMonth.init(); + + this.spinbuttonYear = new SpinButtonDate( + this.yearNode, + null, + this.updateYear.bind(this) + ); + this.spinbuttonYear.init(); + this.spinbuttonYear.noWrap(); + + this.minYear = this.spinbuttonYear.getValueMin(); + this.maxYear = this.spinbuttonYear.getValueMax(); + + this.currentDate = new Date(); + + this.day = this.currentDate.getDate(); + this.month = this.currentDate.getMonth(); + this.year = this.currentDate.getFullYear(); + this.daysInMonth = this.getDaysInMonth(this.year, this.month); + + this.spinbuttonDay.setValue(this.day, false); + this.spinbuttonMonth.setValue(this.month, false); + this.spinbuttonYear.setValue(this.year, false); + + this.updateSpinButtons(); +}; + +DatePickerSpinButtons.prototype.getDaysInMonth = function (year, month) { + return new Date(year, month + 1, 0).getDate(); +}; + +DatePickerSpinButtons.prototype.updateDay = function (day) { + this.day = day; + this.updateSpinButtons(); +}; + +DatePickerSpinButtons.prototype.updateMonth = function (month) { + this.month = month; + this.updateSpinButtons(); +}; + +DatePickerSpinButtons.prototype.updateYear = function (year) { + this.year = year; + this.updateSpinButtons(); +}; + +DatePickerSpinButtons.prototype.updatePreviousDayMonthAndYear = function () { + this.previousYear = this.year - 1; + + this.previousMonth = this.month ? this.month - 1 : 11; + + this.previousDay = this.day - 1; + if (this.previousDay < 1) { + this.previousDay = this.getDaysInMonth(this.year, this.month); + } +}; + +DatePickerSpinButtons.prototype.updateNextDayMonthAndYear = function () { + this.nextYear = this.year + 1; + this.nextMonth = this.month >= 11 ? 0 : this.month + 1; + + this.nextDay = this.day + 1; + var maxDay = this.getDaysInMonth(this.year, this.month); + if (this.nextDay > maxDay) { + this.nextDay = 1; + } +}; + +DatePickerSpinButtons.prototype.updateSpinButtons = function () { + this.daysInMonth = this.getDaysInMonth(this.year, this.month); + this.spinbuttonDay.setValueMax(this.daysInMonth); + if (this.day > this.daysInMonth) { + this.spinbuttonDay.setValue(this.daysInMonth); + return; + } + + this.updatePreviousDayMonthAndYear(); + this.updateNextDayMonthAndYear(); + + this.spinbuttonDay.setValueText(this.valuesDay[this.day]); + + this.spinbuttonDay.setPreviousValue(this.previousDay); + this.spinbuttonMonth.setPreviousValue(this.previousMonth); + this.spinbuttonYear.setPreviousValue(this.previousYear); + + this.spinbuttonDay.setNextValue(this.nextDay); + this.spinbuttonMonth.setNextValue(this.nextMonth); + this.spinbuttonYear.setNextValue(this.nextYear); + + this.currentDate = new Date( + this.year + '-' + (this.month + 1) + '-' + this.day + ); + + this.dateNode.innerHTML = + 'current value is ' + + this.valuesWeek[this.currentDate.getDay()] + + ', ' + + this.spinbuttonMonth.getValueText() + + ' ' + + this.spinbuttonDay.getValueText() + + ', ' + + this.spinbuttonYear.getValue(); +}; + +// Initialize menu button date picker + +window.addEventListener('load', function () { + var spinButtons = document.querySelectorAll('.datepicker-spinbuttons'); + + spinButtons.forEach(function (spinButton) { + var datepicker = new DatePickerSpinButtons(spinButton); + datepicker.init(); + }); +}); diff --git a/test/tests/spinbutton_datepicker.js b/test/tests/spinbutton_datepicker.js new file mode 100644 index 0000000000..6f8f3d7d43 --- /dev/null +++ b/test/tests/spinbutton_datepicker.js @@ -0,0 +1,985 @@ +const { ariaTest } = require('..'); +const { By, Key } = require('selenium-webdriver'); +const assertAttributeValues = require('../util/assertAttributeValues'); +const assertAriaLabelledby = require('../util/assertAriaLabelledby'); +const assertAriaRoles = require('../util/assertAriaRoles'); + +const exampleFile = + 'content/patterns/spinbutton/examples/datepicker-spinbuttons.html'; + +const valuesDay = [ + '', + 'first', + 'second', + 'third', + 'fourth', + 'fifth', + 'sixth', + 'seventh', + 'eighth', + 'ninth', + 'tenth', + 'eleventh', + 'twelfth', + 'thirteenth', + 'fourteenth', + 'fifteenth', + 'sixteenth', + 'seventeenth', + 'eighteenth', + 'nineteenth', + 'twentieth', + 'twenty first', + 'twenty second', + 'twenty third', + 'twenty fourth', + 'twenty fifth', + 'twenty sixth', + 'twenty seventh', + 'twenty eighth', + 'twenty ninth', + 'thirtieth', + 'thirty first', +]; +const valuesMonth = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; + +var getDaysInMonth = function (year, month) { + return new Date(year, month + 1, 0).getDate(); +}; + +var d = new Date(); + +// The date picker uses the current day, month and year to setup +const ex = { + groupSelector: '#example [role="group"]', + spinbuttonSelector: '#example [role="spinbutton"]', + + yearSelector: '#example .spinbutton.year [role="spinbutton"]', + yearMin: '2019', + yearMax: '2040', + yearNow: d.getFullYear().toString(), + + monthSelector: '#example .spinbutton.month [role="spinbutton"]', + monthMin: '0', + monthMax: '11', + monthNow: d.getMonth().toString(), + monthText: valuesMonth[d.getMonth()], + + daySelector: '#example .spinbutton.day [role="spinbutton"]', + dayMin: '1', + dayMax: getDaysInMonth(d.getFullYear(), d.getMonth()).toString(), + dayNow: d.getDate().toString(), + dayText: valuesDay[d.getDate()], + + yearIncreaseSelector: '#example .spinbutton.year .increase', + yearDecreaseSelector: '#example .spinbutton.year .decrease', + yearHiddenPreviousSelector: '#example .spinbutton.year .previous', + yearHiddenNextSelector: '#example .spinbutton.year .next', + + monthIncreaseSelector: '#example .spinbutton.month .increase', + monthDecreaseSelector: '#example .spinbutton.month .decrease', + monthHiddenPreviousSelector: '#example .spinbutton.month .previous', + monthHiddenNextSelector: '#example .spinbutton.month .next', + + dayIncreaseSelector: '#example .spinbutton.day .increase', + dayDecreaseSelector: '#example .spinbutton.day .decrease', + dayHiddenPreviousSelector: '#example .spinbutton.day .previous', + dayHiddenNextSelector: '#example .spinbutton.day .next', +}; + +// Attributes + +ariaTest( + 'role="group" on div element', + exampleFile, + 'group-role', + async (t) => { + await assertAriaRoles(t, 'example', 'group', '1', 'div'); + } +); + +ariaTest( + '"aria-labelledby" attribute on group', + exampleFile, + 'group-aria-labelledby', + async (t) => { + await assertAriaLabelledby(t, ex.groupSelector); + } +); + +ariaTest( + 'role="spinbutton" on div element', + exampleFile, + 'spinbutton-role', + async (t) => { + await assertAriaRoles(t, 'example', 'spinbutton', '3', 'div'); + } +); + +ariaTest( + '"aria-valuemax" represents the minimum value on spinbuttons', + exampleFile, + 'spinbutton-aria-valuemax', + async (t) => { + await assertAttributeValues(t, ex.daySelector, 'aria-valuemax', ex.dayMax); + await assertAttributeValues( + t, + ex.monthSelector, + 'aria-valuemax', + ex.monthMax + ); + await assertAttributeValues( + t, + ex.yearSelector, + 'aria-valuemax', + ex.yearMax + ); + } +); + +ariaTest( + '"aria-valuemin" represents the maximum value on spinbuttons', + exampleFile, + 'spinbutton-aria-valuemin', + async (t) => { + await assertAttributeValues(t, ex.daySelector, 'aria-valuemin', ex.dayMin); + await assertAttributeValues( + t, + ex.monthSelector, + 'aria-valuemin', + ex.monthMin + ); + await assertAttributeValues( + t, + ex.yearSelector, + 'aria-valuemin', + ex.yearMin + ); + } +); + +ariaTest( + '"aria-valuenow" reflects spinbutton value as a number', + exampleFile, + 'spinbutton-aria-valuenow', + async (t) => { + await assertAttributeValues(t, ex.daySelector, 'aria-valuenow', ex.dayNow); + await assertAttributeValues( + t, + ex.monthSelector, + 'aria-valuenow', + ex.monthNow + ); + await assertAttributeValues( + t, + ex.yearSelector, + 'aria-valuenow', + ex.yearNow + ); + } +); + +ariaTest( + '"aria-valuetext" reflects spin button value as a text string', + exampleFile, + 'spinbutton-aria-valuetext', + async (t) => { + await assertAttributeValues( + t, + ex.daySelector, + 'aria-valuetext', + ex.dayText + ); + await assertAttributeValues( + t, + ex.monthSelector, + 'aria-valuetext', + ex.monthText + ); + } +); + +ariaTest( + '"aria-label" provides accessible name for the spin buttons to screen reader users', + exampleFile, + 'spinbutton-aria-label', + async (t) => { + await assertAttributeValues(t, ex.daySelector, 'aria-label', 'Day'); + await assertAttributeValues(t, ex.monthSelector, 'aria-label', 'Month'); + await assertAttributeValues(t, ex.yearSelector, 'aria-label', 'Year'); + } +); + +ariaTest( + '"tabindex=-1" removes previous and next from the tab order of the page', + exampleFile, + 'button-tabindex', + async (t) => { + await assertAttributeValues(t, ex.dayIncreaseSelector, 'tabindex', '-1'); + await assertAttributeValues(t, ex.dayDecreaseSelector, 'tabindex', '-1'); + + await assertAttributeValues(t, ex.monthIncreaseSelector, 'tabindex', '-1'); + await assertAttributeValues(t, ex.monthDecreaseSelector, 'tabindex', '-1'); + + await assertAttributeValues(t, ex.yearIncreaseSelector, 'tabindex', '-1'); + await assertAttributeValues(t, ex.yearDecreaseSelector, 'tabindex', '-1'); + } +); + +ariaTest( + '"aria-label" provides accessible name for the previous and next buttons to screen reader users', + exampleFile, + 'button-aria-label', + async (t) => { + await assertAttributeValues( + t, + ex.dayIncreaseSelector, + 'aria-label', + 'next day' + ); + await assertAttributeValues( + t, + ex.dayDecreaseSelector, + 'aria-label', + 'previous day' + ); + + await assertAttributeValues( + t, + ex.monthIncreaseSelector, + 'aria-label', + 'next month' + ); + await assertAttributeValues( + t, + ex.monthDecreaseSelector, + 'aria-label', + 'previous month' + ); + + await assertAttributeValues( + t, + ex.yearIncreaseSelector, + 'aria-label', + 'next year' + ); + await assertAttributeValues( + t, + ex.yearDecreaseSelector, + 'aria-label', + 'previous year' + ); + } +); + +ariaTest( + '"aria-hidden" hides decorative and redundant content form screen reader users', + exampleFile, + 'spinbutton-aria-hidden', + async (t) => { + await assertAttributeValues( + t, + ex.dayHiddenPreviousSelector, + 'aria-hidden', + 'true' + ); + await assertAttributeValues( + t, + ex.dayHiddenNextSelector, + 'aria-hidden', + 'true' + ); + + await assertAttributeValues( + t, + ex.monthHiddenPreviousSelector, + 'aria-hidden', + 'true' + ); + await assertAttributeValues( + t, + ex.monthHiddenNextSelector, + 'aria-hidden', + 'true' + ); + + await assertAttributeValues( + t, + ex.yearHiddenPreviousSelector, + 'aria-hidden', + 'true' + ); + await assertAttributeValues( + t, + ex.yearHiddenNextSelector, + 'aria-hidden', + 'true' + ); + } +); + +// keys + +ariaTest('up arrow on day', exampleFile, 'spinbutton-up-arrow', async (t) => { + let control = parseInt(ex.dayNow); + let daysInMonth = parseInt(ex.dayMax); + + // Send up arrow to day date spinner + let daySpinner = await t.context.session.findElement(By.css(ex.daySelector)); + await daySpinner.sendKeys(Key.ARROW_UP); + + // Add a day to the control + control = (control + 1) % daysInMonth; + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending 1 up arrow to the day spinner, the day should be: ' + control + ); + + // Send up arrow 30 more times to date spinner + for (let i = 1; i <= 30; i++) { + await daySpinner.sendKeys(Key.ARROW_UP); + } + + // Add 30 days to the control + control = (control + 30) % daysInMonth; + + if (control === 0) { + control = parseInt(ex.dayMax); + } + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending 31 up arrows to the day spinner, the day should be: ' + + control + ); +}); + +ariaTest( + 'down arrow on day', + exampleFile, + 'spinbutton-down-arrow', + async (t) => { + let control = 31; + + // Set to December for a 31 day month + let monthSpinner = await t.context.session.findElement( + By.css(ex.monthSelector) + ); + await monthSpinner.sendKeys(Key.END); + + // Send down arrow to day date spinner + let daySpinner = await t.context.session.findElement( + By.css(ex.daySelector) + ); + + // Set to first of month + await daySpinner.sendKeys(Key.HOME); + + await daySpinner.sendKeys(Key.ARROW_DOWN); + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending 1 down arrow to the day spinner, the day should be: ' + + control + ); + + // Send down arrow 30 more times to date spinner + for (let i = 1; i <= 30; i++) { + await daySpinner.sendKeys(Key.ARROW_DOWN); + } + + // Subtract 30 days to the control + control -= 30; + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending 31 down arrows to the day spinner, the day should be: ' + + control + ); + } +); + +ariaTest('up arrow on month', exampleFile, 'spinbutton-up-arrow', async (t) => { + let date = new Date(); + date.setDate(1); // This is necessary to do the correct date math for months. + + let monthSpinner = await t.context.session.findElement( + By.css(ex.monthSelector) + ); + + // Send up arrow 12 times to date spinner + for (let i = 1; i <= 12; i++) { + await monthSpinner.sendKeys(Key.ARROW_UP); + const index = new Date(date.setMonth(date.getMonth() + 1)).getMonth(); + t.is( + await monthSpinner.getText(), + valuesMonth[index], + `After sending ${i} up arrows to the month spinner, the month should be: ${valuesMonth[index]}` + ); + } +}); + +ariaTest( + 'down arrow on month', + exampleFile, + 'spinbutton-down-arrow', + async (t) => { + let control = 0; + + // Send down arrow to month date spinner + let monthSpinner = await t.context.session.findElement( + By.css(ex.monthSelector) + ); + // Set to January + await monthSpinner.sendKeys(Key.HOME); + + await monthSpinner.sendKeys(Key.ARROW_DOWN); + + // Subtract a month to the control + control = 11; + + t.is( + await monthSpinner.getText(), + valuesMonth[control], + 'After sending 1 down arrow to the month spinner, the month should be: ' + + valuesMonth[control] + ); + + // Send down arrow 30 more times to date spinner + for (let i = 1; i <= 30; i++) { + await monthSpinner.sendKeys(Key.ARROW_DOWN); + } + + // Subtract 30 months to the control + control = 12 + ((control - 30) % 12); + + t.is( + await monthSpinner.getText(), + valuesMonth[control], + 'After sending 31 down arrows to the month spinner, the month should be: ' + + valuesMonth[control] + ); + } +); + +ariaTest('up arrow on year', exampleFile, 'spinbutton-up-arrow', async (t) => { + // Send up arrow to day date spinner + let yearSpinner = await t.context.session.findElement( + By.css(ex.yearSelector) + ); + await yearSpinner.sendKeys(Key.ARROW_UP); + + let nextYear = (parseInt(ex.yearNow) + 1).toString(); + + t.is( + await yearSpinner.getText(), + nextYear, + 'After sending 1 up arrow to the year spinner, the year should be: ' + + nextYear + ); + + let manyYears = parseInt(ex.yearMax) - parseInt(ex.yearNow); + for (let i = 1; i <= manyYears; i++) { + await yearSpinner.sendKeys(Key.ARROW_UP); + } + + t.is( + await yearSpinner.getText(), + ex.yearMax, + 'After sending ' + + (manyYears + 1) + + ' up arrows to the year spinner, the year should be: ' + + ex.yearMax + ); +}); + +ariaTest( + 'down arrow on year', + exampleFile, + 'spinbutton-down-arrow', + async (t) => { + // Send down arrow to year date spinner + let yearSpinner = await t.context.session.findElement( + By.css(ex.yearSelector) + ); + await yearSpinner.sendKeys(Key.ARROW_DOWN); + + // Subtract a year to the control + let lastYear = (parseInt(ex.yearNow) - 1).toString(); + + t.is( + await yearSpinner.getText(), + lastYear, + 'After sending 1 down arrow to the year spinner, the year should be: ' + + lastYear + ); + + let manyYears = parseInt(ex.yearNow) - parseInt(ex.yearMin); + for (let i = 1; i <= manyYears; i++) { + await yearSpinner.sendKeys(Key.ARROW_DOWN); + } + + t.is( + await yearSpinner.getText(), + ex.yearMin, + 'After sending ' + + manyYears + + ' down arrows to the year spinner, the year should be: ' + + ex.yearMin + ); + } +); + +ariaTest('page up on day', exampleFile, 'spinbutton-page-up', async (t) => { + let control = parseInt(ex.dayNow); + + // Set to December for a 31 day month + let monthSpinner = await t.context.session.findElement( + By.css(ex.monthSelector) + ); + await monthSpinner.sendKeys(Key.END); + + // Send page up to day date spinner + let daySpinner = await t.context.session.findElement(By.css(ex.daySelector)); + + // Set to first of month + await daySpinner.sendKeys(Key.HOME); + + await daySpinner.sendKeys(Key.PAGE_UP); + + // Add a day to the control + control = 6; + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending 1 page up to the day spinner, the day should be: ' + control + ); + + // Send page up 5 more times to date spinner + for (let i = 1; i <= 5; i++) { + await daySpinner.sendKeys(Key.PAGE_UP); + } + + // Add 25 days to the control + control = 31; + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending 6 page ups to the day spinner, the day should be: ' + control + ); + + // Set to end of month + await daySpinner.sendKeys(Key.END); + + // Rolls around to start of month + control = 1; + + await daySpinner.sendKeys(Key.PAGE_UP); + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending page up to the day spinner on the last day of the month, the day should be: ' + + control + ); + + // Set to end of month + await daySpinner.sendKeys(Key.END); + + // Set to four days before + for (let i = 1; i <= 4; i++) { + await daySpinner.sendKeys(Key.DOWN); + } + + await daySpinner.sendKeys(Key.PAGE_UP); + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending page up to the day spinner on 4 days before the end of the month, the day should be: ' + + control + ); +}); + +ariaTest('page down on day', exampleFile, 'spinbutton-page-down', async (t) => { + let control = parseInt(ex.dayNow); + + // Set to December for a 31 day month + let monthSpinner = await t.context.session.findElement( + By.css(ex.monthSelector) + ); + await monthSpinner.sendKeys(Key.END); + + // Send page down to day date spinner + let daySpinner = await t.context.session.findElement(By.css(ex.daySelector)); + + // Set to 31st + await daySpinner.sendKeys(Key.END); + + await daySpinner.sendKeys(Key.PAGE_DOWN); + + // Subtract 5 days to the control + control = 26; + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending 1 page down to the day spinner, the day should be: ' + + control + ); + + // Send page down 5 more times to date spinner + for (let i = 1; i <= 5; i++) { + await daySpinner.sendKeys(Key.PAGE_DOWN); + } + + // Subtract 25 days to the control + control -= 25; + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending 6 page downs to the day spinner, the day should be: ' + + control + ); + + await daySpinner.sendKeys(Key.PAGE_DOWN); + + // End of month if hit when at first of month. + control = 31; + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending page downs to the day spinner when set to 1, the day should be: ' + + control + ); + + // Set to first of month + await daySpinner.sendKeys(Key.HOME); + + // Set to five + for (let i = 1; i <= 4; i++) { + await daySpinner.sendKeys(Key.UP); + } + + await daySpinner.sendKeys(Key.PAGE_DOWN); + + t.is( + parseInt(await daySpinner.getText()), + control, + 'After sending page down to the day spinner when set to 5, the day should be: ' + + control + ); +}); + +ariaTest('page up on month', exampleFile, 'spinbutton-page-up', async (t) => { + let control = 1; + + // Send page up to day date spinner + let monthSpinner = await t.context.session.findElement( + By.css(ex.monthSelector) + ); + + // Set to January + await monthSpinner.sendKeys(Key.HOME); + + await monthSpinner.sendKeys(Key.PAGE_UP); + + // Add 5 month to the control + control += 5; + + t.is( + await monthSpinner.getText(), + valuesMonth[control - 1], + 'After sending 1 page up to the month spinner, the month should be: ' + + valuesMonth[control - 1] + ); + + // Send page up 2 more times to date spinner + for (let i = 1; i <= 2; i++) { + await monthSpinner.sendKeys(Key.PAGE_UP); + } + + // Should roll around to January + control = 1; + + t.is( + await monthSpinner.getText(), + valuesMonth[control - 1], + 'After sending 3 page ups to the month spinner, the month should be: ' + + valuesMonth[control - 1] + ); + + // Set to December + await monthSpinner.sendKeys(Key.END); + + // Should roll around to January + await monthSpinner.sendKeys(Key.PAGE_UP); + + t.is( + await monthSpinner.getText(), + valuesMonth[control - 1], + 'After sending page ups to the month spinner in December, the month should be: ' + + valuesMonth[control - 1] + ); + + // Set to December + await monthSpinner.sendKeys(Key.END); + + // Set to August + for (let i = 1; i <= 4; i++) { + await monthSpinner.sendKeys(Key.DOWN); + } + + await monthSpinner.sendKeys(Key.PAGE_UP); + + t.is( + await monthSpinner.getText(), + valuesMonth[control - 1], + 'After sending page up to the month spinner on August, the month should be: ' + + valuesMonth[control - 1] + ); +}); + +ariaTest( + 'page down on month', + exampleFile, + 'spinbutton-page-down', + async (t) => { + let control = 12; + + // Send page down to month date spinner + let monthSpinner = await t.context.session.findElement( + By.css(ex.monthSelector) + ); + + // Set spinner to December + await monthSpinner.sendKeys(Key.END); + + await monthSpinner.sendKeys(Key.PAGE_DOWN); + + // Subtract 5 month to the control + control -= 5; + + t.is( + await monthSpinner.getText(), + valuesMonth[control - 1], + 'After sending 1 page down to the month spinner, the month should be: ' + + valuesMonth[control - 1] + ); + + // Send page down 2 more times to date spinner + for (let i = 1; i <= 2; i++) { + await monthSpinner.sendKeys(Key.PAGE_DOWN); + } + + // Roll around to December + control = 12; + + t.is( + await monthSpinner.getText(), + valuesMonth[control - 1], + 'After sending 3 page downs to the month spinner, the month should be: ' + + valuesMonth[control - 1] + ); + + // Set to January + await monthSpinner.sendKeys(Key.HOME); + + // Roll around to December + await monthSpinner.sendKeys(Key.PAGE_DOWN); + + t.is( + await monthSpinner.getText(), + valuesMonth[control - 1], + 'After sending page down to the month spinner on January, the month should be: ' + + valuesMonth[control - 1] + ); + + // Set to January + await monthSpinner.sendKeys(Key.HOME); + + // Set to May + for (let i = 1; i <= 4; i++) { + await monthSpinner.sendKeys(Key.ARROW_UP); + } + + await monthSpinner.sendKeys(Key.PAGE_DOWN); + + t.is( + await monthSpinner.getText(), + valuesMonth[control - 1], + 'After sending page down to the month spinner on May, the month should be: ' + + valuesMonth[control - 1] + ); + } +); + +ariaTest('page up on year', exampleFile, 'spinbutton-page-up', async (t) => { + // Send page up to day date spinner + let yearSpinner = await t.context.session.findElement( + By.css(ex.yearSelector) + ); + await yearSpinner.sendKeys(Key.PAGE_UP); + + let nextYear = (parseInt(ex.yearNow) + 5).toString(); + + t.is( + await yearSpinner.getText(), + nextYear, + 'After sending 1 page up to the year spinner, the year should be: ' + + nextYear + ); + + let manyYears = Math.ceil((parseInt(ex.yearMax) - parseInt(ex.yearNow)) / 5); + for (let i = 1; i <= manyYears; i++) { + await yearSpinner.sendKeys(Key.PAGE_UP); + } + + t.is( + await yearSpinner.getText(), + ex.yearMax, + 'After sending ' + + (manyYears + 1) + + ' page ups to the year spinner, the year should be: ' + + ex.yearMax + ); +}); + +ariaTest( + 'down arrow on year', + exampleFile, + 'spinbutton-page-down', + async (t) => { + // Send down arrow to year date spinner + let yearSpinner = await t.context.session.findElement( + By.css(ex.yearSelector) + ); + await yearSpinner.sendKeys(Key.PAGE_UP); + await yearSpinner.sendKeys(Key.PAGE_DOWN); + + t.is( + await yearSpinner.getText(), + ex.yearNow, + 'After sending 1 up arrow, then 1 down arrow to the year spinner, the year should be: ' + + ex.yearNow + ); + + let manyYears = Math.ceil( + (parseInt(ex.yearNow) - parseInt(ex.yearMin)) / 5 + ); + for (let i = 1; i <= manyYears; i++) { + await yearSpinner.sendKeys(Key.PAGE_DOWN); + } + + t.is( + await yearSpinner.getText(), + ex.yearMin, + 'After sending ' + + manyYears + + ' down arrows to the year spinner, the year should be: ' + + ex.yearMin + ); + } +); + +ariaTest('home on day', exampleFile, 'spinbutton-home', async (t) => { + // Send home to day date spinner + let daySpinner = await t.context.session.findElement(By.css(ex.daySelector)); + await daySpinner.sendKeys(Key.HOME); + + t.is( + await daySpinner.getText(), + '1', + 'After sending home to the day spinner, the day should be: 1' + ); +}); + +ariaTest('end on day', exampleFile, 'spinbutton-end', async (t) => { + // Send home to day date spinner + let daySpinner = await t.context.session.findElement(By.css(ex.daySelector)); + await daySpinner.sendKeys(Key.END); + + t.is( + await daySpinner.getText(), + ex.dayMax, + 'After sending end to the day spinner, the day should be: ' + ex.dayMax + ); +}); + +ariaTest('home on month', exampleFile, 'spinbutton-home', async (t) => { + // Send home to month date spinner + let monthSpinner = await t.context.session.findElement( + By.css(ex.monthSelector) + ); + await monthSpinner.sendKeys(Key.HOME); + + t.is( + await monthSpinner.getText(), + 'January', + 'After sending home to the month spinner, the month should be: January' + ); +}); + +ariaTest('end on month', exampleFile, 'spinbutton-end', async (t) => { + // Send home to month date spinner + let monthSpinner = await t.context.session.findElement( + By.css(ex.monthSelector) + ); + await monthSpinner.sendKeys(Key.END); + + t.is( + await monthSpinner.getText(), + 'December', + 'After sending end to the month spinner, the month should be: December' + ); +}); + +ariaTest('home on year', exampleFile, 'spinbutton-home', async (t) => { + // Send home to year date spinner + let yearSpinner = await t.context.session.findElement( + By.css(ex.yearSelector) + ); + await yearSpinner.sendKeys(Key.HOME); + + t.is( + await yearSpinner.getText(), + ex.yearMin, + 'After sending home to the year spinner, the year should be: ' + ex.yearMin + ); +}); + +ariaTest('end on year', exampleFile, 'spinbutton-end', async (t) => { + // Send home to year date spinner + let yearSpinner = await t.context.session.findElement( + By.css(ex.yearSelector) + ); + await yearSpinner.sendKeys(Key.END); + + t.is( + await yearSpinner.getText(), + ex.yearMax, + 'After sending end to the year spinner, the year should be: ' + ex.yearMax + ); +}); From b4931dbc44e6f784b121a363bea0d481b562d66e Mon Sep 17 00:00:00 2001 From: Adam Page Date: Wed, 6 Aug 2025 16:16:50 -0700 Subject: [PATCH 13/25] deprecate Date Picker Spin Button --- .../coverage-and-quality-report.html | 22 +++++++++---------- .../coverage-and-quality/prop-coverage.csv | 14 ++++++------ .../coverage-and-quality/role-coverage.csv | 4 ++-- content/index/index.html | 18 +++++++-------- .../examples/datepicker-spinbuttons.html | 11 ++++++++-- 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/content/about/coverage-and-quality/coverage-and-quality-report.html b/content/about/coverage-and-quality/coverage-and-quality-report.html index d490f6bd98..2c3aedef98 100644 --- a/content/about/coverage-and-quality/coverage-and-quality-report.html +++ b/content/about/coverage-and-quality/coverage-and-quality-report.html @@ -373,7 +373,7 @@

    Roles with More than One Guidance or Exa
  • Listbox with Grouped Options
  • Editor Menubar (HC)
  • Color Viewer Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Switch Using HTML Button (HC)
  • File Directory Treeview Using Computed Properties
  • File Directory Treeview Using Declared Properties
  • @@ -633,7 +633,7 @@

    Roles with More than One Guidance or Exa Spinbutton Pattern @@ -1011,7 +1011,7 @@

    Properties and States with More than One
  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Quantity Spin Button
  • Switch Using HTML Button (HC)
  • Switch Using HTML Checkbox Input (HC)
  • @@ -1039,7 +1039,7 @@

    Properties and States with More than One
  • Navigation Menubar (HC)
  • Rating Radio Group (HC)
  • Horizontal Multi-Thumb Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Table
  • Toolbar
  • Treegrid Email Inbox
  • @@ -1079,7 +1079,7 @@

    Properties and States with More than One
  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Switch Using HTML Button (HC)
  • Experimental Tabs with Action Buttons (HC)
  • Tabs with Automatic Activation (HC)
  • @@ -1233,7 +1233,7 @@

    Properties and States with More than One
  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -1249,7 +1249,7 @@

    Properties and States with More than One
  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -1266,7 +1266,7 @@

    Properties and States with More than One
  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -1280,7 +1280,7 @@

    Properties and States with More than One
  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Toolbar
  • @@ -1977,7 +1977,7 @@

    Coding Practices

    - Date Picker Spin Button + (Deprecated) Date Picker Spin Button prototype Yes @@ -2488,7 +2488,7 @@

    SVG and High Contrast Techniques

    Yes - Date Picker Spin Button + (Deprecated) Date Picker Spin Button Yes Yes diff --git a/content/about/coverage-and-quality/prop-coverage.csv b/content/about/coverage-and-quality/prop-coverage.csv index 6de4fdfb79..b35658987c 100644 --- a/content/about/coverage-and-quality/prop-coverage.csv +++ b/content/about/coverage-and-quality/prop-coverage.csv @@ -18,11 +18,11 @@ "aria-flowto","0","0" "aria-grabbed","0","0" "aria-haspopup","0","9","Example: Date Picker Combobox","Example: Editable Combobox with Grid Popup","Example: (Deprecated) Collapsible Dropdown Listbox","Example: Actions Menu Button Using aria-activedescendant","Example: Actions Menu Button Using element.focus()","Example: Navigation Menu Button","Example: Editor Menubar","Example: Navigation Menubar","Example: Toolbar" -"aria-hidden","0","17","Example: Button (IDL Version)","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Editor Menubar","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Quantity Spin Button","Example: Switch Using HTML Button","Example: Switch Using HTML Checkbox Input","Example: Switch","Example: Sortable Table","Example: Toolbar" +"aria-hidden","0","17","Example: Button (IDL Version)","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Editor Menubar","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: (Deprecated) Date Picker Spin Button","Example: Quantity Spin Button","Example: Switch Using HTML Button","Example: Switch Using HTML Checkbox Input","Example: Switch","Example: Sortable Table","Example: Toolbar" "aria-invalid","0","0" "aria-keyshortcuts","0","0" -"aria-label","1","18","Guidance: Naming with a String Attribute Via aria-label","Example: Breadcrumb","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Editable Combobox With Both List and Inline Autocomplete","Example: Editable Combobox With List Autocomplete","Example: Editable Combobox without Autocomplete","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Link","Example: Editor Menubar","Example: Navigation Menubar","Example: Rating Radio Group","Example: Horizontal Multi-Thumb Slider","Example: Date Picker Spin Button","Example: Table","Example: Toolbar","Example: Treegrid Email Inbox","Example: Navigation Treeview" -"aria-labelledby","1","41","Guidance: Naming with Referenced Content Via aria-labelledby","Example: Accordion","Example: Alert Dialog","Example: Checkbox (Two State)","Example: Date Picker Combobox","Example: Select-Only Combobox","Example: Editable Combobox with Grid Popup","Example: Date Picker Dialog","Example: Modal Dialog","Example: Infinite Scrolling Feed","Example: Data Grid","Example: Layout Grid","Example: (Deprecated) Collapsible Dropdown Listbox","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Actions Menu Button Using aria-activedescendant","Example: Actions Menu Button Using element.focus()","Example: Navigation Menu Button","Example: Navigation Menubar","Example: Meter","Example: Radio Group Using aria-activedescendant","Example: Rating Radio Group","Example: Radio Group Using Roving tabindex","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Switch Using HTML Button","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties","Example: Navigation Treeview","Example: Complementary Landmark","Example: Form Landmark","Example: Main Landmark","Example: Navigation Landmark","Example: Region Landmark","Example: Search Landmark" +"aria-label","1","18","Guidance: Naming with a String Attribute Via aria-label","Example: Breadcrumb","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Editable Combobox With Both List and Inline Autocomplete","Example: Editable Combobox With List Autocomplete","Example: Editable Combobox without Autocomplete","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Link","Example: Editor Menubar","Example: Navigation Menubar","Example: Rating Radio Group","Example: Horizontal Multi-Thumb Slider","Example: (Deprecated) Date Picker Spin Button","Example: Table","Example: Toolbar","Example: Treegrid Email Inbox","Example: Navigation Treeview" +"aria-labelledby","1","41","Guidance: Naming with Referenced Content Via aria-labelledby","Example: Accordion","Example: Alert Dialog","Example: Checkbox (Two State)","Example: Date Picker Combobox","Example: Select-Only Combobox","Example: Editable Combobox with Grid Popup","Example: Date Picker Dialog","Example: Modal Dialog","Example: Infinite Scrolling Feed","Example: Data Grid","Example: Layout Grid","Example: (Deprecated) Collapsible Dropdown Listbox","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Actions Menu Button Using aria-activedescendant","Example: Actions Menu Button Using element.focus()","Example: Navigation Menu Button","Example: Navigation Menubar","Example: Meter","Example: Radio Group Using aria-activedescendant","Example: Rating Radio Group","Example: Radio Group Using Roving tabindex","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: (Deprecated) Date Picker Spin Button","Example: Switch Using HTML Button","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties","Example: Navigation Treeview","Example: Complementary Landmark","Example: Form Landmark","Example: Main Landmark","Example: Navigation Landmark","Example: Region Landmark","Example: Search Landmark" "aria-level","0","2","Example: Treegrid Email Inbox","Example: File Directory Treeview Using Declared Properties" "aria-live","0","6","Example: Alert","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Quantity Spin Button" "aria-modal","0","4","Example: Alert Dialog","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Modal Dialog" @@ -43,7 +43,7 @@ "aria-selected","0","17","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Editable Combobox With Both List and Inline Autocomplete","Example: Editable Combobox With List Autocomplete","Example: Editable Combobox without Autocomplete","Example: Date Picker Combobox","Example: Select-Only Combobox","Example: Editable Combobox with Grid Popup","Example: Date Picker Dialog","Example: (Deprecated) Collapsible Dropdown Listbox","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties" "aria-setsize","0","3","Example: Infinite Scrolling Feed","Example: Treegrid Email Inbox","Example: File Directory Treeview Using Declared Properties" "aria-sort","1","2","Guidance: Indicating sort order with aria-sort","Example: Data Grid","Example: Sortable Table" -"aria-valuemax","1","9","Guidance: Using aria-valuemin, aria-valuemax and aria-valuenow","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" -"aria-valuemin","0","9","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" -"aria-valuenow","1","9","Guidance: Using aria-valuemin, aria-valuemax and aria-valuenow","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" -"aria-valuetext","1","5","Guidance: Using aria-valuetext","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: Date Picker Spin Button","Example: Toolbar" +"aria-valuemax","1","9","Guidance: Using aria-valuemin, aria-valuemax and aria-valuenow","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: (Deprecated) Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" +"aria-valuemin","0","9","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: (Deprecated) Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" +"aria-valuenow","1","9","Guidance: Using aria-valuemin, aria-valuemax and aria-valuenow","Example: Meter","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: (Deprecated) Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" +"aria-valuetext","1","5","Guidance: Using aria-valuetext","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: (Deprecated) Date Picker Spin Button","Example: Toolbar" diff --git a/content/about/coverage-and-quality/role-coverage.csv b/content/about/coverage-and-quality/role-coverage.csv index bb97e24eeb..a119b85ef4 100644 --- a/content/about/coverage-and-quality/role-coverage.csv +++ b/content/about/coverage-and-quality/role-coverage.csv @@ -25,7 +25,7 @@ "generic","0","0" "grid","3","5","Guidance: Grid Popup Keyboard Interaction","Guidance: Grid (Interactive Tabular Data and Layout Containers) Pattern","Guidance: Grid and Table Properties","Example: Date Picker Combobox","Example: Editable Combobox with Grid Popup","Example: Date Picker Dialog","Example: Data Grid","Example: Layout Grid" "gridcell","0","3","Example: Editable Combobox with Grid Popup","Example: Layout Grid","Example: Treegrid Email Inbox" -"group","2","10","Guidance: Radio Group Pattern","Guidance: For Radio Group Contained in a Toolbar","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Checkbox (Two State)","Example: Listbox with Grouped Options","Example: Editor Menubar","Example: Color Viewer Slider","Example: Date Picker Spin Button","Example: Switch Using HTML Button","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties","Example: Navigation Treeview" +"group","2","10","Guidance: Radio Group Pattern","Guidance: For Radio Group Contained in a Toolbar","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Checkbox (Two State)","Example: Listbox with Grouped Options","Example: Editor Menubar","Example: Color Viewer Slider","Example: (Deprecated) Date Picker Spin Button","Example: Switch Using HTML Button","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties","Example: Navigation Treeview" "heading","0","0" "img","0","0" "input","0","0" @@ -62,7 +62,7 @@ "searchbox","0","0" "separator","0","1","Example: Editor Menubar" "slider","2","5","Guidance: Slider (Multi-Thumb) Pattern","Guidance: Slider Pattern","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider" -"spinbutton","1","3","Guidance: Spinbutton Pattern","Example: Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" +"spinbutton","1","3","Guidance: Spinbutton Pattern","Example: (Deprecated) Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" "status","0","0" "switch","1","3","Guidance: Switch Pattern","Example: Switch Using HTML Button","Example: Switch Using HTML Checkbox Input","Example: Switch" "tab","1","4","Guidance: Keyboard Navigation Between Components (The Tab Sequence)","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation" diff --git a/content/index/index.html b/content/index/index.html index 877b9221f9..5c2f1a961c 100644 --- a/content/index/index.html +++ b/content/index/index.html @@ -168,7 +168,7 @@

    Examples by Role

  • Listbox with Grouped Options
  • Editor Menubar (HC)
  • Color Viewer Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Switch Using HTML Button (HC)
  • File Directory Treeview Using Computed Properties
  • File Directory Treeview Using Declared Properties
  • @@ -367,7 +367,7 @@

    Examples by Role

    spinbutton @@ -645,7 +645,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Quantity Spin Button
  • Switch Using HTML Button (HC)
  • Switch Using HTML Checkbox Input (HC)
  • @@ -672,7 +672,7 @@

    Examples By Properties and States

  • Navigation Menubar (HC)
  • Rating Radio Group (HC)
  • Horizontal Multi-Thumb Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Table
  • Toolbar
  • Treegrid Email Inbox
  • @@ -711,7 +711,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Switch Using HTML Button (HC)
  • Tabs with Automatic Activation (HC)
  • Tabs with Manual Activation (HC)
  • @@ -871,7 +871,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -887,7 +887,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -903,7 +903,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Quantity Spin Button
  • Toolbar
  • @@ -916,7 +916,7 @@

    Examples By Properties and States

  • Rating Slider (HC)
  • Media Seek Slider (HC)
  • Vertical Temperature Slider (HC)
  • -
  • Date Picker Spin Button
  • +
  • (Deprecated) Date Picker Spin Button
  • Toolbar
  • diff --git a/content/patterns/spinbutton/examples/datepicker-spinbuttons.html b/content/patterns/spinbutton/examples/datepicker-spinbuttons.html index 906b15aed4..86e3af0d9b 100644 --- a/content/patterns/spinbutton/examples/datepicker-spinbuttons.html +++ b/content/patterns/spinbutton/examples/datepicker-spinbuttons.html @@ -3,7 +3,7 @@ - Date Picker Spin Button Example + (Deprecated) Date Picker Spin Button Example @@ -27,10 +27,17 @@
    -

    Date Picker Spin Button Example

    +

    (Deprecated) Date Picker Spin Button Example

    About This Example

    +
    +

    Deprecation Warning

    +

    + This pattern has been deprecated, and will be removed in a future version of the ARIA Authoring Practices. + The Quantity Spin Button should be used as an alternative to this pattern. +

    +

    The following example uses the Spin Button Pattern to implement a date picker. From ac7c190ce62fa7a100f6ff618979ed61ed4a80b3 Mon Sep 17 00:00:00 2001 From: Adam Page Date: Wed, 6 Aug 2025 16:51:00 -0700 Subject: [PATCH 14/25] remove copypasta --- .../patterns/spinbutton/examples/quantity-spinbutton.html | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/content/patterns/spinbutton/examples/quantity-spinbutton.html b/content/patterns/spinbutton/examples/quantity-spinbutton.html index 1e9136a02c..3dd7ca0c39 100644 --- a/content/patterns/spinbutton/examples/quantity-spinbutton.html +++ b/content/patterns/spinbutton/examples/quantity-spinbutton.html @@ -313,12 +313,7 @@

    Role, Property, State, and Tabindex Attributes

    aria-valuemax="NUMBER" input[type="text"] - -
      -
    • Indicates the maximum allowed value for the spin button.
    • -
    • For the Day spin button, this property is updated based on the value of the Month spin button.
    • -
    - + Indicates the maximum allowed value for the spin button. From c1018704f660757e321926cf9c521f0c3e77a78b Mon Sep 17 00:00:00 2001 From: Adam Page Date: Thu, 7 Aug 2025 22:58:11 -0700 Subject: [PATCH 15/25] tweaks for test coverage --- .../coverage-and-quality-report.html | 21 +++--- .../coverage-and-quality/prop-coverage.csv | 2 +- .../coverage-and-quality/role-coverage.csv | 4 +- content/index/index.html | 9 ++- .../examples/js/quantity-spinbutton.js | 10 +-- .../examples/quantity-spinbutton.html | 69 ++++++++++--------- 6 files changed, 60 insertions(+), 55 deletions(-) diff --git a/content/about/coverage-and-quality/coverage-and-quality-report.html b/content/about/coverage-and-quality/coverage-and-quality-report.html index 2c3aedef98..418c918b74 100644 --- a/content/about/coverage-and-quality/coverage-and-quality-report.html +++ b/content/about/coverage-and-quality/coverage-and-quality-report.html @@ -90,6 +90,7 @@

    Roles with No Guidance or Examples (insertion
  • list
  • listitem
  • +
  • log
  • marquee
  • math
  • paragraph
  • @@ -97,7 +98,6 @@

    Roles with No Guidance or Examples (rowheader
  • scrollbar
  • searchbox
  • -
  • status
  • term
  • textbox
  • timer
  • @@ -146,12 +146,6 @@

    Roles with at Least One Guidance or Exampl Link Pattern Link - - - - log - - Quantity Spin Button @@ -184,6 +178,12 @@

    Roles with at Least One Guidance or Exampl separator Editor Menubar + + + + status + + Quantity Spin Button @@ -1114,7 +1114,6 @@

    Properties and States with More than One
  • Auto-Rotating Image Carousel with Tabs for Slide Control (HC)
  • Date Picker Combobox (HC)
  • Date Picker Dialog (HC)
  • -
  • Quantity Spin Button
  • @@ -1998,9 +1997,9 @@

    Coding Practices

    example 2 2 - 8 - 7 - aria-relevant + 6 + 6 + Switch Using HTML Button diff --git a/content/about/coverage-and-quality/prop-coverage.csv b/content/about/coverage-and-quality/prop-coverage.csv index b35658987c..19db885b0f 100644 --- a/content/about/coverage-and-quality/prop-coverage.csv +++ b/content/about/coverage-and-quality/prop-coverage.csv @@ -24,7 +24,7 @@ "aria-label","1","18","Guidance: Naming with a String Attribute Via aria-label","Example: Breadcrumb","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Editable Combobox With Both List and Inline Autocomplete","Example: Editable Combobox With List Autocomplete","Example: Editable Combobox without Autocomplete","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Link","Example: Editor Menubar","Example: Navigation Menubar","Example: Rating Radio Group","Example: Horizontal Multi-Thumb Slider","Example: (Deprecated) Date Picker Spin Button","Example: Table","Example: Toolbar","Example: Treegrid Email Inbox","Example: Navigation Treeview" "aria-labelledby","1","41","Guidance: Naming with Referenced Content Via aria-labelledby","Example: Accordion","Example: Alert Dialog","Example: Checkbox (Two State)","Example: Date Picker Combobox","Example: Select-Only Combobox","Example: Editable Combobox with Grid Popup","Example: Date Picker Dialog","Example: Modal Dialog","Example: Infinite Scrolling Feed","Example: Data Grid","Example: Layout Grid","Example: (Deprecated) Collapsible Dropdown Listbox","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox","Example: Actions Menu Button Using aria-activedescendant","Example: Actions Menu Button Using element.focus()","Example: Navigation Menu Button","Example: Navigation Menubar","Example: Meter","Example: Radio Group Using aria-activedescendant","Example: Rating Radio Group","Example: Radio Group Using Roving tabindex","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider","Example: (Deprecated) Date Picker Spin Button","Example: Switch Using HTML Button","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation","Example: File Directory Treeview Using Computed Properties","Example: File Directory Treeview Using Declared Properties","Example: Navigation Treeview","Example: Complementary Landmark","Example: Form Landmark","Example: Main Landmark","Example: Navigation Landmark","Example: Region Landmark","Example: Search Landmark" "aria-level","0","2","Example: Treegrid Email Inbox","Example: File Directory Treeview Using Declared Properties" -"aria-live","0","6","Example: Alert","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Quantity Spin Button" +"aria-live","0","5","Example: Alert","Example: Auto-Rotating Image Carousel with Buttons for Slide Control","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Date Picker Combobox","Example: Date Picker Dialog" "aria-modal","0","4","Example: Alert Dialog","Example: Date Picker Combobox","Example: Date Picker Dialog","Example: Modal Dialog" "aria-multiline","0","0" "aria-multiselectable","0","1","Example: Listboxes with Rearrangeable Options" diff --git a/content/about/coverage-and-quality/role-coverage.csv b/content/about/coverage-and-quality/role-coverage.csv index a119b85ef4..9a74b8c4ff 100644 --- a/content/about/coverage-and-quality/role-coverage.csv +++ b/content/about/coverage-and-quality/role-coverage.csv @@ -34,7 +34,7 @@ "list","0","0" "listbox","2","8","Guidance: Listbox Popup Keyboard Interaction","Guidance: Listbox Pattern","Example: Editable Combobox With Both List and Inline Autocomplete","Example: Editable Combobox With List Autocomplete","Example: Editable Combobox without Autocomplete","Example: Select-Only Combobox","Example: (Deprecated) Collapsible Dropdown Listbox","Example: Listbox with Grouped Options","Example: Listboxes with Rearrangeable Options","Example: Scrollable Listbox" "listitem","0","0" -"log","0","1","Example: Quantity Spin Button" +"log","0","0" "main","1","1","Guidance: Main","Example: Main Landmark" "marquee","0","0" "math","0","0" @@ -63,7 +63,7 @@ "separator","0","1","Example: Editor Menubar" "slider","2","5","Guidance: Slider (Multi-Thumb) Pattern","Guidance: Slider Pattern","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider" "spinbutton","1","3","Guidance: Spinbutton Pattern","Example: (Deprecated) Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" -"status","0","0" +"status","0","1","Example: Quantity Spin Button" "switch","1","3","Guidance: Switch Pattern","Example: Switch Using HTML Button","Example: Switch Using HTML Checkbox Input","Example: Switch" "tab","1","4","Guidance: Keyboard Navigation Between Components (The Tab Sequence)","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation" "table","2","1","Guidance: Table Pattern","Guidance: Grid and Table Properties","Example: Table" diff --git a/content/index/index.html b/content/index/index.html index 5c2f1a961c..d78ca264fb 100644 --- a/content/index/index.html +++ b/content/index/index.html @@ -195,10 +195,6 @@

    Examples by Role

    - - log - Quantity Spin Button - main Main Landmark @@ -373,6 +369,10 @@

    Examples by Role

    + + status + Quantity Spin Button + switch @@ -745,7 +745,6 @@

    Examples By Properties and States

  • Auto-Rotating Image Carousel with Tabs for Slide Control (HC)
  • Date Picker Combobox (HC)
  • Date Picker Dialog (HC)
  • -
  • Quantity Spin Button
  • diff --git a/content/patterns/spinbutton/examples/js/quantity-spinbutton.js b/content/patterns/spinbutton/examples/js/quantity-spinbutton.js index 7bc1ba31e7..381a5e2952 100644 --- a/content/patterns/spinbutton/examples/js/quantity-spinbutton.js +++ b/content/patterns/spinbutton/examples/js/quantity-spinbutton.js @@ -14,7 +14,7 @@ class SpinButton { this.controls = Array.from( document.querySelectorAll(`button[aria-controls="${this.id}"]`) ); - this.output = document.querySelector(`output[for="${this.id}"]`); + this.status = document.querySelector(`[role="status"][for="${this.id}"]`); this.timer = null; this.setBounds(); el.addEventListener('input', () => this.setValue(el.value)); @@ -72,13 +72,13 @@ class SpinButton { } announce() { - if (!this.output) return; - this.output.textContent = this.el.value; + if (!this.status) return; + this.status.textContent = this.el.value; clearTimeout(this.timer); this.timer = setTimeout(() => { - this.output.textContent = ''; + this.status.textContent = ''; this.timer = null; - }, 3000); + }, this.status.dataset.selfDestruct || 1000); } handleKey(e) { diff --git a/content/patterns/spinbutton/examples/quantity-spinbutton.html b/content/patterns/spinbutton/examples/quantity-spinbutton.html index 3dd7ca0c39..4a7a901ba1 100644 --- a/content/patterns/spinbutton/examples/quantity-spinbutton.html +++ b/content/patterns/spinbutton/examples/quantity-spinbutton.html @@ -87,13 +87,11 @@

    Example

    - - + data-self-destruct="2000" + class="visually-hidden">
    - +
    - +
    @@ -190,7 +196,8 @@

    Accessibility Features

  • The spin button input and its adjacent buttons are visually presented as a singular form field containing an editable value, an - increase operation, and a decrease operation. + increment button, and a + decrement button.
  • When either the spin button input or its adjacent buttons have @@ -202,7 +209,7 @@

    Accessibility Features

    focus indicator appears with subtle animation to draw attention.
  • - The increase and decrease buttons: + The increment and decrement buttons:
    • Are generously sized for ease of use. @@ -270,6 +277,15 @@

      Keyboard Support

      End Increases to maximum value. + + Standard single line text editing keys + +
        +
      • Keys used for cursor movement and text manipulation, such as Delete and Shift + Right Arrow.
      • +
      • An HTML input with type="text" is used for the spin button so the browser will provide platform-specific editing keys.
      • +
      + +
  • @@ -319,7 +335,7 @@

    Role, Property, State, and Tabindex Attributes

    title="NAME_STRING" button - Defines the accessible name for each increase and decrease button (Remove adult, Add adult, Remove kid, Add kid, Remove animal, and Add animal). + Defines the accessible name for each increment and decrement button (Remove adult, Add adult, Remove kid, Add kid, Remove animal, and Add animal). @@ -333,15 +349,15 @@

    Role, Property, State, and Tabindex Attributes

    tabindex="-1" button - Removes the decrease and increase buttons from the page Tab sequence while keeping them focusable so they can be accessed with touch-based and voice-based assistive technologies. + Removes the increment and decrement buttons from the page Tab sequence while keeping them focusable so they can be accessed with touch-based and voice-based assistive technologies. - + aria-disabled="true" button Set when the minimum or maximum value has been reached to inform assistive technologies that the button has been disabled. - + aria-disabled="false" button @@ -351,27 +367,18 @@

    Role, Property, State, and Tabindex Attributes

    aria-hidden="true" span - For assistive technology users, hides the visible minus and plus characters in the decrease and increase buttons since they are symbolic of the superior button labels provided by the title attribute. + For assistive technology users, hides the visible minus and plus characters in the increment and decrement buttons since they are symbolic of the superior button labels provided by the title attribute. - - log + + status - output - Identifies the invisible output element as a log. - - - - - aria-live="polite" - - - output - + div
      -
    • Triggers a screen reader announcement of the output element’s updated content at the next graceful opportunity.
    • -
    • When either the increase or decrease button is pressed, the current value of the spin button is injected into the output element.
    • -
    • In keeping with the log role of the output, its contents are emptied 3 seconds after injection.
    • +
    • Identifies the invisible div as a status element with an implicit aria-live value of polite.
    • +
    • Triggers a screen reader announcement of the status element’s updated content at the next graceful opportunity.
    • +
    • When either the increment or decrement button is pressed, the current value of the spin button is injected into the status element.
    • +
    • Its contents are emptied 2000 milliseconds after injection.
    From 7adc5db2b0ab207ead06b16374275507fe50b44e Mon Sep 17 00:00:00 2001 From: Adam Page Date: Thu, 7 Aug 2025 22:58:57 -0700 Subject: [PATCH 16/25] initial tests --- test/tests/spinbutton_quantity.js | 440 ++++++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 test/tests/spinbutton_quantity.js diff --git a/test/tests/spinbutton_quantity.js b/test/tests/spinbutton_quantity.js new file mode 100644 index 0000000000..be98eba3a4 --- /dev/null +++ b/test/tests/spinbutton_quantity.js @@ -0,0 +1,440 @@ +const { ariaTest } = require('..'); +const { By, Key } = require('selenium-webdriver'); +const assertAttributeValues = require('../util/assertAttributeValues'); +const assertAriaRoles = require('../util/assertAriaRoles'); + +const exampleFile = + 'content/patterns/spinbutton/examples/quantity-spinbutton.html'; + +const ex = { + spin: { + id: 'adults', + sel: '#adults', + min: '1', + max: '8', + now: '1', + }, + + inc: { + sel: '[aria-controls="adults"][data-spinbutton-operation="increment"]', + accname: 'Add adult', + }, + + dec: { + sel: '[aria-controls="adults"][data-spinbutton-operation="decrement"]', + accname: 'Remove adult', + }, + + status: { + sel: 'div[for="adults"]', + selfDestruct: '2000', + }, +}; + +ex.inc.symbolSel = ex.inc.sel + ' > span'; +ex.dec.symbolSel = ex.dec.sel + ' > span'; +(ex.inputScenarios = { + 0: ex.spin.min, + 1: ex.spin.min, + 4: '4', + 8: ex.spin.max, + 13: ex.spin.max, + abc: ex.spin.min, + '-7': '7', +}), + // Attributes + + ariaTest( + 'role="spinbutton" on input element', + exampleFile, + 'spinbutton-role', + async (t) => { + await assertAriaRoles(t, 'example', 'spinbutton', '3', 'input'); + } + ); + +ariaTest( + '"aria-valuemin" represents the minimum value on spinbuttons', + exampleFile, + 'spinbutton-aria-valuemin', + async (t) => { + await assertAttributeValues(t, ex.spin.sel, 'aria-valuemin', ex.spin.min); + } +); + +ariaTest( + '"aria-valuemax" represents the maximum value on spinbuttons', + exampleFile, + 'spinbutton-aria-valuemax', + async (t) => { + await assertAttributeValues(t, ex.spin.sel, 'aria-valuemax', ex.spin.max); + } +); + +ariaTest( + '"aria-valuenow" reflects spinbutton value as a number', + exampleFile, + 'spinbutton-aria-valuenow', + async (t) => { + await assertAttributeValues(t, ex.spin.sel, 'aria-valuenow', ex.spin.now); + } +); + +ariaTest( + '"tabindex=-1" removes increment and decrement buttons from page tab order', + exampleFile, + 'button-tabindex', + async (t) => { + await assertAttributeValues(t, ex.inc.sel, 'tabindex', '-1'); + await assertAttributeValues(t, ex.dec.sel, 'tabindex', '-1'); + } +); + +ariaTest( + 'increment and decrement buttons use aria-controls to reference spinner', + exampleFile, + 'button-aria-controls', + async (t) => { + await assertAttributeValues(t, ex.inc.sel, 'aria-controls', ex.spin.id); + await assertAttributeValues(t, ex.dec.sel, 'aria-controls', ex.spin.id); + } +); + +ariaTest( + '"title" provides accessible name for the increment and decrement buttons', + exampleFile, + 'button-title', + async (t) => { + await assertAttributeValues(t, ex.inc.sel, 'title', ex.inc.accname); + await assertAttributeValues(t, ex.dec.sel, 'title', ex.dec.accname); + } +); + +ariaTest( + '"aria-hidden" hides symbolic accname equivalent from screen reader users', + exampleFile, + 'span-aria-hidden', + async (t) => { + await assertAttributeValues(t, ex.inc.symbolSel, 'aria-hidden', 'true'); + await assertAttributeValues(t, ex.dec.symbolSel, 'aria-hidden', 'true'); + } +); + +ariaTest( + 'role="status" on div element', + exampleFile, + 'status-role', + async (t) => { + await assertAriaRoles(t, 'example', 'status', '3', 'div'); + } +); + +// keys + +ariaTest('end', exampleFile, 'spinbutton-end', async (t) => { + const spinner = await t.context.session.findElement(By.css(ex.spin.sel)); + const max = parseInt(ex.spin.max); + + // Send end key + await spinner.sendKeys(Key.END); + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + max, + `After sending end key, aria-valuenow should be the maximum value: ${max}` + ); + + // Check that the decrement button is not disabled. + await assertAttributeValues(t, ex.dec.sel, 'aria-disabled', 'false'); + + // Check that the increment button is disabled. + await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'true'); +}); + +ariaTest('home', exampleFile, 'spinbutton-home', async (t) => { + const spinner = await t.context.session.findElement(By.css(ex.spin.sel)); + const min = parseInt(ex.spin.min); + + // Send home key + await spinner.sendKeys(Key.HOME); + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + min, + `After sending end key, aria-valuenow should be the minimum value: ${min}` + ); + + // Check that the decrement button is disabled. + await assertAttributeValues(t, ex.dec.sel, 'aria-disabled', 'true'); + + // Check that the increment button is not disabled. + await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'false'); +}); + +ariaTest('up arrow', exampleFile, 'spinbutton-up-arrow', async (t) => { + const spinner = await t.context.session.findElement(By.css(ex.spin.sel)); + const min = parseInt(ex.spin.min); + const max = parseInt(ex.spin.max); + let val = min; + + // Send home key + await spinner.sendKeys(Key.HOME); + + // Send arrow keys up to and including the maximum value. + while (++val <= max) { + await spinner.sendKeys(Key.ARROW_UP); + + // Check that aria-valuenow is updated correctly. + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + val, + `After sending ${val - 1} up arrows, aria-valuenow should be ${val}` + ); + } + + // Check that the decrement button is no longer disabled. + await assertAttributeValues(t, ex.dec.sel, 'aria-disabled', 'false'); + + // Check that the increment button is now disabled. + await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'true'); + + // Send one more and check that aria-valuenow remains at the maximum value. + await spinner.sendKeys(Key.ARROW_UP); + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + max, + `After sending one more up arrow, aria-valuenow should still be ${max}` + ); + + // Check that the decrement button is still not disabled + await assertAttributeValues(t, ex.dec.sel, 'aria-disabled', 'false'); + + // Check that the increment button is still disabled + await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'true'); +}); + +ariaTest('down arrow', exampleFile, 'spinbutton-down-arrow', async (t) => { + const spinner = await t.context.session.findElement(By.css(ex.spin.sel)); + const min = parseInt(ex.spin.min); + const max = parseInt(ex.spin.max); + let val = max; + + // Send end key + await spinner.sendKeys(Key.END); + + // Send arrow keys down to and including the minimum value. + while (--val >= min) { + await spinner.sendKeys(Key.ARROW_DOWN); + + // Check that aria-valuenow is updated correctly. + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + val, + `After sending ${val + 1} down arrows, aria-valuenow should be ${val}` + ); + } + + // Check that the decrement button is now disabled. + await assertAttributeValues(t, ex.dec.sel, 'aria-disabled', 'true'); + + // Check that the increment button is no longer disabled. + await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'false'); + + // Send one more and check that aria-valuenow remains at the maximum value. + await spinner.sendKeys(Key.ARROW_DOWN); + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + min, + `After sending one more up arrow, aria-valuenow should still be ${min}` + ); + + // Check that the decrement button is still disabled. + await assertAttributeValues(t, ex.dec.sel, 'aria-disabled', 'true'); + + // Check that the increment button is still not disabled. + await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'false'); +}); + +// text input + +ariaTest( + 'Expected behavior for direct input of numeric values', + exampleFile, + 'standard-single-line-editing-keys', + async (t) => { + const spinner = await t.context.session.findElement(By.css(ex.spin.sel)); + const min = parseInt(ex.spin.min); + const max = parseInt(ex.spin.max); + + for (const inputScenario of Object.entries(ex.inputScenarios)) { + const [input, expected] = inputScenario; + + // Input the value + await spinner.clear(); + await spinner.sendKeys(Key.chord(Key.COMMAND, 'a')); + await spinner.sendKeys(input); + + // Force blur after input + await t.context.session.executeScript((el) => el.blur(), spinner); + + // Check that aria-valuenow is updated correctly. + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + parseInt(expected), + `After inputting “${input}”, aria-valuenow should be ${expected}` + ); + + // Check that the decrement button has the expected aria-disabled state. + await assertAttributeValues( + t, + ex.dec.sel, + 'aria-disabled', + parseInt(expected) <= parseInt(min) ? 'true' : 'false' + ); + + // Check that the increment button has the expected aria-disabled state. + await assertAttributeValues( + t, + ex.inc.sel, + 'aria-disabled', + parseInt(expected) >= parseInt(max) ? 'true' : 'false' + ); + } + } +); + +// incerment and decrement buttons + +ariaTest('increment button', exampleFile, 'increment-button', async (t) => { + const spinner = await t.context.session.findElement(By.css(ex.spin.sel)); + const status = await t.context.session.findElement(By.css(ex.status.sel)); + const button = await t.context.session.findElement(By.css(ex.inc.sel)); + const min = parseInt(ex.spin.min); + const max = parseInt(ex.spin.max); + let val = min; + + // Send home key + await spinner.sendKeys(Key.HOME); + + // Click increment button up to and including the maximum value. + while (++val <= max) { + await button.click(); + + // Check that aria-valuenow is updated correctly. + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + val, + `After clicking ${val - 1} times, aria-valuenow should be ${val}` + ); + + // Check that the status element has the expected value. + t.is( + await status.getText(), + String(val), + `After clicking ${val - 1} times, status should be ${val}` + ); + } + + // Check that the decrement button is no longer disabled. + await assertAttributeValues(t, ex.dec.sel, 'aria-disabled', 'false'); + + // Check that the increment button is now disabled. + await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'true'); + + // Send one more and check that aria-valuenow remains at the maximum value. + await button.click(); + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + max, + `After clicking once more, aria-valuenow should still be ${max}` + ); + + // Check that the status element has the expected value. + t.is( + await status.getText(), + String(max), + `After clicking once more, status should still be ${max}` + ); + + // Check that the decrement button is still not disabled + await assertAttributeValues(t, ex.dec.sel, 'aria-disabled', 'false'); + + // Check that the increment button is still disabled + await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'true'); + + // Wait for the status to self-destruct + await t.context.session.sleep(parseInt(ex.status.selfDestruct) + 500); + + // Check that the status element is empty + t.is( + await status.getText(), + '', + `After waiting for the status self-destruct timer, it should be empty` + ); +}); + +ariaTest('decrement button', exampleFile, 'decrement-button', async (t) => { + const spinner = await t.context.session.findElement(By.css(ex.spin.sel)); + const status = await t.context.session.findElement(By.css(ex.status.sel)); + const button = await t.context.session.findElement(By.css(ex.dec.sel)); + const min = parseInt(ex.spin.min); + const max = parseInt(ex.spin.max); + let val = max; + + // Send end key + await spinner.sendKeys(Key.END); + + // Click decrement button down to and including the minimum value. + while (--val >= min) { + await button.click(); + + // Check that aria-valuenow is updated correctly. + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + val, + `After clicking ${val + 1} times, aria-valuenow should be ${val}` + ); + + // Check that the status element has the expected value. + t.is( + await status.getText(), + String(val), + `After clicking ${val + 1} times, status should be ${val}` + ); + } + + // Check that the decrement button is now disabled. + await assertAttributeValues(t, ex.dec.sel, 'aria-disabled', 'true'); + + // Check that the increment button is no longer disabled. + await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'false'); + + // Send one more and check that aria-valuenow remains at the maximum value. + await button.click(); + t.is( + parseInt(await spinner.getAttribute('aria-valuenow')), + min, + `After clicking once more, aria-valuenow should still be ${min}` + ); + + // Check that the status element has the expected value. + t.is( + await status.getText(), + String(min), + `After clicking once more, status should still be ${min}` + ); + + // Check that the decrement button is still disabled + await assertAttributeValues(t, ex.dec.sel, 'aria-disabled', 'true'); + + // Check that the increment button is still not disabled + await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'false'); + + // Wait for the status to self-destruct + await t.context.session.sleep(parseInt(ex.status.selfDestruct) + 500); + + // Check that the status element is empty + t.is( + await status.getText(), + '', + `After waiting for the status self-destruct timer, it should be empty` + ); +}); From e37e5a481c65dc233152d470757e050b2034e8a0 Mon Sep 17 00:00:00 2001 From: Adam Page Date: Thu, 7 Aug 2025 23:17:18 -0700 Subject: [PATCH 17/25] use native output --- .../coverage-and-quality-report.html | 19 ++--- .../coverage-and-quality/role-coverage.csv | 2 +- content/index/index.html | 4 -- .../examples/js/quantity-spinbutton.js | 10 +-- .../examples/quantity-spinbutton.html | 27 ++++--- cspell.json | 1 + test/tests/spinbutton_quantity.js | 71 ++++++++++--------- 7 files changed, 62 insertions(+), 72 deletions(-) diff --git a/content/about/coverage-and-quality/coverage-and-quality-report.html b/content/about/coverage-and-quality/coverage-and-quality-report.html index 418c918b74..aa53c9b019 100644 --- a/content/about/coverage-and-quality/coverage-and-quality-report.html +++ b/content/about/coverage-and-quality/coverage-and-quality-report.html @@ -53,8 +53,8 @@

    About These Reports

    • CSV Files of Role, Properties and States Coverage
    • -
    • Roles with no Guidance or Examples (27)
    • -
    • Roles with at Least One Guidance or Example (13)
    • +
    • Roles with no Guidance or Examples (28)
    • +
    • Roles with at Least One Guidance or Example (12)
    • Roles with More than One Guidance or Example (38)
    • Properties and States with no Examples (12)
    • Properties and States with One Examples (8)
    • @@ -72,7 +72,7 @@

      CSV Formatted Reports of Role, Property, and State Coverage

      -

      Roles with No Guidance or Examples (27)

      +

      Roles with No Guidance or Examples (28)

      • application
      • caption
      • @@ -98,13 +98,14 @@

        Roles with No Guidance or Examples (rowheader
      • scrollbar
      • searchbox
      • +
      • status
      • term
      • textbox
      • timer
      -

      Roles with at Least One Guidance or Example (13)

      +

      Roles with at Least One Guidance or Example (12)

      NOTE: The HC abbreviation means example has High Contrast support.
      @@ -178,12 +179,6 @@

      Roles with at Least One Guidance or Exampl

      - - - - - @@ -1995,8 +1990,8 @@

      Coding Practices

      - - + + diff --git a/content/about/coverage-and-quality/role-coverage.csv b/content/about/coverage-and-quality/role-coverage.csv index 9a74b8c4ff..8b06bb226d 100644 --- a/content/about/coverage-and-quality/role-coverage.csv +++ b/content/about/coverage-and-quality/role-coverage.csv @@ -63,7 +63,7 @@ "separator","0","1","Example: Editor Menubar" "slider","2","5","Guidance: Slider (Multi-Thumb) Pattern","Guidance: Slider Pattern","Example: Horizontal Multi-Thumb Slider","Example: Color Viewer Slider","Example: Rating Slider","Example: Media Seek Slider","Example: Vertical Temperature Slider" "spinbutton","1","3","Guidance: Spinbutton Pattern","Example: (Deprecated) Date Picker Spin Button","Example: Quantity Spin Button","Example: Toolbar" -"status","0","1","Example: Quantity Spin Button" +"status","0","0" "switch","1","3","Guidance: Switch Pattern","Example: Switch Using HTML Button","Example: Switch Using HTML Checkbox Input","Example: Switch" "tab","1","4","Guidance: Keyboard Navigation Between Components (The Tab Sequence)","Example: Auto-Rotating Image Carousel with Tabs for Slide Control","Example: Experimental Tabs with Action Buttons","Example: Tabs with Automatic Activation","Example: Tabs with Manual Activation" "table","2","1","Guidance: Table Pattern","Guidance: Grid and Table Properties","Example: Table" diff --git a/content/index/index.html b/content/index/index.html index d78ca264fb..4bda7d3a8f 100644 --- a/content/index/index.html +++ b/content/index/index.html @@ -369,10 +369,6 @@

      Examples by Role

      - - - - - - + - + + diff --git a/cspell.json b/cspell.json index dec723cc19..aa7bfc6548 100644 --- a/cspell.json +++ b/cspell.json @@ -4,6 +4,7 @@ "words": [ "accesskey", "Accesskey", + "accname", "activedescendants", "affordance", "Ahlefeldt", diff --git a/test/tests/spinbutton_quantity.js b/test/tests/spinbutton_quantity.js index be98eba3a4..893f34e011 100644 --- a/test/tests/spinbutton_quantity.js +++ b/test/tests/spinbutton_quantity.js @@ -25,8 +25,8 @@ const ex = { accname: 'Remove adult', }, - status: { - sel: 'div[for="adults"]', + output: { + sel: 'output[for="adults"]', selfDestruct: '2000', }, }; @@ -120,14 +120,15 @@ ariaTest( } ); -ariaTest( - 'role="status" on div element', - exampleFile, - 'status-role', - async (t) => { - await assertAriaRoles(t, 'example', 'status', '3', 'div'); - } -); +ariaTest('output element exists', exampleFile, 'output', async (t) => { + let output = await t.context.queryElements(t, ex.output.sel); + + t.is( + output.length, + 1, + 'One output element should be found by selector: ' + ex.output.sel + ); +}); // keys @@ -301,11 +302,11 @@ ariaTest( } ); -// incerment and decrement buttons +// increment and decrement buttons ariaTest('increment button', exampleFile, 'increment-button', async (t) => { const spinner = await t.context.session.findElement(By.css(ex.spin.sel)); - const status = await t.context.session.findElement(By.css(ex.status.sel)); + const output = await t.context.session.findElement(By.css(ex.output.sel)); const button = await t.context.session.findElement(By.css(ex.inc.sel)); const min = parseInt(ex.spin.min); const max = parseInt(ex.spin.max); @@ -325,11 +326,11 @@ ariaTest('increment button', exampleFile, 'increment-button', async (t) => { `After clicking ${val - 1} times, aria-valuenow should be ${val}` ); - // Check that the status element has the expected value. + // Check that the output element has the expected value. t.is( - await status.getText(), + await output.getText(), String(val), - `After clicking ${val - 1} times, status should be ${val}` + `After clicking ${val - 1} times, output should be ${val}` ); } @@ -347,11 +348,11 @@ ariaTest('increment button', exampleFile, 'increment-button', async (t) => { `After clicking once more, aria-valuenow should still be ${max}` ); - // Check that the status element has the expected value. + // Check that the output element has the expected value. t.is( - await status.getText(), + await output.getText(), String(max), - `After clicking once more, status should still be ${max}` + `After clicking once more, output should still be ${max}` ); // Check that the decrement button is still not disabled @@ -360,20 +361,20 @@ ariaTest('increment button', exampleFile, 'increment-button', async (t) => { // Check that the increment button is still disabled await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'true'); - // Wait for the status to self-destruct - await t.context.session.sleep(parseInt(ex.status.selfDestruct) + 500); + // Wait for the output to self-destruct + await t.context.session.sleep(parseInt(ex.output.selfDestruct) + 500); - // Check that the status element is empty + // Check that the output element is empty t.is( - await status.getText(), + await output.getText(), '', - `After waiting for the status self-destruct timer, it should be empty` + `After waiting for the output self-destruct timer, it should be empty` ); }); ariaTest('decrement button', exampleFile, 'decrement-button', async (t) => { const spinner = await t.context.session.findElement(By.css(ex.spin.sel)); - const status = await t.context.session.findElement(By.css(ex.status.sel)); + const output = await t.context.session.findElement(By.css(ex.output.sel)); const button = await t.context.session.findElement(By.css(ex.dec.sel)); const min = parseInt(ex.spin.min); const max = parseInt(ex.spin.max); @@ -393,11 +394,11 @@ ariaTest('decrement button', exampleFile, 'decrement-button', async (t) => { `After clicking ${val + 1} times, aria-valuenow should be ${val}` ); - // Check that the status element has the expected value. + // Check that the output element has the expected value. t.is( - await status.getText(), + await output.getText(), String(val), - `After clicking ${val + 1} times, status should be ${val}` + `After clicking ${val + 1} times, output should be ${val}` ); } @@ -415,11 +416,11 @@ ariaTest('decrement button', exampleFile, 'decrement-button', async (t) => { `After clicking once more, aria-valuenow should still be ${min}` ); - // Check that the status element has the expected value. + // Check that the output element has the expected value. t.is( - await status.getText(), + await output.getText(), String(min), - `After clicking once more, status should still be ${min}` + `After clicking once more, output should still be ${min}` ); // Check that the decrement button is still disabled @@ -428,13 +429,13 @@ ariaTest('decrement button', exampleFile, 'decrement-button', async (t) => { // Check that the increment button is still not disabled await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'false'); - // Wait for the status to self-destruct - await t.context.session.sleep(parseInt(ex.status.selfDestruct) + 500); + // Wait for the output to self-destruct + await t.context.session.sleep(parseInt(ex.output.selfDestruct) + 500); - // Check that the status element is empty + // Check that the output element is empty t.is( - await status.getText(), + await output.getText(), '', - `After waiting for the status self-destruct timer, it should be empty` + `After waiting for the output self-destruct timer, it should be empty` ); }); From 94a2833b405f5ca58521ed07b1d09ea203d78102 Mon Sep 17 00:00:00 2001 From: Adam Page Date: Thu, 7 Aug 2025 23:29:44 -0700 Subject: [PATCH 18/25] =?UTF-8?q?use=20utility=20to=20specify=20correct=20?= =?UTF-8?q?=E2=80=9Cselect=20all=E2=80=9D=20keystroke=20for=20environment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/tests/spinbutton_quantity.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/tests/spinbutton_quantity.js b/test/tests/spinbutton_quantity.js index 893f34e011..4d069cc25c 100644 --- a/test/tests/spinbutton_quantity.js +++ b/test/tests/spinbutton_quantity.js @@ -2,6 +2,7 @@ const { ariaTest } = require('..'); const { By, Key } = require('selenium-webdriver'); const assertAttributeValues = require('../util/assertAttributeValues'); const assertAriaRoles = require('../util/assertAriaRoles'); +const translatePlatformKey = require('../util/translatePlatformKeys'); const exampleFile = 'content/patterns/spinbutton/examples/quantity-spinbutton.html'; @@ -264,13 +265,15 @@ ariaTest( const spinner = await t.context.session.findElement(By.css(ex.spin.sel)); const min = parseInt(ex.spin.min); const max = parseInt(ex.spin.max); + const selectAllKeys = translatePlatformKey([Key.CONTROL, 'a']); + const selectAllChord = Key.chord(...selectAllKeys); for (const inputScenario of Object.entries(ex.inputScenarios)) { const [input, expected] = inputScenario; // Input the value await spinner.clear(); - await spinner.sendKeys(Key.chord(Key.COMMAND, 'a')); + await spinner.sendKeys(selectAllChord); await spinner.sendKeys(input); // Force blur after input From 6535e08174e7c46211ec7ad7d02512fd6f1d24b9 Mon Sep 17 00:00:00 2001 From: Adam Page Date: Fri, 8 Aug 2025 18:19:29 -0700 Subject: [PATCH 19/25] typos --- test/tests/spinbutton_quantity.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/test/tests/spinbutton_quantity.js b/test/tests/spinbutton_quantity.js index 4d069cc25c..3877a47b35 100644 --- a/test/tests/spinbutton_quantity.js +++ b/test/tests/spinbutton_quantity.js @@ -34,7 +34,7 @@ const ex = { ex.inc.symbolSel = ex.inc.sel + ' > span'; ex.dec.symbolSel = ex.dec.sel + ' > span'; -(ex.inputScenarios = { +ex.inputScenarios = { 0: ex.spin.min, 1: ex.spin.min, 4: '4', @@ -42,17 +42,18 @@ ex.dec.symbolSel = ex.dec.sel + ' > span'; 13: ex.spin.max, abc: ex.spin.min, '-7': '7', -}), - // Attributes - - ariaTest( - 'role="spinbutton" on input element', - exampleFile, - 'spinbutton-role', - async (t) => { - await assertAriaRoles(t, 'example', 'spinbutton', '3', 'input'); - } - ); +}; + +// Attributes + +ariaTest( + 'role="spinbutton" on input element', + exampleFile, + 'spinbutton-role', + async (t) => { + await assertAriaRoles(t, 'example', 'spinbutton', '3', 'input'); + } +); ariaTest( '"aria-valuemin" represents the minimum value on spinbuttons', From f0141208c2100257b76240c9a39a998d3b4cff2b Mon Sep 17 00:00:00 2001 From: Adam Page Date: Fri, 8 Aug 2025 18:21:10 -0700 Subject: [PATCH 20/25] lint --- .../spinbutton/examples/css/quantity-spinbutton.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/content/patterns/spinbutton/examples/css/quantity-spinbutton.css b/content/patterns/spinbutton/examples/css/quantity-spinbutton.css index a9fbba55c6..c9af4f4df1 100644 --- a/content/patterns/spinbutton/examples/css/quantity-spinbutton.css +++ b/content/patterns/spinbutton/examples/css/quantity-spinbutton.css @@ -25,7 +25,9 @@ border: 1px solid color-mix(in srgb, ghostwhite, darkblue 10%); border-radius: 0.5rem; - *, *::before, *::after { + *, + *::before, + *::after { box-sizing: border-box; margin: 0; padding: 0; @@ -93,7 +95,8 @@ outline-offset: var(--length-s); } - input, button { + input, + button { appearance: none; font: inherit; font-weight: bold; @@ -113,7 +116,9 @@ field-sizing: content; font-variant-numeric: tabular-nums; - &, &:hover, &:focus { + &, + &:hover, + &:focus { border: none; } } From c30dd2255f4b473cb0f658b0138c024caf7306f4 Mon Sep 17 00:00:00 2001 From: Adam Page Date: Tue, 19 Aug 2025 11:10:49 -0700 Subject: [PATCH 21/25] Update test/tests/spinbutton_quantity.js Co-authored-by: Howard Edwards --- test/tests/spinbutton_quantity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tests/spinbutton_quantity.js b/test/tests/spinbutton_quantity.js index 3877a47b35..05e7fc8ee0 100644 --- a/test/tests/spinbutton_quantity.js +++ b/test/tests/spinbutton_quantity.js @@ -162,7 +162,7 @@ ariaTest('home', exampleFile, 'spinbutton-home', async (t) => { t.is( parseInt(await spinner.getAttribute('aria-valuenow')), min, - `After sending end key, aria-valuenow should be the minimum value: ${min}` + `After sending home key, aria-valuenow should be the minimum value: ${min}` ); // Check that the decrement button is disabled. From ad8fba8e8b75f63af018b05fb32cdfcd6ed67ae8 Mon Sep 17 00:00:00 2001 From: Adam Page Date: Tue, 19 Aug 2025 14:01:28 -0700 Subject: [PATCH 22/25] add white space input test --- test/tests/spinbutton_quantity.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tests/spinbutton_quantity.js b/test/tests/spinbutton_quantity.js index 05e7fc8ee0..484c67068c 100644 --- a/test/tests/spinbutton_quantity.js +++ b/test/tests/spinbutton_quantity.js @@ -42,6 +42,7 @@ ex.inputScenarios = { 13: ex.spin.max, abc: ex.spin.min, '-7': '7', + ' ': ex.spin.min, }; // Attributes From daaabe010474201196a7ae5995e7aad49f1c5bcd Mon Sep 17 00:00:00 2001 From: Adam Page Date: Tue, 19 Aug 2025 14:10:07 -0700 Subject: [PATCH 23/25] remove sleep buffer --- test/tests/spinbutton_quantity.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tests/spinbutton_quantity.js b/test/tests/spinbutton_quantity.js index 484c67068c..9eaf64a2e1 100644 --- a/test/tests/spinbutton_quantity.js +++ b/test/tests/spinbutton_quantity.js @@ -367,7 +367,7 @@ ariaTest('increment button', exampleFile, 'increment-button', async (t) => { await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'true'); // Wait for the output to self-destruct - await t.context.session.sleep(parseInt(ex.output.selfDestruct) + 500); + await t.context.session.sleep(parseInt(ex.output.selfDestruct)); // Check that the output element is empty t.is( @@ -435,7 +435,7 @@ ariaTest('decrement button', exampleFile, 'decrement-button', async (t) => { await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'false'); // Wait for the output to self-destruct - await t.context.session.sleep(parseInt(ex.output.selfDestruct) + 500); + await t.context.session.sleep(parseInt(ex.output.selfDestruct)); // Check that the output element is empty t.is( From b7775b14c0416811e6af1070c063f459ef4700df Mon Sep 17 00:00:00 2001 From: Adam Page Date: Tue, 19 Aug 2025 14:31:59 -0700 Subject: [PATCH 24/25] add help text for min and max --- .../coverage-and-quality/coverage-and-quality-report.html | 4 ++-- .../spinbutton/examples/css/quantity-spinbutton.css | 4 ++++ .../patterns/spinbutton/examples/quantity-spinbutton.html | 6 ++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/content/about/coverage-and-quality/coverage-and-quality-report.html b/content/about/coverage-and-quality/coverage-and-quality-report.html index aa53c9b019..60e342bfe6 100644 --- a/content/about/coverage-and-quality/coverage-and-quality-report.html +++ b/content/about/coverage-and-quality/coverage-and-quality-report.html @@ -1992,9 +1992,9 @@

      Coding Practices

      + - - + diff --git a/content/patterns/spinbutton/examples/css/quantity-spinbutton.css b/content/patterns/spinbutton/examples/css/quantity-spinbutton.css index c9af4f4df1..1ad2e330ef 100644 --- a/content/patterns/spinbutton/examples/css/quantity-spinbutton.css +++ b/content/patterns/spinbutton/examples/css/quantity-spinbutton.css @@ -70,6 +70,10 @@ label { font-size: 1.2rem; } + + small { + font-size: 1rem; + } } .spinner { diff --git a/content/patterns/spinbutton/examples/quantity-spinbutton.html b/content/patterns/spinbutton/examples/quantity-spinbutton.html index 4433c03bc4..0fb92b5b36 100644 --- a/content/patterns/spinbutton/examples/quantity-spinbutton.html +++ b/content/patterns/spinbutton/examples/quantity-spinbutton.html @@ -66,6 +66,7 @@

      Example

      Example + 1 to 8Example Example + 0 to 8 Example Example + 0 to 12 Date: Thu, 21 Aug 2025 18:16:29 -0700 Subject: [PATCH 25/25] reintroduced self destruct buffer with until() --- test/tests/spinbutton_quantity.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/test/tests/spinbutton_quantity.js b/test/tests/spinbutton_quantity.js index 9eaf64a2e1..8f9c68ee01 100644 --- a/test/tests/spinbutton_quantity.js +++ b/test/tests/spinbutton_quantity.js @@ -1,5 +1,5 @@ const { ariaTest } = require('..'); -const { By, Key } = require('selenium-webdriver'); +const { By, Key, until } = require('selenium-webdriver'); const assertAttributeValues = require('../util/assertAttributeValues'); const assertAriaRoles = require('../util/assertAriaRoles'); const translatePlatformKey = require('../util/translatePlatformKeys'); @@ -7,6 +7,8 @@ const translatePlatformKey = require('../util/translatePlatformKeys'); const exampleFile = 'content/patterns/spinbutton/examples/quantity-spinbutton.html'; +const BUFFER_VAL = 500; // until() buffer, in milliseconds + const ex = { spin: { id: 'adults', @@ -367,14 +369,11 @@ ariaTest('increment button', exampleFile, 'increment-button', async (t) => { await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'true'); // Wait for the output to self-destruct - await t.context.session.sleep(parseInt(ex.output.selfDestruct)); - - // Check that the output element is empty - t.is( - await output.getText(), - '', - `After waiting for the output self-destruct timer, it should be empty` + await t.context.session.wait( + until.elementTextIs(output, ''), + output.selfDestruct + BUFFER_VAL ); + t.pass('After waiting for the output self-destruct timer, output is empty'); }); ariaTest('decrement button', exampleFile, 'decrement-button', async (t) => { @@ -435,12 +434,9 @@ ariaTest('decrement button', exampleFile, 'decrement-button', async (t) => { await assertAttributeValues(t, ex.inc.sel, 'aria-disabled', 'false'); // Wait for the output to self-destruct - await t.context.session.sleep(parseInt(ex.output.selfDestruct)); - - // Check that the output element is empty - t.is( - await output.getText(), - '', - `After waiting for the output self-destruct timer, it should be empty` + await t.context.session.wait( + until.elementTextIs(output, ''), + output.selfDestruct + BUFFER_VAL ); + t.pass('After waiting for the output self-destruct timer, output is empty'); });
      separator Editor Menubar -
      statusQuantity Spin Button
      example2211 6 6
      statusQuantity Spin Button
      switch diff --git a/content/patterns/spinbutton/examples/js/quantity-spinbutton.js b/content/patterns/spinbutton/examples/js/quantity-spinbutton.js index 381a5e2952..85c54354eb 100644 --- a/content/patterns/spinbutton/examples/js/quantity-spinbutton.js +++ b/content/patterns/spinbutton/examples/js/quantity-spinbutton.js @@ -14,7 +14,7 @@ class SpinButton { this.controls = Array.from( document.querySelectorAll(`button[aria-controls="${this.id}"]`) ); - this.status = document.querySelector(`[role="status"][for="${this.id}"]`); + this.output = document.querySelector(`output[for="${this.id}"]`); this.timer = null; this.setBounds(); el.addEventListener('input', () => this.setValue(el.value)); @@ -72,13 +72,13 @@ class SpinButton { } announce() { - if (!this.status) return; - this.status.textContent = this.el.value; + if (!this.output) return; + this.output.textContent = this.el.value; clearTimeout(this.timer); this.timer = setTimeout(() => { - this.status.textContent = ''; + this.output.textContent = ''; this.timer = null; - }, this.status.dataset.selfDestruct || 1000); + }, this.output.dataset.selfDestruct || 1000); } handleKey(e) { diff --git a/content/patterns/spinbutton/examples/quantity-spinbutton.html b/content/patterns/spinbutton/examples/quantity-spinbutton.html index 4a7a901ba1..4433c03bc4 100644 --- a/content/patterns/spinbutton/examples/quantity-spinbutton.html +++ b/content/patterns/spinbutton/examples/quantity-spinbutton.html @@ -87,11 +87,10 @@

      Example

      -
      + class="visually-hidden">
      -
      + class="visually-hidden">
      -
      + class="visually-hidden"> @@ -369,15 +366,15 @@

      Role, Property, State, and Tabindex Attributes

      span For assistive technology users, hides the visible minus and plus characters in the increment and decrement buttons since they are symbolic of the superior button labels provided by the title attribute.
      status
      divoutput
        -
      • Identifies the invisible div as a status element with an implicit aria-live value of polite.
      • -
      • Triggers a screen reader announcement of the status element’s updated content at the next graceful opportunity.
      • -
      • When either the increment or decrement button is pressed, the current value of the spin button is injected into the status element.
      • +
      • An element with an implicit role of status and an implicit aria-live value of polite.
      • +
      • Triggers a screen reader announcement of the output element’s updated content at the next graceful opportunity.
      • +
      • When either the increment or decrement button is pressed, the current value of the spin button is injected into the output element.
      • Its contents are emptied 2000 milliseconds after injection.
      example 1 17 66aria-describedby
      Switch Using HTML Button