Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
04f356c
initial commit
adampage Aug 4, 2025
d1e8388
coverage report and reference tables
adampage Aug 4, 2025
0b150bf
remove Date Picker Spin Button example and tests
adampage Aug 5, 2025
79a8be1
whitelist “nums”
adampage Aug 5, 2025
5485185
lint
adampage Aug 5, 2025
71d069e
typo
adampage Aug 5, 2025
bd99192
change class names to avoid collision
adampage Aug 5, 2025
f6fb7d8
prevent overrides
adampage Aug 5, 2025
a5201ba
prevent overrides
adampage Aug 5, 2025
39f6b8d
lint
adampage Aug 5, 2025
02eddbc
prettier
adampage Aug 5, 2025
6d840e2
Revert "remove Date Picker Spin Button example and tests"
adampage Aug 6, 2025
b4931db
deprecate Date Picker Spin Button
adampage Aug 6, 2025
ac7c190
remove copypasta
adampage Aug 6, 2025
c101870
tweaks for test coverage
adampage Aug 8, 2025
7adc5db
initial tests
adampage Aug 8, 2025
e37e5a4
use native output
adampage Aug 8, 2025
94a2833
use utility to specify correct “select all” keystroke for environment
adampage Aug 8, 2025
6535e08
typos
adampage Aug 9, 2025
f014120
lint
adampage Aug 9, 2025
c30dd22
Update test/tests/spinbutton_quantity.js
adampage Aug 19, 2025
ad8fba8
add white space input test
adampage Aug 19, 2025
daaabe0
remove sleep buffer
adampage Aug 19, 2025
b7775b1
add help text for min and max
adampage Aug 19, 2025
f46f563
reintroduced self destruct buffer with until()
adampage Aug 22, 2025
8bf3e7d
Merge remote-tracking branch 'upstream/main' into spinbutton-redesign
adampage Sep 8, 2025
3066ace
add permissive input with validation
adampage Sep 9, 2025
84647c5
use aria-errormessage to associate error
adampage Sep 9, 2025
5366a0e
mention `aria-invalid` and `aria-errormessage` in a11y features
adampage Sep 10, 2025
a8e98d9
fix value type for ariaInvalid and ariaDisabled props
adampage Sep 10, 2025
026ec09
Update content/patterns/spinbutton/examples/quantity-spinbutton.html
adampage Sep 16, 2025
2bce4d0
Update content/patterns/spinbutton/examples/quantity-spinbutton.html
adampage Sep 16, 2025
405d950
Update content/patterns/spinbutton/examples/quantity-spinbutton.html
adampage Sep 16, 2025
bddd041
Update content/patterns/spinbutton/examples/quantity-spinbutton.html
adampage Sep 16, 2025
288e9df
Update content/patterns/spinbutton/examples/quantity-spinbutton.html
adampage Sep 16, 2025
397b14d
Merge remote-tracking branch 'upstream/main' into spinbutton-redesign
adampage Sep 16, 2025
a90d848
update coverage report and reference tables
adampage Sep 16, 2025
a6afbd3
remove aria-describedby association for help text
adampage Sep 16, 2025
6e4f707
Update test/tests/spinbutton_quantity.js
adampage Sep 17, 2025
cd15985
Update test/tests/spinbutton_quantity.js
adampage Sep 17, 2025
689bb03
Update test/tests/spinbutton_quantity.js
adampage Sep 17, 2025
aa25a2a
Update test/tests/spinbutton_quantity.js
adampage Sep 17, 2025
67f73a1
Merge remote-tracking branch 'upstream/main' into spinbutton-redesign
adampage Sep 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
initial commit
  • Loading branch information
adampage committed Aug 4, 2025
commit 04f356c6bbd209e30bc13d436617f539eb442cfd
130 changes: 130 additions & 0 deletions content/patterns/spinbutton/examples/css/quantity-spinbutton.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
95 changes: 95 additions & 0 deletions content/patterns/spinbutton/examples/js/quantity-spinbutton.js
Original file line number Diff line number Diff line change
@@ -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))
);
Loading