Skip to content

Commit c7f3ede

Browse files
authored
Merge branch 'dev' into energy-panel
2 parents f45e998 + 20d0548 commit c7f3ede

File tree

7 files changed

+201
-32
lines changed

7 files changed

+201
-32
lines changed

src/common/keyboard/shortcuts.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { tinykeys } from "tinykeys";
2+
import { canOverrideAlphanumericInput } from "../dom/can-override-input";
3+
4+
/**
5+
* A function to handle a keyboard shortcut.
6+
*/
7+
export type ShortcutHandler = (event: KeyboardEvent) => void;
8+
9+
/**
10+
* Configuration for a keyboard shortcut.
11+
*/
12+
export interface ShortcutConfig {
13+
handler: ShortcutHandler;
14+
/**
15+
* If true, allows shortcuts even when text is selected.
16+
* Default is false to avoid interrupting copy/paste.
17+
*/
18+
allowWhenTextSelected?: boolean;
19+
}
20+
21+
/**
22+
* Register keyboard shortcuts using tinykeys.
23+
* Automatically blocks shortcuts in input fields and during text selection.
24+
*/
25+
function registerShortcuts(
26+
shortcuts: Record<string, ShortcutConfig>
27+
): () => void {
28+
const wrappedShortcuts: Record<string, ShortcutHandler> = {};
29+
30+
Object.entries(shortcuts).forEach(([key, config]) => {
31+
wrappedShortcuts[key] = (event: KeyboardEvent) => {
32+
if (!canOverrideAlphanumericInput(event.composedPath())) {
33+
return;
34+
}
35+
if (!config.allowWhenTextSelected && window.getSelection()?.toString()) {
36+
return;
37+
}
38+
config.handler(event);
39+
};
40+
});
41+
42+
return tinykeys(window, wrappedShortcuts);
43+
}
44+
45+
/**
46+
* Manages keyboard shortcuts registration and cleanup.
47+
*/
48+
export class ShortcutManager {
49+
private _disposer?: () => void;
50+
51+
/**
52+
* Register keyboard shortcuts.
53+
* Uses tinykeys syntax: https://github.com/jamiebuilds/tinykeys#usage
54+
*/
55+
public add(shortcuts: Record<string, ShortcutConfig>) {
56+
this._disposer?.();
57+
this._disposer = registerShortcuts(shortcuts);
58+
}
59+
60+
/**
61+
* Remove all registered shortcuts.
62+
*/
63+
public remove() {
64+
this._disposer?.();
65+
this._disposer = undefined;
66+
}
67+
}

src/panels/lovelace/card-features/hui-bar-gauge-card-feature.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { css, LitElement, nothing, html } from "lit";
22
import { customElement, property, state } from "lit/decorators";
33
import { computeDomain } from "../../../common/entity/compute_domain";
4+
import { isNumericFromAttributes } from "../../../common/number/format_number";
45
import type { HomeAssistant } from "../../../types";
5-
import type { LovelaceCardFeature } from "../types";
6+
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
67
import type {
78
LovelaceCardFeatureContext,
89
BarGaugeCardFeatureConfig,
@@ -17,7 +18,7 @@ export const supportsBarGaugeCardFeature = (
1718
: undefined;
1819
if (!stateObj) return false;
1920
const domain = computeDomain(stateObj.entity_id);
20-
return domain === "sensor" && stateObj.attributes.unit_of_measurement === "%";
21+
return domain === "sensor" && isNumericFromAttributes(stateObj.attributes);
2122
};
2223

2324
@customElement("hui-bar-gauge-card-feature")
@@ -34,6 +35,11 @@ class HuiBarGaugeCardFeature extends LitElement implements LovelaceCardFeature {
3435
};
3536
}
3637

