feat(input-range): create input-range component

This commit is contained in:
erik 2019-12-04 15:34:47 +01:00 committed by Thomas Allmer
parent a0c9f0bd3e
commit d81e5ea547
9 changed files with 419 additions and 0 deletions

View file

@ -53,6 +53,7 @@ The accessibility column indicates whether the functionality is accessible in it
| [input-datepicker](./packages/input-datepicker) | [![input-datepicker](https://img.shields.io/npm/v/@lion/input-datepicker.svg)](https://www.npmjs.com/package/@lion/input-datepicker) | Input element for dates with a datepicker | ✔️ |
| [input-email](./packages/input-email) | [![input-email](https://img.shields.io/npm/v/@lion/input-email.svg)](https://www.npmjs.com/package/@lion/input-email) | Input element for e-mails | [#169][i169] |
| [input-iban](./packages/input-iban) | [![input-iban](https://img.shields.io/npm/v/@lion/input-iban.svg)](https://www.npmjs.com/package/@lion/input-iban) | Input element for IBANs | [#169][i169] |
| [input-range](./packages/input-range) | [![input-range](https://img.shields.io/npm/v/@lion/input-range.svg)](https://www.npmjs.com/package/@lion/input-range) | Input element for a range of values | ✔️ |
| [radio](./packages/radio) | [![radio](https://img.shields.io/npm/v/@lion/radio.svg)](https://www.npmjs.com/package/@lion/radio) | Radio from element | ✔️ |
| [radio-group](./packages/radio-group) | [![radio-group](https://img.shields.io/npm/v/@lion/radio-group.svg)](https://www.npmjs.com/package/@lion/radio-group) | Group of radios | ✔️ |
| [select](./packages/select) | [![select](https://img.shields.io/npm/v/@lion/select.svg)](https://www.npmjs.com/package/@lion/select) | Simple native dropdown element | ✔️ |

View file

@ -0,0 +1,32 @@
# Input range
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
`lion-input-range` component is based on the native range input.
Its purpose is to provide a way for users to select one value from a range of values.
## Features
- Based on [lion-input](../input).
- Shows `modelValue` and `unit` above the range input.
- Shows `min` and `max` value after the range input.
- Can hide the `min` and `max` value via `no-min-max-labels`.
- Makes use of [formatNumber](../localize/docs/number.md) for formatting and parsing.
## How to use
### Installation
```sh
npm i --save @lion/input-range
```
```js
import '@lion/input-range/lion-input-range.js';
```
### Example
```html
<lion-input-range min="200" max="500" .modelValue="${300}" label="Input range"></lion-input-range>
```

View file

@ -0,0 +1 @@
export { LionInputRange } from './src/LionInputRange.js';

View file

@ -0,0 +1,3 @@
import { LionInputRange } from './src/LionInputRange.js';
customElements.define('lion-input-range', LionInputRange);

View file

@ -0,0 +1,43 @@
{
"name": "@lion/input-range",
"version": "0.0.0",
"description": "Provide a way for users to select one value from a range of values",
"author": "ing-bank",
"homepage": "https://github.com/ing-bank/lion/",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/ing-bank/lion.git",
"directory": "packages/input-range"
},
"scripts": {
"prepublishOnly": "../../scripts/npm-prepublish.js"
},
"keywords": [
"lion",
"web-components",
"input-range",
"slider"
],
"main": "index.js",
"module": "index.js",
"files": [
"src",
"stories",
"test",
"*.js"
],
"dependencies": {
"@lion/core": "0.3.0",
"@lion/field": "0.6.8",
"@lion/input": "0.4.0",
"@lion/localize": "0.7.1"
},
"devDependencies": {
"@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4"
}
}

View file

@ -0,0 +1,129 @@
/* eslint-disable import/no-extraneous-dependencies */
import { LocalizeMixin, formatNumber } from '@lion/localize';
import { FieldCustomMixin } from '@lion/field';
import { LionInput } from '@lion/input';
import { html, css, unsafeCSS } from '@lion/core';
/**
* LionInputRange: extension of lion-input
*
* @customElement `lion-input-range`
* @extends LionInput
*/
export class LionInputRange extends FieldCustomMixin(LocalizeMixin(LionInput)) {
static get properties() {
return {
min: Number,
max: Number,
unit: String,
step: {
type: Number,
reflect: true,
},
noMinMaxLabels: {
type: Boolean,
attribute: 'no-min-max-labels',
},
};
}
static rangeStyles(scope) {
return css`
/* Custom input range styling comes here, be aware that this won't work for polyfilled browsers */
.${scope} .form-control {
width: 100%;
box-shadow: none;
outline: none;
}
`;
}
connectedCallback() {
if (super.connectedCallback) super.connectedCallback();
this.type = 'range';
/* eslint-disable-next-line wc/no-self-class */
this.classList.add(this.scopedClass);
this.__setupStyleTag();
}
disconnectedCallback() {
if (super.disconnectedCallback) super.disconnectedCallback();
this.__teardownStyleTag();
}
constructor() {
super();
this.parser = modelValue => parseFloat(modelValue);
this.scopedClass = `${this.localName}-${Math.floor(Math.random() * 10000)}`;
}
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('min')) {
this._inputNode.min = this.min;
}
if (changedProperties.has('max')) {
this._inputNode.max = this.max;
}
if (changedProperties.has('step')) {
this._inputNode.step = this.step;
}
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
if (changedProperties.has('modelValue')) {
// TODO: find out why this hack is needed to display the initial modelValue
this.updateComplete.then(() => {
this._inputNode.value = this.modelValue;
});
}
}
inputGroupTemplate() {
return html`
<div>
<span class="input-range__value">${formatNumber(this.formattedValue)}</span>
<span class="input-range__unit">${this.unit}</span>
</div>
<div class="input-group">
${this.inputGroupBeforeTemplate()}
<div class="input-group__container">
${this.inputGroupPrefixTemplate()} ${this.inputGroupInputTemplate()}
${this.inputGroupSuffixTemplate()}
</div>
${this.inputGroupAfterTemplate()}
</div>
`;
}
inputGroupInputTemplate() {
return html`
<div class="input-group__input">
<slot name="input"></slot>
${!this.noMinMaxLabels
? html`
<div class="input-range__limits">
<span>${formatNumber(this.min)}</span>
<span>${formatNumber(this.max)}</span>
</div>
`
: ''}
</div>
`;
}
__setupStyleTag() {
this.__styleTag = document.createElement('style');
this.__styleTag.innerHTML = this.constructor.rangeStyles(unsafeCSS(this.scopedClass));
this.insertBefore(this.__styleTag, this.childNodes[0]);
}
__teardownStyleTag() {
this.removeChild(this.__styleTag);
}
}

View file

@ -0,0 +1,111 @@
import { storiesOf, html } from '@open-wc/demoing-storybook';
import { css } from '@lion/core';
import '../lion-input-range.js';
const rangeDemoStyle = css`
.demo-range {
max-width: 400px;
}
`;
storiesOf('Forms | Input Range', module)
.add(
'Default',
() => html`
<style>
${rangeDemoStyle}
</style>
<lion-input-range
class="demo-range"
min="200"
max="500"
label="Input range"
></lion-input-range>
`,
)
.add(
'Help text',
() => html`
<style>
${rangeDemoStyle}
</style>
<lion-input-range
class="demo-range"
min="200"
max="500"
.modelValue="${300}"
label="Input range"
help-text="A help text can show additional hints"
></lion-input-range>
`,
)
.add(
'Units',
() => html`
<style>
${rangeDemoStyle}
</style>
<lion-input-range
class="demo-range"
min="0"
max="100"
.modelValue="${50}"
unit="%"
label="Percentage"
></lion-input-range>
`,
)
.add(
'With steps',
() => html`
<style>
${rangeDemoStyle}
</style>
<lion-input-range
class="demo-range"
min="200"
max="500"
step="50"
.modelValue="${300}"
label="Input range"
help-text="This slider uses increments of 50"
></lion-input-range>
`,
)
.add(
'No min max labels',
() => html`
<style>
${rangeDemoStyle}
</style>
<lion-input-range
class="demo-range"
label="Input range"
min="0"
max="100"
no-min-max-labels
></lion-input-range>
`,
)
.add(
'Disabled',
() => html`
<style>
${rangeDemoStyle}
</style>
<lion-input-range
class="demo-range"
min="200"
max="500"
.modelValue="${300}"
disabled
label="Input range"
></lion-input-range>
`,
);

View file

@ -0,0 +1,98 @@
import { expect, fixture, nextFrame, html } from '@open-wc/testing';
import '../lion-input-range.js';
describe('<lion-input-range>', () => {
it('has a type = range', async () => {
const el = await fixture(`<lion-input-range></lion-input-range>`);
expect(el._inputNode.type).to.equal('range');
});
it('contain the scoped css class for the slotted input style', async () => {
const el = await fixture(`
<lion-input-range></lion-input-range>
`);
expect(el.classList.contains(el.scopedClass)).to.equal(true);
});
it('adds a style tag as the first child which contains a class selector to the element', async () => {
const el = await fixture(`
<lion-input-range></lion-input-range>
`);
expect(el.children[0].tagName).to.equal('STYLE');
expect(el.children[0].innerHTML).to.contain(el.scopedClass);
});
it('does cleanup of the style tag when moving or deleting the el', async () => {
const wrapper = await fixture(`
<div></div>
`);
const wrapper2 = await fixture(`
<div></div>
`);
const el = document.createElement('lion-input-range');
wrapper.appendChild(el);
wrapper2.appendChild(el);
expect(el.children[1].tagName).to.not.equal('STYLE');
});
it('displays the modelValue and unit', async () => {
const el = await fixture(html`
<lion-input-range .modelValue=${75} unit="${`%`}"></lion-input-range>
`);
expect(el.shadowRoot.querySelector('.input-range__value').innerText).to.equal('75');
expect(el.shadowRoot.querySelector('.input-range__unit').innerText).to.equal('%');
});
it('displays 2 tick labels (min and max values) by default', async () => {
const el = await fixture(`<lion-input-range min="100" max="200"></lion-input-range>`);
expect(el.shadowRoot.querySelectorAll('.input-range__limits span').length).to.equal(2);
expect(el.shadowRoot.querySelectorAll('.input-range__limits span')[0].innerText).to.equal(
el.min,
);
expect(el.shadowRoot.querySelectorAll('.input-range__limits span')[1].innerText).to.equal(
el.max,
);
});
it('update min and max attributes when min and max property change', async () => {
const el = await fixture(`<lion-input-range min="100" max="200"></lion-input-range>`);
el.min = '120';
el.max = '220';
await nextFrame();
expect(el._inputNode.min).to.equal(el.min);
expect(el._inputNode.max).to.equal(el.max);
});
it('can hide the tick labels', async () => {
const el = await fixture(
`<lion-input-range min="100" max="200" no-min-max-labels></lion-input-range>`,
);
expect(el.shadowRoot.querySelectorAll('.input-group__input')[0]).dom.to.equal(`
<div class="input-group__input">
<slot name="input"></slot>
</div>
`);
});
it('parser method should return a value parsed into a number format', async () => {
const el = await fixture(html`
<lion-input-range min="100" max="200" .modelValue=${150}></lion-input-range>
`);
expect(el.modelValue).to.equal(150);
el._inputNode.value = '130';
el._inputNode.dispatchEvent(new Event('input'));
expect(el.modelValue).to.equal(130);
});
it('is accessible', async () => {
const el = await fixture(`<lion-input-range label="range"></lion-input-range>`);
await expect(el).to.be.accessible();
});
it('is accessible when disabled', async () => {
const el = await fixture(`<lion-input-range label="range" disabled></lion-input-range>`);
await expect(el).to.be.accessible();
});
});

View file

@ -8,6 +8,7 @@ import '../packages/input-date/stories/index.stories.js';
import '../packages/input-datepicker/stories/index.stories.js';
import '../packages/input-email/stories/index.stories.js';
import '../packages/input-iban/stories/index.stories.js';
import '../packages/input-range/stories/index.stories.js';
import '../packages/option/stories/index.stories.js';
import '../packages/select/stories/index.stories.js';
import '../packages/fieldset/stories/index.stories.js';