lion/packages/select-rich/stories/index.stories.mdx
Thomas Allmer 89b835a799 feat: improved storybook demos
Co-authored-by: Joren Broekema <Joren.Broekema@ing.com>
Co-authored-by: Alex Ghiu <Alex.Ghiu@ing.com>
Co-authored-by: Thijs Louisse <Thijs.Louisse@ing.com>
2020-01-13 13:58:03 +01:00

518 lines
17 KiB
Text

import { Story, Meta, html, Preview } from '@open-wc/demoing-storybook';
import { loadDefaultFeedbackMessages, Required } from '@lion/validate';
import '@lion/option/lion-option.js';
import '../lion-options.js';
import '../lion-select-rich.js';
<Meta title="Forms/Select Rich" parameters={{ component: 'lion-select-rich' }} />
# Select Rich
`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>
<Story name="Default">
{html`
<lion-select-rich name="favoriteColor" label="Favorite color">
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
`}
</Story>
```html
<lion-select-rich name="favoriteColor" label="Favorite color">
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
```
## 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';
```
> No need to npm install `@lion/option` separately, it comes with the rich select as a dependency
## Examples
### Model value
You can set the full `modelValue` for each option, which includes the checked property for whether it is checked or not.
```html
<lion-option .modelValue=${{ value: 'red', checked: false }}>Red</lion-option>
```
### Options with HTML
The main feature of this rich select that makes it rich, is that your options can contain HTML.
<Story name="Options with HTML">
{html`
<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>
`}
</Story>
```html
<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>
```
### Many Options with Scrolling
<Story name="Many options with scrolling">
{html`
<style>
#scrollSelectRich lion-options {
max-height: 200px;
overflow-y: auto;
display: block;
}
</style>
<lion-select-rich id="scrollSelectRich" 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>
</lion-option>
<lion-option .modelValue=${{ value: 'hotpink', checked: true }}>
<p style="color: hotpink;">I am hotpink</p>
</lion-option>
<lion-option .modelValue=${{ value: 'teal', checked: false }}>
<p style="color: teal;">I am teal</p>
</lion-option>
<lion-option .modelValue=${{ value: 'green', checked: false }}>
<p style="color: green;">I am green</p>
</lion-option>
<lion-option .modelValue=${{ value: 'blue', checked: false }}>
<p style="color: blue;">I am blue</p>
</lion-option>
</lion-options>
</lion-select-rich>
`}
</Story>
```html
<style>
lion-options {
max-height: 200px;
overflow-y: auto;
display: block;
}
</style>
<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>
</lion-option>
<lion-option .modelValue=${{ value: 'hotpink', checked: true }}>
<p style="color: hotpink;">I am hotpink</p>
</lion-option>
<lion-option .modelValue=${{ value: 'teal', checked: false }}>
<p style="color: teal;">I am teal</p>
</lion-option>
<lion-option .modelValue=${{ value: 'green', checked: false }}>
<p style="color: green;">I am green</p>
</lion-option>
<lion-option .modelValue=${{ value: 'blue', checked: false }}>
<p style="color: blue;">I am blue</p>
</lion-option>
</lion-options>
</lion-select-rich>
```
### Read only prefilled
You can set the rich select as read only.
This will block the user from opening the select.
The readonly attribute is delegated to the invoker for disabling opening the overlay, and for styling purposes.
<Story name="Read only prefilled">
{html`
<lion-select-rich label="Read-only select" readonly 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>
`}
</Story>
### Disabled
You can set the `disabled` attribute to disable either specific options or the entire select.
If you disable the entire select, the disabled attribute is also delegated to the invoker, similar to readonly.
<Story name="Disabled">
{html`
<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>
`}
</Story>
```html
<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>
```
```html
<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>
```
### Validation
Validation can be used on this field as well, same as with other fields. Below is an example with required.
It can be triggered by opening the select and selecting a valid option, then selecting the first option again, of which the `modelValue` is `null`.
<Story name="Validation">
{() => {
loadDefaultFeedbackMessages();
return html`
<lion-select-rich
id="color"
name="color"
label="Favorite color"
.validators="${[new 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'} disabled>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
`;
}}
</Story>
```js
import { loadDefaultFeedbackMessages, Required } from '@lion/validate';
```
```html
<lion-select-rich
id="color"
name="color"
label="Favorite color"
.validators="${[new 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'} disabled>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
```
### Render options
The choiceValue can also be a complex value like an Object.
It is up to you how to render this Object in the DOM.
<Story name="Render Options">
{() => {
const objs = [
{ type: 'mastercard', label: 'Master Card', amount: 12000, active: true },
{ type: 'visacard', label: 'Visa Card', amount: 0, active: false },
];
function showOutput() {
document.getElementById('demoRenderOutput').innerHTML = JSON.stringify(
this.checkedValue,
null,
2,
);
}
return html`
<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>
<p>Full value:</p>
<pre id="demoRenderOutput"></pre>
`;
}}
</Story>
```js
const objs = [
{ type: 'mastercard', label: 'Master Card', amount: 12000, active: true },
{ type: 'visacard', label: 'Visa Card', amount: 0, active: false },
];
```
```html
<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>
```
### Interaction Mode
You can set the interaction mode to either `mac` or `windows/linux`.
By default, it will choose based on the user Operating System, but it can be forced.
This changes the keyboard interaction.
<Story name="Interaction mode">
{html`
<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>
`}
</Story>
```html
<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>
```
```html
<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>
```
### Checked index & value
You can get/set the checkedIndex and checkedValue.
<Story name="Checked index and value">
{html`
<style>
.log-button {
margin: 10px 0;
}
</style>
<div>
<label id="label-richSelectCheckedInput" for="richSelectCheckedInput">
Set the checkedIndex
</label>
<input
id="richSelectCheckedInput"
aria-labelledby="label-richSelectCheckedInput"
type="number"
@change=${(e) => {
const selectEl = document.getElementById('checkedRichSelect');
selectEl.checkedIndex = e.target.value;
}}
>
</div>
<button class="log-button" @click=${() => {
const selectEl = document.getElementById('checkedRichSelect');
console.log(`checkedIndex: ${selectEl.checkedIndex}`);
console.log(`checkedValue: ${selectEl.checkedValue}`);
}}>Console log checked index and value</button>
<lion-select-rich id="checkedRichSelect" name="favoriteColor" label="Favorite color">
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
`}
</Story>
```html
<lion-select-rich name="favoriteColor" label="Favorite color">
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-options>
</lion-select-rich>
```
```js
console.log(`checkedIndex: ${selectEl.checkedIndex}`); // 1
console.log(`checkedValue: ${selectEl.checkedValue}`); // 'hotpink'
selectEl.checkedIndex = 0;
console.log(`checkedIndex: ${selectEl.checkedIndex}`); // 0
console.log(`checkedValue: ${selectEl.checkedValue}`); // 'red'
```
### Custom Invoker
You can provide a custom invoker using the invoker slot.
This means it will get the selected value(s) as an input property `.selectedElement`.
You can use this `selectedElement` to then render the content to your own invoker.
```html
<lion-select-rich>
<my-invoker-button slot="invoker"></my-invoker-button>
<lion-options slot="input">
...
</lion-options>
</lion-select-rich>
```
An example of how such a custom invoker class could look like:
```js
class MyInvokerButton extends LitElement() {
static get properties() {
return {
selectedElement: {
type: Object,
};
}
}
_contentTemplate() {
if (this.selectedElement) {
const labelNodes = Array.from(this.selectedElement.querySelectorAll('*'));
// Nested html in the selected option
if (labelNodes.length > 0) {
// Cloning is important if you plan on passing nodes straight to a lit template
return labelNodes.map(node => node.cloneNode(true));
}
// Or if it is just text inside the selected option, no html
return this.selectedElement.textContent;
}
return ``;
}
render() {
return html`
<div>
${this._contentTemplate()}
</div>
`;
}
}
```
> This example only works if your option elements don't have ShadowDOM boundaries themselves.
> Cloning deeply only works up until the first shadow boundary.