diff --git a/src/common/search/search-input.ts b/src/common/search/search-input.ts
index 0b0038b9af..97a52b1864 100644
--- a/src/common/search/search-input.ts
+++ b/src/common/search/search-input.ts
@@ -51,18 +51,14 @@ class SearchInput extends LitElement {
@value-changed=${this._filterInputChanged}
.noLabelFloat=${this.noLabelFloat}
>
-
+
+
+
${this.filter &&
html`
diff --git a/src/common/string/filter/sequence-matching.ts b/src/common/string/filter/sequence-matching.ts
index e870ffe976..f8ea1f40cf 100644
--- a/src/common/string/filter/sequence-matching.ts
+++ b/src/common/string/filter/sequence-matching.ts
@@ -59,6 +59,7 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
: fuzzySequentialMatch(filter, item.text);
return item;
})
+ .filter((item) => item.score === undefined || item.score > 0)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
);
diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts
index b7c099855c..2f204be372 100644
--- a/src/dialogs/quick-bar/ha-quick-bar.ts
+++ b/src/dialogs/quick-bar/ha-quick-bar.ts
@@ -1,8 +1,9 @@
-import "../../components/ha-circular-progress";
-import "../../components/ha-header-bar";
-import "@polymer/paper-input/paper-input";
-import "@material/mwc-list/mwc-list-item";
import "@material/mwc-list/mwc-list";
+import type { List } from "@material/mwc-list/mwc-list";
+import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
+import "@material/mwc-list/mwc-list-item";
+import type { ListItem } from "@material/mwc-list/mwc-list-item";
+import { mdiConsoleLine } from "@mdi/js";
import {
css,
customElement,
@@ -10,25 +11,32 @@ import {
internalProperty,
LitElement,
property,
+ PropertyValues,
query,
} from "lit-element";
+import { ifDefined } from "lit-html/directives/if-defined";
+import { styleMap } from "lit-html/directives/style-map";
+import { scroll } from "lit-virtualizer";
+import memoizeOne from "memoize-one";
+import { componentsWithService } from "../../common/config/components_with_service";
import { fireEvent } from "../../common/dom/fire_event";
-import "../../components/ha-dialog";
-import { haStyleDialog } from "../../resources/styles";
-import { HomeAssistant } from "../../types";
-import { PolymerChangedEvent } from "../../polymer-types";
+import { computeDomain } from "../../common/entity/compute_domain";
+import { computeStateName } from "../../common/entity/compute_state_name";
+import { domainIcon } from "../../common/entity/domain_icon";
+import "../../common/search/search-input";
+import { compare } from "../../common/string/compare";
import {
fuzzyFilterSort,
ScorableTextItem,
} from "../../common/string/filter/sequence-matching";
-import { componentsWithService } from "../../common/config/components_with_service";
-import { domainIcon } from "../../common/entity/domain_icon";
-import { computeDomain } from "../../common/entity/compute_domain";
+import { debounce } from "../../common/util/debounce";
+import "../../components/ha-circular-progress";
+import "../../components/ha-dialog";
+import "../../components/ha-header-bar";
import { domainToName } from "../../data/integration";
+import { haStyleDialog } from "../../resources/styles";
+import { HomeAssistant } from "../../types";
import { QuickBarParams } from "./show-dialog-quick-bar";
-import { compare } from "../../common/string/compare";
-import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
-import { computeStateName } from "../../common/entity/compute_state_name";
interface QuickBarItem extends ScorableTextItem {
icon: string;
@@ -43,9 +51,9 @@ export class QuickBar extends LitElement {
@internalProperty() private _entityItems: QuickBarItem[] = [];
- @internalProperty() private _items: QuickBarItem[] = [];
+ @internalProperty() private _items?: QuickBarItem[] = [];
- @internalProperty() private _itemFilter = "";
+ @internalProperty() private _filter = "";
@internalProperty() private _opened = false;
@@ -53,144 +61,214 @@ export class QuickBar extends LitElement {
@internalProperty() private _commandTriggered = -1;
- @internalProperty() private _activatedIndex = 0;
+ @internalProperty() private _done = false;
- @query("paper-input", false) private _filterInputField?: HTMLElement;
+ @query("search-input", false) private _filterInputField?: HTMLElement;
- @query("mwc-list-item", false) private _firstListItem?: HTMLElement;
+ private _focusSet = false;
public async showDialog(params: QuickBarParams) {
this._commandMode = params.commandMode || false;
- this._opened = true;
this._commandItems = this._generateCommandItems();
this._entityItems = this._generateEntityItems();
+ this._opened = true;
}
public closeDialog() {
this._opened = false;
- this._itemFilter = "";
+ this._done = false;
+ this._focusSet = false;
+ this._filter = "";
this._commandTriggered = -1;
- this._resetActivatedIndex();
+ this._items = [];
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
+ protected updated(changedProperties: PropertyValues) {
+ if (
+ this._opened &&
+ (changedProperties.has("_filter") ||
+ changedProperties.has("_commandMode"))
+ ) {
+ this._setFilteredItems();
+ }
+ }
+
protected render() {
if (!this._opened) {
return html``;
}
return html`
-
-
+ ${this._itemFilter}` : this._itemFilter}
+ .filter=${this._commandMode ? `>${this._filter}` : this._filter}
@keydown=${this._handleInputKeyDown}
- @focus=${this._resetActivatedIndex}
- >
-
- ${this._items.map(
- (item, index) => html`
-
-
- ${item.text}
- ${item.altText
- ? html`
- ${item.altText}
- `
- : null}
- ${this._commandTriggered === index
- ? html``
- : null}
-
- `
- )}
-
+ @focus=${this._setFocusFirstListItem}
+ >
+ ${this._commandMode
+ ? html``
+ : ""}
+
+ ${!this._items
+ ? html``
+ : html`
+ ${scroll({
+ items: this._items,
+ renderItem: (item: QuickBarItem, index?: number) =>
+ this._renderItem(item, index),
+ })}
+ `}
`;
}
- private async processItemAndCloseDialog(ev: SingleSelectedEvent) {
- const index = ev.detail.index;
- const item = (ev.target as any).items[index].item;
+ private _handleOpened() {
+ this._setFilteredItems();
+ this.updateComplete.then(() => {
+ this._done = true;
+ });
+ }
+ private async _handleRangeChanged(e) {
+ if (this._focusSet) {
+ return;
+ }
+ if (e.firstVisible > -1) {
+ this._focusSet = true;
+ await this.updateComplete;
+ this._setFocusFirstListItem();
+ }
+ }
+
+ private _renderItem(item: QuickBarItem, index?: number) {
+ return html`
+
+
+ ${item.text}
+ ${item.altText
+ ? html`
+ ${item.altText}
+ `
+ : null}
+ ${this._commandTriggered === index
+ ? html``
+ : null}
+
+ `;
+ }
+
+ private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
this._commandTriggered = index;
await item.action();
this.closeDialog();
}
- private _resetActivatedIndex() {
- this._activatedIndex = 0;
+ private _handleSelected(ev: SingleSelectedEvent) {
+ const index = ev.detail.index;
+ const item = ((ev.target as List).items[index] as any).item;
+ this.processItemAndCloseDialog(item, index);
}
private _handleInputKeyDown(ev: KeyboardEvent) {
if (ev.code === "Enter") {
- this._firstListItem?.click();
+ if (!this._items?.length) {
+ return;
+ }
+
+ this.processItemAndCloseDialog(this._items[0], 0);
} else if (ev.code === "ArrowDown") {
ev.preventDefault();
- this._firstListItem?.focus();
+ this._getItemAtIndex(0)?.focus();
+ this._getItemAtIndex(1)?.focus();
}
}
+ private _getItemAtIndex(index: number): ListItem | null {
+ return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
+ }
+
+ private _handleSearchChange(ev: CustomEvent): void {
+ const newFilter = ev.detail.value;
+ const oldCommandMode = this._commandMode;
+
+ if (newFilter.startsWith(">")) {
+ this._commandMode = true;
+ this._debouncedSetFilter(newFilter.substring(1));
+ } else {
+ this._commandMode = false;
+ this._debouncedSetFilter(newFilter);
+ }
+
+ if (oldCommandMode !== this._commandMode) {
+ this._items = undefined;
+ }
+ }
+
+ private _debouncedSetFilter = debounce((filter: string) => {
+ this._filter = filter;
+ }, 100);
+
+ private _setFocusFirstListItem() {
+ // @ts-ignore
+ this._getItemAtIndex(0)?.rippleHandlers.startFocus();
+ }
+
private _handleListItemKeyDown(ev: KeyboardEvent) {
const isSingleCharacter = ev.key.length === 1;
- const isFirstListItem = (ev.target as any).index === 0;
+ const isFirstListItem =
+ (ev.target as HTMLElement).getAttribute("index") === "0";
if (ev.key === "ArrowUp") {
if (isFirstListItem) {
this._filterInputField?.focus();
- } else {
- this._activatedIndex--;
}
- } else if (ev.key === "ArrowDown") {
- this._activatedIndex++;
}
-
if (ev.key === "Backspace" || isSingleCharacter) {
+ (ev.currentTarget as List).scrollTop = 0;
this._filterInputField?.focus();
- this._resetActivatedIndex();
- }
- }
-
- private _entityFilterChanged(ev: PolymerChangedEvent) {
- const newFilter = ev.detail.value;
-
- if (newFilter.startsWith(">")) {
- this._commandMode = true;
- this._itemFilter = newFilter.substring(1);
- } else {
- this._commandMode = false;
- this._itemFilter = newFilter;
- }
-
- this._items = this._commandMode ? this._commandItems : this._entityItems;
-
- if (this._itemFilter !== "") {
- this._items = fuzzyFilterSort(
- this._itemFilter.trimLeft(),
- this._items
- );
}
}
@@ -227,12 +305,24 @@ export class QuickBar extends LitElement {
.sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()));
}
+ private _setFilteredItems() {
+ const items = this._commandMode ? this._commandItems : this._entityItems;
+ this._items = this._filter
+ ? this._filterItems(items || [], this._filter)
+ : items;
+ }
+
+ private _filterItems = memoizeOne(
+ (items: QuickBarItem[], filter: string): QuickBarItem[] =>
+ fuzzyFilterSort(filter.trimLeft(), items)
+ );
+
static get styles() {
return [
haStyleDialog,
css`
.heading {
- padding: 20px 20px 0px;
+ padding: 8px 20px 0px;
}
mwc-list-item span[slot="secondary"],
@@ -242,7 +332,7 @@ export class QuickBar extends LitElement {
ha-dialog {
--dialog-z-index: 8;
- --dialog-content-padding: 0px 24px 20px;
+ --dialog-content-padding: 0;
}
@media (min-width: 800px) {
@@ -254,6 +344,30 @@ export class QuickBar extends LitElement {
--mdc-dialog-max-height: calc(100% - 72px);
}
}
+
+ ha-icon {
+ color: var(--secondary-text-color);
+ }
+
+ ha-svg-icon.prefix {
+ margin: 8px;
+ }
+
+ .uni-virtualizer-host {
+ display: block;
+ position: relative;
+ contain: strict;
+ overflow: auto;
+ height: 100%;
+ }
+
+ .uni-virtualizer-host > * {
+ box-sizing: border-box;
+ }
+
+ mwc-list-item {
+ width: 100%;
+ }
`,
];
}
diff --git a/src/resources/styles.ts b/src/resources/styles.ts
index db838e3d8e..161c953a67 100644
--- a/src/resources/styles.ts
+++ b/src/resources/styles.ts
@@ -75,6 +75,7 @@ export const derivedStyles = {
"mdc-theme-on-secondary": "var(--text-primary-color)",
"mdc-theme-on-surface": "var(--primary-text-color)",
"mdc-theme-text-primary-on-background": "var(--primary-text-color)",
+ "mdc-theme-text-secondary-on-background": "var(--secondary-text-color)",
"app-header-text-color": "var(--text-primary-color)",
"app-header-background-color": "var(--primary-color)",
"material-body-text-color": "var(--primary-text-color)",