Add hold and double tap action in the UI for every card that supports it. (#24824)

* Add double tap action to button card UI editor

* Add double tap action to light card UI editor

* Add hold action and double tap action to gauge card UI editor

* Add hold action and double tap action to picture glance card UI editor

* Add hold action and double tap action to picture card UI editor

* Add hold action and double tap action to entity card UI editor

* Add hold action and double tap action to elements
This commit is contained in:
Paul Bottein 2025-03-28 13:12:07 +01:00 committed by Bram Kragten
parent 909fc119b7
commit 916dec101f
20 changed files with 397 additions and 118 deletions

View File

@ -78,9 +78,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
return {
type: "button",
tap_action: {
action: "toggle",
},
entity: foundEntities[0] || "",
};
}
@ -164,6 +161,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
action: getEntityDefaultButtonAction(config.entity),
},
hold_action: { action: "more-info" },
double_tap_action: { action: "none" },
show_icon: true,
show_name: true,
state_color: true,

View File

@ -46,7 +46,10 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
throw new Error("Image required");
}
this._config = config;
this._config = {
tap_action: { action: "more-info" },
...config,
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {

View File

@ -6,9 +6,11 @@ import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_elemen
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-card";
import type { CameraEntity } from "../../../data/camera";
import type { ImageEntity } from "../../../data/image";
import { computeImageUrl } from "../../../data/image";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { PersonEntity } from "../../../data/person";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entities";
@ -19,8 +21,6 @@ import "../components/hui-image";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { PictureEntityCardConfig } from "./types";
import type { CameraEntity } from "../../../data/camera";
import type { PersonEntity } from "../../../data/person";
export const STUB_IMAGE =
"https://demo.home-assistant.io/stub_config/bedroom.png";
@ -75,7 +75,12 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
throw new Error("No image source configured");
}
this._config = { show_name: true, show_state: true, ...config };
this._config = {
show_name: true,
show_state: true,
tap_action: { action: "more-info" },
...config,
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {

View File

@ -105,7 +105,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
});
this._config = {
hold_action: { action: "more-info" },
tap_action: { action: "more-info" },
...config,
};
}

View File

