diff --git a/.changeset/sweet-cows-happen.md b/.changeset/sweet-cows-happen.md
new file mode 100644
index 000000000..ce2d62634
--- /dev/null
+++ b/.changeset/sweet-cows-happen.md
@@ -0,0 +1,5 @@
+---
+'@lion/ui': minor
+---
+
+[input-stepper] add aria-valuemin, aria-valuemax and an option to set aria-valuetext
diff --git a/docs/components/input-stepper/use-cases.md b/docs/components/input-stepper/use-cases.md
index 76d8e88dd..ff5a9ed32 100644
--- a/docs/components/input-stepper/use-cases.md
+++ b/docs/components/input-stepper/use-cases.md
@@ -49,6 +49,36 @@ Use `min` and `max` attribute to specify a range.
>
```
+### 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`
+
+ `;
+};
+```
+
### 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.
diff --git a/packages/ui/components/input-stepper/src/LionInputStepper.js b/packages/ui/components/input-stepper/src/LionInputStepper.js
index 8dc50d864..13b25361b 100644
--- a/packages/ui/components/input-stepper/src/LionInputStepper.js
+++ b/packages/ui/components/input-stepper/src/LionInputStepper.js
@@ -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');
+ }
}
/**
diff --git a/packages/ui/components/input-stepper/test/lion-input-stepper.test.js b/packages/ui/components/input-stepper/test/lion-input-stepper.test.js
index e33294f0b..4af54879d 100644
--- a/packages/ui/components/input-stepper/test/lion-input-stepper.test.js
+++ b/packages/ui/components/input-stepper/test/lion-input-stepper.test.js
@@ -280,9 +280,68 @@ describe('', () => {
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`
+
+ `);
+ 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-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-valuenow', '1');
+ expect(el).to.have.attribute('aria-valuemax', '200');
+
+ el.max = 1000;
+ await el.updateComplete;
+ expect(el).to.have.attribute('aria-valuemax', '1000');
});
});
});