mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
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
This commit is contained in:
parent
f25dac7f68
commit
10abaa538d
@ -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`
|
||||
<div class="shape">
|
||||
if (this.imageUrl) {
|
||||
const imageStyle = this.imageStyle || DEFAULT_TILE_ICON_BORDER_STYLE;
|
||||
return html`
|
||||
<div class="container ${classMap({ [imageStyle]: this.imageUrl })}">
|
||||
<img alt="" src=${this.imageUrl} />
|
||||
</div>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="container ${this.interactive ? "background" : ""}">
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -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`
|
||||
<div class="image ${this.imageStyle}">
|
||||
${this.imageUrl
|
||||
? html`<img alt=${ifDefined(this.imageAlt)} src=${this.imageUrl} />`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<string, TileImageStyle> = {
|
||||
const DOMAIN_IMAGE_SHAPE: Record<string, TileIconImageStyle> = {
|
||||
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`
|
||||
<ha-card>
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<div class="icon-container">
|
||||
<ha-tile-icon>
|
||||
<ha-svg-icon .path=${mdiHelp}></ha-svg-icon>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-icon>
|
||||
<ha-svg-icon slot="icon" .path=${mdiHelp}></ha-svg-icon>
|
||||
<ha-tile-badge class="not-found">
|
||||
<ha-svg-icon .path=${mdiExclamationThick}></ha-svg-icon>
|
||||
</ha-tile-badge>
|
||||
</div>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info
|
||||
.primary=${entityId}
|
||||
secondary=${this.hass.localize("ui.card.tile.not_found")}
|
||||
@ -275,45 +272,36 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
role=${ifDefined(this.hasCardAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)}
|
||||
role=${ifDefined(this._hasCardAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)}
|
||||
aria-labelledby="info"
|
||||
>
|
||||
<ha-ripple .disabled=${!this.hasCardAction}></ha-ripple>
|
||||
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<div
|
||||
class="icon-container"
|
||||
role=${ifDefined(this.hasIconAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.hasIconAction ? "0" : undefined)}
|
||||
<ha-tile-icon
|
||||
role=${ifDefined(this._hasIconAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this._hasIconAction ? "0" : undefined)}
|
||||
@action=${this._handleIconAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.icon_hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.icon_double_tap_action),
|
||||
})}
|
||||
.interactive=${this._hasIconAction}
|
||||
.imageStyle=${DOMAIN_IMAGE_SHAPE[domain]}
|
||||
.imageUrl=${imageUrl}
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
>
|
||||
${imageUrl
|
||||
? html`
|
||||
<ha-tile-image
|
||||
.imageStyle=${DOMAIN_IMAGE_STYLE[domain] || "circle"}
|
||||
.imageUrl=${imageUrl}
|
||||
></ha-tile-image>
|
||||
`
|
||||
: html`
|
||||
<ha-tile-icon
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
>
|
||||
<ha-state-icon
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
</ha-tile-icon>
|
||||
`}
|
||||
<ha-state-icon
|
||||
slot="icon"
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
${renderTileBadge(stateObj, this.hass)}
|
||||
</div>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info
|
||||
id="info"
|
||||
.primary=${name}
|
||||
@ -363,6 +351,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
[role="button"]:focus {
|
||||
outline: none;
|
||||
@ -392,53 +381,28 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
gap: 10px;
|
||||
}
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.vertical .icon-container {
|
||||
margin-bottom: 10px;
|
||||
margin-right: 0;
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.vertical ha-tile-info {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
.icon-container {
|
||||
position: relative;
|
||||
flex: none;
|
||||
margin-right: 10px;
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 10px;
|
||||
direction: var(--direction);
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
.icon-container ha-tile-icon,
|
||||
.icon-container ha-tile-image {
|
||||
ha-tile-icon {
|
||||
--tile-icon-color: var(--tile-color);
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
.icon-container ha-tile-badge {
|
||||
ha-tile-badge {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
inset-inline-end: -3px;
|
||||
inset-inline-start: initial;
|
||||
}
|
||||
.icon-container[role="button"] {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.icon-container[role="button"]:focus-visible,
|
||||
.icon-container[role="button"]:active {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
ha-tile-info {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
Loading…
x
Reference in New Issue
Block a user