diff --git a/src/components.d.ts b/src/components.d.ts index 90dda7aa9..e06086b49 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -450,6 +450,10 @@ export namespace Components { * Sets focus on the menu. */ "setFocus": () => Promise; + /** + * Initiates type-to-select logic, which automatically selects an option based on what the user is currently typing. The key passed will be appended to the internal query and the selection will be updated. After a brief period, the internal query is cleared automatically. This method is intended to be used with the keydown event. Useful for enabling type-to-select when the menu doesn't have focus. + */ + "typeToSelect": (key: string) => Promise; } interface SlMenuDivider { } diff --git a/src/components/dropdown/dropdown.tsx b/src/components/dropdown/dropdown.tsx index 3019f131b..92be6f032 100644 --- a/src/components/dropdown/dropdown.tsx +++ b/src/components/dropdown/dropdown.tsx @@ -212,21 +212,13 @@ export class Dropdown { event.preventDefault(); } - const menu = this.getMenu(); - // If a menu is present, focus on it when certain keys are pressed + const menu = this.getMenu(); if (menu && ['ArrowDown', 'ArrowUp'].includes(event.key)) { event.preventDefault(); menu.setFocus(); return; } - - // All other keys focus the menu and pass the event through to it (necessary for type-to-search to work) - if (menu && event.target !== menu) { - menu.setFocus(); - requestAnimationFrame(() => menu.dispatchEvent(new KeyboardEvent(event.type, event))); - return; - } } handleDocumentMouseDown(event: MouseEvent) { @@ -255,6 +247,14 @@ export class Dropdown { event.preventDefault(); event.stopPropagation(); } + + // All other keys focus the menu and initiate type-to-select + const menu = this.getMenu(); + if (menu && event.target !== menu) { + menu.setFocus(); + menu.typeToSelect(event.key); + return; + } } togglePanel() { diff --git a/src/components/menu/menu.tsx b/src/components/menu/menu.tsx index 5513763c3..3349cb7ba 100644 --- a/src/components/menu/menu.tsx +++ b/src/components/menu/menu.tsx @@ -20,7 +20,7 @@ export class Menu { ignoreMouseEvents = false; ignoreMouseTimeout: any; menu: HTMLElement; - typeToSelect = ''; + typeToSelectString = ''; typeToSelectTimeout: any; /** Emitted when the menu gains focus. */ @@ -148,20 +148,31 @@ export class Menu { } } - // Handle type-to-search behavior when non-control characters are entered + // Handle type-to-select 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; + 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; - } + /** + * Initiates type-to-select logic, which automatically selects an option based on what the user is currently typing. + * The key passed will be appended to the internal query and the selection will be updated. After a brief period, the + * internal query is cleared automatically. This method is intended to be used with the keydown event. Useful for + * enabling type-to-select when the menu doesn't have focus. + */ + @Method() + async typeToSelect(key: string) { + clearTimeout(this.typeToSelectTimeout); + this.typeToSelectTimeout = setTimeout(() => (this.typeToSelectString = ''), 750); + this.typeToSelectString += key.toLowerCase(); + + 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.typeToSelectString.length) === this.typeToSelectString) { + items.map(i => (i.active = i === item)); + break; } } }