blob: 295391ac1528d7bb3008c5dac624e0a8d9170dd4 [file]
/*
* 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.useAriaNotify = typeof el.ariaNotify === 'function';
if (this.useAriaNotify && this.output) {
this.output.remove();
this.output = null;
}
this.timer = null;
this.setBounds();
el.addEventListener('input', () => this.setValue(el.value, true));
el.addEventListener('blur', () => {
if (el.value === '' && this.hasMin) {
this.setValue(this.min, 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;
}
isValid(val) {
if (val === '' || val === null) return true;
const numVal = +val;
return !(
(this.hasMin && numVal < this.min) ||
(this.hasMax && numVal > this.max)
);
}
snapValue(val, currentVal) {
let result = val;
if (this.hasMin) result = Math.max(result, this.min);
if (this.hasMax) result = Math.min(result, this.max);
if (
(result < currentVal && val > currentVal) ||
(result > currentVal && val < currentVal)
) {
return currentVal;
}
return result;
}
setValue(raw, fromInput = false) {
let val = typeof raw === 'number' ? raw : this.parseValue(raw);
if (!fromInput) {
const currentVal = +this.el.value || 0;
val = this.snapValue(val, currentVal);
}
this.el.value = val;
this.el.ariaValueNow = val;
this.el.ariaInvalid = !this.isValid(val) ? 'true' : null;
this.updateButtonStates();
}
updateButtonStates() {
const val = +this.el.value;
this.controls.forEach((btn) => {
const op = btn.getAttribute('data-spinbutton-operation');
btn.ariaDisabled = (
op === 'decrement' ? val <= this.min : val >= this.max
)
? 'true'
: null;
});
}
announce() {
if (this.useAriaNotify) {
this.el.ariaNotify(String(this.el.value));
return;
}
if (!this.output) return;
this.output.textContent = this.el.value;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.output.textContent = '';
this.timer = null;
}, this.output.dataset.selfDestruct || 1000);
}
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))
);