From 10abaa538d4eed24b6ec1a09f3d4859e6d2601fa Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 12 Feb 2025 10:49:31 +0100 Subject: [PATCH] Improve tile card interactions (#24175) * Use none instead of more info for icon * Improve tile icon accessibility * Remove background shape for tile card icon when no action * Add hover opacity * Fix wrong type * Remove padding around icon and increase hover opacity --- src/components/tile/ha-tile-icon.ts | 91 ++++++++++++++---- src/components/tile/ha-tile-image.ts | 53 ----------- src/panels/lovelace/cards/hui-tile-card.ts | 102 +++++++-------------- 3 files changed, 105 insertions(+), 141 deletions(-) delete mode 100644 src/components/tile/ha-tile-image.ts diff --git a/src/components/tile/ha-tile-icon.ts b/src/components/tile/ha-tile-icon.ts index bd26530a59..5c9f29f0f2 100644 --- a/src/components/tile/ha-tile-icon.ts +++ b/src/components/tile/ha-tile-icon.ts @@ -1,25 +1,81 @@ import type { TemplateResult } from "lit"; import { LitElement, css, html } from "lit"; -import { customElement } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; import "../ha-icon"; import "../ha-svg-icon"; +import { classMap } from "lit/directives/class-map"; + +export type TileIconImageStyle = "square" | "rounded-square" | "circle"; + +export const DEFAULT_TILE_ICON_BORDER_STYLE = "circle"; @customElement("ha-tile-icon") export class HaTileIcon extends LitElement { + @property({ type: Boolean, reflect: true }) + public interactive = false; + + @property({ attribute: "border-style", type: String }) + public imageStyle?: TileIconImageStyle; + + @property({ attribute: false }) + public imageUrl?: string; + protected render(): TemplateResult { - return html` -
+ if (this.imageUrl) { + const imageStyle = this.imageStyle || DEFAULT_TILE_ICON_BORDER_STYLE; + return html` +
+ +
+ `; + } + + return html` +
+
+ `; } static styles = css` :host { --tile-icon-color: var(--disabled-color); - --mdc-icon-size: 22px; + --tile-icon-opacity: 0.2; + --tile-icon-hover-opacity: 0.35; + --mdc-icon-size: 24px; + position: relative; + user-select: none; + transition: transform 180ms ease-in-out; } - .shape::before { + :host([interactive]:active) { + transform: scale(1.2); + } + :host([interactive]:hover) { + --tile-icon-opacity: var(--tile-icon-hover-opacity); + } + .container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 18px; + overflow: hidden; + transition: box-shadow 180ms ease-in-out; + } + :host([interactive]:focus-visible) .container { + box-shadow: 0 0 0 2px var(--tile-icon-color); + } + .container.rounded-square { + border-radius: 8px; + } + .container.square { + border-radius: 0; + } + .container.background::before { content: ""; position: absolute; top: 0; @@ -27,24 +83,21 @@ export class HaTileIcon extends LitElement { height: 100%; width: 100%; background-color: var(--tile-icon-color); - transition: background-color 180ms ease-in-out; - opacity: 0.2; + transition: + background-color 180ms ease-in-out, + opacity 180ms ease-in-out; + opacity: var(--tile-icon-opacity); } - .shape { - position: relative; - width: 36px; - height: 36px; - border-radius: 18px; - display: flex; - align-items: center; - justify-content: center; - transition: color 180ms ease-in-out; - overflow: hidden; - } - .shape ::slotted(*) { + .container ::slotted([slot="icon"]) { display: flex; color: var(--tile-icon-color); transition: color 180ms ease-in-out; + pointer-events: none; + } + .container img { + width: 100%; + height: 100%; + object-fit: cover; } `; } diff --git a/src/components/tile/ha-tile-image.ts b/src/components/tile/ha-tile-image.ts deleted file mode 100644 index b1e100b996..0000000000 --- a/src/components/tile/ha-tile-image.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { LitElement, css, html, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; -import { ifDefined } from "lit/directives/if-defined"; - -export type TileImageStyle = "square" | "rounded-square" | "circle"; -@customElement("ha-tile-image") -export class HaTileImage extends LitElement { - @property({ attribute: false }) public imageUrl?: string; - - @property({ attribute: false }) public imageAlt?: string; - - @property({ attribute: false }) public imageStyle: TileImageStyle = "circle"; - - protected render() { - return html` -
- ${this.imageUrl - ? html`${ifDefined(this.imageAlt)}` - : nothing} -
- `; - } - - static styles = css` - .image { - position: relative; - width: 36px; - height: 36px; - border-radius: 18px; - display: flex; - flex: none; - align-items: center; - justify-content: center; - overflow: hidden; - } - .image.rounded-square { - border-radius: 8%; - } - .image.square { - border-radius: 0; - } - .image img { - width: 100%; - height: 100%; - } - `; -} - -declare global { - interface HTMLElementTagNameMap { - "ha-tile-image": HaTileImage; - } -} diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index f4385b63ab..8158f1f525 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -18,8 +18,7 @@ import "../../../components/ha-state-icon"; import "../../../components/ha-svg-icon"; import "../../../components/tile/ha-tile-badge"; import "../../../components/tile/ha-tile-icon"; -import "../../../components/tile/ha-tile-image"; -import type { TileImageStyle } from "../../../components/tile/ha-tile-image"; +import type { TileIconImageStyle } from "../../../components/tile/ha-tile-icon"; import "../../../components/tile/ha-tile-info"; import { cameraUrlWithWidthHeight } from "../../../data/camera"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; @@ -36,7 +35,7 @@ import type { LovelaceGridOptions, } from "../types"; import { renderTileBadge } from "./tile/badges/tile-badge"; -import type { ThermostatCardConfig, TileCardConfig } from "./types"; +import type { TileCardConfig } from "./types"; export const getEntityDefaultTileIconAction = (entityId: string) => { const domain = computeDomain(entityId); @@ -44,10 +43,10 @@ export const getEntityDefaultTileIconAction = (entityId: string) => { DOMAINS_TOGGLE.has(domain) || ["button", "input_button", "scene"].includes(domain); - return supportsIconAction ? "toggle" : "more-info"; + return supportsIconAction ? "toggle" : "none"; }; -const DOMAIN_IMAGE_STYLE: Record = { +const DOMAIN_IMAGE_SHAPE: Record = { update: "square", media_player: "rounded-square", }; @@ -84,7 +83,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { @state() private _config?: TileCardConfig; - public setConfig(config: ThermostatCardConfig): void { + public setConfig(config: TileCardConfig): void { if (!config.entity) { throw new Error("Specify an entity"); } @@ -196,7 +195,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { } ); - get hasCardAction() { + private get _hasCardAction() { return ( !this._config?.tap_action || hasAction(this._config?.tap_action) || @@ -205,7 +204,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { ); } - get hasIconAction() { + private get _hasIconAction() { return ( !this._config?.icon_tap_action || hasAction(this._config?.icon_tap_action) ); @@ -224,14 +223,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard { return html`
-
- - - + + -
+ - +
-
- ${imageUrl - ? html` - - ` - : html` - - - - `} + ${renderTileBadge(stateObj, this.hass)} -
+