38+
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
39+
await import("../editor/config-elements/hui-bar-gauge-card-feature-editor");
40+
return document.createElement("hui-bar-gauge-card-feature-editor");
41+
}
42+
3743
public setConfig(config: BarGaugeCardFeatureConfig): void {
3844
if (!config) {
3945
throw new Error("Invalid configuration");
@@ -53,8 +59,20 @@ class HuiBarGaugeCardFeature extends LitElement implements LovelaceCardFeature {
5359
return nothing;
5460
}
5561
const stateObj = this.hass.states[this.context.entity_id];
56-
const value = stateObj.state;
57-
return html`<div style="width: ${value}%"></div>
62+
const min = this._config.min ?? 0;
63+
const max = this._config.max ?? 100;
64+
const value = parseFloat(stateObj.state);
65+
66+
if (isNaN(value) || min >= max) {
67+
return nothing;
68+
}
69+
70+
const percentage = Math.max(
71+
0,
72+
Math.min(100, ((value - min) / (max - min)) * 100)
73+
);
74+
75+
return html`<div style="width: ${percentage}%"></div>
5876
<div class="bar-gauge-background"></div>`;
5977
}
6078

src/panels/lovelace/card-features/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ export interface AreaControlsCardFeatureConfig {
226226

227227
export interface BarGaugeCardFeatureConfig {
228228
type: "bar-gauge";
229+
min?: number;
230+
max?: number;
229231
}
230232

231233
export type LovelaceCardFeaturePosition = "bottom" | "inline";
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { html, LitElement, nothing } from "lit";
2+
import { customElement, property, state } from "lit/decorators";
3+
import memoizeOne from "memoize-one";
4+
import { fireEvent } from "../../../../common/dom/fire_event";
5+
import "../../../../components/ha-form/ha-form";
6+
import type { SchemaUnion } from "../../../../components/ha-form/types";
7+
import type { HomeAssistant } from "../../../../types";
8+
import type {
9+
BarGaugeCardFeatureConfig,
10+
LovelaceCardFeatureContext,
11+
} from "../../card-features/types";
12+
import type { LovelaceCardFeatureEditor } from "../../types";
13+
14+
@customElement("hui-bar-gauge-card-feature-editor")
15+
export class HuiBarGaugeCardFeatureEditor
16+
extends LitElement
17+
implements LovelaceCardFeatureEditor
18+
{
19+
@property({ attribute: false }) public hass?: HomeAssistant;
20+
21+
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
22+
23+
@state() private _config?: BarGaugeCardFeatureConfig;
24+
25+
public setConfig(config: BarGaugeCardFeatureConfig): void {
26+
this._config = config;
27+
}
28+
29+
private _schema = memoizeOne(
30+
() =>
31+
[
32+
{
33+
name: "min",
34+
default: 0,
35+
selector: {
36+
number: {
37+
mode: "box",
38+
},
39+
},
40+
},
41+
{
42+
name: "max",
43+
default: 100,
44+
selector: {
45+
number: {
46+
mode: "box",
47+
},
48+
},
49+
},
50+
] as const
51+
);
52+
53+
protected render() {
54+
if (!this.hass || !this._config) {
55+
return nothing;
56+
}
57+
58+
const schema = this._schema();
59+
60+
return html`
61+
<ha-form
62+
.hass=${this.hass}
63+
.data=${this._config}
64+
.schema=${schema}
65+
.computeLabel=${this._computeLabelCallback}
66+
@value-changed=${this._valueChanged}
67+
></ha-form>
68+
`;
69+
}
70+
71+
private _valueChanged(ev: CustomEvent): void {
72+
fireEvent(this, "config-changed", { config: ev.detail.value });
73+
}
74+
75+
private _computeLabelCallback = (
76+
schema: SchemaUnion<ReturnType<typeof this._schema>>
77+
) =>
78+
this.hass!.localize(
79+
`ui.panel.lovelace.editor.features.types.bar-gauge.${schema.name}`
80+
);
81+
}
82+
83+
declare global {
84+
interface HTMLElementTagNameMap {
85+
"hui-bar-gauge-card-feature-editor": HuiBarGaugeCardFeatureEditor;
86+
}
87+
}

src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number];
123123
const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
124124
"alarm-modes",
125125
"area-controls",
126+
"bar-gauge",
126127
"button",
127128
"climate-fan-modes",
128129
"climate-hvac-modes",

src/state/quick-bar-mixin.ts

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { PropertyValues } from "lit";
2-
import { tinykeys } from "tinykeys";
32
import memoizeOne from "memoize-one";
43
import { isComponentLoaded } from "../common/config/is_component_loaded";
54
import { mainWindow } from "../common/dom/get_main_window";
@@ -12,9 +11,9 @@ import type { Constructor, HomeAssistant } from "../types";
1211
import { storeState } from "../util/ha-pref-storage";
1312
import { showToast } from "../util/toast";
1413
import type { HassElement } from "./hass-element";
14+
import { ShortcutManager } from "../common/keyboard/shortcuts";
1515
import { extractSearchParamsObject } from "../common/url/search-params";
1616
import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
17-
import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
1817
import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog";
1918
import type { Redirects } from "../panels/my/ha-panel-my";
2019

@@ -62,21 +61,22 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
6261
}
6362

6463
private _registerShortcut() {
65-
tinykeys(window, {
64+
const shortcutManager = new ShortcutManager();
65+
shortcutManager.add({
6666
// Those are for latin keyboards that have e, c, m keys
67-
e: (ev) => this._showQuickBar(ev),
68-
c: (ev) => this._showQuickBar(ev, QuickBarMode.Command),
69-
m: (ev) => this._createMyLink(ev),
70-
a: (ev) => this._showVoiceCommandDialog(ev),
71-
d: (ev) => this._showQuickBar(ev, QuickBarMode.Device),
67+
e: { handler: (ev) => this._showQuickBar(ev) },
68+
c: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) },
69+
m: { handler: (ev) => this._createMyLink(ev) },
70+
a: { handler: (ev) => this._showVoiceCommandDialog(ev) },
71+
d: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) },
7272
// Workaround see https://github.com/jamiebuilds/tinykeys/issues/130
73-
"Shift+?": (ev) => this._showShortcutDialog(ev),
73+
"Shift+?": { handler: (ev) => this._showShortcutDialog(ev) },
7474
// Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts)
75-
KeyE: (ev) => this._showQuickBar(ev),
76-
KeyC: (ev) => this._showQuickBar(ev, QuickBarMode.Command),
77-
KeyM: (ev) => this._createMyLink(ev),
78-
KeyA: (ev) => this._showVoiceCommandDialog(ev),
79-
KeyD: (ev) => this._showQuickBar(ev, QuickBarMode.Device),
75+
KeyE: { handler: (ev) => this._showQuickBar(ev) },
76+
KeyC: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) },
77+
KeyM: { handler: (ev) => this._createMyLink(ev) },
78+
KeyA: { handler: (ev) => this._showVoiceCommandDialog(ev) },
79+
KeyD: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) },
8080
});
8181
}
8282

@@ -87,7 +87,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
8787
private _showVoiceCommandDialog(e: KeyboardEvent) {
8888
if (
8989
!this.hass?.enableShortcuts ||
90-
!canOverrideAlphanumericInput(e.composedPath()) ||
9190
!this._conversation(this.hass.config.components)
9291
) {
9392
return;
@@ -105,7 +104,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
105104
e: KeyboardEvent,
106105
mode: QuickBarMode = QuickBarMode.Entity
107106
) {
108-
if (!this._canShowQuickBar(e)) {
107+
if (!this._canShowQuickBar()) {
109108
return;
110109
}
111110

@@ -118,7 +117,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
118117
}
119118

120119
private _showShortcutDialog(e: KeyboardEvent) {
121-
if (!this._canShowQuickBar(e)) {
120+
if (!this._canShowQuickBar()) {
122121
return;
123122
}
124123

@@ -131,10 +130,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
131130
}
132131

133132
private async _createMyLink(e: KeyboardEvent) {
134-
if (
135-
!this.hass?.enableShortcuts ||
136-
!canOverrideAlphanumericInput(e.composedPath())
137-
) {
133+
if (!this.hass?.enableShortcuts) {
138134
return;
139135
}
140136

@@ -193,11 +189,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
193189
});
194190
}
195191

196-
private _canShowQuickBar(e: KeyboardEvent) {
197-
return (
198-
this.hass?.user?.is_admin &&
199-
this.hass.enableShortcuts &&
200-
canOverrideAlphanumericInput(e.composedPath())
201-
);
192+
private _canShowQuickBar() {
193+
return this.hass?.user?.is_admin && this.hass.enableShortcuts;
202194
}
203195
};

src/translations/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8424,7 +8424,9 @@
84248424
"no_compatible_controls": "No compatible controls available for this area"
84258425
},
84268426
"bar-gauge": {
8427-
"label": "Bar gauge"
8427+
"label": "Bar gauge",
8428+
"min": "Minimum value",
8429+
"max": "Maximum value"
84288430
},
84298431
"trend-graph": {
84308432
"label": "Trend graph"

0 commit comments

Comments
 (0)