Merge pull request #156 from ing-bank/feat/implSelect
feat: add select-rich
This commit is contained in:
commit
784462d65a
23 changed files with 2577 additions and 0 deletions
49
packages/option/README.md
Normal file
49
packages/option/README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# LionOption
|
||||||
|
|
||||||
|
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||||
|
|
||||||
|
`lion-option` is a selectable within a [lion-select-rich](../select-rich/)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- has checked state
|
||||||
|
- has a modelValue
|
||||||
|
- can be disabled
|
||||||
|
- fully accessible
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i --save @lion/select-rich
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import '@lion/select-rich/lion-select-rich.js';
|
||||||
|
import '@lion/select-rich/lion-options.js';
|
||||||
|
import '@lion/option/lion-option.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```html
|
||||||
|
<lion-select-rich
|
||||||
|
name="favoriteColor"
|
||||||
|
label="Favorite color"
|
||||||
|
.errorValidators=${[['required']]}
|
||||||
|
>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||||
|
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also set the full modelValue for each option.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<lion-option .modelValue=${{ value: 'red', checked: false }}>Red</lion-option>
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details please see [lion-select-rich](../select-rich/).
|
||||||
1
packages/option/index.js
Normal file
1
packages/option/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { LionOption } from './src/LionOption.js';
|
||||||
3
packages/option/lion-option.js
Normal file
3
packages/option/lion-option.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionOption } from './src/LionOption.js';
|
||||||
|
|
||||||
|
customElements.define('lion-option', LionOption);
|
||||||
44
packages/option/package.json
Normal file
44
packages/option/package.json
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "@lion/option",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Allows to provide options for a rich select",
|
||||||
|
"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/option"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "../../scripts/npm-prepublish.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"lion",
|
||||||
|
"web-components",
|
||||||
|
"option"
|
||||||
|
],
|
||||||
|
"main": "index.js",
|
||||||
|
"module": "index.js",
|
||||||
|
"files": [
|
||||||
|
"docs",
|
||||||
|
"src",
|
||||||
|
"stories",
|
||||||
|
"test",
|
||||||
|
"translations",
|
||||||
|
"*.js"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@lion/core": "^0.1.13",
|
||||||
|
"@lion/field": "^0.1.38",
|
||||||
|
"@lion/choice-input": "^0.2.18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@open-wc/demoing-storybook": "^0.2.0",
|
||||||
|
"@open-wc/testing": "^2.0.6",
|
||||||
|
"sinon": "^7.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
115
packages/option/src/LionOption.js
Normal file
115
packages/option/src/LionOption.js
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { html, css, LitElement, DisabledMixin } from '@lion/core';
|
||||||
|
import { FormRegisteringMixin } from '@lion/field';
|
||||||
|
import { ChoiceInputMixin } from '@lion/choice-input';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option
|
||||||
|
* Can be a child of datalist/select, or role="listbox"
|
||||||
|
*
|
||||||
|
* Element gets state supplied externally, reflects this to attributes,
|
||||||
|
* enabling SubClassers to style based on those states
|
||||||
|
*/
|
||||||
|
export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMixin(LitElement))) {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
background-color: white;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([active]) {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([checked]) {
|
||||||
|
background-color: #bde4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([disabled]) {
|
||||||
|
color: #adadad;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.active = false;
|
||||||
|
this.__registerEventListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestUpdate(name, oldValue) {
|
||||||
|
super._requestUpdate(name, oldValue);
|
||||||
|
|
||||||
|
if (name === 'active') {
|
||||||
|
this.dispatchEvent(new Event('active-changed', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has('checked')) {
|
||||||
|
this.setAttribute('aria-selected', `${this.checked}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperties.has('disabled')) {
|
||||||
|
this.setAttribute('aria-disabled', `${this.disabled}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="choice-field__label">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.setAttribute('role', 'option');
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.__unRegisterEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
__registerEventListener() {
|
||||||
|
this.__onClick = () => {
|
||||||
|
if (!this.disabled) {
|
||||||
|
this.checked = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.__onMouseEnter = () => {
|
||||||
|
if (!this.disabled) {
|
||||||
|
this.active = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.__onMouseLeave = () => {
|
||||||
|
if (!this.disabled) {
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.addEventListener('click', this.__onClick);
|
||||||
|
this.addEventListener('mouseenter', this.__onMouseEnter);
|
||||||
|
this.addEventListener('mouseleave', this.__onMouseLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
__unRegisterEventListeners() {
|
||||||
|
this.removeEventListener('click', this.__onClick);
|
||||||
|
this.removeEventListener('mouseenter', this.__onMouseEnter);
|
||||||
|
this.removeEventListener('mouseleave', this.__onMouseLeave);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
packages/option/stories/index.stories.js
Normal file
35
packages/option/stories/index.stories.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
||||||
|
|
||||||
|
import '../lion-option.js';
|
||||||
|
|
||||||
|
storiesOf('Forms|Option', module)
|
||||||
|
.add(
|
||||||
|
'States',
|
||||||
|
() => html`
|
||||||
|
<lion-option>Default</lion-option><br />
|
||||||
|
<lion-option disabled>Disabled</lion-option>
|
||||||
|
<lion-option>
|
||||||
|
<p style="color: red;">With html</p>
|
||||||
|
<p>and multi Line</p>
|
||||||
|
</lion-option>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'Values',
|
||||||
|
() => html`
|
||||||
|
<lion-option .modelValue=${{ value: 10, checked: false }}>setting modelValue</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 10, checked: false }} active
|
||||||
|
>setting modelValue active</lion-option
|
||||||
|
>
|
||||||
|
<lion-option .modelValue=${{ value: 10, checked: true }}
|
||||||
|
>setting modelValue checked</lion-option
|
||||||
|
>
|
||||||
|
<lion-option .modelValue=${{ value: 10, checked: false }} disabled
|
||||||
|
>setting modelValue disabled</lion-option
|
||||||
|
>
|
||||||
|
<lion-option .choiceValue=${10}>setting choiceValue</lion-option>
|
||||||
|
<lion-option .choiceValue=${10} active>setting choiceValue active</lion-option>
|
||||||
|
<lion-option .choiceValue=${10} checked>setting choiceValue checked</lion-option>
|
||||||
|
<lion-option .choiceValue=${10} disabled>setting choiceValue disabled</lion-option>
|
||||||
|
`,
|
||||||
|
);
|
||||||
151
packages/option/test/lion-option.test.js
Normal file
151
packages/option/test/lion-option.test.js
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import '../lion-option.js';
|
||||||
|
|
||||||
|
describe('lion-option', () => {
|
||||||
|
describe('Values', () => {
|
||||||
|
it('has a modelValue', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10}></lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.modelValue).to.deep.equal({ value: 10, checked: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be checked', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10} checked></lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.modelValue).to.deep.equal({ value: 10, checked: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('has the "option" role', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option></lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.getAttribute('role')).to.equal('option');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has "aria-selected" attribute when checked', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10} checked>Item 1</lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.getAttribute('aria-selected')).to.equal('true');
|
||||||
|
|
||||||
|
el.checked = false;
|
||||||
|
// check that dom update is async
|
||||||
|
expect(el.getAttribute('aria-selected')).to.equal('true');
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.getAttribute('aria-selected')).to.equal('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('asynchronously adds the attributes "aria-disabled" and "disabled" when disabled', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10} disabled>Item 1</lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||||
|
expect(el.hasAttribute('disabled')).to.be.true;
|
||||||
|
|
||||||
|
el.disabled = false;
|
||||||
|
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||||
|
expect(el.hasAttribute('disabled')).to.be.true;
|
||||||
|
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.getAttribute('aria-disabled')).to.equal('false');
|
||||||
|
expect(el.hasAttribute('disabled')).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('State reflection', () => {
|
||||||
|
it('asynchronously adds the attribute "active" when active', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10}></lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.active).to.equal(false);
|
||||||
|
expect(el.hasAttribute('active')).to.be.false;
|
||||||
|
|
||||||
|
el.active = true;
|
||||||
|
expect(el.active).to.be.true;
|
||||||
|
expect(el.hasAttribute('active')).to.be.false;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.hasAttribute('active')).to.be.true;
|
||||||
|
|
||||||
|
el.active = false;
|
||||||
|
expect(el.active).to.be.false;
|
||||||
|
expect(el.hasAttribute('active')).to.be.true;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.hasAttribute('active')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does become active on [mouseenter]', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10}></lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.active).to.be.false;
|
||||||
|
el.dispatchEvent(new Event('mouseenter'));
|
||||||
|
expect(el.active).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does become un-active on [mouseleave]', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10} active></lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.active).to.be.true;
|
||||||
|
el.dispatchEvent(new Event('mouseleave'));
|
||||||
|
expect(el.active).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does become checked on [click]', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10}></lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.checked).to.be.false;
|
||||||
|
el.click();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.checked).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires active-changed event', async () => {
|
||||||
|
const activeSpy = sinon.spy();
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10} @active-changed="${activeSpy}"></lion-option>
|
||||||
|
`);
|
||||||
|
expect(activeSpy.callCount).to.equal(0);
|
||||||
|
el.active = true;
|
||||||
|
expect(activeSpy.callCount).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Disabled', () => {
|
||||||
|
it('does not becomes active on [mouseenter]', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10} disabled></lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.active).to.be.false;
|
||||||
|
el.dispatchEvent(new Event('mouseenter'));
|
||||||
|
expect(el.active).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not become checked on [click]', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10} disabled></lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.checked).to.be.false;
|
||||||
|
el.click();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.checked).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not become un-active on [mouseleave]', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-option .choiceValue=${10} active disabled></lion-option>
|
||||||
|
`);
|
||||||
|
expect(el.active).to.be.true;
|
||||||
|
el.dispatchEvent(new Event('mouseleave'));
|
||||||
|
expect(el.active).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
79
packages/select-rich/README.md
Normal file
79
packages/select-rich/README.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
# Select Rich
|
||||||
|
|
||||||
|
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||||
|
|
||||||
|
`lion-select-rich` component is a 'rich' version of the native `<select>` element.
|
||||||
|
It allows to provide fully customized options and a fully customized invoker button.
|
||||||
|
The component is meant to be used whenever the native `<select>` doesn't provide enough
|
||||||
|
styling/theming/user interaction opportunities.
|
||||||
|
|
||||||
|
Its implementation is based on the following Design pattern:
|
||||||
|
<https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-collapsible.html>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- fully accessible
|
||||||
|
- flexible api
|
||||||
|
- fully customizable option elements
|
||||||
|
- fully customizable invoker element
|
||||||
|
- Mimics native select interaction mode (windows/linux and mac)
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i --save @lion/select-rich
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import '@lion/select-rich/lion-select-rich.js';
|
||||||
|
import '@lion/select-rich/lion-options.js';
|
||||||
|
import '@lion/option/lion-option.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```html
|
||||||
|
<lion-select-rich
|
||||||
|
name="favoriteColor"
|
||||||
|
label="Favorite color"
|
||||||
|
.errorValidators=${[['required']]}
|
||||||
|
>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||||
|
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also set the full modelValue for each option.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<lion-option .modelValue=${{ value: 'red', checked: false }}>Red</lion-option>
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get/set the the checkedIndex and checkedValue
|
||||||
|
|
||||||
|
```js
|
||||||
|
const el = document.querySelector('lion-select-rich');
|
||||||
|
console.log(el.checkedIndex); // 1
|
||||||
|
console.log(el.checkedValue); // 'hotpink'
|
||||||
|
console.log(el.modelValue); // [{ value: 'red', checked: false }, { value: 'hotpink', checked: true }]
|
||||||
|
```
|
||||||
|
|
||||||
|
You can provide an invoker rendering a custom invoker that gets the selected value(s) as an
|
||||||
|
input property `.selectedElement`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<lion-select-rich>
|
||||||
|
<my-invoker-button slot="invoker"></my-invoker-button>
|
||||||
|
<lion-options slot="input">
|
||||||
|
...
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Other Resources
|
||||||
|
|
||||||
|
- [Design Considerations](./docs/DesignConsiderations.md)
|
||||||
3
packages/select-rich/lion-options.js
Normal file
3
packages/select-rich/lion-options.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionOptions } from './src/LionOptions.js';
|
||||||
|
|
||||||
|
customElements.define('lion-options', LionOptions);
|
||||||
3
packages/select-rich/lion-select-invoker.js
Normal file
3
packages/select-rich/lion-select-invoker.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionSelectInvoker } from './src/LionSelectInvoker.js';
|
||||||
|
|
||||||
|
customElements.define('lion-select-invoker', LionSelectInvoker);
|
||||||
3
packages/select-rich/lion-select-rich.js
Normal file
3
packages/select-rich/lion-select-rich.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionSelectRich } from './src/LionSelectRich.js';
|
||||||
|
|
||||||
|
customElements.define('lion-select-rich', LionSelectRich);
|
||||||
51
packages/select-rich/package.json
Normal file
51
packages/select-rich/package.json
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"name": "@lion/select-rich",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Provides a select with options that can contain html",
|
||||||
|
"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/select-rich"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "../../scripts/npm-prepublish.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"lion",
|
||||||
|
"web-components",
|
||||||
|
"select",
|
||||||
|
"listbox",
|
||||||
|
"field",
|
||||||
|
"form",
|
||||||
|
"option"
|
||||||
|
],
|
||||||
|
"main": "index.js",
|
||||||
|
"module": "index.js",
|
||||||
|
"files": [
|
||||||
|
"docs",
|
||||||
|
"src",
|
||||||
|
"stories",
|
||||||
|
"test",
|
||||||
|
"translations",
|
||||||
|
"*.js"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@lion/core": "^0.1.13",
|
||||||
|
"@lion/overlays": "^0.3.11",
|
||||||
|
"@lion/button": "^0.2.0",
|
||||||
|
"@lion/option": "^0.0.0",
|
||||||
|
"@lion/validate": "^0.2.22",
|
||||||
|
"@lion/field": "^0.1.38"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@open-wc/demoing-storybook": "^0.2.0",
|
||||||
|
"@open-wc/testing": "^2.0.6",
|
||||||
|
"@lion/form": "^0.1.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
packages/select-rich/src/LionOptions.js
Normal file
35
packages/select-rich/src/LionOptions.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LionOptions
|
||||||
|
*
|
||||||
|
* @customElement
|
||||||
|
* @extends LitElement
|
||||||
|
*/
|
||||||
|
export class LionOptions extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
role: {
|
||||||
|
type: String,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
tabIndex: {
|
||||||
|
type: Number,
|
||||||
|
reflect: true,
|
||||||
|
attribute: 'tabindex',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.role = 'listbox';
|
||||||
|
// we made it a Lit-Element property because of this
|
||||||
|
// eslint-disable-next-line wc/no-constructor-attributes
|
||||||
|
this.tabIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
packages/select-rich/src/LionSelectInvoker.js
Normal file
64
packages/select-rich/src/LionSelectInvoker.js
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { LionButton } from '@lion/button';
|
||||||
|
import { html } from '@lion/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LionSelectInvoker: invoker button consuming a selected element
|
||||||
|
*
|
||||||
|
* @customElement
|
||||||
|
* @extends LionButton
|
||||||
|
*/
|
||||||
|
export class LionSelectInvoker extends LionButton {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
selectedElement: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get slots() {
|
||||||
|
return {
|
||||||
|
...super.slots,
|
||||||
|
after: () => {
|
||||||
|
const icon = document.createElement('span');
|
||||||
|
icon.textContent = '▼';
|
||||||
|
return icon;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get contentWrapper() {
|
||||||
|
return this.shadowRoot.getElementById('content-wrapper');
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.selectedElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_contentTemplate() {
|
||||||
|
if (this.selectedElement) {
|
||||||
|
const labelNodes = Array.from(this.selectedElement.querySelectorAll('*'));
|
||||||
|
if (labelNodes.length > 0) {
|
||||||
|
return labelNodes.map(node => node.cloneNode(true));
|
||||||
|
}
|
||||||
|
return this.selectedElement.textContent;
|
||||||
|
}
|
||||||
|
return ``;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderBefore() {
|
||||||
|
return html`
|
||||||
|
<div id="content-wrapper">
|
||||||
|
${this._contentTemplate()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_renderAfter() {
|
||||||
|
return html`
|
||||||
|
<slot name="after"></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
607
packages/select-rich/src/LionSelectRich.js
Normal file
607
packages/select-rich/src/LionSelectRich.js
Normal file
|
|
@ -0,0 +1,607 @@
|
||||||
|
import { html, css, LitElement, SlotMixin } from '@lion/core';
|
||||||
|
import { LocalOverlayController, overlays } from '@lion/overlays';
|
||||||
|
import { FormControlMixin, InteractionStateMixin, FormRegistrarMixin } from '@lion/field';
|
||||||
|
import { ValidateMixin } from '@lion/validate';
|
||||||
|
import './differentKeyNamesShimIE.js';
|
||||||
|
|
||||||
|
import '../lion-select-invoker.js';
|
||||||
|
|
||||||
|
function uuid() {
|
||||||
|
return Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substr(2, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectInteractionMode() {
|
||||||
|
if (navigator.appVersion.indexOf('Mac') !== -1) {
|
||||||
|
return 'mac';
|
||||||
|
}
|
||||||
|
return 'windows/linux';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LionSelectRich: wraps the <lion-listbox> element
|
||||||
|
*
|
||||||
|
* @customElement
|
||||||
|
* @extends LionField
|
||||||
|
*/
|
||||||
|
export class LionSelectRich extends FormRegistrarMixin(
|
||||||
|
InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement)))),
|
||||||
|
) {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
...super.properties,
|
||||||
|
checkedValue: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
opened: {
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
interactionMode: {
|
||||||
|
type: String,
|
||||||
|
attribute: 'interaction-mode',
|
||||||
|
},
|
||||||
|
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([disabled]) {
|
||||||
|
color: #adadad;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static _isPrefilled(modelValue) {
|
||||||
|
if (!modelValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const checkedModelValue = modelValue.find(subModelValue => subModelValue.checked === true);
|
||||||
|
if (!checkedModelValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value } = checkedModelValue;
|
||||||
|
return super._isPrefilled(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get slots() {
|
||||||
|
return {
|
||||||
|
...super.slots,
|
||||||
|
invoker: () => {
|
||||||
|
return document.createElement('lion-select-invoker');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get _invokerNode() {
|
||||||
|
return this.querySelector('[slot=invoker]');
|
||||||
|
}
|
||||||
|
|
||||||
|
get _listboxNode() {
|
||||||
|
return this.querySelector('[slot=input]');
|
||||||
|
}
|
||||||
|
|
||||||
|
get _listboxActiveDescendantNode() {
|
||||||
|
return this._listboxNode.querySelector(`#${this._listboxActiveDescendant}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get checkedIndex() {
|
||||||
|
if (this.modelValue) {
|
||||||
|
return this.modelValue.findIndex(el => el.value === this.checkedValue);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
set checkedIndex(index) {
|
||||||
|
if (this.formElements[index]) {
|
||||||
|
this.formElements[index].checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeIndex() {
|
||||||
|
return this.formElements.findIndex(el => el.active === true);
|
||||||
|
}
|
||||||
|
|
||||||
|
set activeIndex(index) {
|
||||||
|
if (this.formElements[index]) {
|
||||||
|
this.formElements[index].active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.interactionMode = 'auto';
|
||||||
|
this.disabled = false;
|
||||||
|
this.opened = false;
|
||||||
|
// for interaction states
|
||||||
|
// we use a different event as 'model-value-changed' would bubble up from all options
|
||||||
|
this._valueChangedEvent = 'select-model-value-changed';
|
||||||
|
this._listboxActiveDescendant = null;
|
||||||
|
this.__hasInitialSelectedFormElement = false;
|
||||||
|
|
||||||
|
this.__setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
if (super.connectedCallback) {
|
||||||
|
super.connectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__setupOverlay();
|
||||||
|
this.__setupInvokerNode();
|
||||||
|
this.__setupListboxNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
if (super.disconnectedCallback) {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
this.__teardownEventListeners();
|
||||||
|
this.__teardownOverlay();
|
||||||
|
this.__teardownInvokerNode();
|
||||||
|
this.__teardownListboxNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestUpdate(name, oldValue) {
|
||||||
|
super._requestUpdate(name, oldValue);
|
||||||
|
if (
|
||||||
|
name === 'checkedValue' &&
|
||||||
|
!this.__isSyncingCheckedAndModelValue &&
|
||||||
|
this.modelValue &&
|
||||||
|
this.modelValue.length > 0
|
||||||
|
) {
|
||||||
|
if (this.checkedIndex) {
|
||||||
|
this.checkedIndex = this.checkedIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'modelValue') {
|
||||||
|
this.dispatchEvent(new CustomEvent('select-model-value-changed'));
|
||||||
|
this.__onModelValueChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'interactionMode') {
|
||||||
|
if (this.interactionMode === 'auto') {
|
||||||
|
this.interactionMode = detectInteractionMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(changedProps) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has('opened')) {
|
||||||
|
if (this.opened) {
|
||||||
|
this.__overlay.show();
|
||||||
|
} else {
|
||||||
|
this.__overlay.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has('disabled')) {
|
||||||
|
if (this.disabled) {
|
||||||
|
this._invokerNode.makeRequestToBeDisabled();
|
||||||
|
this.__requestOptionsToBeDisabled();
|
||||||
|
} else {
|
||||||
|
this._invokerNode.retractRequestToBeDisabled();
|
||||||
|
this.__retractRequestOptionsToBeDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.opened = !this.opened;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line
|
||||||
|
inputGroupInputTemplate() {
|
||||||
|
return html`
|
||||||
|
<div class="input-group__input">
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="input"></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides FormRegistrar adding to make sure children have specific default states when added
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
* @param {*} child
|
||||||
|
*/
|
||||||
|
addFormElement(child) {
|
||||||
|
super.addFormElement(child);
|
||||||
|
// we need to adjust the elements being registered
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
child.id = child.id || `${this.localName}-option-${uuid()}`;
|
||||||
|
|
||||||
|
if (this.disabled) {
|
||||||
|
child.makeRequestToBeDisabled();
|
||||||
|
}
|
||||||
|
// the first elements checked by default
|
||||||
|
if (!this.__hasInitialSelectedFormElement && (!child.disabled || this.disabled)) {
|
||||||
|
child.active = true;
|
||||||
|
child.checked = true;
|
||||||
|
this.__hasInitialSelectedFormElement = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__setAttributeForAllFormElements('aria-setsize', this.formElements.length);
|
||||||
|
child.setAttribute('aria-posinset', this.formElements.length);
|
||||||
|
|
||||||
|
this.__onChildModelValueChanged({ target: child });
|
||||||
|
this.resetInteractionState();
|
||||||
|
/* eslint-enable no-param-reassign */
|
||||||
|
}
|
||||||
|
|
||||||
|
_getFromAllFormElements(property) {
|
||||||
|
return this.formElements.map(e => e[property]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add same aria-label to invokerNode as inputElement
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
_onAriaLabelledbyChanged({ _ariaLabelledby }) {
|
||||||
|
if (this.inputElement) {
|
||||||
|
this.inputElement.setAttribute('aria-labelledby', _ariaLabelledby);
|
||||||
|
}
|
||||||
|
if (this._invokerNode) {
|
||||||
|
this._invokerNode.setAttribute(
|
||||||
|
'aria-labelledby',
|
||||||
|
`${_ariaLabelledby} ${this._invokerNode.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add same aria-label to invokerNode as inputElement
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
_onAriaDescribedbyChanged({ _ariaDescribedby }) {
|
||||||
|
if (this.inputElement) {
|
||||||
|
this.inputElement.setAttribute('aria-describedby', _ariaDescribedby);
|
||||||
|
}
|
||||||
|
if (this._invokerNode) {
|
||||||
|
this._invokerNode.setAttribute('aria-describedby', _ariaDescribedby);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__setupEventListeners() {
|
||||||
|
this.__onChildActiveChanged = this.__onChildActiveChanged.bind(this);
|
||||||
|
this.__onChildModelValueChanged = this.__onChildModelValueChanged.bind(this);
|
||||||
|
this.__onKeyUp = this.__onKeyUp.bind(this);
|
||||||
|
|
||||||
|
this.addEventListener('active-changed', this.__onChildActiveChanged);
|
||||||
|
this.addEventListener('model-value-changed', this.__onChildModelValueChanged);
|
||||||
|
this.addEventListener('keyup', this.__onKeyUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
__teardownEventListeners() {
|
||||||
|
this.removeEventListener('active-changed', this.__onChildActiveChanged);
|
||||||
|
this.removeEventListener('model-value-changed', this.__onChildModelValueChanged);
|
||||||
|
this.removeEventListener('keyup', this.__onKeyUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
__onChildActiveChanged({ target }) {
|
||||||
|
if (target.active === true) {
|
||||||
|
this.formElements.forEach(formElement => {
|
||||||
|
if (formElement !== target) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
formElement.active = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._listboxNode.setAttribute('aria-activedescendant', target.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__setAttributeForAllFormElements(attribute, value) {
|
||||||
|
this.formElements.forEach(formElement => {
|
||||||
|
formElement.setAttribute(attribute, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
__onChildModelValueChanged({ target }) {
|
||||||
|
if (target.checked) {
|
||||||
|
this.formElements.forEach(formElement => {
|
||||||
|
if (formElement !== target) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
formElement.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.modelValue = this._getFromAllFormElements('modelValue');
|
||||||
|
}
|
||||||
|
|
||||||
|
__onModelValueChanged() {
|
||||||
|
this.__isSyncingCheckedAndModelValue = true;
|
||||||
|
|
||||||
|
const foundChecked = this.modelValue.find(subModelValue => subModelValue.checked);
|
||||||
|
if (foundChecked && foundChecked.value !== this.checkedValue) {
|
||||||
|
this.checkedValue = foundChecked.value;
|
||||||
|
|
||||||
|
// sync to invoker
|
||||||
|
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__isSyncingCheckedAndModelValue = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
__getNextEnabledOption(currentIndex, offset = 1) {
|
||||||
|
for (let i = currentIndex + offset; i < this.formElements.length; i += 1) {
|
||||||
|
if (this.formElements[i] && !this.formElements[i].disabled) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
__getPreviousEnabledOption(currentIndex, offset = -1) {
|
||||||
|
for (let i = currentIndex + offset; i >= 0; i -= 1) {
|
||||||
|
if (this.formElements[i] && !this.formElements[i].disabled) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc
|
||||||
|
* Handle various keyboard controls; UP/DOWN will shift focus; SPACE selects
|
||||||
|
* an item.
|
||||||
|
*
|
||||||
|
* @param ev - the keydown event object
|
||||||
|
*/
|
||||||
|
__listboxOnKeyUp(ev) {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { key } = ev;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'Escape':
|
||||||
|
ev.preventDefault();
|
||||||
|
this.opened = false;
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
ev.preventDefault();
|
||||||
|
if (this.interactionMode === 'mac') {
|
||||||
|
this.checkedIndex = this.activeIndex;
|
||||||
|
}
|
||||||
|
this.opened = false;
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
ev.preventDefault();
|
||||||
|
this.activeIndex = this.__getPreviousEnabledOption(this.activeIndex);
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
ev.preventDefault();
|
||||||
|
this.activeIndex = this.__getNextEnabledOption(this.activeIndex);
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
ev.preventDefault();
|
||||||
|
this.activeIndex = this.__getNextEnabledOption(0, 0);
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
ev.preventDefault();
|
||||||
|
this.activeIndex = this.__getPreviousEnabledOption(this.formElements.length - 1, 0);
|
||||||
|
break;
|
||||||
|
/* no default */
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = ['ArrowUp', 'ArrowDown', 'Home', 'End'];
|
||||||
|
if (keys.includes(key) && this.interactionMode === 'windows/linux') {
|
||||||
|
this.checkedIndex = this.activeIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__listboxOnKeyDown(ev) {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { key } = ev;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'Tab':
|
||||||
|
// Tab can only be caught in keydown
|
||||||
|
ev.preventDefault();
|
||||||
|
this.opened = false;
|
||||||
|
break;
|
||||||
|
/* no default */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__onKeyUp(ev) {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.opened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { key } = ev;
|
||||||
|
switch (key) {
|
||||||
|
case 'ArrowUp':
|
||||||
|
ev.preventDefault();
|
||||||
|
if (this.interactionMode === 'mac') {
|
||||||
|
this.opened = true;
|
||||||
|
} else {
|
||||||
|
this.checkedIndex = this.__getPreviousEnabledOption(this.checkedIndex);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
ev.preventDefault();
|
||||||
|
if (this.interactionMode === 'mac') {
|
||||||
|
this.opened = true;
|
||||||
|
} else {
|
||||||
|
this.checkedIndex = this.__getNextEnabledOption(this.checkedIndex);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
/* no default */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__requestOptionsToBeDisabled() {
|
||||||
|
this.formElements.forEach(el => {
|
||||||
|
if (el.makeRequestToBeDisabled) {
|
||||||
|
el.makeRequestToBeDisabled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
__retractRequestOptionsToBeDisabled() {
|
||||||
|
this.formElements.forEach(el => {
|
||||||
|
if (el.retractRequestToBeDisabled) {
|
||||||
|
el.retractRequestToBeDisabled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
__setupInvokerNode() {
|
||||||
|
this._invokerNode.id = `invoker-${this._inputId}`;
|
||||||
|
this._invokerNode.setAttribute('aria-haspopup', 'listbox');
|
||||||
|
|
||||||
|
this.__setupInvokerNodeEventListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
__setupInvokerNodeEventListener() {
|
||||||
|
this.__invokerOnClick = () => {
|
||||||
|
if (!this.disabled) {
|
||||||
|
this.toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._invokerNode.addEventListener('click', this.__invokerOnClick);
|
||||||
|
|
||||||
|
this.__invokerOnBlur = () => {
|
||||||
|
this.dispatchEvent(new Event('blur'));
|
||||||
|
};
|
||||||
|
this._invokerNode.addEventListener('blur', this.__invokerOnBlur);
|
||||||
|
}
|
||||||
|
|
||||||
|
__teardownInvokerNode() {
|
||||||
|
this._invokerNode.removeEventListener('click', this.__invokerOnClick);
|
||||||
|
this._invokerNode.removeEventListener('blur', this.__invokerOnBlur);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For ShadyDom the listboxNode is available right from the start so we can add those events
|
||||||
|
* immediately.
|
||||||
|
* For native ShadowDom the select gets render before the listboxNode is available so we
|
||||||
|
* will add an event to the slotchange and add the events once available.
|
||||||
|
*/
|
||||||
|
__setupListboxNode() {
|
||||||
|
if (this._listboxNode) {
|
||||||
|
this.__setupListboxNodeEventListener();
|
||||||
|
} else {
|
||||||
|
const inputSlot = this.shadowRoot.querySelector('slot[name=input]');
|
||||||
|
if (inputSlot) {
|
||||||
|
inputSlot.addEventListener('slotchange', () => {
|
||||||
|
this.__setupListboxNodeEventListener();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__setupListboxNodeEventListener() {
|
||||||
|
this.__listboxOnClick = () => {
|
||||||
|
this.opened = false;
|
||||||
|
};
|
||||||
|
this._listboxNode.addEventListener('click', this.__listboxOnClick);
|
||||||
|
|
||||||
|
this.__listboxOnKeyUp = this.__listboxOnKeyUp.bind(this);
|
||||||
|
this._listboxNode.addEventListener('keyup', this.__listboxOnKeyUp);
|
||||||
|
|
||||||
|
this.__listboxOnKeyDown = this.__listboxOnKeyDown.bind(this);
|
||||||
|
this._listboxNode.addEventListener('keydown', this.__listboxOnKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
__teardownListboxNode() {
|
||||||
|
if (this._listboxNode) {
|
||||||
|
this._listboxNode.removeEventListener('click', this.__listboxOnClick);
|
||||||
|
this._listboxNode.removeEventListener('keyup', this.__listboxOnKeyUp);
|
||||||
|
this._listboxNode.removeEventListener('keydown', this.__listboxOnKeyDown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__setupOverlay() {
|
||||||
|
this.__overlay = overlays.add(
|
||||||
|
new LocalOverlayController({
|
||||||
|
contentNode: this._listboxNode,
|
||||||
|
invokerNode: this._invokerNode,
|
||||||
|
hidesOnEsc: false,
|
||||||
|
hidesOnOutsideClick: true,
|
||||||
|
inheritsReferenceObjectWidth: true,
|
||||||
|
popperConfig: {
|
||||||
|
placement: 'bottom-start',
|
||||||
|
modifiers: {
|
||||||
|
offset: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.__overlayOnShow = () => {
|
||||||
|
this.opened = true;
|
||||||
|
if (this.checkedIndex) {
|
||||||
|
this.activeIndex = this.checkedIndex;
|
||||||
|
}
|
||||||
|
this._listboxNode.focus();
|
||||||
|
};
|
||||||
|
this.__overlay.addEventListener('show', this.__overlayOnShow);
|
||||||
|
|
||||||
|
this.__overlayOnHide = () => {
|
||||||
|
this.opened = false;
|
||||||
|
this._invokerNode.focus();
|
||||||
|
};
|
||||||
|
this.__overlay.addEventListener('hide', this.__overlayOnHide);
|
||||||
|
}
|
||||||
|
|
||||||
|
__teardownOverlay() {
|
||||||
|
this.__overlay.removeEventListener('show', this.__overlayOnShow);
|
||||||
|
this.__overlay.removeEventListener('hide', this.__overlayOnHide);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
__isRequired(modelValue) {
|
||||||
|
const checkedModelValue = modelValue.find(subModelValue => subModelValue.checked === true);
|
||||||
|
if (!checkedModelValue) {
|
||||||
|
return { required: false };
|
||||||
|
}
|
||||||
|
const { value } = checkedModelValue;
|
||||||
|
return {
|
||||||
|
required:
|
||||||
|
(typeof value === 'string' && value !== '') ||
|
||||||
|
(typeof value !== 'string' && value !== undefined && value !== null),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/select-rich/src/differentKeyNamesShimIE.js
Normal file
33
packages/select-rich/src/differentKeyNamesShimIE.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
const event = KeyboardEvent.prototype;
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(event, 'key');
|
||||||
|
if (descriptor) {
|
||||||
|
const keys = {
|
||||||
|
Win: 'Meta',
|
||||||
|
Scroll: 'ScrollLock',
|
||||||
|
Spacebar: ' ',
|
||||||
|
|
||||||
|
Down: 'ArrowDown',
|
||||||
|
Left: 'ArrowLeft',
|
||||||
|
Right: 'ArrowRight',
|
||||||
|
Up: 'ArrowUp',
|
||||||
|
|
||||||
|
Del: 'Delete',
|
||||||
|
Apps: 'ContextMenu',
|
||||||
|
Esc: 'Escape',
|
||||||
|
|
||||||
|
Multiply: '*',
|
||||||
|
Add: '+',
|
||||||
|
Subtract: '-',
|
||||||
|
Decimal: '.',
|
||||||
|
Divide: '/',
|
||||||
|
};
|
||||||
|
Object.defineProperty(event, 'key', {
|
||||||
|
// eslint-disable-next-line object-shorthand, func-names
|
||||||
|
get: function() {
|
||||||
|
const key = descriptor.get.call(this);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
return keys.hasOwnProperty(key) ? keys[key] : key;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
183
packages/select-rich/stories/index.stories.js
Normal file
183
packages/select-rich/stories/index.stories.js
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
||||||
|
import { css } from '@lion/core';
|
||||||
|
|
||||||
|
import '@lion/form/lion-form.js';
|
||||||
|
import '@lion/option/lion-option.js';
|
||||||
|
|
||||||
|
import '../lion-select-rich.js';
|
||||||
|
import '../lion-options.js';
|
||||||
|
|
||||||
|
const selectRichDemoStyle = css`
|
||||||
|
.demo-area {
|
||||||
|
margin: 50px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
storiesOf('Forms|Select Rich', module)
|
||||||
|
.add(
|
||||||
|
'Default',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${selectRichDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-area">
|
||||||
|
<lion-select-rich label="Favorite color" name="color">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .modelValue=${{ value: 'red', checked: false }}>Red</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 'hotpink', checked: true }}>Hotpink</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 'teal', checked: false }}>Teal</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'Options with HTML',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${selectRichDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-area">
|
||||||
|
<lion-select-rich label="Favorite color" name="color">
|
||||||
|
<lion-options slot="input" class="demo-listbox">
|
||||||
|
<lion-option .modelValue=${{ value: 'red', checked: false }}>
|
||||||
|
<p style="color: red;">I am red</p>
|
||||||
|
<p>and multi Line</p>
|
||||||
|
</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 'hotpink', checked: true }}>
|
||||||
|
<p style="color: hotpink;">I am hotpink</p>
|
||||||
|
<p>and multi Line</p>
|
||||||
|
</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 'teal', checked: false }}>
|
||||||
|
<p style="color: teal;">I am teal</p>
|
||||||
|
<p>and multi Line</p>
|
||||||
|
</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'Disabled',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${selectRichDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-area">
|
||||||
|
<lion-select-rich label="Disabled select" disabled name="color">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .modelValue=${{ value: 'red', checked: false }}>Red</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 'hotpink', checked: true }}>Hotpink</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 'teal', checked: false }}>Teal</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
|
||||||
|
<lion-select-rich label="Disabled options" name="color">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${'red'} disabled>Red</lion-option>
|
||||||
|
<lion-option .choiceValue=${'blue'}>Blue</lion-option>
|
||||||
|
<lion-option .choiceValue=${'hotpink'} disabled>Hotpink</lion-option>
|
||||||
|
<lion-option .choiceValue=${'green'}>Green</lion-option>
|
||||||
|
<lion-option .choiceValue=${'teal'} disabled>Teal</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add('Validation', () => {
|
||||||
|
const submit = () => {
|
||||||
|
const form = document.querySelector('#form');
|
||||||
|
if (form.errorState === false) {
|
||||||
|
console.log(form.serializeGroup());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${selectRichDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-area">
|
||||||
|
<lion-form id="form" @submit="${submit}">
|
||||||
|
<form>
|
||||||
|
<lion-select-rich
|
||||||
|
id="color"
|
||||||
|
name="color"
|
||||||
|
label="Favorite color"
|
||||||
|
.errorValidators="${[['required']]}"
|
||||||
|
>
|
||||||
|
<lion-options slot="input" class="demo-listbox">
|
||||||
|
<lion-option .choiceValue=${null}>select a color</lion-option>
|
||||||
|
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||||
|
<lion-option .choiceValue=${'hotpink'}>Hotpink</lion-option>
|
||||||
|
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
<lion-button type="submit">Submit</lion-button>
|
||||||
|
</form>
|
||||||
|
</lion-form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.add('Render Options', () => {
|
||||||
|
const objs = [
|
||||||
|
{ type: 'mastercard', label: 'Master Card', amount: 12000, active: true },
|
||||||
|
{ type: 'visacard', label: 'Visa Card', amount: 0, active: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
function showOutput() {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
output.innerHTML = JSON.stringify(this.checkedValue, null, 2);
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${selectRichDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-area">
|
||||||
|
<lion-form>
|
||||||
|
<form>
|
||||||
|
<lion-select-rich
|
||||||
|
label="Credit Card"
|
||||||
|
name="color"
|
||||||
|
@select-model-value-changed=${showOutput}
|
||||||
|
>
|
||||||
|
<lion-options slot="input">
|
||||||
|
${objs.map(
|
||||||
|
obj => html`
|
||||||
|
<lion-option .modelValue=${{ value: obj, checked: false }}
|
||||||
|
>${obj.label}</lion-option
|
||||||
|
>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
<pre id="output"></pre>
|
||||||
|
</form>
|
||||||
|
</lion-form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.add(
|
||||||
|
'Interaction mode',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${selectRichDemoStyle}
|
||||||
|
</style>
|
||||||
|
<p>By default the select-rich uses the same interaction-mode as the operating system.</p>
|
||||||
|
<div class="demo-area">
|
||||||
|
<lion-select-rich label="Mac mode" name="color" interaction-mode="mac">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .modelValue=${{ value: 'red', checked: false }}>Red</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 'hotpink', checked: true }}>Hotpink</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 'teal', checked: false }}>Teal</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
|
||||||
|
<lion-select-rich label="Windows/Linux mode" name="color" interaction-mode="windows/linux">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .modelValue=${{ value: 'red', checked: false }}>Red</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 'hotpink', checked: true }}>Hotpink</lion-option>
|
||||||
|
<lion-option .modelValue=${{ value: 'teal', checked: false }}>Teal</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
);
|
||||||
49
packages/select-rich/test/keyboardEventShimIE.js
Normal file
49
packages/select-rich/test/keyboardEventShimIE.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
if (typeof window.KeyboardEvent !== 'function') {
|
||||||
|
// e.g. is IE and needs "polyfill"
|
||||||
|
const KeyboardEvent = (event, _params) => {
|
||||||
|
// current spec for it https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent
|
||||||
|
const params = {
|
||||||
|
bubbles: false,
|
||||||
|
cancelable: false,
|
||||||
|
view: document.defaultView,
|
||||||
|
key: false,
|
||||||
|
location: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
altKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
repeat: false,
|
||||||
|
..._params,
|
||||||
|
};
|
||||||
|
const modifiersListArray = [];
|
||||||
|
if (params.ctrlKey) {
|
||||||
|
modifiersListArray.push('Control');
|
||||||
|
}
|
||||||
|
if (params.shiftKey) {
|
||||||
|
modifiersListArray.push('Shift');
|
||||||
|
}
|
||||||
|
if (params.altKey) {
|
||||||
|
modifiersListArray.push('Alt');
|
||||||
|
}
|
||||||
|
if (params.metaKey) {
|
||||||
|
modifiersListArray.push('Meta');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ev = document.createEvent('KeyboardEvent');
|
||||||
|
// IE Spec for it https://technet.microsoft.com/en-us/windows/ff975297(v=vs.60)
|
||||||
|
ev.initKeyboardEvent(
|
||||||
|
event,
|
||||||
|
params.bubbles,
|
||||||
|
params.cancelable,
|
||||||
|
params.view,
|
||||||
|
params.key,
|
||||||
|
params.location,
|
||||||
|
modifiersListArray.join(' '),
|
||||||
|
params.repeat ? 1 : 0,
|
||||||
|
params.locale,
|
||||||
|
);
|
||||||
|
return ev;
|
||||||
|
};
|
||||||
|
KeyboardEvent.prototype = window.Event.prototype;
|
||||||
|
window.KeyboardEvent = KeyboardEvent;
|
||||||
|
}
|
||||||
12
packages/select-rich/test/lion-options.test.js
Normal file
12
packages/select-rich/test/lion-options.test.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
|
import '../lion-options.js';
|
||||||
|
|
||||||
|
describe('lion-options', () => {
|
||||||
|
it('should have role="listbox"', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-options></lion-options>
|
||||||
|
`);
|
||||||
|
expect(el.role).to.equal('listbox');
|
||||||
|
});
|
||||||
|
});
|
||||||
76
packages/select-rich/test/lion-select-invoker.test.js
Normal file
76
packages/select-rich/test/lion-select-invoker.test.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { expect, fixture, html, defineCE } from '@open-wc/testing';
|
||||||
|
import { LionButton } from '@lion/button';
|
||||||
|
import { LionSelectInvoker } from '../src/LionSelectInvoker.js';
|
||||||
|
|
||||||
|
import '../lion-select-invoker.js';
|
||||||
|
|
||||||
|
describe('lion-select-invoker', () => {
|
||||||
|
it('should behave as a button', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-invoker></lion-select-invoker>
|
||||||
|
`);
|
||||||
|
expect(el instanceof LionButton).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders invoker info based on selectedElement child elements', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-invoker></lion-select-invoker>
|
||||||
|
`);
|
||||||
|
el.selectedElement = await fixture(`<div class="option"><h2>I am</h2><p>2 lines</p></div>`);
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(el.contentWrapper).lightDom.to.equal(
|
||||||
|
`
|
||||||
|
<h2>I am</h2>
|
||||||
|
<p>2 lines</p>
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
ignoreAttributes: ['class'], // ShadyCss automatically adds classes
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders invoker info based on selectedElement textContent', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-invoker></lion-select-invoker>
|
||||||
|
`);
|
||||||
|
el.selectedElement = await fixture(`<div class="option">just textContent</div>`);
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(el.contentWrapper).lightDom.to.equal('just textContent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has tabindex="0"', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-invoker></lion-select-invoker>
|
||||||
|
`);
|
||||||
|
expect(el.tabIndex).to.equal(0);
|
||||||
|
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Subclassers', () => {
|
||||||
|
it('supports a custom _contentTemplate', async () => {
|
||||||
|
const myTag = defineCE(
|
||||||
|
class extends LionSelectInvoker {
|
||||||
|
_contentTemplate() {
|
||||||
|
if (this.selectedElement && this.selectedElement.textContent === 'cat') {
|
||||||
|
return html`
|
||||||
|
cat selected
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return `no valid selection`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const el = await fixture(`<${myTag}></${myTag}>`);
|
||||||
|
|
||||||
|
el.selectedElement = await fixture(`<div class="option">cat</div>`);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.contentWrapper).lightDom.to.equal('cat selected');
|
||||||
|
|
||||||
|
el.selectedElement = await fixture(`<div class="option">dog</div>`);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.contentWrapper).lightDom.to.equal('no valid selection');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
657
packages/select-rich/test/lion-select-rich-interaction.test.js
Normal file
657
packages/select-rich/test/lion-select-rich-interaction.test.js
Normal file
|
|
@ -0,0 +1,657 @@
|
||||||
|
import { expect, fixture, html, triggerFocusFor, triggerBlurFor } from '@open-wc/testing';
|
||||||
|
import './keyboardEventShimIE.js';
|
||||||
|
|
||||||
|
import '@lion/option/lion-option.js';
|
||||||
|
import '../lion-options.js';
|
||||||
|
import '../lion-select-rich.js';
|
||||||
|
|
||||||
|
describe('lion-select-rich interactions', () => {
|
||||||
|
describe('values', () => {
|
||||||
|
it('registers options', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.formElements.length).to.equal(2);
|
||||||
|
expect(el.formElements).to.eql([
|
||||||
|
el.querySelectorAll('lion-option')[0],
|
||||||
|
el.querySelectorAll('lion-option')[1],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has the first element by default checked and active', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(el.querySelector('lion-option').checked).to.be.true;
|
||||||
|
expect(el.querySelector('lion-option').active).to.be.true;
|
||||||
|
expect(el.modelValue).to.deep.equal([
|
||||||
|
{ value: 10, checked: true },
|
||||||
|
{ value: 20, checked: false },
|
||||||
|
]);
|
||||||
|
expect(el.checkedValue).to.equal(10);
|
||||||
|
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows null choiceValue', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${null}>Please select value</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.modelValue).to.deep.equal([
|
||||||
|
{ value: null, checked: true },
|
||||||
|
{ value: 20, checked: false },
|
||||||
|
]);
|
||||||
|
expect(el.checkedValue).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has the checked option as modelValue', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.modelValue).to.deep.equal([
|
||||||
|
{ value: 10, checked: false },
|
||||||
|
{ value: 20, checked: true },
|
||||||
|
]);
|
||||||
|
expect(el.checkedValue).to.equal(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs checkedValue to modelValue', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el.checkedValue = 20;
|
||||||
|
expect(el.modelValue).to.deep.equal([
|
||||||
|
{ value: 10, checked: false },
|
||||||
|
{ value: 20, checked: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has an activeIndex', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
|
||||||
|
el.querySelectorAll('lion-option')[1].active = true;
|
||||||
|
expect(el.querySelectorAll('lion-option')[0].active).to.be.false;
|
||||||
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Keyboard navigation', () => {
|
||||||
|
it('does not allow to navigate above the first or below the last option', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(() => {
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' }));
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
}).to.not.throw();
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates to first and last option with [Home] and [End] keys', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened interaction-mode="windows/linux">
|
||||||
|
<lion-options slot="input" name="foo">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
<lion-option .choiceValue=${30} checked>Item 3</lion-option>
|
||||||
|
<lion-option .choiceValue=${40}>Item 4</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.checkedValue).to.equal(30);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' }));
|
||||||
|
expect(el.checkedValue).to.equal(10);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
|
||||||
|
expect(el.checkedValue).to.equal(40);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: nice to have
|
||||||
|
it.skip('selects a value with single [character] key', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input" name="foo">
|
||||||
|
<lion-option .choiceValue=${'a'}>A</lion-option>
|
||||||
|
<lion-option .choiceValue=${'b'}>B</lion-option>
|
||||||
|
<lion-option .choiceValue=${'c'}>C</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.choiceValue).to.equal('a');
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'C' }));
|
||||||
|
expect(el.choiceValue).to.equal('c');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('selects a value with multiple [character] keys', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input" name="foo">
|
||||||
|
<lion-option .choiceValue=${'bar'}>Bar</lion-option>
|
||||||
|
<lion-option .choiceValue=${'far'}>Far</lion-option>
|
||||||
|
<lion-option .choiceValue=${'foo'}>Foo</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'F' }));
|
||||||
|
expect(el.choiceValue).to.equal('far');
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'O' }));
|
||||||
|
expect(el.choiceValue).to.equal('foo');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Keyboard navigation Windows', () => {
|
||||||
|
it('navigates through list with [ArrowDown] [ArrowUp] keys activates and checks the option', async () => {
|
||||||
|
function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) {
|
||||||
|
options.forEach((option, i) => {
|
||||||
|
if (i === selectedIndex) {
|
||||||
|
expect(option.checked).to.be.true;
|
||||||
|
} else {
|
||||||
|
expect(option.checked).to.be.false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened interaction-mode="windows/linux">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const options = Array.from(el.querySelectorAll('lion-option'));
|
||||||
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
expect(el.checkedIndex).to.equal(1);
|
||||||
|
expectOnlyGivenOneOptionToBeChecked(options, 1);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' }));
|
||||||
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates through list with [ArrowDown] [ArrowUp] keys checks the option while listbox unopened', async () => {
|
||||||
|
function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) {
|
||||||
|
options.forEach((option, i) => {
|
||||||
|
if (i === selectedIndex) {
|
||||||
|
expect(option.checked).to.be.true;
|
||||||
|
} else {
|
||||||
|
expect(option.checked).to.be.false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich interaction-mode="windows/linux">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const options = Array.from(el.querySelectorAll('lion-option'));
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
||||||
|
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
expect(el.checkedIndex).to.equal(1);
|
||||||
|
expectOnlyGivenOneOptionToBeChecked(options, 1);
|
||||||
|
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' }));
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Keyboard navigation Mac', () => {
|
||||||
|
it('navigates through open list with [ArrowDown] [ArrowUp] keys activates the option', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened interaction-mode="mac">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' }));
|
||||||
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Disabled', () => {
|
||||||
|
it('cannot be focused if disabled', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich disabled></lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el._invokerNode.tabIndex).to.equal(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still has a checked value', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich disabled>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.checkedValue).to.equal(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot be navigated with keyboard if disabled', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich disabled>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
expect(el.checkedValue).to.equal(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot be opened via click if disabled', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich disabled> </lion-select-rich>
|
||||||
|
`);
|
||||||
|
el._invokerNode.click();
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reflects disabled attribute to invoker', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich disabled> </lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el._invokerNode.hasAttribute('disabled')).to.be.true;
|
||||||
|
el.removeAttribute('disabled');
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el._invokerNode.hasAttribute('disabled')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips disabled options while navigating through list with [ArrowDown] [ArrowUp] keys', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20} disabled>Item 2</lion-option>
|
||||||
|
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
expect(el.activeIndex).to.equal(2);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' }));
|
||||||
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips disabled options while navigates to first and last option with [Home] and [End] keys', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input" name="foo">
|
||||||
|
<lion-option .choiceValue=${10} disabled>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
<lion-option .choiceValue=${30} checked>Item 3</lion-option>
|
||||||
|
<lion-option .choiceValue=${40} disabled>Item 4</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.activeIndex).to.equal(2);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' }));
|
||||||
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
|
||||||
|
expect(el.activeIndex).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checks the first enabled option', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10} disabled>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
expect(el.checkedIndex).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sync its disabled state to all options', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10} disabled>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
const options = [...el.querySelectorAll('lion-option')];
|
||||||
|
el.disabled = true;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(options[0].disabled).to.be.true;
|
||||||
|
expect(options[1].disabled).to.be.true;
|
||||||
|
|
||||||
|
el.disabled = false;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(options[0].disabled).to.be.true;
|
||||||
|
expect(options[1].disabled).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be enabled (incl. its options) even if it starts as disabled', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich disabled>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10} disabled>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
const options = [...el.querySelectorAll('lion-option')];
|
||||||
|
expect(options[0].disabled).to.be.true;
|
||||||
|
expect(options[1].disabled).to.be.true;
|
||||||
|
|
||||||
|
el.disabled = false;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(options[0].disabled).to.be.true;
|
||||||
|
expect(options[1].disabled).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: nice to have
|
||||||
|
describe.skip('Read only', () => {
|
||||||
|
it('can be focused if readonly', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-listbox readonly> </lion-listbox>
|
||||||
|
`);
|
||||||
|
expect(el.tabIndex).to.equal('-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot be navigated with keyboard if readonly', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input" name="foo">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
expect(el.choiceValue).to.equal(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Programmatic interaction', () => {
|
||||||
|
it('can set active state', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20} id="myId">Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
const opt = el.querySelectorAll('lion-option')[1];
|
||||||
|
opt.active = true;
|
||||||
|
expect(el._listboxNode.getAttribute('aria-activedescendant')).to.equal('myId');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can set checked state', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
const option = el.querySelectorAll('lion-option')[1];
|
||||||
|
option.checked = true;
|
||||||
|
expect(el.modelValue).to.deep.equal([
|
||||||
|
{ value: 10, checked: false },
|
||||||
|
{ value: 20, checked: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not allow to set checkedIndex or activeIndex to be out of bound', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(() => {
|
||||||
|
el.activeIndex = -1;
|
||||||
|
el.activeIndex = 1;
|
||||||
|
el.checkedIndex = -1;
|
||||||
|
el.checkedIndex = 1;
|
||||||
|
}).to.not.throw();
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unsets checked on other options when option becomes checked', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
const options = el.querySelectorAll('lion-option');
|
||||||
|
expect(options[0].checked).to.be.true;
|
||||||
|
options[1].checked = true;
|
||||||
|
expect(options[0].checked).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unsets active on other options when option becomes active', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
const options = el.querySelectorAll('lion-option');
|
||||||
|
expect(options[0].active).to.be.true;
|
||||||
|
options[1].active = true;
|
||||||
|
expect(options[0].active).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Interaction states', () => {
|
||||||
|
it('becomes dirty if value changed once', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.dirty).to.be.false;
|
||||||
|
el.checkedValue = 20;
|
||||||
|
expect(el.dirty).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('becomes touched if blurred once', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.touched).to.be.false;
|
||||||
|
await triggerFocusFor(el._invokerNode);
|
||||||
|
await triggerBlurFor(el._invokerNode);
|
||||||
|
expect(el.touched).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is prefilled if there is a value on init', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.prefilled).to.be.true;
|
||||||
|
|
||||||
|
const elEmpty = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${null}>Please select a value</lion-option>
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(elEmpty.prefilled).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validation', () => {
|
||||||
|
it('can be required', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich .errorValidators=${['required']}>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${null}>Please select a value</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.error.required).to.be.true;
|
||||||
|
el.checkedValue = 20;
|
||||||
|
expect(el.error.required).to.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('creates unique ids for all children', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input" name="foo">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20} selected>Item 2</lion-option>
|
||||||
|
<lion-option .choiceValue=${30} id="predefined">Item 3</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.querySelectorAll('lion-option')[0].id).to.exist;
|
||||||
|
expect(el.querySelectorAll('lion-option')[1].id).to.exist;
|
||||||
|
expect(el.querySelectorAll('lion-option')[2].id).to.equal('predefined');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a reference to the selected option', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input" name="foo">
|
||||||
|
<lion-option .choiceValue=${10} id="first">Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20} checked id="second">Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el._listboxNode.getAttribute('aria-activedescendant')).to.equal('first');
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
expect(el._listboxNode.getAttribute('aria-activedescendant')).to.equal('second');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('puts "aria-setsize" on all options to indicate the total amount of options', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input" name="foo">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
const optionEls = [].slice.call(el.querySelectorAll('lion-option'));
|
||||||
|
optionEls.forEach(optionEl => {
|
||||||
|
expect(optionEl.getAttribute('aria-setsize')).to.equal('3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('puts "aria-posinset" on all options to indicate their position in the listbox', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
<lion-option .choiceValue=${30}>Item 3</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
const optionEls = [].slice.call(el.querySelectorAll('lion-option'));
|
||||||
|
optionEls.forEach((oEl, i) => {
|
||||||
|
expect(oEl.getAttribute('aria-posinset')).to.equal(`${i + 1}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
321
packages/select-rich/test/lion-select-rich.test.js
Normal file
321
packages/select-rich/test/lion-select-rich.test.js
Normal file
|
|
@ -0,0 +1,321 @@
|
||||||
|
import { expect, fixture, html, aTimeout } from '@open-wc/testing';
|
||||||
|
import './keyboardEventShimIE.js';
|
||||||
|
|
||||||
|
import '@lion/option/lion-option.js';
|
||||||
|
import '../lion-options.js';
|
||||||
|
import '../lion-select-rich.js';
|
||||||
|
|
||||||
|
describe('lion-select-rich', () => {
|
||||||
|
it('does not have a tabindex', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich></lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.hasAttribute('tabindex')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Invoker', () => {
|
||||||
|
it('generates an lion-select-invoker if no invoker is provided', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich></lion-select-rich>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(el._invokerNode).to.exist;
|
||||||
|
expect(el._invokerNode.tagName).to.equal('LION-SELECT-INVOKER');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs the selected element to the invoker', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
const options = Array.from(el.querySelectorAll('lion-option'));
|
||||||
|
expect(el._invokerNode.selectedElement).to.equal(options[0]);
|
||||||
|
|
||||||
|
el.checkedIndex = 1;
|
||||||
|
expect(el._invokerNode.selectedElement).to.equal(el.querySelectorAll('lion-option')[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('overlay', () => {
|
||||||
|
it('should be closed by default', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich></lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows/hides the listbox via opened attribute', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el.opened = true;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el._listboxNode.style.display).to.be.equal('inline-block');
|
||||||
|
|
||||||
|
el.opened = false;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el._listboxNode.style.display).to.be.equal('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs opened state with overlay shown', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
const outerEl = await fixture('<button>somewhere</button>');
|
||||||
|
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
// a click on the button will trigger hide on outside click
|
||||||
|
// which we then need to sync back to "opened"
|
||||||
|
outerEl.click();
|
||||||
|
await aTimeout();
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will focus the listbox on open and invoker on close', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el.opened = true;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(document.activeElement === el._listboxNode).to.be.true;
|
||||||
|
expect(document.activeElement === el._invokerNode).to.be.false;
|
||||||
|
|
||||||
|
el.opened = false;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(document.activeElement === el._listboxNode).to.be.false;
|
||||||
|
expect(document.activeElement === el._invokerNode).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the listbox with checked option as active', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el.opened = true;
|
||||||
|
await el.updateComplete;
|
||||||
|
const options = Array.from(el.querySelectorAll('lion-option'));
|
||||||
|
|
||||||
|
expect(options[1].active).to.be.true;
|
||||||
|
expect(options[1].checked).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('interaction-mode', () => {
|
||||||
|
it('allows to specify an interaction-mode which determines other behaviors', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich interaction-mode="mac"></lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.interactionMode).to.equal('mac');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Keyboard navigation', () => {
|
||||||
|
it('opens the listbox with [Enter] key via click handler', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el._invokerNode.click();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the listbox with [ ](Space) key via click handler', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el._invokerNode.click();
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the listbox with [Escape] key once opened', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the listbox with [Tab] key once opened', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
// tab can only be caught via keydown
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mouse navigation', () => {
|
||||||
|
it('opens the listbox via click on invoker', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
el._invokerNode.click();
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the listbox when an option gets clicked', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
el.querySelector('lion-option').click();
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Keyboard navigation Windows', () => {
|
||||||
|
it('closes the listbox with [Enter] key once opened', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened>
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Keyboard navigation Mac', () => {
|
||||||
|
it('checks active item and closes the listbox with [Enter] key via click handler once opened', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich opened interaction-mode="mac">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// changes active but not checked
|
||||||
|
el.activeIndex = 1;
|
||||||
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
|
||||||
|
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
expect(el.checkedIndex).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the listbox with [ArrowUp] key', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich interaction-mode="mac">
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' }));
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the listbox with [ArrowDown] key', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich interaction-mode="mac">
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('has the right references to its inner elements', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich label="age">
|
||||||
|
<lion-options slot="input">
|
||||||
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el._invokerNode.getAttribute('aria-labelledby')).to.contain(el._labelNode.id);
|
||||||
|
expect(el._invokerNode.getAttribute('aria-labelledby')).to.contain(el._invokerNode.id);
|
||||||
|
expect(el._invokerNode.getAttribute('aria-describedby')).to.contain(el._helpTextNode.id);
|
||||||
|
expect(el._invokerNode.getAttribute('aria-describedby')).to.contain(el._feedbackNode.id);
|
||||||
|
expect(el._invokerNode.getAttribute('aria-haspopup')).to.equal('listbox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notifies when the listbox is expanded or not', async () => {
|
||||||
|
// smoke test for overlay functionality
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich>
|
||||||
|
<lion-options slot="input"></lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el._invokerNode.getAttribute('aria-expanded')).to.equal('false');
|
||||||
|
el.opened = true;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el._invokerNode.getAttribute('aria-expanded')).to.equal('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Use cases', () => {
|
||||||
|
it('works for complex array data', async () => {
|
||||||
|
const objs = [
|
||||||
|
{ type: 'mastercard', label: 'Master Card', amount: 12000, active: true },
|
||||||
|
{ type: 'visacard', label: 'Visa Card', amount: 0, active: false },
|
||||||
|
];
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-select-rich label="Favorite color" name="color">
|
||||||
|
<lion-options slot="input">
|
||||||
|
${objs.map(
|
||||||
|
obj => html`
|
||||||
|
<lion-option .modelValue=${{ value: obj, checked: false }}
|
||||||
|
>${obj.label}</lion-option
|
||||||
|
>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</lion-options>
|
||||||
|
</lion-select-rich>
|
||||||
|
`);
|
||||||
|
expect(el.checkedValue).to.deep.equal({
|
||||||
|
type: 'mastercard',
|
||||||
|
label: 'Master Card',
|
||||||
|
amount: 12000,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
el.checkedIndex = 1;
|
||||||
|
expect(el.checkedValue).to.deep.equal({
|
||||||
|
type: 'visacard',
|
||||||
|
label: 'Visa Card',
|
||||||
|
amount: 0,
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -24,3 +24,6 @@ import '../packages/overlays/stories/index.stories.js';
|
||||||
import '../packages/popup/stories/index.stories.js';
|
import '../packages/popup/stories/index.stories.js';
|
||||||
import '../packages/tooltip/stories/index.stories.js';
|
import '../packages/tooltip/stories/index.stories.js';
|
||||||
import '../packages/calendar/stories/index.stories.js';
|
import '../packages/calendar/stories/index.stories.js';
|
||||||
|
|
||||||
|
import '../packages/option/stories/index.stories.js';
|
||||||
|
import '../packages/select-rich/stories/index.stories.js';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue