mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 12:09:26 +00:00
Add type-to-search to menu
This commit is contained in:
@@ -11,6 +11,9 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-menu-item value="cat">Cat</sl-menu-item>
|
||||
<sl-menu-item value="dog">Dog</sl-menu-item>
|
||||
<sl-menu-item value="monkey">Monkey</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-menu-item value="option-4">Option 4</sl-menu-item>
|
||||
<sl-menu-item value="option-5">Option 5</sl-menu-item>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
The roadmap tracks the status of components and features that are planned, in development, and under consideration.
|
||||
|
||||
## 1.0.0 📦
|
||||
|
||||
### Components
|
||||
## 2.0 🚀
|
||||
|
||||
- [x] Alert
|
||||
- [x] Avatar
|
||||
@@ -14,19 +12,19 @@ The roadmap tracks the status of components and features that are planned, in de
|
||||
- [x] Details
|
||||
- [x] Dialog
|
||||
- [x] Dropdown
|
||||
- [ ] Form (necessary to make shadowed form controls easier to consume)
|
||||
- [ ] Form
|
||||
- [x] Icon
|
||||
- [x] Input
|
||||
- [x] Menu
|
||||
- [x] Menu Divider
|
||||
- [x] Menu Item
|
||||
- [x] Menu Label
|
||||
- [ ] Panel (aka "drawer")
|
||||
- [ ] Panel
|
||||
- [x] Progress Bar
|
||||
- [x] Progress Ring
|
||||
- [x] Radio
|
||||
- [x] Range
|
||||
- [x] Select (single + multi)
|
||||
- [x] Select
|
||||
- [x] Spinner
|
||||
- [x] Switch
|
||||
- [x] Tab List
|
||||
@@ -36,29 +34,20 @@ The roadmap tracks the status of components and features that are planned, in de
|
||||
- [x] Textarea
|
||||
- [x] Tooltip
|
||||
|
||||
### Features / Misc.
|
||||
## 2.1 📦
|
||||
|
||||
- [ ] Type-ahead selection for menu
|
||||
- [ ] Form control labels
|
||||
- [ ] Form control validation states
|
||||
- [ ] Expose [parts](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) to allow more granular styling of all components
|
||||
- [ ] Ensure components are making proper use of design tokens
|
||||
|
||||
## Planned 🗺
|
||||
|
||||
- [ ] Alert service
|
||||
- [ ] Alert service (aka "toast")
|
||||
- [ ] Button group
|
||||
- [ ] Card
|
||||
- [ ] Content Placeholder (multiple variations)
|
||||
- [ ] Date & Time Picker (possibly wrap [flatpickr](https://flatpickr.js.org/))
|
||||
- [ ] Input group
|
||||
- [ ] Popover
|
||||
- [ ] Stepper
|
||||
|
||||
## Under Consideration 🤔
|
||||
|
||||
- [ ] Card
|
||||
- [ ] Carousel
|
||||
- [ ] Content Placeholders
|
||||
- [ ] Date & Time Picker
|
||||
- [ ] File Button / Uploader
|
||||
- [ ] Fit text to container
|
||||
- [ ] Graphing components (based on [Chart.js](https://www.chartjs.org/))
|
||||
- [ ] Table (probably a `<table>` wrapper that implements styles, sorting, and some other features)
|
||||
- [ ] Graphing components based on [Chart.js](https://www.chartjs.org/)
|
||||
- [ ] Popover
|
||||
- [ ] Stepper
|
||||
|
||||
8
src/components.d.ts
vendored
8
src/components.d.ts
vendored
@@ -372,6 +372,14 @@ export namespace Components {
|
||||
"value": string;
|
||||
}
|
||||
interface SlMenu {
|
||||
/**
|
||||
* Removes focus from the menu.
|
||||
*/
|
||||
"removeFocus": () => Promise<void>;
|
||||
/**
|
||||
* Passes key presses to the control. Useful for managing the menu when other elements have focus.
|
||||
*/
|
||||
"sendKeyEvent": (event: KeyboardEvent) => Promise<void>;
|
||||
/**
|
||||
* Sets focus on the menu.
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
transition: var(--sl-transition-fast) opacity;
|
||||
|
||||
::slotted(sl-menu) {
|
||||
--max-height: 50vh;
|
||||
max-height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,13 @@ export class Dropdown {
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
return this.panel
|
||||
.querySelector('slot')
|
||||
.assignedElements({ flatten: true })
|
||||
.filter(el => el.tagName.toLowerCase() === 'sl-menu')[0] as HTMLSlMenuElement;
|
||||
}
|
||||
|
||||
handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
// Close when escape is pressed
|
||||
if (event.key === 'Escape') {
|
||||
@@ -182,15 +189,20 @@ export class Dropdown {
|
||||
});
|
||||
}
|
||||
|
||||
// If a menu is present, focus on it when up/down is pressed
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
const elements = this.panel.querySelector('slot').assignedElements({ flatten: true });
|
||||
const menu = elements.filter(el => el.tagName.toLowerCase() === 'sl-menu')[0] as HTMLSlMenuElement;
|
||||
const menu = this.getMenu();
|
||||
|
||||
if (menu) {
|
||||
menu.setFocus();
|
||||
event.preventDefault();
|
||||
}
|
||||
// If a menu is present, focus on it when certain keys are pressed
|
||||
if (menu && ['ArrowDown', 'ArrowUp'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
menu.setFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
// All other keys should focus the menu and pass through the event to type-to-search
|
||||
if (menu && event.target !== menu) {
|
||||
menu.setFocus();
|
||||
menu.sendKeyEvent(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,39 +22,6 @@
|
||||
transition: var(--sl-transition-fast) color, var(--sl-transition-fast) border, var(--sl-transition-fast) box-shadow;
|
||||
cursor: inherit;
|
||||
|
||||
.sl-input__control {
|
||||
flex: 1 1 auto;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
color: var(--sl-input-color);
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: inherit;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&::-webkit-search-decoration,
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-results-button,
|
||||
&::-webkit-search-results-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--sl-input-placeholder-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.sl-input--disabled) {
|
||||
background-color: var(--sl-input-background-color-hover);
|
||||
border-color: var(--sl-input-border-color-hover);
|
||||
@@ -82,14 +49,47 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sl-input__prefix,
|
||||
.sl-input__suffix {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
color: var(--sl-input-icon-color);
|
||||
.sl-input__control {
|
||||
flex: 1 1 auto;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
color: var(--sl-input-color);
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: inherit;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&::-webkit-search-decoration,
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-results-button,
|
||||
&::-webkit-search-results-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--sl-input-placeholder-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sl-input__prefix,
|
||||
.sl-input__suffix {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
color: var(--sl-input-icon-color);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
@import 'component';
|
||||
|
||||
/**
|
||||
* @prop --max-height: The maximum height of the menu. */
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
--max-height: none;
|
||||
}
|
||||
|
||||
.sl-menu {
|
||||
max-height: var(--max-height);
|
||||
padding: var(--sl-spacing-x-small) 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: none;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
:host(:focus) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, Element, Event, EventEmitter, Method, h } from '@stencil/core';
|
||||
import { Component, Element, Event, EventEmitter, Host, Method, h } from '@stencil/core';
|
||||
import { scrollIntoView } from '../../utilities/scroll';
|
||||
import { getTextContent } from '../../utilities/slot';
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
@@ -17,6 +18,8 @@ export class Menu {
|
||||
ignoreMouseEvents = false;
|
||||
ignoreMouseTimeout: any;
|
||||
menu: HTMLElement;
|
||||
typeToSelect = '';
|
||||
typeToSelectTimeout: any;
|
||||
|
||||
constructor() {
|
||||
this.handleBlur = this.handleBlur.bind(this);
|
||||
@@ -42,11 +45,23 @@ export class Menu {
|
||||
/** Sets focus on the menu. */
|
||||
@Method()
|
||||
async setFocus() {
|
||||
this.menu.focus();
|
||||
this.host.focus();
|
||||
}
|
||||
|
||||
/** Removes focus from the menu. */
|
||||
@Method()
|
||||
async removeFocus() {
|
||||
this.host.blur();
|
||||
}
|
||||
|
||||
/** Passes key presses to the control. Useful for managing the menu when other elements have focus. */
|
||||
@Method()
|
||||
async sendKeyEvent(event: KeyboardEvent) {
|
||||
this.handleKeyDown(event);
|
||||
}
|
||||
|
||||
getItems() {
|
||||
const slot = this.menu.querySelector('slot');
|
||||
const slot = this.host.shadowRoot.querySelector('slot');
|
||||
return [...slot.assignedElements({ flatten: true })].filter(
|
||||
(el: any) => el.tagName.toLowerCase() === 'sl-menu-item' && !el.disabled
|
||||
) as [HTMLSlMenuItemElement];
|
||||
@@ -62,7 +77,7 @@ export class Menu {
|
||||
|
||||
scrollItemIntoView(item: HTMLSlMenuItemElement) {
|
||||
if (item) {
|
||||
scrollIntoView(item, this.menu);
|
||||
scrollIntoView(item, this.host);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +98,6 @@ export class Menu {
|
||||
const target = event.target as HTMLElement;
|
||||
const item = target.closest('sl-menu-item');
|
||||
|
||||
this.menu.focus();
|
||||
|
||||
if (item && !item.disabled) {
|
||||
this.slSelect.emit({ item });
|
||||
}
|
||||
@@ -98,8 +111,13 @@ export class Menu {
|
||||
this.ignoreMouseTimeout = setTimeout(() => (this.ignoreMouseEvents = false), 500);
|
||||
this.ignoreMouseEvents = true;
|
||||
|
||||
// Make a selection when pressing enter or space
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
// Prevent scrolling when certain keys are pressed
|
||||
if ([' ', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Make a selection when pressing enter
|
||||
if (event.key === 'Enter') {
|
||||
const item = this.getActiveItem();
|
||||
event.preventDefault();
|
||||
|
||||
@@ -136,11 +154,33 @@ export class Menu {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle type-to-search behavior when non-control characters are entered
|
||||
if (event.key === ' ' || /^[\d\w]$/i.test(event.key)) {
|
||||
clearTimeout(this.typeToSelectTimeout);
|
||||
this.typeToSelectTimeout = setTimeout(() => (this.typeToSelect = ''), 750);
|
||||
this.typeToSelect += event.key;
|
||||
|
||||
const items = this.getItems();
|
||||
for (const item of items) {
|
||||
const slot = item.shadowRoot.querySelector('slot:not([name])') as HTMLSlotElement;
|
||||
const label = getTextContent(slot).toLowerCase().trim();
|
||||
if (label.substring(0, this.typeToSelect.length) === this.typeToSelect) {
|
||||
items.map(i => (i.active = i === item));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseDown(event: MouseEvent) {
|
||||
// Prevent the menu's focus from being lost when interacting with items, dividers, and headers
|
||||
event.preventDefault();
|
||||
const target = event.target as HTMLElement;
|
||||
const menuItem = target.closest('sl-menu-item:not([disabled])');
|
||||
|
||||
// Prevent the menu's focus from being lost when interacting with non-menu items
|
||||
if (!menuItem) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseOver(event: MouseEvent) {
|
||||
@@ -163,9 +203,7 @@ export class Menu {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
ref={el => (this.menu = el)}
|
||||
class="sl-menu"
|
||||
<Host
|
||||
tabIndex={0}
|
||||
role="menu"
|
||||
onClick={this.handleClick}
|
||||
@@ -177,7 +215,7 @@ export class Menu {
|
||||
onMouseOut={this.handleMouseOut}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export class Select {
|
||||
this.handleBlur = this.handleBlur.bind(this);
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleMenuKeyDown = this.handleMenuKeyDown.bind(this);
|
||||
this.handleMenuHide = this.handleMenuHide.bind(this);
|
||||
this.handleMenuShow = this.handleMenuShow.bind(this);
|
||||
this.handleMenuSelect = this.handleMenuSelect.bind(this);
|
||||
@@ -116,12 +117,18 @@ export class Select {
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
if (!this.isOpen && (event.key === 'Enter' || event.key === ' ')) {
|
||||
// Open the dropdown when enter is pressed
|
||||
if (!this.isOpen && event.key === 'Enter') {
|
||||
this.dropdown.show();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleMenuKeyDown(event: KeyboardEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
handleMenuSelect(event: CustomEvent) {
|
||||
const item = event.detail.item;
|
||||
|
||||
@@ -261,7 +268,12 @@ export class Select {
|
||||
<sl-icon slot="suffix" class="sl-select__icon" name="chevron-down" />
|
||||
</sl-input>
|
||||
|
||||
<sl-menu ref={el => (this.menu = el)} class="sl-select__menu" onSlSelect={this.handleMenuSelect}>
|
||||
<sl-menu
|
||||
ref={el => (this.menu = el)}
|
||||
class="sl-select__menu"
|
||||
onSlSelect={this.handleMenuSelect}
|
||||
onKeyDown={this.handleMenuKeyDown}
|
||||
>
|
||||
<slot />
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
|
||||
@@ -20,36 +20,6 @@
|
||||
transition: var(--sl-transition-fast) color, var(--sl-transition-fast) border, var(--sl-transition-fast) box-shadow;
|
||||
cursor: inherit;
|
||||
|
||||
.sl-textarea__control {
|
||||
flex: 1 1 auto;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4;
|
||||
color: var(--sl-input-color);
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
cursor: inherit;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&::-webkit-search-decoration,
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-results-button,
|
||||
&::-webkit-search-results-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--sl-input-placeholder-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.sl-textarea--disabled) {
|
||||
background-color: var(--sl-input-background-color-hover);
|
||||
border-color: var(--sl-input-border-color-hover);
|
||||
@@ -78,6 +48,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sl-textarea__control {
|
||||
flex: 1 1 auto;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4;
|
||||
color: var(--sl-input-color);
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
cursor: inherit;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&::-webkit-search-decoration,
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-results-button,
|
||||
&::-webkit-search-results-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--sl-input-placeholder-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Size modifiers
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Reference in New Issue
Block a user