From bb584d89ad3fe381e3429a8f6712c5eb2645a3d7 Mon Sep 17 00:00:00 2001 From: gerjanvangeest Date: Mon, 2 Dec 2024 15:22:30 +0100 Subject: [PATCH 1/6] feat(input-stepper): add aria-valuemin, aria-valuemax and an option to set aria-valuetext --- .changeset/sweet-cows-happen.md | 5 ++ docs/components/input-stepper/use-cases.md | 35 ++++++++++++++ .../input-stepper/src/LionInputStepper.js | 46 +++++++++++++++++-- .../test/lion-input-stepper.test.js | 37 +++++++++++++++ 4 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 .changeset/sweet-cows-happen.md 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..138bb1061 100644 --- a/docs/components/input-stepper/use-cases.md +++ b/docs/components/input-stepper/use-cases.md @@ -49,6 +49,41 @@ Use `min` and `max` attribute to specify a range. > ``` +### Value text + +Use the `.valueText` property to override the value with a text. + +```js preview-story +export const valueText = () => { + const values = [ + 'first', + 'second', + 'third', + 'fourth', + 'fifth', + 'sixth', + 'seventh', + 'eighth', + 'ninth', + 'tenth', + ]; + function onModelValueChanged(ev) { + const inputStepper = ev.target; + inputStepper.valueText = values[inputStepper.modelValue - 1]; + } + const format = { locale: 'nl-NL' }; + 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..8cab669d4 100644 --- a/packages/ui/components/input-stepper/src/LionInputStepper.js +++ b/packages/ui/components/input-stepper/src/LionInputStepper.js @@ -36,6 +36,11 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { type: Number, reflect: true, }, + valueText: { + type: Number, + reflect: true, + attribute: 'value-text', + }, step: { type: Number, reflect: true, @@ -66,6 +71,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { this.formatter = formatNumber; this.min = Infinity; this.max = Infinity; + /** @type {string | undefined} */ + this.valueText = undefined; this.step = 1; this.values = { max: this.max, @@ -110,15 +117,36 @@ 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('valueText')) { + const displayValue = this._inputNode.value; + if (this.valueText) { + this.setAttribute('aria-valuetext', `${this.valueText}`); + } else if (displayValue) { + this.setAttribute('aria-valuetext', `${displayValue}`); + } else { + this.removeAttribute('aria-valuetext'); + } + } + if (changedProperties.has('step')) { this._inputNode.step = `${this.step}`; this.values.step = this.step; @@ -193,8 +221,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 +233,19 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { } decrementButton[disableDecrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true'); incrementButton[disableIncrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true'); - this.setAttribute('aria-valuenow', `${this.currentValue}`); + + const displayValue = this._inputNode.value; + if (displayValue) { + this.setAttribute('aria-valuenow', `${displayValue}`); + if (!this.valueText) { + // VoiceOver announces percentages once the valuemin or valuemax are used. + // This can be fixed by setting valuetext to the same value as 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..5a45a43ff 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 @@ -283,6 +283,43 @@ describe('', () => { const incrementButton = el.querySelector('[slot=suffix]'); incrementButton?.dispatchEvent(new Event('click')); expect(el).to.have.attribute('aria-valuenow', '1'); + + el._inputNode.value = ''; + await el.updateComplete; + expect(el).to.not.have.attribute('aria-valuenow'); + }); + + it('updates aria-valuetext when stepper is changed', async () => { + const el = await fixture(defaultInputStepper); + const incrementButton = el.querySelector('[slot=suffix]'); + incrementButton?.dispatchEvent(new Event('click')); + expect(el).to.have.attribute('aria-valuetext', '1'); + + el._inputNode.value = ''; + await el.updateComplete; + expect(el).to.not.have.attribute('aria-valuetext'); + }); + + 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-valuemax', '200'); + + el.max = 1000; + await el.updateComplete; + expect(el).to.have.attribute('aria-valuemax', '1000'); }); }); }); From a60c1a0f0b7900a585774efb983252028fc5ea80 Mon Sep 17 00:00:00 2001 From: gerjanvangeest Date: Wed, 4 Dec 2024 10:01:13 +0100 Subject: [PATCH 2/6] chore: change valueText into an Object --- docs/components/input-stepper/use-cases.md | 33 +++++++--------- .../input-stepper/src/LionInputStepper.js | 32 +++++++++------ .../test/lion-input-stepper.test.js | 39 ++++++++++++++----- 3 files changed, 63 insertions(+), 41 deletions(-) diff --git a/docs/components/input-stepper/use-cases.md b/docs/components/input-stepper/use-cases.md index 138bb1061..25b558852 100644 --- a/docs/components/input-stepper/use-cases.md +++ b/docs/components/input-stepper/use-cases.md @@ -55,30 +55,25 @@ Use the `.valueText` property to override the value with a text. ```js preview-story export const valueText = () => { - const values = [ - 'first', - 'second', - 'third', - 'fourth', - 'fifth', - 'sixth', - 'seventh', - 'eighth', - 'ninth', - 'tenth', - ]; - function onModelValueChanged(ev) { - const inputStepper = ev.target; - inputStepper.valueText = values[inputStepper.modelValue - 1]; - } - const format = { locale: 'nl-NL' }; + const values = { + 1: 'first', + 2: 'second', + 3: 'third', + 4: 'fourth', + 5: 'fifth', + 6: 'sixth', + 7: 'seventh', + 8: 'eighth', + 9: 'ninth', + 10: 'tenth', + }; return html` `; }; diff --git a/packages/ui/components/input-stepper/src/LionInputStepper.js b/packages/ui/components/input-stepper/src/LionInputStepper.js index 8cab669d4..381882573 100644 --- a/packages/ui/components/input-stepper/src/LionInputStepper.js +++ b/packages/ui/components/input-stepper/src/LionInputStepper.js @@ -37,7 +37,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { reflect: true, }, valueText: { - type: Number, + type: Object, reflect: true, attribute: 'value-text', }, @@ -71,8 +71,11 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { this.formatter = formatNumber; this.min = Infinity; this.max = Infinity; - /** @type {string | undefined} */ - this.valueText = undefined; + /** + * The aria-valuetext attribute defines the human-readable text alternative of aria-valuenow. + * @type {[ Number: String] | []} + */ + this.valueText = []; this.step = 1; this.values = { max: this.max, @@ -137,14 +140,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { } if (changedProperties.has('valueText')) { - const displayValue = this._inputNode.value; - if (this.valueText) { - this.setAttribute('aria-valuetext', `${this.valueText}`); - } else if (displayValue) { - this.setAttribute('aria-valuetext', `${displayValue}`); - } else { - this.removeAttribute('aria-valuetext'); - } + this._updateAriaAttributes(); } if (changedProperties.has('step')) { @@ -234,10 +230,22 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { decrementButton[disableDecrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true'); incrementButton[disableIncrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true'); + this._updateAriaAttributes(); + } + + /** + * @protected + */ + _updateAriaAttributes() { const displayValue = this._inputNode.value; if (displayValue) { this.setAttribute('aria-valuenow', `${displayValue}`); - if (!this.valueText) { + if ( + Object.keys(this.valueText).length !== 0 && + Object.keys(this.valueText).find(key => Number(key) === this.currentValue) + ) { + this.setAttribute('aria-valuetext', `${this.valueText[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 this.setAttribute('aria-valuetext', `${displayValue}`); 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 5a45a43ff..86c902449 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,24 +280,43 @@ describe('', () => { it('updates aria-valuenow when stepper is changed', async () => { const el = await fixture(defaultInputStepper); - const incrementButton = el.querySelector('[slot=suffix]'); - incrementButton?.dispatchEvent(new Event('click')); - expect(el).to.have.attribute('aria-valuenow', '1'); + el.modelValue = 1; + + await el.updateComplete; + expect(el.hasAttribute('aria-valuenow')).to.be.true; + expect(el.getAttribute('aria-valuenow')).to.equal('1'); - el._inputNode.value = ''; + el.modelValue = ''; await el.updateComplete; - expect(el).to.not.have.attribute('aria-valuenow'); + expect(el.hasAttribute('aria-valuenow')).to.be.false; }); it('updates aria-valuetext when stepper is changed', async () => { const el = await fixture(defaultInputStepper); - const incrementButton = el.querySelector('[slot=suffix]'); - incrementButton?.dispatchEvent(new Event('click')); - expect(el).to.have.attribute('aria-valuetext', '1'); + el.modelValue = 1; + await el.updateComplete; - el._inputNode.value = ''; + 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).to.not.have.attribute('aria-valuetext'); + 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 () => { From f9833a7f943d43d7a6fc679a889ef28193ec97cc Mon Sep 17 00:00:00 2001 From: gerjanvangeest Date: Wed, 4 Dec 2024 10:11:19 +0100 Subject: [PATCH 3/6] chore: remove reflect of valueText --- packages/ui/components/input-stepper/src/LionInputStepper.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/components/input-stepper/src/LionInputStepper.js b/packages/ui/components/input-stepper/src/LionInputStepper.js index 381882573..064f7c424 100644 --- a/packages/ui/components/input-stepper/src/LionInputStepper.js +++ b/packages/ui/components/input-stepper/src/LionInputStepper.js @@ -38,7 +38,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { }, valueText: { type: Object, - reflect: true, attribute: 'value-text', }, step: { From 4046dc74fd2c30c76497a2ac4237aeb7562ea371 Mon Sep 17 00:00:00 2001 From: gerjanvangeest Date: Wed, 4 Dec 2024 10:22:48 +0100 Subject: [PATCH 4/6] Update packages/ui/components/input-stepper/src/LionInputStepper.js Co-authored-by: Oleksii Kadurin --- packages/ui/components/input-stepper/src/LionInputStepper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/components/input-stepper/src/LionInputStepper.js b/packages/ui/components/input-stepper/src/LionInputStepper.js index 064f7c424..95b818def 100644 --- a/packages/ui/components/input-stepper/src/LionInputStepper.js +++ b/packages/ui/components/input-stepper/src/LionInputStepper.js @@ -72,7 +72,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { this.max = Infinity; /** * The aria-valuetext attribute defines the human-readable text alternative of aria-valuenow. - * @type {[ Number: String] | []} + * @type {{[key: number]: string}} */ this.valueText = []; this.step = 1; From 71f1d7c79472d0374d51624fd8e78c927b247be0 Mon Sep 17 00:00:00 2001 From: gerjanvangeest Date: Wed, 4 Dec 2024 10:22:55 +0100 Subject: [PATCH 5/6] Update packages/ui/components/input-stepper/src/LionInputStepper.js Co-authored-by: Oleksii Kadurin --- packages/ui/components/input-stepper/src/LionInputStepper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/components/input-stepper/src/LionInputStepper.js b/packages/ui/components/input-stepper/src/LionInputStepper.js index 95b818def..fbc9e34b5 100644 --- a/packages/ui/components/input-stepper/src/LionInputStepper.js +++ b/packages/ui/components/input-stepper/src/LionInputStepper.js @@ -74,7 +74,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { * The aria-valuetext attribute defines the human-readable text alternative of aria-valuenow. * @type {{[key: number]: string}} */ - this.valueText = []; + this.valueText = {}; this.step = 1; this.values = { max: this.max, From 0f9cff93bc08d57c19cb2382b8fc67166797cb52 Mon Sep 17 00:00:00 2001 From: gerjanvangeest Date: Wed, 4 Dec 2024 11:41:21 +0100 Subject: [PATCH 6/6] chore: rename valueText to valueTextMapping --- docs/components/input-stepper/use-cases.md | 6 +++--- .../input-stepper/src/LionInputStepper.js | 13 +++++++------ .../input-stepper/test/lion-input-stepper.test.js | 5 ++++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/components/input-stepper/use-cases.md b/docs/components/input-stepper/use-cases.md index 25b558852..ff5a9ed32 100644 --- a/docs/components/input-stepper/use-cases.md +++ b/docs/components/input-stepper/use-cases.md @@ -51,10 +51,10 @@ Use `min` and `max` attribute to specify a range. ### Value text -Use the `.valueText` property to override the value with a text. +Use the `.valueTextMapping` property to override the value with a text. ```js preview-story -export const valueText = () => { +export const valueTextMapping = () => { const values = { 1: 'first', 2: 'second', @@ -73,7 +73,7 @@ export const valueText = () => { min="1" max="10" name="value" - .valueText="${values}" + .valueTextMapping="${values}" > `; }; diff --git a/packages/ui/components/input-stepper/src/LionInputStepper.js b/packages/ui/components/input-stepper/src/LionInputStepper.js index fbc9e34b5..13b25361b 100644 --- a/packages/ui/components/input-stepper/src/LionInputStepper.js +++ b/packages/ui/components/input-stepper/src/LionInputStepper.js @@ -36,7 +36,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { type: Number, reflect: true, }, - valueText: { + valueTextMapping: { type: Object, attribute: 'value-text', }, @@ -74,7 +74,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { * The aria-valuetext attribute defines the human-readable text alternative of aria-valuenow. * @type {{[key: number]: string}} */ - this.valueText = {}; + this.valueTextMapping = {}; this.step = 1; this.values = { max: this.max, @@ -138,7 +138,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { this.__toggleSpinnerButtonsState(); } - if (changedProperties.has('valueText')) { + if (changedProperties.has('valueTextMapping')) { this._updateAriaAttributes(); } @@ -240,13 +240,14 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { if (displayValue) { this.setAttribute('aria-valuenow', `${displayValue}`); if ( - Object.keys(this.valueText).length !== 0 && - Object.keys(this.valueText).find(key => Number(key) === this.currentValue) + Object.keys(this.valueTextMapping).length !== 0 && + Object.keys(this.valueTextMapping).find(key => Number(key) === this.currentValue) ) { - this.setAttribute('aria-valuetext', `${this.valueText[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 { 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 86c902449..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 @@ -292,6 +292,9 @@ describe('', () => { }); 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; @@ -311,7 +314,7 @@ describe('', () => { 3: 'third', }; const el = await fixture(html` - + `); el.modelValue = 1; await el.updateComplete;