feat: add input stepper component
This commit is contained in:
parent
526aee2006
commit
ba72b32b00
11 changed files with 527 additions and 18 deletions
5
.changeset/stale-moons-wait.md
Normal file
5
.changeset/stale-moons-wait.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/input-stepper': minor
|
||||
---
|
||||
|
||||
Release initial version of an input stepper
|
||||
|
|
@ -54,6 +54,7 @@ The accessibility column indicates whether the functionality is accessible in it
|
|||
| [input-email](https://lion-web-components.netlify.app/?path=/docs/forms-input-email--main) | [](https://www.npmjs.com/package/@lion/input-email) | Input element for e-mails | ✔️ |
|
||||
| [input-iban](https://lion-web-components.netlify.app/?path=/docs/forms-input-iban--main) | [](https://www.npmjs.com/package/@lion/input-iban) | Input element for IBANs | ✔️ |
|
||||
| [input-range](https://lion-web-components.netlify.app/?path=/docs/forms-input-range--main) | [](https://www.npmjs.com/package/@lion/input-range) | Input element for a range of values | ✔️ |
|
||||
| [input-stepper](https://lion-web-components.netlify.app/?path=/docs/forms-input-stepper--main) | [](https://www.npmjs.com/package/@lion/input-stepper) | Input stepper element for the predefined range | ✔️ |
|
||||
| [radio-group](https://lion-web-components.netlify.app/?path=/docs/forms-radio-group--main) | [](https://www.npmjs.com/package/@lion/radio-group) | Group of radios | ✔️ |
|
||||
| [select](https://lion-web-components.netlify.app/?path=/docs/forms-select--main) | [](https://www.npmjs.com/package/@lion/select) | Simple native dropdown element | ✔️ |
|
||||
| [select-rich](https://lion-web-components.netlify.app/?path=/docs/forms-select-rich--main) | [](https://www.npmjs.com/package/@lion/select-rich) | 'rich' version of the native dropdown element | [#243][i243] |
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ For a more in depth description look into the [Form System Overview](?path=/docs
|
|||
## Packages
|
||||
|
||||
| Package | Version | Description |
|
||||
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
||||
| [checkbox-group](?path=/docs/forms-checkbox-group--main) | [](https://www.npmjs.com/package/@lion/checkbox-group) | Group of checkboxes |
|
||||
| [form-core](?path=/docs/forms-system-overview--page) | [](https://www.npmjs.com/package/@lion/form-core) | Core functionality for all controls |
|
||||
| [form-integrations](?path=/docs/forms-features-overview--main) | [](https://www.npmjs.com/package/@lion/form-integrations) | Shows form elements in an integrated way |
|
||||
|
|
@ -34,6 +34,7 @@ For a more in depth description look into the [Form System Overview](?path=/docs
|
|||
| [input-email](?path=/docs/forms-input-email--main) | [](https://www.npmjs.com/package/@lion/input-email) | Input element for e-mails |
|
||||
| [input-iban](?path=/docs/forms-input-iban--main) | [](https://www.npmjs.com/package/@lion/input-iban) | Input element for IBANs |
|
||||
| [input-range](?path=/docs/forms-input-range--main) | [](https://www.npmjs.com/package/@lion/input-range) | Input element for a range of values |
|
||||
| [input-stepper](?path=/docs/forms-input-stepper--main) | [](https://www.npmjs.com/package/@lion/input-stepper) | Input stepper element for the predefined range |
|
||||
| [radio-group](?path=/docs/forms-radio-group--main) | [](https://www.npmjs.com/package/@lion/radio-group) | Group of radios |
|
||||
| [select](?path=/docs/forms-select--main) | [](https://www.npmjs.com/package/@lion/select) | Simple native dropdown element |
|
||||
| [select-rich](?path=/docs/forms-select-rich--main) | [](https://www.npmjs.com/package/@lion/select-rich) | 'rich' version of the native dropdown element |
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import '@lion/input-datepicker/lion-input-datepicker.js';
|
|||
import '@lion/input-email/lion-input-email.js';
|
||||
import '@lion/input-iban/lion-input-iban.js';
|
||||
import '@lion/input-range/lion-input-range.js';
|
||||
import '@lion/input-stepper/lion-input-stepper.js';
|
||||
import '@lion/input/lion-input.js';
|
||||
import '@lion/radio-group/lion-radio-group.js';
|
||||
import '@lion/radio-group/lion-radio.js';
|
||||
|
|
@ -123,6 +124,12 @@ export const main = () => {
|
|||
>
|
||||
<lion-checkbox label="I blindly accept all terms and conditions"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<lion-input-stepper max="5" min="0" name="rsvp">
|
||||
<label slot="label">RSVP</label>
|
||||
<div slot="help-text">
|
||||
Max. 5 guests
|
||||
</div>
|
||||
</lion-input-stepper>
|
||||
<lion-textarea name="comments" label="Comments"></lion-textarea>
|
||||
<div class="buttons">
|
||||
<lion-button raised>Submit</lion-button>
|
||||
|
|
|
|||
1
packages/input-stepper/CHANGELOG.md
Normal file
1
packages/input-stepper/CHANGELOG.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Change Log
|
||||
136
packages/input-stepper/README.md
Normal file
136
packages/input-stepper/README.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Input Stepper
|
||||
|
||||
`lion-input-stepper` enables the user to increase and decrease a numeric value by predefined range. It is a combination of two buttons and a number input field with an optional slot `after` to suffix the extra information.
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import '@lion/form/lion-form.js';
|
||||
import './lion-input-stepper.js';
|
||||
|
||||
export default {
|
||||
title: 'Forms/Input Stepper',
|
||||
};
|
||||
```
|
||||
|
||||
```js preview-story
|
||||
export const main = () => html`
|
||||
<lion-input-stepper max="5" min="0" name="count">
|
||||
<label slot="label">RSVP</label>
|
||||
<div slot="help-text">Max. 5 guests</div>
|
||||
</lion-input-stepper>
|
||||
`;
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Based on [lion-input](?path=/docs/forms-input--main#input).
|
||||
- Set `min` and `max` value to define the range.
|
||||
- Set `step` value in integer or decimal to increase and decrease the value.
|
||||
- Use `ArrowUp` or `ArrowDown` to update the value.
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm i --save @lion/input-stepper
|
||||
```
|
||||
|
||||
```js
|
||||
import { LionInputStepper } from '@lion/input-stepper';
|
||||
// or
|
||||
import '@lion/input-stepper/lion-input-stepper.js';
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```html
|
||||
<lion-input-stepper max="5" min="0" name="count">
|
||||
<label slot="label">RSVP</label>
|
||||
<div slot="help-text">Max. 5 guests</div>
|
||||
</lion-input-stepper>
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
#### Default with no specification
|
||||
|
||||
When no range or step is defined, it can go infinite with default step value as `1`. You can also specify prefix content using `after` slot.
|
||||
|
||||
```js preview-story
|
||||
export const defaultMode = () => html`
|
||||
<label>How old is the existence?</label>
|
||||
<lion-input-stepper name="year">
|
||||
<div slot="after">In Billion Years</div>
|
||||
</lion-input-stepper>
|
||||
`;
|
||||
```
|
||||
|
||||
#### Step and Value
|
||||
|
||||
Use `step` attribute to specify the incrementor or decrementor difference and `value` to set the default value.
|
||||
|
||||
```js preview-story
|
||||
export const steps = () => html`
|
||||
<p><strong>Min:</strong> 100, <strong>Value:</strong> 200, <strong>Step:</strong> 100</p>
|
||||
<lion-input-stepper min="100" step="100" name="value" value="200"></lion-input-stepper>
|
||||
`;
|
||||
```
|
||||
|
||||
#### Range
|
||||
|
||||
Use `min` and `max` attribute to specify range.
|
||||
|
||||
```js preview-story
|
||||
export const range = () => html`
|
||||
<p><strong>Min:</strong> 200, <strong>Max:</strong> 500, <strong>Step:</strong> 100</p>
|
||||
<lion-input-stepper min="200" max="500" name="value" step="100" value="200"></lion-input-stepper>
|
||||
`;
|
||||
```
|
||||
|
||||
#### Validation
|
||||
|
||||
Only numbers are allowed in the field.
|
||||
|
||||
```js preview-story
|
||||
export const validation = () => html`
|
||||
<p>
|
||||
<strong>Min:</strong> 100, <strong>Max:</strong> 500, <strong>Step:</strong> 100,
|
||||
<strong>Value:</strong> Test
|
||||
</p>
|
||||
<lion-input-stepper min="100" max="500" name="value" step="100" value="Test"></lion-input-stepper>
|
||||
`;
|
||||
```
|
||||
|
||||
#### Form
|
||||
|
||||
```js preview-story
|
||||
export const usingForm = () => html`
|
||||
<lion-form
|
||||
@submit=${e => {
|
||||
console.log(e.target.serializedValue);
|
||||
const code = document.getElementById('code');
|
||||
code.style = 'background-color:#DEDEDE;padding:12px;';
|
||||
code.innerHTML = `<code>${JSON.stringify(e.target.serializedValue, null, 4)}</code>`;
|
||||
}}
|
||||
>
|
||||
<form>
|
||||
<lion-input name="fullname" label="Full name"></lion-input>
|
||||
<br />
|
||||
<lion-input-stepper min="0" max="5" name="count">
|
||||
<label slot="label">RSVP</label>
|
||||
<div slot="help-text">Max. 5 guests</div>
|
||||
</lion-input-stepper>
|
||||
<br />
|
||||
<lion-button raised>Submit</lion-button>
|
||||
<lion-button
|
||||
type="button"
|
||||
raised
|
||||
@click=${ev => ev.currentTarget.parentElement.parentElement.resetGroup()}
|
||||
>Reset</lion-button
|
||||
>
|
||||
</form>
|
||||
</lion-form>
|
||||
<pre id="code"></pre>
|
||||
`;
|
||||
```
|
||||
1
packages/input-stepper/index.js
Normal file
1
packages/input-stepper/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { LionInputStepper } from './src/LionInputStepper.js';
|
||||
3
packages/input-stepper/lion-input-stepper.js
Normal file
3
packages/input-stepper/lion-input-stepper.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { LionInputStepper } from './src/LionInputStepper.js';
|
||||
|
||||
customElements.define('lion-input-stepper', LionInputStepper);
|
||||
48
packages/input-stepper/package.json
Normal file
48
packages/input-stepper/package.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "@lion/input-stepper",
|
||||
"version": "0.0.0",
|
||||
"description": "This component enables the user to increase and decrease a numeric value by predefined range.",
|
||||
"license": "MIT",
|
||||
"author": "ing-bank",
|
||||
"homepage": "https://github.com/ing-bank/lion/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ing-bank/lion.git",
|
||||
"directory": "packages/input-stepper"
|
||||
},
|
||||
"main": "index.js",
|
||||
"module": "index.js",
|
||||
"files": [
|
||||
"*.d.ts",
|
||||
"*.js",
|
||||
"docs",
|
||||
"src",
|
||||
"test",
|
||||
"test-helpers",
|
||||
"translations",
|
||||
"types"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "../../scripts/npm-prepublish.js",
|
||||
"start": "cd ../../ && yarn dev-server --open packages/input-stepper/README.md",
|
||||
"test": "cd ../../ && yarn test:browser --grep \"packages/input-stepper/test/**/*.test.js\"",
|
||||
"test:watch": "cd ../../ && yarn test:browser:watch --grep \"packages/input-stepper/test/**/*.test.js\""
|
||||
},
|
||||
"sideEffects": [
|
||||
"lion-input-stepper.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@lion/core": "0.12.0",
|
||||
"@lion/form-core": "0.6.1",
|
||||
"@lion/input": "0.9.2"
|
||||
},
|
||||
"keywords": [
|
||||
"input",
|
||||
"input-stepper",
|
||||
"lion",
|
||||
"web-components"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
238
packages/input-stepper/src/LionInputStepper.js
Normal file
238
packages/input-stepper/src/LionInputStepper.js
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
import { html, css } from '@lion/core';
|
||||
import { LionInput } from '@lion/input';
|
||||
import { IsNumber, MinNumber, MaxNumber } from '@lion/form-core';
|
||||
|
||||
/**
|
||||
* `LionInputStepper` is a class for custom input-stepper element (`<lion-input-stepper>` web component).
|
||||
*
|
||||
* @customElement lion-input-stepper
|
||||
* @extends LitElement
|
||||
*/
|
||||
export class LionInputStepper extends LionInput {
|
||||
static get styles() {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
.input-group__container > .input-group__input ::slotted(.form-control) {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
min: Number,
|
||||
max: Number,
|
||||
step: Number,
|
||||
modelValue: Number,
|
||||
__disableIncrementor: Boolean,
|
||||
__disableDecrementor: Boolean,
|
||||
};
|
||||
}
|
||||
|
||||
get currentValue() {
|
||||
return parseFloat(this.value || 0);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.parser = modelValue => parseFloat(modelValue);
|
||||
this.__disableIncrementor = false;
|
||||
this.__disableDecrementor = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
this.role = 'spinbutton';
|
||||
this.addEventListener('keydown', this.__keyDownHandler);
|
||||
this._inputNode.setAttribute('inputmode', 'decimal');
|
||||
this._inputNode.setAttribute('autocomplete', 'off');
|
||||
this.step = this.hasAttribute('step') ? this.step : 1;
|
||||
this.__setAriaLabelsAndValidator();
|
||||
this.__toggleSpinnerButtonsState();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
this.removeEventListener('keydown', this.__keyDownHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update native input values
|
||||
* @param {Object} changedProps - changed props
|
||||
*/
|
||||
updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has('min')) {
|
||||
this._inputNode.min = this.min;
|
||||
}
|
||||
|
||||
if (changedProps.has('max')) {
|
||||
this._inputNode.max = this.max;
|
||||
}
|
||||
|
||||
if (changedProps.has('step')) {
|
||||
this._inputNode.step = this.step;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set aria labels and apply validators
|
||||
* @private
|
||||
*/
|
||||
__setAriaLabelsAndValidator() {
|
||||
this.values = {
|
||||
max: parseFloat(this.max || Infinity),
|
||||
min: parseFloat(this.min || Infinity),
|
||||
step: parseFloat(this.step),
|
||||
};
|
||||
|
||||
const ariaAttributes = {
|
||||
'aria-valuemax': this.values.max,
|
||||
'aria-valuemin': this.values.min,
|
||||
};
|
||||
|
||||
let validators = Object.entries(ariaAttributes)
|
||||
.map(([key, val]) => {
|
||||
if (val !== Infinity) {
|
||||
this.setAttribute(key, val);
|
||||
return key === 'aria-valuemax' ? new MaxNumber(val) : new MinNumber(val);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(validator => validator);
|
||||
validators = [new IsNumber(), ...validators];
|
||||
this.defaultValidators.push(...validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update values on keyboard arrow up and down event
|
||||
* @param {KeyboardEvent} e - keyboard event
|
||||
* @private
|
||||
*/
|
||||
__keyDownHandler(e) {
|
||||
if (e.key === 'ArrowUp') {
|
||||
this.__increment();
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
this.__decrement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle disabled state for the buttons
|
||||
* @private
|
||||
*/
|
||||
__toggleSpinnerButtonsState() {
|
||||
const { min, max } = this.values;
|
||||
this.__disableIncrementor = this.currentValue >= max && max !== Infinity;
|
||||
this.__disableDecrementor = this.currentValue <= min && min !== Infinity;
|
||||
this.setAttribute('aria-valuenow', this.currentValue);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('user-input-changed', {
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the value based on given step or default step value is 1
|
||||
* @private
|
||||
*/
|
||||
__increment() {
|
||||
const { step, max } = this.values;
|
||||
const newValue = this.currentValue + step;
|
||||
if (newValue <= max || max === Infinity) {
|
||||
this.value = newValue;
|
||||
this.__toggleSpinnerButtonsState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the value based on given step or default step value is 1
|
||||
* @private
|
||||
*/
|
||||
__decrement() {
|
||||
const { step, min } = this.values;
|
||||
const newValue = this.currentValue - step;
|
||||
if (newValue >= min || min === Infinity) {
|
||||
this.value = newValue;
|
||||
this.__toggleSpinnerButtonsState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle +/- buttons on change
|
||||
* @override
|
||||
*/
|
||||
_onChange() {
|
||||
super._onChange();
|
||||
this.__toggleSpinnerButtonsState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override after template to none
|
||||
* @override
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_inputGroupAfterTemplate() {
|
||||
return html``;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override before template to none
|
||||
* @override
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_inputGroupBeforeTemplate() {
|
||||
return html``;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override prefix template for the increment button
|
||||
* @override
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_inputGroupPrefixTemplate() {
|
||||
return html`
|
||||
<button
|
||||
?disabled=${this.disabled || this.readOnly || this.__disableDecrementor}
|
||||
@click=${this.__decrement}
|
||||
tabindex="-1"
|
||||
name="decrement"
|
||||
type="button"
|
||||
aria-label="decrement"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override suffix template for the decrement button and add after slot
|
||||
* @override
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_inputGroupSuffixTemplate() {
|
||||
return html`
|
||||
<button
|
||||
?disabled=${this.disabled || this.readOnly || this.__disableIncrementor}
|
||||
@click=${this.__increment}
|
||||
tabindex="-1"
|
||||
name="increment"
|
||||
type="button"
|
||||
aria-label="increment"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<slot name="after"></slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
68
packages/input-stepper/test/lion-input-stepper.test.js
Normal file
68
packages/input-stepper/test/lion-input-stepper.test.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { expect, fixture, nextFrame, html } from '@open-wc/testing';
|
||||
import '../lion-input-stepper.js';
|
||||
|
||||
const defaultInputStepper = html`
|
||||
<lion-input-stepper name="year" label="Years"></lion-input-stepper>
|
||||
`;
|
||||
const inputStepperWithAttrs = html`<lion-input-stepper min="100" max="200"></lion-input-stepper>`;
|
||||
|
||||
describe('<lion-input-stepper>', () => {
|
||||
describe('Stepper', () => {
|
||||
it('has a type text', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
expect(el._inputNode.type).to.equal('text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('User interaction', () => {
|
||||
it('should increment the value to 1 on + button click', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
expect(el.value).to.equal('');
|
||||
const incrementButton = el.shadowRoot.querySelector('[name=increment]');
|
||||
incrementButton.dispatchEvent(new Event('click'));
|
||||
expect(el.value).to.equal('1');
|
||||
});
|
||||
|
||||
it('should decrement the value to -1 on - button click', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
expect(el.value).to.equal('');
|
||||
const incrementButton = el.shadowRoot.querySelector('[name=decrement]');
|
||||
incrementButton.dispatchEvent(new Event('click'));
|
||||
expect(el.value).to.equal('-1');
|
||||
});
|
||||
|
||||
it('should update min and max attributes when min and max property change', async () => {
|
||||
const el = await fixture(inputStepperWithAttrs);
|
||||
el.min = '100';
|
||||
el.max = '200';
|
||||
await nextFrame();
|
||||
expect(el._inputNode.min).to.equal(el.min);
|
||||
expect(el._inputNode.max).to.equal(el.max);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('is a11y AXE accessible', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('is accessible when disabled', async () => {
|
||||
const el = await fixture(`<lion-input-stepper label="rsvp" disabled></lion-input-stepper>`);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('adds aria-valuemax and aria-valuemin when min and max are provided', async () => {
|
||||
const el = await fixture(inputStepperWithAttrs);
|
||||
expect(el).to.have.attribute('aria-valuemax', '200');
|
||||
expect(el).to.have.attribute('aria-valuemin', '100');
|
||||
});
|
||||
|
||||
it('updates aria-valuenow when stepper is changed', async () => {
|
||||
const el = await fixture(inputStepperWithAttrs);
|
||||
const incrementButton = el.shadowRoot.querySelector('[name=increment]');
|
||||
incrementButton.dispatchEvent(new Event('click'));
|
||||
expect(el).to.have.attribute('aria-valuenow', '1');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue