* [RFC] Proof-of-concept commit for submenu support
This is a Request For Comments to seek directional guidance towards
implementing the submenu slot of menu-item.
Includes:
- SubmenuController to manage event listeners on menu-item.
- Example usage in menu-item documentation.
- Trivial tests to check rendering.
Outstanding questions include:
- Accessibility concerns. E.g. where to handle 'ArrowRight',
'ArrowLeft'?
- Should selection of menu-item denoting submenu be possible or
customizable?
- How to parameterize contained popup?
- Implementation concerns:
- Use of ref / id
- delegation of some rendering to the controller
- What to test
Related to [#620](https://github.com/shoelace-style/shoelace/issues/620).
* Update submenu-controller.ts
Removed extraneous `console.log()`.
* PoC working of ArrowRight to focus on submenu.
* Revert "PoC working of ArrowRight to focus on submenu."
(Didn't mean to publish this.)
This reverts commit be04e9a221.
* [WIP] Submenu WIP continues.
- Submenus now close on change-of-focus, not a timeout.
- Keyboard navigation support added.
- Skidding fix for better alignment.
- Submenu documentation moved to Menu page.
- Tests for accessibility, right and left arrow keys.
* Cleanup: Removed dead code and dead code comments.
* style: Eslint warnings and errors fixed. npm run verify now passes.
* fix: 2 changes to menu / submenu on-click behavior:
1. Close submenu on click explicitly, so this occurs even if the menu is
not inside of an sl-dropdown.
2. In menu, ignore clicks that do not explicitly target a menu-item.
Clicks that were on (e.g. a menu-border) were emitting select events.
* fix: Prevent menu's extraneous Enter / space key propagation.
Menu's handleKeyDown calls item.click (to emit the selection).
Propagating the keyboard event on Enter / space would the cause re-entry
into a submenu, so prevent the needless propagation.
* Submenu tweaks ...
- 100 ms delay when opening submenus on mouseover
- Shadows added
- Distance added to popup to have submenus overlap menu slightly.
* polish up submenu stuff
* stay highlighted when submenu is open
* update changelog
* resolve feedback
---------
Co-authored-by: Bryce Moore <bryce.moore@gmail.com>
16 KiB
meta, layout
| meta | layout | ||||
|---|---|---|---|---|---|
|
component |
<sl-select>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
<sl-option value="option-4">Option 4</sl-option>
<sl-option value="option-5">Option 5</sl-option>
<sl-option value="option-6">Option 6</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
<SlOption value="option-4">Option 4</SlOption>
<SlOption value="option-5">Option 5</SlOption>
<SlOption value="option-6">Option 6</SlOption>
</SlSelect>
);
:::tip
This component works with standard <form> elements. Please refer to the section on form controls to learn more about form submission and client-side validation.
:::
Examples
Labels
Use the label attribute to give the select an accessible label. For labels that contain HTML, use the label slot instead.
<sl-select label="Select one">
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect label="Select one">
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
);
Help Text
Add descriptive help text to a select with the help-text attribute. For help texts that contain HTML, use the help-text slot instead.
<sl-select label="Experience" help-text="Please tell us your skill level.">
<sl-option value="1">Novice</sl-option>
<sl-option value="2">Intermediate</sl-option>
<sl-option value="3">Advanced</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect label="Experience" help-text="Please tell us your skill level.">
<SlOption value="1">Novice</SlOption>
<SlOption value="2">Intermediate</SlOption>
<SlOption value="3">Advanced</SlOption>
</SlSelect>
);
Placeholders
Use the placeholder attribute to add a placeholder.
<sl-select placeholder="Select one">
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect placeholder="Select one">
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
);
Clearable
Use the clearable attribute to make the control clearable. The clear button only appears when an option is selected.
<sl-select clearable value="option-1">
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect placeholder="Clearable" clearable>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
);
Filled Selects
Add the filled attribute to draw a filled select.
<sl-select filled>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect filled>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
);
Pill
Use the pill attribute to give selects rounded edges.
<sl-select pill>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect pill>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
);
Disabled
Use the disabled attribute to disable a select.
<sl-select placeholder="Disabled" disabled>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect placeholder="Disabled" disabled>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
);
Multiple
To allow multiple options to be selected, use the multiple attribute. It's a good practice to use clearable when this option is enabled. To set multiple values at once, set value to a space-delimited list of values.
<sl-select label="Select a Few" value="option-1 option-2 option-3" multiple clearable>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
<sl-option value="option-4">Option 4</sl-option>
<sl-option value="option-5">Option 5</sl-option>
<sl-option value="option-6">Option 6</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect label="Select a Few" value="option-1 option-2 option-3" multiple clearable>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
<SlOption value="option-4">Option 4</SlOption>
<SlOption value="option-5">Option 5</SlOption>
<SlOption value="option-6">Option 6</SlOption>
</SlSelect>
);
:::tip
Note that multi-select options may wrap, causing the control to expand vertically. You can use the max-options-visible attribute to control the maximum number of selected options to show at once.
:::
Setting Initial Values
Use the value attribute to set the initial selection.
When using multiple, the value attribute uses space-delimited values to select more than one option. Because of this, <sl-option> values cannot contain spaces. If you're accessing the value property through Javascript, it will be an array.
<sl-select value="option-1 option-2" multiple clearable>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
<sl-option value="option-4">Option 4</sl-option>
</sl-select>
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect value="option-1 option-2" multiple clearable>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
);
Grouping Options
Use <sl-divider> to group listbox items visually. You can also use <small> to provide labels, but they won't be announced by most assistive devices.
<sl-select>
<small>Section 1</small>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
<sl-divider></sl-divider>
<small>Section 2</small>
<sl-option value="option-4">Option 4</sl-option>
<sl-option value="option-5">Option 5</sl-option>
<sl-option value="option-6">Option 6</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
<SlOption value="option-4">Option 4</SlOption>
<SlOption value="option-5">Option 5</SlOption>
<SlOption value="option-6">Option 6</SlOption>
</SlSelect>
);
Sizes
Use the size attribute to change a select's size. Note that size does not apply to listbox options.
<sl-select placeholder="Small" size="small">
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
<br />
<sl-select placeholder="Medium" size="medium">
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
<br />
<sl-select placeholder="Large" size="large">
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<>
<SlSelect placeholder="Small" size="small">
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
<br />
<SlSelect placeholder="Medium" size="medium">
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
<br />
<SlSelect placeholder="Large" size="large">
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
</>
);
Placement
The preferred placement of the select's listbox can be set with the placement attribute. Note that the actual position may vary to ensure the panel remains in the viewport. Valid placements are top and bottom.
<sl-select placement="top">
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect placement="top">
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlDropdown>
);
Prefix Icons
Use the prefix slot to prepend an icon to the control.
<sl-select placeholder="Small" size="small" clearable>
<sl-icon name="house" slot="prefix"></sl-icon>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
<br />
<sl-select placeholder="Medium" size="medium" clearable>
<sl-icon name="house" slot="prefix"></sl-icon>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
<br />
<sl-select placeholder="Large" size="large" clearable>
<sl-icon name="house" slot="prefix"></sl-icon>
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<>
<SlSelect placeholder="Small" size="small">
<SlIcon name="house" slot="prefix"></SlIcon>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
<br />
<SlSelect placeholder="Medium" size="medium">
<SlIcon name="house" slot="prefix"></SlIcon>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
<br />
<SlSelect placeholder="Large" size="large">
<SlIcon name="house" slot="prefix"></SlIcon>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
</SlSelect>
</>
);
Custom Tags
When multiple options can be selected, you can provide custom tags by passing a function to the getTag property. Your function can return a string of HTML, a Lit Template, or an HTMLElement. The getTag() function will be called for each option. The first argument is an <sl-option> element and the second argument is the tag's index (its position in the tag list).
Remember that custom tags are rendered in a shadow root. To style them, you can use the style attribute in your template or you can add your own parts and target them with the ::part() selector.
<sl-select
placeholder="Select one"
value="email phone"
multiple
clearable
class="custom-tag"
>
<sl-option value="email">
<sl-icon slot="prefix" name="envelope"></sl-icon>
Email
</sl-option>
<sl-option value="phone">
<sl-icon slot="prefix" name="telephone"></sl-icon>
Phone
</sl-option>
<sl-option value="chat">
<sl-icon slot="prefix" name="chat-dots"></sl-icon>
Chat
</sl-option>
</sl-select>
<script type="module">
const select = document.querySelector('.custom-tag');
select.getTag = (option, index) => {
// Use the same icon used in the <sl-option>
const name = option.querySelector('sl-icon[slot="prefix"]').name;
// You can return a string, a Lit Template, or an HTMLElement here
return `
<sl-tag removable>
<sl-icon name="${name}" style="padding-inline-end: .5rem;"></sl-icon>
${option.getTextLabel()}
</sl-tag>
`;
};
</script>
:::warning
Be sure you trust the content you are outputting! Passing unsanitized user input to getTag() can result in XSS vulnerabilities.
:::