mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 07:16:39 +00:00
Improve performance of quick bar (#7359)
Co-authored-by: Donnie <donniekarnsinsb@hotmail.com>
This commit is contained in:
parent
4a5935ee36
commit
54ec37994c
@ -51,18 +51,14 @@ class SearchInput extends LitElement {
|
||||
@value-changed=${this._filterInputChanged}
|
||||
.noLabelFloat=${this.noLabelFloat}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiMagnify}
|
||||
slot="prefix"
|
||||
class="prefix"
|
||||
></ha-svg-icon>
|
||||
<slot name="prefix" slot="prefix">
|
||||
<ha-svg-icon class="prefix" .path=${mdiMagnify}></ha-svg-icon>
|
||||
</slot>
|
||||
${this.filter &&
|
||||
html`
|
||||
<mwc-icon-button
|
||||
slot="suffix"
|
||||
class="suffix"
|
||||
@click=${this._clearSearch}
|
||||
alt="Clear"
|
||||
title="Clear"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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,57 +61,124 @@ 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`
|
||||
<ha-dialog .heading=${true} open @closed=${this.closeDialog} hideActions>
|
||||
<paper-input
|
||||
<ha-dialog
|
||||
.heading=${true}
|
||||
open
|
||||
@opened=${this._handleOpened}
|
||||
@closed=${this.closeDialog}
|
||||
hideActions
|
||||
>
|
||||
<search-input
|
||||
dialogInitialFocus
|
||||
no-label-float
|
||||
slot="heading"
|
||||
class="heading"
|
||||
@value-changed=${this._entityFilterChanged}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.quick-bar.filter_placeholder"
|
||||
)}
|
||||
type="search"
|
||||
value=${this._commandMode ? `>${this._itemFilter}` : this._itemFilter}
|
||||
.filter=${this._commandMode ? `>${this._filter}` : this._filter}
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
@focus=${this._resetActivatedIndex}
|
||||
></paper-input>
|
||||
<mwc-list activatable @selected=${this.processItemAndCloseDialog}>
|
||||
${this._items.map(
|
||||
(item, index) => html`
|
||||
@focus=${this._setFocusFirstListItem}
|
||||
>
|
||||
${this._commandMode
|
||||
? html`<ha-svg-icon
|
||||
slot="prefix"
|
||||
class="prefix"
|
||||
.path=${mdiConsoleLine}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
</search-input>
|
||||
${!this._items
|
||||
? html`<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`<mwc-list
|
||||
activatable
|
||||
@rangechange=${this._handleRangeChanged}
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
@selected=${this._handleSelected}
|
||||
style=${styleMap({
|
||||
height: `${Math.min(
|
||||
this._items.length * (this._commandMode ? 56 : 72) + 26,
|
||||
this._done ? 500 : 0
|
||||
)}px`,
|
||||
})}
|
||||
>
|
||||
${scroll({
|
||||
items: this._items,
|
||||
renderItem: (item: QuickBarItem, index?: number) =>
|
||||
this._renderItem(item, index),
|
||||
})}
|
||||
</mwc-list>`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
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`
|
||||
<mwc-list-item
|
||||
.twoline=${Boolean(item.altText)}
|
||||
.activated=${index === this._activatedIndex}
|
||||
.item=${item}
|
||||
.index=${index}
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
index=${ifDefined(index)}
|
||||
hasMeta
|
||||
graphic=${item.altText ? "avatar" : "icon"}
|
||||
>
|
||||
@ -111,9 +186,7 @@ export class QuickBar extends LitElement {
|
||||
<span>${item.text}</span>
|
||||
${item.altText
|
||||
? html`
|
||||
<span slot="secondary" class="secondary"
|
||||
>${item.altText}</span
|
||||
>
|
||||
<span slot="secondary" class="secondary">${item.altText}</span>
|
||||
`
|
||||
: null}
|
||||
${this._commandTriggered === index
|
||||
@ -124,73 +197,78 @@ export class QuickBar extends LitElement {
|
||||
></ha-circular-progress>`
|
||||
: null}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async processItemAndCloseDialog(ev: SingleSelectedEvent) {
|
||||
const index = ev.detail.index;
|
||||
const item = (ev.target as any).items[index].item;
|
||||
|
||||
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<string>) {
|
||||
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<QuickBarItem>(
|
||||
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<QuickBarItem>(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%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -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)",
|
||||
|
Loading…
x
Reference in New Issue
Block a user