@ -1,12 +1,13 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import type { LovelacePictureElementEditor } from "../../../types";
import type { IconElementConfig } from "../../../elements/types";
import type { LovelacePictureElementEditor } from "../../../types";
import { actionConfigStruct } from "../../structs/action-struct";
const iconElementConfigStruct = object({
@ -25,16 +26,35 @@ const SCHEMA = [
{ name: "title", selector: { text: {} } },
{ name: "entity", selector: { entity: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
{ name: "style", selector: { object: {} } },
] as const;

View File

@ -1,12 +1,13 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import type { LovelacePictureElementEditor } from "../../../types";
import type { ImageElementConfig } from "../../../elements/types";
import type { LovelacePictureElementEditor } from "../../../types";
import { actionConfigStruct } from "../../structs/action-struct";
const imageElementConfigStruct = object({
@ -30,16 +31,35 @@ const SCHEMA = [
{ name: "entity", selector: { entity: {} } },
{ name: "title", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
{ name: "image", selector: { image: {} } },
{ name: "camera_image", selector: { entity: { domain: "camera" } } },

View File

@ -1,6 +1,7 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
@ -23,16 +24,35 @@ const SCHEMA = [
{ name: "entity", required: true, selector: { entity: {} } },
{ name: "title", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
{ name: "style", selector: { object: {} } },
] as const;

View File

@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
@ -10,11 +11,11 @@ import {
string,
} from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import type { LovelacePictureElementEditor } from "../../../types";
import type { StateIconElementConfig } from "../../../elements/types";
import type { LovelacePictureElementEditor } from "../../../types";
import { actionConfigStruct } from "../../structs/action-struct";
const stateIconElementConfigStruct = object({
@ -35,16 +36,35 @@ const SCHEMA = [
{ name: "title", selector: { text: {} } },
{ name: "state_color", default: true, selector: { boolean: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
{ name: "style", selector: { object: {} } },
] as const;

View File

@ -1,12 +1,13 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import type { LovelacePictureElementEditor } from "../../../types";
import type { StateLabelElementConfig } from "../../../elements/types";
import type { LovelacePictureElementEditor } from "../../../types";
import { actionConfigStruct } from "../../structs/action-struct";
const stateLabelElementConfigStruct = object({
@ -35,16 +36,35 @@ const SCHEMA = [
{ name: "suffix", selector: { text: {} } },
{ name: "title", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
{ name: "style", selector: { object: {} } },
] as const;

View File

@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@ -28,6 +29,7 @@ const cardConfigStruct = assign(
icon_height: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
theme: optional(string()),
show_state: optional(boolean()),
})
@ -86,20 +88,43 @@ export class HuiButtonCardEditor
],
},
{
name: "tap_action",
selector: {
ui_action: {
default_action: getEntityDefaultButtonAction(entityId),
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: getEntityDefaultButtonAction(entityId),
},
},
},
},
},
{
name: "hold_action",
selector: {
ui_action: {
default_action: "more-info",
{
name: "hold_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: [
{
name: "double_tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
],
},
],
},
] as const satisfies readonly HaFormSchema[]
);

View File

@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@ -16,14 +17,21 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import { DEFAULT_MAX, DEFAULT_MIN } from "../../cards/hui-gauge-card";
import type { GaugeCardConfig } from "../../cards/types";
import type { UiAction } from "../../components/hui-action-editor";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { DEFAULT_MIN, DEFAULT_MAX } from "../../cards/hui-gauge-card";
import type { UiAction } from "../../components/hui-action-editor";
const TAP_ACTIONS: UiAction[] = ["navigate", "url", "perform-action", "none"];
const TAP_ACTIONS: UiAction[] = [
"more-info",
"navigate",
"url",
"perform-action",
"assist",
"none",
];
const gaugeSegmentStruct = object({
from: number(),
@ -134,13 +142,37 @@ export class HuiGaugeCardEditor
] as const)
: []),
{
name: "tap_action",
selector: {
ui_action: {
actions: TAP_ACTIONS,
default_action: "more-info",
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
actions: TAP_ACTIONS,
default_action: "more-info",
},
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
actions: TAP_ACTIONS,
default_action: "none" as const,
},
},
})
),
},
],
},
] as const
);
@ -231,7 +263,13 @@ export class HuiGaugeCardEditor
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.unit"
);
case "interactions":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.interactions"
);
case "tap_action":
case "hold_action":
case "double_tap_action":
return `${this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
)} (${this.hass!.localize(

View File

@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
@ -19,6 +20,7 @@ const cardConfigStruct = assign(
entity: optional(string()),
theme: optional(string()),
icon: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
})
@ -48,12 +50,43 @@ const SCHEMA = [
},
{ name: "theme", selector: { theme: {} } },
{
name: "hold_action",
selector: { ui_action: {} },
},
{
name: "double_tap_action",
selector: { ui_action: {} },
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "toggle",
},
},
},
{
name: "hold_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: [
{
name: "double_tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
],
},
],
},
] as const;

View File

@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
@ -18,6 +19,7 @@ const cardConfigStruct = assign(
image_entity: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
theme: optional(string()),
alt_text: optional(string()),
})
@ -32,12 +34,35 @@ const SCHEMA = [
{ name: "alt_text", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "tap_action",
selector: { ui_action: {} },
},
{
name: "hold_action",
selector: { ui_action: {} },
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const;

View File

@ -1,18 +1,19 @@
import { mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import { STUB_IMAGE } from "../../cards/hui-picture-entity-card";
import type { PictureEntityCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { configElementStyle } from "./config-elements-style";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { STUB_IMAGE } from "../../cards/hui-picture-entity-card";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@ -25,6 +26,7 @@ const cardConfigStruct = assign(
aspect_ratio: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
show_name: optional(boolean()),
show_state: optional(boolean()),
theme: optional(string()),
@ -64,12 +66,35 @@ const SCHEMA = [
},
{ name: "theme", selector: { theme: {} } },
{
name: "tap_action",
selector: { ui_action: {} },
},
{
name: "hold_action",
selector: { ui_action: {} },
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const;
@ -132,6 +157,7 @@ export class HuiPictureEntityCardEditor
case "theme":
case "tap_action":
case "hold_action":
case "double_tap_action":
return `${this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
)} (${this.hass!.localize(

View File

@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { array, assert, assign, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
@ -29,6 +30,7 @@ const cardConfigStruct = assign(
aspect_ratio: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
entities: array(entitiesConfigStruct),
theme: optional(string()),
})
@ -56,12 +58,35 @@ const SCHEMA = [
{ name: "entity", selector: { entity: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "tap_action",
selector: { ui_action: {} },
},
{
name: "hold_action",
selector: { ui_action: {} },
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const;
@ -136,6 +161,7 @@ export class HuiPictureGlanceCardEditor
case "theme":
case "tap_action":
case "hold_action":
case "double_tap_action":
return `${this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
)} (${this.hass!.localize(

View File

@ -31,7 +31,7 @@ export class HuiIconElement extends LitElement implements LovelaceElement {
throw Error("Icon required");
}
this._config = { hold_action: { action: "more-info" }, ...config };
this._config = { tap_action: { action: "more-info" }, ...config };
}
protected render() {

View File

@ -29,7 +29,7 @@ export class HuiImageElement extends LitElement implements LovelaceElement {
throw Error("Invalid configuration");
}
this._config = { hold_action: { action: "more-info" }, ...config };
this._config = { tap_action: { action: "more-info" }, ...config };
this.classList.toggle(
"clickable",

View File

@ -60,7 +60,7 @@ export class HuiStateBadgeElement
throw Error("Entity required");
}
this._config = { hold_action: { action: "more-info" }, ...config };
this._config = { tap_action: { action: "more-info" }, ...config };
}
protected shouldUpdate(changedProps: PropertyValues): boolean {

View File

@ -59,7 +59,7 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement {
this._config = {
state_color: true,
hold_action: { action: "more-info" },
tap_action: { action: "more-info" },
...config,
};
}

View File

@ -56,7 +56,7 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
throw Error("Entity required");
}
this._config = { hold_action: { action: "more-info" }, ...config };
this._config = { tap_action: { action: "more-info" }, ...config };
}
protected shouldUpdate(changedProps: PropertyValues): boolean {