feat(input-stepper): add aria-valuemin, aria-valuemax and an option t… (#2423)

* feat(input-stepper): add aria-valuemin, aria-valuemax and an option to set aria-valuetext

* chore: change valueText into an Object

* chore: remove reflect of valueText

* Update packages/ui/components/input-stepper/src/LionInputStepper.js

Co-authored-by: Oleksii Kadurin <ovkadurin@gmail.com>

* Update packages/ui/components/input-stepper/src/LionInputStepper.js

Co-authored-by: Oleksii Kadurin <ovkadurin@gmail.com>

* chore: rename valueText to valueTextMapping

---------

Co-authored-by: Oleksii Kadurin <ovkadurin@gmail.com>
This commit is contained in:
gerjanvangeest 2024-12-04 11:59:09 +01:00 committed by GitHub
parent b975615c8b
commit 5dc2205d7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 146 additions and 4 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ui': minor
---
[input-stepper] add aria-valuemin, aria-valuemax and an option to set aria-valuetext

View file

@ -49,6 +49,36 @@ Use `min` and `max` attribute to specify a range.
></lion-input-stepper>
```
### Value text
Use the `.valueTextMapping` property to override the value with a text.
```js preview-story
export const valueTextMapping = () => {
const values = {
1: 'first',
2: 'second',
3: 'third',
4: 'fourth',
5: 'fifth',
6: 'sixth',
7: 'seventh',
8: 'eighth',
9: 'ninth',
10: 'tenth',
};
return html`
<lion-input-stepper
label="Order"
min="1"
max="10"
name="value"
.valueTextMapping="${values}"
></lion-input-stepper>
`;
};
```
### Formatting
Just like with the `input-amount` you can add the `formatOptions` to format the numbers to your preferences, to a different locale or adjust the amount of fractions.

View file

@ -36,6 +36,10 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
type: Number,
reflect: true,
},
valueTextMapping: {
type: Object,
attribute: 'value-text',
},
step: {
type: Number,
reflect: true,
@ -66,6 +70,11 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
this.formatter = formatNumber;
this.min = Infinity;
this.max = Infinity;
/**
* The aria-valuetext attribute defines the human-readable text alternative of aria-valuenow.
* @type {{[key: number]: string}}
*/
this.valueTextMapping = {};
this.step = 1;
this.values = {
max: this.max,
@ -110,15 +119,29 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
if (changedProperties.has('min')) {
this._inputNode.min = `${this.min}`;
this.values.min = this.min;
if (this.min !== Infinity) {
this.setAttribute('aria-valuemin', `${this.min}`);
} else {
this.removeAttribute('aria-valuemin');
}
this.__toggleSpinnerButtonsState();
}
if (changedProperties.has('max')) {
this._inputNode.max = `${this.max}`;
this.values.max = this.max;
if (this.max !== Infinity) {
this.setAttribute('aria-valuemax', `${this.max}`);
} else {
this.removeAttribute('aria-valuemax');
}
this.__toggleSpinnerButtonsState();
}
if (changedProperties.has('valueTextMapping')) {
this._updateAriaAttributes();
}
if (changedProperties.has('step')) {
this._inputNode.step = `${this.step}`;
this.values.step = this.step;
@ -193,8 +216,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
*/
__toggleSpinnerButtonsState() {
const { min, max } = this.values;
const decrementButton = this.__getSlot('prefix');
const incrementButton = this.__getSlot('suffix');
const decrementButton = /** @type {HTMLButtonElement} */ (this.__getSlot('prefix'));
const incrementButton = /** @type {HTMLButtonElement} */ (this.__getSlot('suffix'));
const disableIncrementor = this.currentValue >= max && max !== Infinity;
const disableDecrementor = this.currentValue <= min && min !== Infinity;
if (
@ -205,7 +228,32 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
}
decrementButton[disableDecrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
incrementButton[disableIncrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
this.setAttribute('aria-valuenow', `${this.currentValue}`);
this._updateAriaAttributes();
}
/**
* @protected
*/
_updateAriaAttributes() {
const displayValue = this._inputNode.value;
if (displayValue) {
this.setAttribute('aria-valuenow', `${displayValue}`);
if (
Object.keys(this.valueTextMapping).length !== 0 &&
Object.keys(this.valueTextMapping).find(key => Number(key) === this.currentValue)
) {
this.setAttribute('aria-valuetext', `${this.valueTextMapping[this.currentValue]}`);
} else {
// VoiceOver announces percentages once the valuemin or valuemax are used.
// 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
this.setAttribute('aria-valuetext', `${displayValue}`);
}
} else {
this.removeAttribute('aria-valuenow');
this.removeAttribute('aria-valuetext');
}
}
/**

View file

@ -280,9 +280,68 @@ describe('<lion-input-stepper>', () => {
it('updates aria-valuenow when stepper is changed', async () => {
const el = await fixture(defaultInputStepper);
el.modelValue = 1;
await el.updateComplete;
expect(el.hasAttribute('aria-valuenow')).to.be.true;
expect(el.getAttribute('aria-valuenow')).to.equal('1');
el.modelValue = '';
await el.updateComplete;
expect(el.hasAttribute('aria-valuenow')).to.be.false;
});
it('updates aria-valuetext when stepper is changed', async () => {
// VoiceOver announces percentages once the valuemin or valuemax are used.
// 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
const el = await fixture(defaultInputStepper);
el.modelValue = 1;
await el.updateComplete;
expect(el.hasAttribute('aria-valuetext')).to.be.true;
expect(el.getAttribute('aria-valuetext')).to.equal('1');
el.modelValue = '';
await el.updateComplete;
expect(el.hasAttribute('aria-valuetext')).to.be.false;
});
it('can give aria-valuetext to override default value as a human-readable text alternative', async () => {
const values = {
1: 'first',
2: 'second',
3: 'third',
};
const el = await fixture(html`
<lion-input-stepper min="1" max="3" .valueTextMapping="${values}"></lion-input-stepper>
`);
el.modelValue = 1;
await el.updateComplete;
expect(el.hasAttribute('aria-valuetext')).to.be.true;
expect(el.getAttribute('aria-valuetext')).to.equal('first');
});
it('updates aria-valuemin when stepper is changed', async () => {
const el = await fixture(inputStepperWithAttrs);
const incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
expect(el).to.have.attribute('aria-valuenow', '1');
expect(el).to.have.attribute('aria-valuemin', '100');
el.min = 0;
await el.updateComplete;
expect(el).to.have.attribute('aria-valuemin', '0');
});
it('updates aria-valuemax when stepper is changed', async () => {
const el = await fixture(inputStepperWithAttrs);
const incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
expect(el).to.have.attribute('aria-valuemax', '200');
el.max = 1000;
await el.updateComplete;
expect(el).to.have.attribute('aria-valuemax', '1000');
});
});
});