mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 07:16:39 +00:00
Make enter key execute first filtered item in Quick Bar (#7288)
* Make enter key execute first filtered item in quick open dialog * Add support for arrow up and down moving in and out of list * Add support for typing letters even when arrowing around the list * Address review comments and clean up the single-character keyboard event logic * Address comments. Move activated index along with arrow keys * Add loading spinner and remove memoization
This commit is contained in:
parent
1f361b7b10
commit
6165cb0f83
@ -1,3 +1,4 @@
|
|||||||
|
import "../../components/ha-circular-progress";
|
||||||
import "../../components/ha-header-bar";
|
import "../../components/ha-header-bar";
|
||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
@ -9,6 +10,7 @@ import {
|
|||||||
internalProperty,
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
|
query,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import "../../components/ha-dialog";
|
import "../../components/ha-dialog";
|
||||||
@ -23,7 +25,6 @@ import { domainToName } from "../../data/integration";
|
|||||||
import { QuickBarParams } from "./show-dialog-quick-bar";
|
import { QuickBarParams } from "./show-dialog-quick-bar";
|
||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { compare } from "../../common/string/compare";
|
import { compare } from "../../common/string/compare";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
|
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
|
||||||
|
|
||||||
interface CommandItem extends ServiceCallRequest {
|
interface CommandItem extends ServiceCallRequest {
|
||||||
@ -44,6 +45,14 @@ export class QuickBar extends LitElement {
|
|||||||
|
|
||||||
@internalProperty() private _commandMode = false;
|
@internalProperty() private _commandMode = false;
|
||||||
|
|
||||||
|
@internalProperty() private _commandTriggered = -1;
|
||||||
|
|
||||||
|
@internalProperty() private _activatedIndex = 0;
|
||||||
|
|
||||||
|
@query("paper-input", false) private _filterInputField?: HTMLElement;
|
||||||
|
|
||||||
|
@query("mwc-list-item", false) private _firstListItem?: HTMLElement;
|
||||||
|
|
||||||
public async showDialog(params: QuickBarParams) {
|
public async showDialog(params: QuickBarParams) {
|
||||||
this._commandMode = params.commandMode || false;
|
this._commandMode = params.commandMode || false;
|
||||||
this._opened = true;
|
this._opened = true;
|
||||||
@ -56,6 +65,8 @@ export class QuickBar extends LitElement {
|
|||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this._opened = false;
|
this._opened = false;
|
||||||
this._itemFilter = "";
|
this._itemFilter = "";
|
||||||
|
this._commandTriggered = -1;
|
||||||
|
this._resetActivatedIndex();
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,46 +88,68 @@ export class QuickBar extends LitElement {
|
|||||||
)}
|
)}
|
||||||
type="search"
|
type="search"
|
||||||
value=${this._commandMode ? `>${this._itemFilter}` : this._itemFilter}
|
value=${this._commandMode ? `>${this._itemFilter}` : this._itemFilter}
|
||||||
|
@keydown=${this._handleInputKeyDown}
|
||||||
|
@focus=${this._resetActivatedIndex}
|
||||||
></paper-input>
|
></paper-input>
|
||||||
${this._commandMode
|
${this._commandMode
|
||||||
? this.renderCommandsList(this._itemFilter)
|
? this.renderCommandsList()
|
||||||
: this.renderEntityList(this._itemFilter)}
|
: this.renderEntityList()}
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderCommandsList = memoizeOne((filter) => {
|
protected renderCommandsList() {
|
||||||
const items = this._filterCommandItems(this._commandItems, filter);
|
const items = this._filterCommandItems(
|
||||||
|
this._commandItems,
|
||||||
|
this._itemFilter
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<mwc-list @selected=${this._processItemAndCloseDialog}>
|
<mwc-list activatable @selected=${this._processCommand}>
|
||||||
${items.map(
|
${items.map(
|
||||||
(item) => html`
|
(item, index) => html`
|
||||||
<mwc-list-item .item=${item} graphic="icon">
|
<mwc-list-item
|
||||||
|
.activated=${index === this._activatedIndex}
|
||||||
|
.item=${item}
|
||||||
|
.index=${index}
|
||||||
|
@keydown=${this._handleListItemKeyDown}
|
||||||
|
hasMeta
|
||||||
|
graphic="icon"
|
||||||
|
>
|
||||||
<ha-icon
|
<ha-icon
|
||||||
.icon=${domainIcon(item.domain)}
|
.icon=${domainIcon(item.domain)}
|
||||||
slot="graphic"
|
slot="graphic"
|
||||||
></ha-icon>
|
></ha-icon>
|
||||||
${item.text}
|
${item.text}
|
||||||
|
${this._commandTriggered === index
|
||||||
|
? html`<ha-circular-progress
|
||||||
|
size="small"
|
||||||
|
active
|
||||||
|
slot="meta"
|
||||||
|
></ha-circular-progress>`
|
||||||
|
: null}
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
});
|
}
|
||||||
|
|
||||||
protected renderEntityList = memoizeOne((filter) => {
|
protected renderEntityList() {
|
||||||
const entities = this._filterEntityItems(filter);
|
const entities = this._filterEntityItems(this._itemFilter);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<mwc-list activatable @selected=${this._entityMoreInfo}>
|
<mwc-list activatable @selected=${this._entityMoreInfo}>
|
||||||
${entities.map((entity) => {
|
${entities.map((entity, index) => {
|
||||||
const domain = computeDomain(entity.entity_id);
|
const domain = computeDomain(entity.entity_id);
|
||||||
return html`
|
return html`
|
||||||
<mwc-list-item
|
<mwc-list-item
|
||||||
twoline
|
twoline
|
||||||
.entityId=${entity.entity_id}
|
.entityId=${entity.entity_id}
|
||||||
graphic="avatar"
|
graphic="avatar"
|
||||||
|
.activated=${index === this._activatedIndex}
|
||||||
|
.index=${index}
|
||||||
|
@keydown=${this._handleListItemKeyDown}
|
||||||
>
|
>
|
||||||
<ha-icon .icon=${domainIcon(domain)} slot="graphic"></ha-icon>
|
<ha-icon .icon=${domainIcon(domain)} slot="graphic"></ha-icon>
|
||||||
${entity.attributes?.friendly_name
|
${entity.attributes?.friendly_name
|
||||||
@ -136,7 +169,39 @@ export class QuickBar extends LitElement {
|
|||||||
})}
|
})}
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private _resetActivatedIndex() {
|
||||||
|
this._activatedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleInputKeyDown(ev: KeyboardEvent) {
|
||||||
|
if (ev.code === "Enter") {
|
||||||
|
this._firstListItem?.click();
|
||||||
|
} else if (ev.code === "ArrowDown") {
|
||||||
|
ev.preventDefault();
|
||||||
|
this._firstListItem?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleListItemKeyDown(ev: KeyboardEvent) {
|
||||||
|
const isSingleCharacter = ev.key.length === 1;
|
||||||
|
const isFirstListItem = (ev.target as any).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) {
|
||||||
|
this._filterInputField?.focus();
|
||||||
|
this._resetActivatedIndex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _entityFilterChanged(ev: PolymerChangedEvent<string>) {
|
private _entityFilterChanged(ev: PolymerChangedEvent<string>) {
|
||||||
const newFilter = ev.detail.value;
|
const newFilter = ev.detail.value;
|
||||||
@ -180,32 +245,49 @@ export class QuickBar extends LitElement {
|
|||||||
private _filterEntityItems(filter: string): HassEntity[] {
|
private _filterEntityItems(filter: string): HassEntity[] {
|
||||||
return this._entities
|
return this._entities
|
||||||
.filter(({ entity_id, attributes: { friendly_name } }) => {
|
.filter(({ entity_id, attributes: { friendly_name } }) => {
|
||||||
const values = [entity_id];
|
const values = [entity_id];
|
||||||
if (friendly_name) {
|
if (friendly_name) {
|
||||||
values.push(friendly_name);
|
values.push(friendly_name);
|
||||||
}
|
}
|
||||||
return fuzzySequentialMatch(filter.toLowerCase(), values);}
|
return fuzzySequentialMatch(filter.toLowerCase(), values);
|
||||||
)
|
})
|
||||||
.sort((entityA, entityB) =>
|
.sort((entityA, entityB) =>
|
||||||
compare(entityA.entity_id, entityB.entity_id)
|
compare(entityA.entity_id, entityB.entity_id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _processItemAndCloseDialog(ev: SingleSelectedEvent) {
|
private async _processCommand(ev: SingleSelectedEvent) {
|
||||||
const _index = ev.detail.index;
|
const index = ev.detail.index;
|
||||||
const item = (ev.target as any).items[_index].item;
|
const item = (ev.target as any).items[index].item;
|
||||||
|
|
||||||
await this.hass.callService(item.domain, item.service, item.serviceData);
|
this._commandTriggered = index;
|
||||||
|
|
||||||
this.closeDialog();
|
this._runCommandAndCloseDialog({
|
||||||
|
domain: item.domain,
|
||||||
|
service: item.service,
|
||||||
|
serviceData: item.serviceData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _runCommandAndCloseDialog(request?: ServiceCallRequest) {
|
||||||
|
if (!request) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hass
|
||||||
|
.callService(request.domain, request.service, request.serviceData)
|
||||||
|
.then(() => this.closeDialog());
|
||||||
}
|
}
|
||||||
|
|
||||||
private _entityMoreInfo(ev: SingleSelectedEvent) {
|
private _entityMoreInfo(ev: SingleSelectedEvent) {
|
||||||
const _index = ev.detail.index;
|
const index = ev.detail.index;
|
||||||
const entityId = (ev.target as any).items[_index].entityId;
|
const entityId = (ev.target as any).items[index].entityId;
|
||||||
|
|
||||||
|
this._launchMoreInfoDialog(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _launchMoreInfoDialog(entityId) {
|
||||||
fireEvent(this, "hass-more-info", { entityId });
|
fireEvent(this, "hass-more-info", { entityId });
|
||||||
|
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user