JSPM

  • Created
  • Published
  • Downloads 251
  • Score
    100M100P100Q107853F
  • License MIT

Provides a select with options that can contain html

Package Exports

  • @lion/select-rich
  • @lion/select-rich/lion-option.js
  • @lion/select-rich/lion-options.js
  • @lion/select-rich/lion-select-rich.js
  • @lion/select-rich/src/differentKeyNamesShimIE

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@lion/select-rich) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

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

import { html } from '@lion/core';
import { Required } from '@lion/form-core';
import { loadDefaultFeedbackMessages } from '@lion/validate-messages';

import './lion-option.js';
import './lion-options.js';
import './lion-select-rich.js';

export default {
  title: 'Forms/Select Rich',
};
loadDefaultFeedbackMessages();
export const main = () => 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

npm i --save @lion/select-rich
import { LionSelectRich, LionOptions, LionOption } from '@lion/select-rich';
// or
import '@lion/select-rich/lion-select-rich.js';
import '@lion/select-rich/lion-options.js';
import '@lion/select-rich/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.

<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.

export const optionsWithHTML = () => 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

export const manyOptionsWithScrolling = () => 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>
`;

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.

export const readOnlyPrefilled = () => 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>
`;

Disabled Select

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.

export const disabledSelect = () => 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>
`;
export const disabledOption = () => 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.

export const validation = () => {
  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>
  `;
};

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.

export const renderOptions = () => {
  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>
  `;
};

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.

export const interactionMode = () => 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>
`;

Checked index & value

You can get/set the checkedIndex and checkedValue.

export const checkedIndexAndValue = () => 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>
`;

No default selection

If you want to set a placeholder option with something like 'Please select', you can of course do this, the same way you would do it in a native select.

Simply put an option with a modelValue that is null.

<lion-option .choiceValue="${null}">select a color</lion-option>

However, this allows the user to explicitly select this option.

Often, you may want a placeholder that appears initially, but cannot be selected explicitly by the user. For this you can use has-no-default-selected attribute.

Both methods work with the Required validator.

export const noDefaultSelection = () => html`
  <lion-select-rich name="favoriteColor" label="Favorite color" has-no-default-selected>
    <lion-options slot="input">
      <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>
`;

By default, the placeholder is completely empty in the LionSelectInvoker, but subclassers can easily override this in their extension, by the overriding _noSelectionTemplate() method.

Single Option

If there is a single option rendered, then singleOption property is set to true on lion-select-rich and invoker as well. Invoker also gets single-option which can be used to having desired templating and styling. As in here the arrow is not displayed for single option

export const singleOption = () => html`
  <lion-select-rich label="Single Option" name="color">
    <lion-options slot="input">
      <lion-option .choiceValue=${'red'}>Red</lion-option>
    </lion-options>
  </lion-select-rich>
`;

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.

<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:

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.