Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sweet-cows-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lion/ui': minor
---

[input-stepper] add aria-valuemin, aria-valuemax and an option to set aria-valuetext
30 changes: 30 additions & 0 deletions docs/components/input-stepper/use-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,36 @@ Use `min` and `max` attribute to specify a range.
></lion-input-stepper>
```

### Value text

Use the `.valueText` property to override the value with a text.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, can we rename it to .valueTextMapping ?


```js preview-story
export const valueText = () => {
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"
.valueText="${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.
Expand Down
54 changes: 51 additions & 3 deletions packages/ui/components/input-stepper/src/LionInputStepper.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
type: Number,
reflect: true,
},
valueText: {
type: Object,
reflect: true,
gerjanvangeest marked this conversation as resolved.
Show resolved Hide resolved
attribute: 'value-text',
},
step: {
type: Number,
reflect: true,
Expand Down Expand Up @@ -66,6 +71,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 {[ Number: String] | []}
gerjanvangeest marked this conversation as resolved.
Show resolved Hide resolved
*/
this.valueText = [];
gerjanvangeest marked this conversation as resolved.
Show resolved Hide resolved
this.step = 1;
this.values = {
max: this.max,
Expand Down Expand Up @@ -110,15 +120,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('valueText')) {
this._updateAriaAttributes();
}

if (changedProperties.has('step')) {
this._inputNode.step = `${this.step}`;
this.values.step = this.step;
Expand Down Expand Up @@ -193,8 +217,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 (
Expand All @@ -205,7 +229,31 @@ 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.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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a source explaining this behaviour + workaround?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When used with progressbar and scrollbar, assistive technologies announce the value to users as a percent. When aria-valuemin and aria-valuemax are both defined, the percent value is calculated as a position on the range. Otherwise, the actual value is announced as a percent.

When the value to be announced, either the actual value or the value as a percent, may not be clear to users, the aria-valuetext attribute should be used to provide a user-friendly representation of the value.

https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow

At least for VoiceOver the same goes up for spinbutton.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Maybe add the link in the test as well?

this.setAttribute('aria-valuetext', `${displayValue}`);
}
} else {
this.removeAttribute('aria-valuenow');
this.removeAttribute('aria-valuetext');
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,65 @@ 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 () => {
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" .valueText="${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-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');
});
});
});
Loading