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 1 commit
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
35 changes: 35 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,41 @@ 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 = [
Copy link
Member

Choose a reason for hiding this comment

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

Since an input step will always have a set of discrete steps and labels (value-texts) belonging to it, maybe a better api would be an index based array corresponding with the amount of steps

Otherwise you would have to do this always (there's no use case for providing just one valueText

'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`
<lion-input-stepper
label="Quality of oranges"
min="1"
max="10"
name="value"
@model-value-changed="${ev => onModelValueChanged(ev)}"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
@model-value-changed="${ev => onModelValueChanged(ev)}"
@model-value-changed="${onModelValueChanged}"

></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
46 changes: 43 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: Number,
okadurin marked this conversation as resolved.
Show resolved Hide resolved
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,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
this.formatter = formatNumber;
this.min = Infinity;
this.max = Infinity;
/** @type {string | undefined} */
Copy link
Member

Choose a reason for hiding this comment

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

Maybe some description, so that this will be picked up in our api tables?
https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaValueText

this.valueText = undefined;
this.step = 1;
this.values = {
max: this.max,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
Expand All @@ -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
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 @@ -283,6 +283,43 @@ describe('<lion-input-stepper>', () => {
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');
});
});
});
Loading