feat(switch): component to toggle a property on or off
This commit is contained in:
parent
8223b4ad3d
commit
8e4360220b
12 changed files with 523 additions and 1 deletions
29
packages/switch/README.md
Normal file
29
packages/switch/README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Switch
|
||||
|
||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
`lion-input-switch` is a component that is used to toggle a property or feature on or off.
|
||||
|
||||
## Features
|
||||
|
||||
- Get or set the checked state (boolean) - `choiceChecked()`
|
||||
- Get or set the value of the choice - `choiceValue()`
|
||||
- Pre-select an option by setting the `checked` boolean attribute
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
npm i --save @lion/switch
|
||||
```
|
||||
|
||||
```js
|
||||
import '@lion/swith/lion-input-switch.js';
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```html
|
||||
<lion-input-switch name="airplaneMode" label="Airplane mode" checked></lion-input-switch>
|
||||
```
|
||||
2
packages/switch/index.js
Normal file
2
packages/switch/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { LionInputSwitch } from './src/LionInputSwitch.js';
|
||||
export { LionButtonSwitch } from './src/LionButtonSwitch.js';
|
||||
3
packages/switch/lion-button-switch.js
Normal file
3
packages/switch/lion-button-switch.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { LionButtonSwitch } from './src/LionButtonSwitch.js';
|
||||
|
||||
customElements.define('lion-button-switch', LionButtonSwitch);
|
||||
3
packages/switch/lion-input-switch.js
Normal file
3
packages/switch/lion-input-switch.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { LionInputSwitch } from './src/LionInputSwitch.js';
|
||||
|
||||
customElements.define('lion-input-switch', LionInputSwitch);
|
||||
48
packages/switch/package.json
Normal file
48
packages/switch/package.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "@lion/switch",
|
||||
"version": "0.0.1",
|
||||
"description": "A Switch is used for switching a property or feature on and off",
|
||||
"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/switch"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "../../scripts/npm-prepublish.js"
|
||||
},
|
||||
"keywords": [
|
||||
"lion",
|
||||
"web-components",
|
||||
"switch"
|
||||
],
|
||||
"main": "index.js",
|
||||
"module": "index.js",
|
||||
"files": [
|
||||
"docs",
|
||||
"src",
|
||||
"stories",
|
||||
"test",
|
||||
"translations",
|
||||
"*.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@lion/button": "^0.3.29",
|
||||
"@lion/choice-input": "^0.2.39",
|
||||
"@lion/core": "^0.2.1",
|
||||
"@lion/field": "^0.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lion/form": "^0.1.68",
|
||||
"@lion/localize": "^0.4.20",
|
||||
"@lion/validate": "^0.2.37",
|
||||
"@open-wc/demoing-storybook": "^0.2.0",
|
||||
"@open-wc/testing": "^2.3.4",
|
||||
"sinon": "^7.2.2"
|
||||
}
|
||||
}
|
||||
105
packages/switch/src/LionButtonSwitch.js
Normal file
105
packages/switch/src/LionButtonSwitch.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { LionButton } from '@lion/button';
|
||||
import { html, css } from '@lion/core';
|
||||
|
||||
export class LionButtonSwitch extends LionButton {
|
||||
static get properties() {
|
||||
return {
|
||||
checked: {
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 16px;
|
||||
/* Override "button" styles */
|
||||
min-height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 100%;
|
||||
/* Override "button" styles */
|
||||
min-height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.button-switch__track {
|
||||
background: #eee;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.button-switch__thumb {
|
||||
background: #ccc;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
:host([checked]) .button-switch__thumb {
|
||||
right: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_renderBefore() {
|
||||
return html`
|
||||
<div class="button-switch__track"></div>
|
||||
<div class="button-switch__thumb"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.role = 'switch';
|
||||
this.checked = false;
|
||||
this.addEventListener('click', this.__handleToggleStateChange);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute('aria-checked', `${this.checked}`);
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.removeAttribute('type');
|
||||
}
|
||||
|
||||
__handleToggleStateChange() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this.checked = !this.checked;
|
||||
this.dispatchEvent(
|
||||
new Event('checked-changed', {
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* We synchronously update aria-checked to support voice over on safari.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_requestUpdate(name, oldValue) {
|
||||
super._requestUpdate(name, oldValue);
|
||||
if (this.isConnected && name === 'checked') {
|
||||
this.setAttribute('aria-checked', `${this.checked}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
packages/switch/src/LionInputSwitch.js
Normal file
61
packages/switch/src/LionInputSwitch.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { html, css } from '@lion/core';
|
||||
import { LionField } from '@lion/field';
|
||||
import { ChoiceInputMixin } from '@lion/choice-input';
|
||||
|
||||
import '../lion-button-switch.js';
|
||||
|
||||
export class LionInputSwitch extends ChoiceInputMixin(LionField) {
|
||||
static get styles() {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host([disabled]) {
|
||||
color: #adadad;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
input: () => document.createElement('lion-button-switch'),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="input-switch__container">
|
||||
<slot name="label"></slot>
|
||||
<slot name="help-text"></slot>
|
||||
<slot name="feedback"></slot>
|
||||
</div>
|
||||
<slot name="input"></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.inputElement.addEventListener(
|
||||
'checked-changed',
|
||||
this.__handleButtonSwitchCheckedChanged.bind(this),
|
||||
);
|
||||
this._syncButtonSwitch();
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
this._syncButtonSwitch();
|
||||
}
|
||||
|
||||
__handleButtonSwitchCheckedChanged() {
|
||||
// TODO: should be replaced by "_inputNode" after the next breaking change
|
||||
// https://github.com/ing-bank/lion/blob/master/packages/field/src/FormControlMixin.js#L78
|
||||
this.checked = this.inputElement.checked;
|
||||
}
|
||||
|
||||
_syncButtonSwitch() {
|
||||
this.inputElement.checked = this.checked;
|
||||
this.inputElement.disabled = this.disabled;
|
||||
}
|
||||
}
|
||||
86
packages/switch/stories/index.stories.js
Normal file
86
packages/switch/stories/index.stories.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
||||
import { LitElement } from '@lion/core';
|
||||
|
||||
import { LocalizeMixin } from '@lion/localize';
|
||||
|
||||
import '../lion-input-switch.js';
|
||||
import '@lion/form/lion-form.js';
|
||||
|
||||
storiesOf('Forms|Switch', module)
|
||||
.add(
|
||||
'All text slots',
|
||||
() => html`
|
||||
<lion-input-switch label="Label" help-text="Help text"> </lion-input-switch>
|
||||
`,
|
||||
)
|
||||
.add(
|
||||
'Disabled',
|
||||
() => html`
|
||||
<lion-input-switch label="Disabled label" disabled> </lion-input-switch>
|
||||
`,
|
||||
)
|
||||
.add('Validation', () => {
|
||||
const isTrue = value => value && value.checked && value.checked === true;
|
||||
const isTrueValidator = (...factoryParams) => [
|
||||
(...params) => ({
|
||||
isTrue: isTrue(...params),
|
||||
}),
|
||||
...factoryParams,
|
||||
];
|
||||
const tagName = 'lion-switch-validation-demo';
|
||||
if (!customElements.get(tagName)) {
|
||||
customElements.define(
|
||||
tagName,
|
||||
class extends LocalizeMixin(LitElement) {
|
||||
static get localizeNamespaces() {
|
||||
const result = [
|
||||
{
|
||||
'lion-validate+isTrue': () =>
|
||||
Promise.resolve({
|
||||
info: {
|
||||
isTrue: 'You will not get the latest news!',
|
||||
},
|
||||
}),
|
||||
},
|
||||
...super.localizeNamespaces,
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<lion-form id="postsForm" @submit="${this.submit}">
|
||||
<form>
|
||||
<lion-input-switch name="emailAddress" label="Share email address">
|
||||
</lion-input-switch>
|
||||
<lion-input-switch name="subjectField" label="Show subject field" checked>
|
||||
</lion-input-switch>
|
||||
<lion-input-switch name="characterCount" label="Character count">
|
||||
</lion-input-switch>
|
||||
<lion-input-switch
|
||||
name="newsletterCheck"
|
||||
label="* Subscribe to newsletter"
|
||||
.infoValidators="${[isTrueValidator()]}"
|
||||
>
|
||||
</lion-input-switch>
|
||||
<button type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</lion-form>
|
||||
`;
|
||||
}
|
||||
|
||||
submit() {
|
||||
const form = this.shadowRoot.querySelector('#postsForm');
|
||||
if (form.errorState === false) {
|
||||
console.log(form.serializeGroup());
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
return html`
|
||||
<lion-switch-validation-demo></lion-switch-validation-demo>
|
||||
`;
|
||||
});
|
||||
114
packages/switch/test/lion-button-switch.test.js
Normal file
114
packages/switch/test/lion-button-switch.test.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { LionButton } from '@lion/button';
|
||||
|
||||
import '../lion-button-switch.js';
|
||||
|
||||
describe('lion-button-switch', () => {
|
||||
let el;
|
||||
beforeEach(async () => {
|
||||
el = await fixture(html`
|
||||
<lion-button-switch></lion-button-switch>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should behave like a button', () => {
|
||||
expect(el instanceof LionButton).to.be.true;
|
||||
});
|
||||
|
||||
it('should be focusable', () => {
|
||||
expect(el.tabIndex).to.equal(0);
|
||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||
});
|
||||
|
||||
it('should not have a [type]', () => {
|
||||
expect(el.hasAttribute('type')).to.be.false;
|
||||
});
|
||||
|
||||
it('should have checked=false by default', () => {
|
||||
expect(el.checked).to.equal(false);
|
||||
expect(el.hasAttribute('checked')).to.be.false;
|
||||
});
|
||||
|
||||
it('should toggle the value of "checked" on click', async () => {
|
||||
expect(el.checked).to.be.false;
|
||||
expect(el.hasAttribute('checked')).to.be.false;
|
||||
el.click();
|
||||
await el.updateComplete;
|
||||
expect(el.checked).to.be.true;
|
||||
expect(el.hasAttribute('checked')).to.be.true;
|
||||
el.click();
|
||||
await el.updateComplete;
|
||||
expect(el.checked).to.be.false;
|
||||
expect(el.hasAttribute('checked')).to.be.false;
|
||||
});
|
||||
|
||||
it('can be disabled', async () => {
|
||||
el.disabled = true;
|
||||
expect(el.checked).to.be.false;
|
||||
el.click();
|
||||
await el.updateComplete;
|
||||
expect(el.checked).to.be.false;
|
||||
expect(el.hasAttribute('checked')).to.be.false;
|
||||
el.disabled = true;
|
||||
el.checked = true;
|
||||
el.click();
|
||||
await el.updateComplete;
|
||||
expect(el.checked).to.be.true;
|
||||
expect(el.hasAttribute('checked')).to.be.true;
|
||||
});
|
||||
|
||||
it('should dispatch "checked-changed" event when toggled', () => {
|
||||
const handlerSpy = sinon.spy();
|
||||
el.addEventListener('checked-changed', handlerSpy);
|
||||
el.click();
|
||||
el.click();
|
||||
expect(handlerSpy.callCount).to.equal(2);
|
||||
const checkCall = call => {
|
||||
expect(call.args).to.have.a.lengthOf(1);
|
||||
const e = call.args[0];
|
||||
expect(e).to.be.an.instanceof(Event);
|
||||
expect(e.bubbles).to.be.true;
|
||||
expect(e.composed).to.be.true;
|
||||
};
|
||||
checkCall(handlerSpy.getCall(0), true);
|
||||
checkCall(handlerSpy.getCall(1), false);
|
||||
});
|
||||
|
||||
it('should not dispatch "checked-changed" event if disabled', () => {
|
||||
const handlerSpy = sinon.spy();
|
||||
el.disabled = true;
|
||||
el.addEventListener('checked-changed', handlerSpy);
|
||||
el.click();
|
||||
expect(handlerSpy.called).to.be.false;
|
||||
});
|
||||
|
||||
describe('a11y', () => {
|
||||
it('should manage "aria-checked"', async () => {
|
||||
expect(el.hasAttribute('aria-checked')).to.be.true;
|
||||
expect(el.getAttribute('aria-checked')).to.equal('false');
|
||||
|
||||
el.click();
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('aria-checked')).to.equal('true');
|
||||
el.click();
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('aria-checked')).to.equal('false');
|
||||
|
||||
el.checked = true;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('aria-checked')).to.equal('true');
|
||||
el.checked = false;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('aria-checked')).to.equal('false');
|
||||
|
||||
el.setAttribute('checked', true);
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('aria-checked')).to.equal('true');
|
||||
el.removeAttribute('checked');
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('aria-checked')).to.equal('false');
|
||||
});
|
||||
});
|
||||
});
|
||||
70
packages/switch/test/lion-input-switch.test.js
Normal file
70
packages/switch/test/lion-input-switch.test.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../lion-input-switch.js';
|
||||
|
||||
describe('lion-input-switch', () => {
|
||||
it('should have default "input" element', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-input-switch></lion-input-switch>
|
||||
`);
|
||||
expect(el.querySelector('[slot="input"]')).not.to.be.null;
|
||||
});
|
||||
|
||||
it('should sync its "disabled" state to child button', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-input-switch disabled></lion-input-switch>
|
||||
`);
|
||||
expect(el.inputElement.disabled).to.be.true;
|
||||
expect(el.inputElement.hasAttribute('disabled')).to.be.true;
|
||||
el.disabled = false;
|
||||
await el.updateComplete;
|
||||
expect(el.inputElement.disabled).to.be.false;
|
||||
expect(el.inputElement.hasAttribute('disabled')).to.be.false;
|
||||
});
|
||||
|
||||
it('should sync its "checked" state to child button', async () => {
|
||||
const uncheckedEl = await fixture(html`
|
||||
<lion-input-switch></lion-input-switch>
|
||||
`);
|
||||
const checkedEl = await fixture(html`
|
||||
<lion-input-switch checked></lion-input-switch>
|
||||
`);
|
||||
expect(uncheckedEl.inputElement.checked).to.be.false;
|
||||
expect(checkedEl.inputElement.checked).to.be.true;
|
||||
uncheckedEl.checked = true;
|
||||
checkedEl.checked = false;
|
||||
await uncheckedEl.updateComplete;
|
||||
await checkedEl.updateComplete;
|
||||
expect(uncheckedEl.inputElement.checked).to.be.true;
|
||||
expect(checkedEl.inputElement.checked).to.be.false;
|
||||
});
|
||||
|
||||
it('should sync "checked" state received from child button', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-input-switch></lion-input-switch>
|
||||
`);
|
||||
const button = el.inputElement;
|
||||
expect(el.checked).to.be.false;
|
||||
button.click();
|
||||
expect(el.checked).to.be.true;
|
||||
button.click();
|
||||
expect(el.checked).to.be.false;
|
||||
});
|
||||
|
||||
it('synchronizes modelValue to checked state and vice versa', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-input-switch .choiceValue=${'foo'}></lion-input-switch>
|
||||
`);
|
||||
expect(el.checked).to.be.false;
|
||||
expect(el.modelValue).to.deep.equal({
|
||||
checked: false,
|
||||
value: 'foo',
|
||||
});
|
||||
el.checked = true;
|
||||
expect(el.checked).to.be.true;
|
||||
expect(el.modelValue).to.deep.equal({
|
||||
checked: true,
|
||||
value: 'foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -30,3 +30,4 @@ import '../packages/calendar/stories/index.stories.js';
|
|||
|
||||
import '../packages/option/stories/index.stories.js';
|
||||
import '../packages/select-rich/stories/index.stories.js';
|
||||
import '../packages/switch/stories/index.stories.js';
|
||||
|
|
|
|||
|
|
@ -2012,7 +2012,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.14.2.tgz#4ad2a6fedc22992c6048729fbf7104e472f590d3"
|
||||
integrity sha512-+ENGbkgoruTtuNGUVLi9hCC6+IJVBM/lnFDGjKBUt7T6RyAgidBTV+GPMzpQtfHhLRasVJmcjRW8D2him8HvAA==
|
||||
|
||||
"@open-wc/testing-helpers@^1.2.1":
|
||||
"@open-wc/testing-helpers@^1.0.0", "@open-wc/testing-helpers@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-1.2.1.tgz#eecba5ccfe808f9667caf149e68cd80d781f28e0"
|
||||
integrity sha512-FZBjqM81GQc+Q8W4YdWNwwk64+PW6frCvHyeov5ivCR2K7SJXwTPRcQIi0qU/6VVRVDlQeQ39PH8oSnjnIYpvQ==
|
||||
|
|
|
|||
Loading…
Reference in a new issue