Improve performance of quick bar (#7359)

Co-authored-by: Donnie <donniekarnsinsb@hotmail.com>
This commit is contained in:
Bram Kragten 2020-10-21 13:07:44 +02:00 committed by GitHub
parent 4a5935ee36
commit 54ec37994c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 217 additions and 105 deletions

View File

@ -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>

View File

@ -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
);

View File

@ -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`
<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`
<mwc-list-item
.twoline=${Boolean(item.altText)}
.activated=${index === this._activatedIndex}
.item=${item}
.index=${index}
@keydown=${this._handleListItemKeyDown}
hasMeta
graphic=${item.altText ? "avatar" : "icon"}
>
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
<span>${item.text}</span>
${item.altText
? html`
<span slot="secondary" class="secondary"
>${item.altText}</span
>
`
: null}
${this._commandTriggered === index
? html`<ha-circular-progress
size="small"
active
slot="meta"
></ha-circular-progress>`
: null}
</mwc-list-item>
`
)}
</mwc-list>
@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 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`
<mwc-list-item
.twoline=${Boolean(item.altText)}
.item=${item}
index=${ifDefined(index)}
hasMeta
graphic=${item.altText ? "avatar" : "icon"}
>
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
<span>${item.text}</span>
${item.altText
? html`
<span slot="secondary" class="secondary">${item.altText}</span>
`
: null}
${this._commandTriggered === index
? html`<ha-circular-progress
size="small"
active
slot="meta"
></ha-circular-progress>`
: null}
</mwc-list-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%;
}
`,
];
}

View File

@ -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)",