fix(input-stepper): move role=spinbutton to input node (#2426)
* fix(input-stepper): move role=spinbutton to input node * chore: rename __boundOnEnterButton and __boundOnLeaveButton
This commit is contained in:
parent
fc1e66775f
commit
67f5735538
3 changed files with 123 additions and 54 deletions
5
.changeset/dirty-scissors-invent.md
Normal file
5
.changeset/dirty-scissors-invent.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
[input-stepper] move role="spinbutton" and relevant aria attributes to the inputNode
|
||||||
|
|
@ -21,6 +21,19 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
.input-group__container > .input-group__input ::slotted(.form-control) {
|
.input-group__container > .input-group__input ::slotted(.form-control) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-stepper__value {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(100%);
|
||||||
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +96,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
|
|
||||||
this.__increment = this.__increment.bind(this);
|
this.__increment = this.__increment.bind(this);
|
||||||
this.__decrement = this.__decrement.bind(this);
|
this.__decrement = this.__decrement.bind(this);
|
||||||
this.__boundOnLeaveButton = this._onLeaveButton.bind(this);
|
this._onEnterButton = this._onEnterButton.bind(this);
|
||||||
|
this._onLeaveButton = this._onLeaveButton.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
|
@ -93,11 +107,12 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
min: this.min,
|
min: this.min,
|
||||||
step: this.step,
|
step: this.step,
|
||||||
};
|
};
|
||||||
|
if (this._inputNode) {
|
||||||
this.role = 'spinbutton';
|
this._inputNode.role = 'spinbutton';
|
||||||
this.addEventListener('keydown', this.__keyDownHandler);
|
|
||||||
this._inputNode.setAttribute('inputmode', 'decimal');
|
this._inputNode.setAttribute('inputmode', 'decimal');
|
||||||
this._inputNode.setAttribute('autocomplete', 'off');
|
this._inputNode.setAttribute('autocomplete', 'off');
|
||||||
|
}
|
||||||
|
this.addEventListener('keydown', this.__keyDownHandler);
|
||||||
this.__setDefaultValidators();
|
this.__setDefaultValidators();
|
||||||
this.__toggleSpinnerButtonsState();
|
this.__toggleSpinnerButtonsState();
|
||||||
}
|
}
|
||||||
|
|
@ -119,9 +134,9 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
this._inputNode.min = `${this.min}`;
|
this._inputNode.min = `${this.min}`;
|
||||||
this.values.min = this.min;
|
this.values.min = this.min;
|
||||||
if (this.min !== Infinity) {
|
if (this.min !== Infinity) {
|
||||||
this.setAttribute('aria-valuemin', `${this.min}`);
|
this._inputNode.setAttribute('aria-valuemin', `${this.min}`);
|
||||||
} else {
|
} else {
|
||||||
this.removeAttribute('aria-valuemin');
|
this._inputNode.removeAttribute('aria-valuemin');
|
||||||
}
|
}
|
||||||
this.__toggleSpinnerButtonsState();
|
this.__toggleSpinnerButtonsState();
|
||||||
}
|
}
|
||||||
|
|
@ -130,9 +145,9 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
this._inputNode.max = `${this.max}`;
|
this._inputNode.max = `${this.max}`;
|
||||||
this.values.max = this.max;
|
this.values.max = this.max;
|
||||||
if (this.max !== Infinity) {
|
if (this.max !== Infinity) {
|
||||||
this.setAttribute('aria-valuemax', `${this.max}`);
|
this._inputNode.setAttribute('aria-valuemax', `${this.max}`);
|
||||||
} else {
|
} else {
|
||||||
this.removeAttribute('aria-valuemax');
|
this._inputNode.removeAttribute('aria-valuemax');
|
||||||
}
|
}
|
||||||
this.__toggleSpinnerButtonsState();
|
this.__toggleSpinnerButtonsState();
|
||||||
}
|
}
|
||||||
|
|
@ -145,14 +160,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
this._inputNode.step = `${this.step}`;
|
this._inputNode.step = `${this.step}`;
|
||||||
this.values.step = this.step;
|
this.values.step = this.step;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperties.has('_ariaLabelledNodes')) {
|
|
||||||
this.__reflectAriaAttrToSpinButton('aria-labelledby', this._ariaLabelledNodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedProperties.has('_ariaDescribedNodes')) {
|
|
||||||
this.__reflectAriaAttrToSpinButton('aria-describedby', this._ariaDescribedNodes);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get slots() {
|
get slots() {
|
||||||
|
|
@ -163,22 +170,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Based on FormControlMixin __reflectAriaAttr()
|
|
||||||
*
|
|
||||||
* Will handle help text, validation feedback and character counter,
|
|
||||||
* prefix/suffix/before/after (if they contain data-description flag attr).
|
|
||||||
* Also, contents of id references that will be put in the <lion-field>._ariaDescribedby property
|
|
||||||
* from an external context, will be read by a screen reader.
|
|
||||||
* @param {string} attrName
|
|
||||||
* @param {Element[]} nodes
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
__reflectAriaAttrToSpinButton(attrName, nodes) {
|
|
||||||
const string = nodes.map(n => n.id).join(' ');
|
|
||||||
this.setAttribute(attrName, string);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set aria labels and apply validators
|
* Set aria labels and apply validators
|
||||||
* @private
|
* @private
|
||||||
|
|
@ -227,7 +218,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
}
|
}
|
||||||
decrementButton[disableDecrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
|
decrementButton[disableDecrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
|
||||||
incrementButton[disableIncrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
|
incrementButton[disableIncrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
|
||||||
|
|
||||||
this._updateAriaAttributes();
|
this._updateAriaAttributes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,21 +227,22 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
_updateAriaAttributes() {
|
_updateAriaAttributes() {
|
||||||
const displayValue = this._inputNode.value;
|
const displayValue = this._inputNode.value;
|
||||||
if (displayValue) {
|
if (displayValue) {
|
||||||
this.setAttribute('aria-valuenow', `${displayValue}`);
|
this._inputNode.setAttribute('aria-valuenow', `${displayValue}`);
|
||||||
if (
|
if (
|
||||||
Object.keys(this.valueTextMapping).length !== 0 &&
|
Object.keys(this.valueTextMapping).length !== 0 &&
|
||||||
Object.keys(this.valueTextMapping).find(key => Number(key) === this.currentValue)
|
Object.keys(this.valueTextMapping).find(key => Number(key) === this.currentValue)
|
||||||
) {
|
) {
|
||||||
this.setAttribute('aria-valuetext', `${this.valueTextMapping[this.currentValue]}`);
|
this.__valueText = this.valueTextMapping[this.currentValue];
|
||||||
} else {
|
} else {
|
||||||
// VoiceOver announces percentages once the valuemin or valuemax are used.
|
// VoiceOver announces percentages once the valuemin or valuemax are used.
|
||||||
// This can be fixed by setting valuetext to the same value as valuenow
|
// This can be fixed by setting valuetext to the same value as valuenow
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow
|
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow
|
||||||
this.setAttribute('aria-valuetext', `${displayValue}`);
|
this.__valueText = displayValue;
|
||||||
}
|
}
|
||||||
|
this._inputNode.setAttribute('aria-valuetext', `${this.__valueText}`);
|
||||||
} else {
|
} else {
|
||||||
this.removeAttribute('aria-valuenow');
|
this._inputNode.removeAttribute('aria-valuenow');
|
||||||
this.removeAttribute('aria-valuetext');
|
this._inputNode.removeAttribute('aria-valuetext');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -373,7 +364,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
<button
|
<button
|
||||||
?disabled=${this.disabled || this.readOnly}
|
?disabled=${this.disabled || this.readOnly}
|
||||||
@click=${this.__decrement}
|
@click=${this.__decrement}
|
||||||
@blur=${this.__boundOnLeaveButton}
|
@focus=${this._onEnterButton}
|
||||||
|
@blur=${this._onLeaveButton}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="${this.msgLit('lion-input-stepper:decrease')}"
|
aria-label="${this.msgLit('lion-input-stepper:decrease')}"
|
||||||
>
|
>
|
||||||
|
|
@ -392,7 +384,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
<button
|
<button
|
||||||
?disabled=${this.disabled || this.readOnly}
|
?disabled=${this.disabled || this.readOnly}
|
||||||
@click=${this.__increment}
|
@click=${this.__increment}
|
||||||
@blur=${this.__boundOnLeaveButton}
|
@focus=${this._onEnterButton}
|
||||||
|
@blur=${this._onLeaveButton}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="${this.msgLit('lion-input-stepper:increase')}"
|
aria-label="${this.msgLit('lion-input-stepper:increase')}"
|
||||||
>
|
>
|
||||||
|
|
@ -401,6 +394,33 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @protected */
|
||||||
|
_inputGroupTemplate() {
|
||||||
|
return html`
|
||||||
|
<div class="input-stepper__value">${this.__valueText}</div>
|
||||||
|
<div class="input-group">
|
||||||
|
${this._inputGroupBeforeTemplate()}
|
||||||
|
<div class="input-group__container">
|
||||||
|
${this._inputGroupPrefixTemplate()} ${this._inputGroupInputTemplate()}
|
||||||
|
${this._inputGroupSuffixTemplate()}
|
||||||
|
</div>
|
||||||
|
${this._inputGroupAfterTemplate()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
* @param {Event} ev
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
_onEnterButton(ev) {
|
||||||
|
const valueNode = /** @type {HTMLElement} */ (
|
||||||
|
this.shadowRoot?.querySelector('.input-stepper__value')
|
||||||
|
);
|
||||||
|
valueNode.setAttribute('aria-live', 'assertive');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redispatch leave event on host when catching leave event
|
* Redispatch leave event on host when catching leave event
|
||||||
* on the incrementor and decrementor button.
|
* on the incrementor and decrementor button.
|
||||||
|
|
@ -410,8 +430,16 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
||||||
*
|
*
|
||||||
* Interacting with the buttons is "user interactions"
|
* Interacting with the buttons is "user interactions"
|
||||||
* the same way as focusing + blurring the field (native input)
|
* the same way as focusing + blurring the field (native input)
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {Event} ev
|
||||||
*/
|
*/
|
||||||
_onLeaveButton() {
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
_onLeaveButton(ev) {
|
||||||
|
const valueNode = /** @type {HTMLElement} */ (
|
||||||
|
this.shadowRoot?.querySelector('.input-stepper__value')
|
||||||
|
);
|
||||||
|
valueNode.removeAttribute('aria-live');
|
||||||
this.dispatchEvent(new Event(this._leaveEvent));
|
this.dispatchEvent(new Event(this._leaveEvent));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -278,17 +278,23 @@ describe('<lion-input-stepper>', () => {
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has role="spinbutton"', async () => {
|
||||||
|
const el = await fixture(defaultInputStepper);
|
||||||
|
expect(el._inputNode.hasAttribute('role')).to.be.true;
|
||||||
|
expect(el._inputNode.getAttribute('role')).to.equal('spinbutton');
|
||||||
|
});
|
||||||
|
|
||||||
it('updates aria-valuenow when stepper is changed', async () => {
|
it('updates aria-valuenow when stepper is changed', async () => {
|
||||||
const el = await fixture(defaultInputStepper);
|
const el = await fixture(defaultInputStepper);
|
||||||
el.modelValue = 1;
|
el.modelValue = 1;
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.hasAttribute('aria-valuenow')).to.be.true;
|
expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.true;
|
||||||
expect(el.getAttribute('aria-valuenow')).to.equal('1');
|
expect(el._inputNode.getAttribute('aria-valuenow')).to.equal('1');
|
||||||
|
|
||||||
el.modelValue = '';
|
el.modelValue = '';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.hasAttribute('aria-valuenow')).to.be.false;
|
expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates aria-valuetext when stepper is changed', async () => {
|
it('updates aria-valuetext when stepper is changed', async () => {
|
||||||
|
|
@ -299,12 +305,12 @@ describe('<lion-input-stepper>', () => {
|
||||||
el.modelValue = 1;
|
el.modelValue = 1;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(el.hasAttribute('aria-valuetext')).to.be.true;
|
expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
|
||||||
expect(el.getAttribute('aria-valuetext')).to.equal('1');
|
expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('1');
|
||||||
|
|
||||||
el.modelValue = '';
|
el.modelValue = '';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.hasAttribute('aria-valuetext')).to.be.false;
|
expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can give aria-valuetext to override default value as a human-readable text alternative', async () => {
|
it('can give aria-valuetext to override default value as a human-readable text alternative', async () => {
|
||||||
|
|
@ -318,30 +324,60 @@ describe('<lion-input-stepper>', () => {
|
||||||
`);
|
`);
|
||||||
el.modelValue = 1;
|
el.modelValue = 1;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.hasAttribute('aria-valuetext')).to.be.true;
|
expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
|
||||||
expect(el.getAttribute('aria-valuetext')).to.equal('first');
|
expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('first');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates aria-valuemin when stepper is changed', async () => {
|
it('updates aria-valuemin when stepper is changed', async () => {
|
||||||
const el = await fixture(inputStepperWithAttrs);
|
const el = await fixture(inputStepperWithAttrs);
|
||||||
const incrementButton = el.querySelector('[slot=suffix]');
|
const incrementButton = el.querySelector('[slot=suffix]');
|
||||||
incrementButton?.dispatchEvent(new Event('click'));
|
incrementButton?.dispatchEvent(new Event('click'));
|
||||||
expect(el).to.have.attribute('aria-valuemin', '100');
|
expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
|
||||||
|
expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('100');
|
||||||
|
|
||||||
el.min = 0;
|
el.min = 0;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el).to.have.attribute('aria-valuemin', '0');
|
expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
|
||||||
|
expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates aria-valuemax when stepper is changed', async () => {
|
it('updates aria-valuemax when stepper is changed', async () => {
|
||||||
const el = await fixture(inputStepperWithAttrs);
|
const el = await fixture(inputStepperWithAttrs);
|
||||||
const incrementButton = el.querySelector('[slot=suffix]');
|
const incrementButton = el.querySelector('[slot=suffix]');
|
||||||
incrementButton?.dispatchEvent(new Event('click'));
|
incrementButton?.dispatchEvent(new Event('click'));
|
||||||
expect(el).to.have.attribute('aria-valuemax', '200');
|
expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
|
||||||
|
expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('200');
|
||||||
|
|
||||||
el.max = 1000;
|
el.max = 1000;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el).to.have.attribute('aria-valuemax', '1000');
|
expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
|
||||||
|
expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('1000');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when decrease button gets focus, it sets aria-live to input-stepper__value', async () => {
|
||||||
|
const el = await fixture(inputStepperWithAttrs);
|
||||||
|
const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
|
||||||
|
const decrementButton = el.querySelector('[slot=prefix]');
|
||||||
|
|
||||||
|
decrementButton?.dispatchEvent(new Event('focus'));
|
||||||
|
expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
|
||||||
|
expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
|
||||||
|
|
||||||
|
decrementButton?.dispatchEvent(new Event('blur'));
|
||||||
|
expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when increase button gets focus, it sets aria-live to input-stepper__value', async () => {
|
||||||
|
const el = await fixture(inputStepperWithAttrs);
|
||||||
|
const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
|
||||||
|
const incrementButton = el.querySelector('[slot=suffix]');
|
||||||
|
|
||||||
|
incrementButton?.dispatchEvent(new Event('focus'));
|
||||||
|
expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
|
||||||
|
expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
|
||||||
|
|
||||||
|
incrementButton?.dispatchEvent(new Event('blur'));
|
||||||
|
expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue