Add dialog to show keyboard shortcuts (#24918)

* Add dialog to show keyboard shortcuts we have

* Add missing translation

* No need for function anymore

* Run updated prettier

* Replace translation keys

* Replace translation keys

* Remove automations for now

* Check whether shortcuts are enabled

* Use plain css for shortcuts
This commit is contained in:
Jan-Philipp Benecke 2025-04-06 09:02:52 +02:00 committed by GitHub
parent daf4158fa0
commit 671049beb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 378 additions and 25 deletions

View File

@ -0,0 +1,213 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import { createCloseHeading } from "../../components/ha-dialog";
import type { HomeAssistant } from "../../types";
import { haStyleDialog } from "../../resources/styles";
import "../../components/ha-alert";
import "../../components/chips/ha-assist-chip";
import type { LocalizeKeys } from "../../common/translations/localize";
interface Text {
type: "text";
key: LocalizeKeys;
}
interface Shortcut {
type: "shortcut";
shortcut: string[];
key: LocalizeKeys;
}
interface Section {
key: LocalizeKeys;
items: (Text | Shortcut)[];
}
const _SHORTCUTS: Section[] = [
{
key: "ui.dialogs.shortcuts.searching.title",
items: [
{ type: "text", key: "ui.dialogs.shortcuts.searching.on_any_page" },
{
type: "shortcut",
shortcut: ["C"],
key: "ui.dialogs.shortcuts.searching.search_command",
},
{
type: "shortcut",
shortcut: ["E"],
key: "ui.dialogs.shortcuts.searching.search_entities",
},
{
type: "shortcut",
shortcut: ["D"],
key: "ui.dialogs.shortcuts.searching.search_devices",
},
{
type: "text",
key: "ui.dialogs.shortcuts.searching.on_pages_with_tables",
},
{
type: "shortcut",
shortcut: ["CRTL/CMND", "F"],
key: "ui.dialogs.shortcuts.searching.search_in_table",
},
],
},
{
key: "ui.dialogs.shortcuts.assist.title",
items: [
{
type: "shortcut",
shortcut: ["A"],
key: "ui.dialogs.shortcuts.assist.open_assist",
},
],
},
{
key: "ui.dialogs.shortcuts.charts.title",
items: [
{
type: "shortcut",
shortcut: ["CRTL/CMND", "DRAG"],
key: "ui.dialogs.shortcuts.charts.drag_to_zoom",
},
{
type: "shortcut",
shortcut: ["CRTL/CMND", "SCROLL WHEEL"],
key: "ui.dialogs.shortcuts.charts.scroll_to_zoom",
},
{
type: "shortcut",
shortcut: ["DOUBLE CLICK"],
key: "ui.dialogs.shortcuts.charts.double_click",
},
],
},
{
key: "ui.dialogs.shortcuts.other.title",
items: [
{
type: "shortcut",
shortcut: ["M"],
key: "ui.dialogs.shortcuts.other.my_link",
},
],
},
];
@customElement("dialog-shortcuts")
class DialogShortcuts extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
public async showDialog(): Promise<void> {
this._opened = true;
}
public async closeDialog(): Promise<void> {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _renderShortcut(shortcut: string[], translationKey: LocalizeKeys) {
return html`
<div class="shortcut">
${shortcut.map((key) => html` <span>${key}</span>`)}
${this.hass.localize(translationKey)}
</div>
`;
}
protected render() {
if (!this._opened) {
return nothing;
}
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
defaultAction="ignore"
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.shortcuts.title")
)}
>
<div class="content">
${_SHORTCUTS.map(
(section) => html`
<h3>${this.hass.localize(section.key)}</h3>
<div class="items">
${section.items.map((item) => {
if (item.type === "text") {
return html`<p>${this.hass.localize(item.key)}</p>`;
}
if (item.type === "shortcut") {
return this._renderShortcut(item.shortcut, item.key);
}
return nothing;
})}
</div>
`
)}
</div>
<ha-alert>
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
user_profile: html`<a href="/profile/general#shortcuts"
>${this.hass.localize(
"ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile"
)}</a
>`,
})}
</ha-alert>
</ha-dialog>
`;
}
static styles = [
haStyleDialog,
css`
ha-dialog {
--dialog-z-index: 15;
}
h3:first-of-type {
margin-top: 0;
}
.content {
margin-bottom: 24px;
}
.shortcut {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin: 4px 0;
}
span {
padding: 8px;
border: 1px solid var(--divider-color);
border-radius: 8px;
}
.items p {
margin-bottom: 8px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-shortcuts": DialogShortcuts;
}
}

View File

@ -0,0 +1,8 @@
import { fireEvent } from "../../common/dom/fire_event";
export const showShortcutsDialog = (element: HTMLElement) =>
fireEvent(element, "show-dialog", {
dialogTag: "dialog-shortcuts",
dialogImport: () => import("./dialog-shortcuts"),
dialogParams: {},
});

View File

@ -48,8 +48,9 @@ import { configSections } from "../ha-panel-config";
import "../repairs/ha-config-repairs"; import "../repairs/ha-config-repairs";
import "./ha-config-navigation"; import "./ha-config-navigation";
import "./ha-config-updates"; import "./ha-config-updates";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
const randomTip = (hass: HomeAssistant, narrow: boolean) => { const randomTip = (openFn: any, hass: HomeAssistant, narrow: boolean) => {
const weighted: string[] = []; const weighted: string[] = [];
let tips = [ let tips = [
{ {
@ -105,18 +106,28 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
]; ];
if (hass?.enableShortcuts) { if (hass?.enableShortcuts) {
const localizeParam = {
keyboard_shortcut: html`<a href="#" @click=${openFn}
>${hass.localize("ui.tips.keyboard_shortcut")}</a
>`,
};
tips.push( tips.push(
{ {
content: hass.localize("ui.tips.key_c_hint"), content: hass.localize("ui.tips.key_c_tip", localizeParam),
weight: 1, weight: 1,
narrow: false, narrow: false,
}, },
{ {
content: hass.localize("ui.tips.key_m_hint"), content: hass.localize("ui.tips.key_m_tip", localizeParam),
weight: 1, weight: 1,
narrow: false, narrow: false,
}, },
{ content: hass.localize("ui.tips.key_a_hint"), weight: 1, narrow: false } {
content: hass.localize("ui.tips.key_a_tip", localizeParam),
weight: 1,
narrow: false,
}
); );
} }
@ -318,10 +329,16 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
super.updated(changedProps); super.updated(changedProps);
if (!this._tip && changedProps.has("hass")) { if (!this._tip && changedProps.has("hass")) {
this._tip = randomTip(this.hass, this.narrow); this._tip = randomTip(this._openShortcutDialog, this.hass, this.narrow);
} }
} }
private _openShortcutDialog(ev: Event) {
ev.preventDefault();
showShortcutsDialog(this);
}
private _filterUpdateEntitiesWithInstall = memoizeOne( private _filterUpdateEntitiesWithInstall = memoizeOne(
( (
entities: HomeAssistant["states"], entities: HomeAssistant["states"],
@ -339,10 +356,16 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
); );
private _showQuickBar(): void { private _showQuickBar(): void {
const params = {
keyboard_shortcut: html`<a href="#" @click=${this._openShortcutDialog}
>${this.hass.localize("ui.tips.keyboard_shortcut")}</a
>`,
};
showQuickBar(this, { showQuickBar(this, {
mode: QuickBarMode.Command, mode: QuickBarMode.Command,
hint: this.hass.enableShortcuts hint: this.hass.enableShortcuts
? this.hass.localize("ui.dialogs.quick-bar.key_c_hint") ? this.hass.localize("ui.dialogs.quick-bar.key_c_tip", params)
: undefined, : undefined,
}); });
} }

View File

@ -4,7 +4,9 @@ import {
mdiFileDocument, mdiFileDocument,
mdiHandsPray, mdiHandsPray,
mdiHelp, mdiHelp,
mdiKeyboard,
mdiNewspaperVariant, mdiNewspaperVariant,
mdiOpenInNew,
mdiTshirtCrew, mdiTshirtCrew,
} from "@mdi/js"; } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
@ -23,6 +25,8 @@ import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types"; import type { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import "../../../components/ha-icon-next";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
const JS_TYPE = __BUILD__; const JS_TYPE = __BUILD__;
const JS_VERSION = __VERSION__; const JS_VERSION = __VERSION__;
@ -170,12 +174,26 @@ class HaConfigInfo extends LitElement {
<ha-card outlined class="pages"> <ha-card outlined class="pages">
<mwc-list> <mwc-list>
<ha-list-item graphic="avatar" @click=${this._showShortcuts}>
<div
slot="graphic"
class="icon-background"
style="background-color: #9e4dd1;"
>
<ha-svg-icon .path=${mdiKeyboard}></ha-svg-icon>
</div>
<span
>${this.hass.localize("ui.panel.config.info.shortcuts")}</span
>
</ha-list-item>
${PAGES.map( ${PAGES.map(
(page) => html` (page) => html`
<ha-clickable-list-item <ha-clickable-list-item
graphic="avatar" graphic="avatar"
open-new-tab open-new-tab
href=${documentationUrl(this.hass, page.path)} href=${documentationUrl(this.hass, page.path)}
hasMeta
> >
<div <div
slot="graphic" slot="graphic"
@ -189,6 +207,10 @@ class HaConfigInfo extends LitElement {
`ui.panel.config.info.items.${page.name}` `ui.panel.config.info.items.${page.name}`
)} )}
</span> </span>
<ha-svg-icon
slot="meta"
.path=${mdiOpenInNew}
></ha-svg-icon>
</ha-clickable-list-item> </ha-clickable-list-item>
` `
)} )}
@ -240,6 +262,11 @@ class HaConfigInfo extends LitElement {
this._osInfo = osInfo; this._osInfo = osInfo;
} }
private _showShortcuts(ev): void {
ev.stopPropagation();
showShortcutsDialog(this);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
@ -331,11 +358,12 @@ class HaConfigInfo extends LitElement {
--mdc-list-vertical-padding: 0; --mdc-list-vertical-padding: 0;
} }
ha-clickable-list-item { ha-clickable-list-item,
ha-list-item {
height: 64px; height: 64px;
} }
ha-svg-icon { .icon-background ha-svg-icon {
height: 24px; height: 24px;
width: 24px; width: 24px;
display: block; display: block;

View File

@ -33,6 +33,7 @@ import "../../../components/search-input";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
@customElement("developer-tools-state") @customElement("developer-tools-state")
class HaPanelDevState extends LitElement { class HaPanelDevState extends LitElement {
@ -130,7 +131,13 @@ class HaPanelDevState extends LitElement {
></ha-entity-picker> ></ha-entity-picker>
${this.hass.enableShortcuts ${this.hass.enableShortcuts
? html`<ha-tip .hass=${this.hass} ? html`<ha-tip .hass=${this.hass}
>${this.hass.localize("ui.tips.key_e_hint")}</ha-tip >${this.hass.localize("ui.tips.key_e_tip", {
keyboard_shortcut: html`<a
href="#"
@click=${this._openShortcutDialog}
>${this.hass.localize("ui.tips.keyboard_shortcut")}</a
>`,
})}</ha-tip
>` >`
: nothing} : nothing}
<ha-textfield <ha-textfield
@ -566,6 +573,11 @@ class HaPanelDevState extends LitElement {
this._validJSON = ev.detail.isValid; this._validJSON = ev.detail.isValid;
} }
private _openShortcutDialog(ev: Event) {
ev.preventDefault();
showShortcutsDialog(this);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -79,6 +79,7 @@ import "./views/hui-view";
import "./views/hui-view-container"; import "./views/hui-view-container";
import type { HUIView } from "./views/hui-view"; import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background"; import "./views/hui-view-background";
import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog";
@customElement("hui-root") @customElement("hui-root")
class HUIRoot extends LitElement { class HUIRoot extends LitElement {
@ -154,6 +155,7 @@ class HUIRoot extends LitElement {
visible: boolean | undefined; visible: boolean | undefined;
overflow: boolean; overflow: boolean;
overflow_can_promote?: boolean; overflow_can_promote?: boolean;
suffix?: string;
}[] = [ }[] = [
{ {
icon: mdiFormatListBulletedTriangle, icon: mdiFormatListBulletedTriangle,
@ -185,20 +187,22 @@ class HUIRoot extends LitElement {
}, },
{ {
icon: mdiMagnify, icon: mdiMagnify,
key: "ui.panel.lovelace.menu.search", key: "ui.panel.lovelace.menu.search_entities",
buttonAction: this._showQuickBar, buttonAction: this._showQuickBar,
overflowAction: this._handleShowQuickBar, overflowAction: this._handleShowQuickBar,
visible: !this._editMode, visible: !this._editMode,
overflow: this.narrow, overflow: this.narrow,
suffix: this.hass.enableShortcuts ? "(E)" : undefined,
}, },
{ {
icon: mdiCommentProcessingOutline, icon: mdiCommentProcessingOutline,
key: "ui.panel.lovelace.menu.assist", key: "ui.panel.lovelace.menu.assist_tooltip",
buttonAction: this._showVoiceCommandDialog, buttonAction: this._showVoiceCommandDialog,
overflowAction: this._handleShowVoiceCommandDialog, overflowAction: this._handleShowVoiceCommandDialog,
visible: visible:
!this._editMode && this._conversation(this.hass.config.components), !this._editMode && this._conversation(this.hass.config.components),
overflow: this.narrow, overflow: this.narrow,
suffix: this.hass.enableShortcuts ? "(A)" : undefined,
}, },
{ {
icon: mdiRefresh, icon: mdiRefresh,
@ -247,12 +251,16 @@ class HUIRoot extends LitElement {
buttonItems.forEach((i) => { buttonItems.forEach((i) => {
result.push( result.push(
html`<ha-icon-button html`<ha-tooltip
slot="actionItems" slot="actionItems"
.label=${this.hass!.localize(i.key)} placement="bottom"
.path=${i.icon} .content=${[this.hass!.localize(i.key), i.suffix].join(" ")}
@click=${i.buttonAction} >
></ha-icon-button>` <ha-icon-button
.path=${i.icon}
@click=${i.buttonAction}
></ha-icon-button>
</ha-tooltip>`
); );
}); });
if (overflowItems.length && !overflowCanPromote) { if (overflowItems.length && !overflowCanPromote) {
@ -263,7 +271,7 @@ class HUIRoot extends LitElement {
graphic="icon" graphic="icon"
@request-selected=${i.overflowAction} @request-selected=${i.overflowAction}
> >
${this.hass!.localize(i.key)} ${[this.hass!.localize(i.key), i.suffix].join(" ")}
<ha-svg-icon slot="graphic" .path=${i.icon}></ha-svg-icon> <ha-svg-icon slot="graphic" .path=${i.icon}></ha-svg-icon>
</mwc-list-item>` </mwc-list-item>`
); );
@ -689,10 +697,16 @@ class HUIRoot extends LitElement {
} }
private _showQuickBar(): void { private _showQuickBar(): void {
const params = {
keyboard_shortcut: html`<a href="#" @click=${this._openShortcutDialog}
>${this.hass.localize("ui.tips.keyboard_shortcut")}</a
>`,
};
showQuickBar(this, { showQuickBar(this, {
mode: QuickBarMode.Entity, mode: QuickBarMode.Entity,
hint: this.hass.enableShortcuts hint: this.hass.enableShortcuts
? this.hass.localize("ui.tips.key_e_hint") ? this.hass.localize("ui.tips.key_e_tip", params)
: undefined, : undefined,
}); });
} }
@ -1005,6 +1019,11 @@ class HUIRoot extends LitElement {
root.appendChild(view); root.appendChild(view);
} }
private _openShortcutDialog(ev: Event) {
ev.preventDefault();
showShortcutsDialog(this);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -27,6 +27,7 @@ import "./ha-pick-time-zone-row";
import "./ha-push-notifications-row"; import "./ha-push-notifications-row";
import "./ha-set-suspend-row"; import "./ha-set-suspend-row";
import "./ha-set-vibrate-row"; import "./ha-set-vibrate-row";
import { nextRender } from "../../common/util/render-status";
@customElement("ha-profile-section-general") @customElement("ha-profile-section-general")
class HaProfileSectionGeneral extends LitElement { class HaProfileSectionGeneral extends LitElement {
@ -54,6 +55,8 @@ class HaProfileSectionGeneral extends LitElement {
if (this.hass) { if (this.hass) {
this._getCoreData(); this._getCoreData();
} }
this._scrollToHash();
} }
public firstUpdated() { public firstUpdated() {
@ -70,6 +73,21 @@ class HaProfileSectionGeneral extends LitElement {
} }
} }
private async _scrollToHash() {
await nextRender();
const hash = window.location.hash.substring(1);
if (hash) {
const element = this.shadowRoot!.getElementById(hash);
element?.scrollIntoView();
this._clearHash();
}
}
private _clearHash() {
history.replaceState(null, "", window.location.pathname);
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
@ -202,6 +220,7 @@ class HaProfileSectionGeneral extends LitElement {
.hass=${this.hass} .hass=${this.hass}
></ha-set-suspend-row> ></ha-set-suspend-row>
<ha-enable-shortcuts-row <ha-enable-shortcuts-row
id="shortcuts"
.narrow=${this.narrow} .narrow=${this.narrow}
.hass=${this.hass} .hass=${this.hass}
></ha-enable-shortcuts-row> ></ha-enable-shortcuts-row>

View File

@ -1225,7 +1225,7 @@
"filter_placeholder": "Search entities", "filter_placeholder": "Search entities",
"filter_placeholder_devices": "Search devices", "filter_placeholder_devices": "Search devices",
"title": "Quick search", "title": "Quick search",
"key_c_hint": "Press 'c' on any page to open the command dialog", "key_c_tip": "[%key:ui::tips::key_c_tip%]",
"nothing_found": "Nothing found!" "nothing_found": "Nothing found!"
}, },
"voice_command": { "voice_command": {
@ -1890,6 +1890,34 @@
"code_instructions": "Search for the sharing mode in the app of your controller, and activate it. You will get a setup code, enter that below.", "code_instructions": "Search for the sharing mode in the app of your controller, and activate it. You will get a setup code, enter that below.",
"setup_code": "Setup code" "setup_code": "Setup code"
} }
},
"shortcuts": {
"title": "Shortcuts",
"enable_shortcuts_hint": "For keyboard shortcuts to work, make sure you have them enabled in your {user_profile}.",
"enable_shortcuts_hint_user_profile": "user profile",
"searching": {
"title": "Searching",
"on_any_page": "On any page",
"on_pages_with_tables": "On pages with tables",
"search_command": "search command",
"search_entities": "search entities",
"search_devices": "search devices",
"search_in_table": "to search in tables"
},
"assist": {
"title": "Assist",
"open_assist": "open Assist dialog"
},
"charts": {
"title": "Charts",
"drag_to_zoom": "to zoom in part of a chart",
"scroll_to_zoom": "to zoom in or out",
"double_click": "to zoom in on part of the chart"
},
"other": {
"title": "Other",
"my_link": "get My Home Assistant link"
}
} }
}, },
"weekdays": { "weekdays": {
@ -3052,7 +3080,8 @@
"bug": "Bug reports", "bug": "Bug reports",
"help": "Help", "help": "Help",
"license": "License" "license": "License"
} },
"shortcuts": "Shortcuts"
}, },
"logs": { "logs": {
"caption": "Logs", "caption": "Logs",
@ -6507,8 +6536,9 @@
"menu": { "menu": {
"configure_ui": "Edit dashboard", "configure_ui": "Edit dashboard",
"help": "Help", "help": "Help",
"search": "Entity search", "search_entities": "Search entities",
"assist": "Assist", "assist": "Assist",
"assist_tooltip": "Assist",
"reload_resources": "Reload resources", "reload_resources": "Reload resources",
"exit_edit_mode": "Done", "exit_edit_mode": "Done",
"close": "Close" "close": "Close"
@ -8554,10 +8584,11 @@
} }
}, },
"tips": { "tips": {
"key_c_hint": "Press 'c' on any page to open the command dialog", "keyboard_shortcut": "keyboard shortcut",
"key_e_hint": "Press 'e' on any page to open the entity search dialog", "key_c_tip": "Press {keyboard_shortcut} 'c' on any page to open the command dialog",
"key_m_hint": "Press 'm' on any page to get the My Home Assistant link", "key_e_tip": "Press {keyboard_shortcut} 'e' on any page to open the entity search dialog",
"key_a_hint": "Press 'a' on any page to open the Assist dialog" "key_m_tip": "Press {keyboard_shortcut} 'm' on any page to get the My Home Assistant link",
"key_a_tip": "Press {keyboard_shortcut} 'a' on any page to open the Assist dialog"
} }
}, },
"landing-page": { "landing-page": {