From 25a1c1452362f38bcaa8e8ed69bf8b9ac0e83e31 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 23 Mar 2026 16:29:48 +0100 Subject: [PATCH] Refactor light color favorites card feature and button (#30281) --- .../lights/ha-favorite-color-button.ts | 125 +++++++----------- .../hui-light-color-favorites-card-feature.ts | 120 ++++++++--------- 2 files changed, 106 insertions(+), 139 deletions(-) diff --git a/src/dialogs/more-info/components/lights/ha-favorite-color-button.ts b/src/dialogs/more-info/components/lights/ha-favorite-color-button.ts index b5b94947d9..d37b8acfb6 100644 --- a/src/dialogs/more-info/components/lights/ha-favorite-color-button.ts +++ b/src/dialogs/more-info/components/lights/ha-favorite-color-button.ts @@ -1,4 +1,3 @@ -import type { CSSResultGroup } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property, query } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; @@ -10,17 +9,10 @@ import { temperature2rgb, } from "../../../../common/color/convert-light-color"; import { luminosity } from "../../../../common/color/rgb"; -import "../../../../components/ha-outlined-icon-button"; -import type { HaOutlinedIconButton } from "../../../../components/ha-outlined-icon-button"; -import "../../../../components/ha-svg-icon"; import type { LightColor, LightEntity } from "../../../../data/light"; @customElement("ha-favorite-color-button") -class MoreInfoViewLightColorPicker extends LitElement { - public override focus() { - this._button?.focus(); - } - +class HaFavoriteColorButton extends LitElement { @property({ attribute: false }) label?: string; @property({ type: Boolean, reflect: true }) disabled = false; @@ -29,10 +21,12 @@ class MoreInfoViewLightColorPicker extends LitElement { @property({ attribute: false }) color!: LightColor; - @property({ type: Boolean, reflect: true }) wide = false; + @query("button", true) + private _button!: HTMLButtonElement; - @query("ha-outlined-icon-button", true) - private _button?: HaOutlinedIconButton; + public override focus() { + this._button?.focus(); + } private get _rgbColor(): [number, number, number] { if (this.color) { @@ -63,83 +57,64 @@ class MoreInfoViewLightColorPicker extends LitElement { protected render() { const backgroundColor = rgb2hex(this._rgbColor); const isLight = luminosity(this._rgbColor) > 0.8; - const iconColor = isLight - ? ([33, 33, 33] as [number, number, number]) - : ([255, 255, 255] as [number, number, number]); - const hexIconColor = rgb2hex(iconColor); - const rgbIconColor = iconColor.join(", "); + const borderColor = isLight ? "var(--divider-color)" : "transparent"; return html` - + > `; } - static get styles(): CSSResultGroup { - return [ - css` - :host { - display: block; - } - ha-outlined-icon-button { - --ha-icon-display: block; - --md-sys-color-on-surface: var( - --icon-color, - var(--secondary-text-color) - ); - --md-sys-color-on-surface-variant: var( - --icon-color, - var(--secondary-text-color) - ); - --md-sys-color-on-surface-rgb: var( - --rgb-icon-color, - var(--rgb-secondary-text-color) - ); - --md-sys-color-outline: var(--divider-color); - --md-ripple-focus-color: 0; - --md-ripple-hover-opacity: 0; - --md-ripple-pressed-opacity: 0; - border-radius: var(--ha-border-radius-pill); - } - :host([wide]) ha-outlined-icon-button { - width: 100%; - border-radius: var(--ha-favorite-color-button-border-radius); - --_container-shape: var(--ha-favorite-color-button-border-radius); - --_container-shape-start-start: var( - --ha-favorite-color-button-border-radius - ); - --_container-shape-start-end: var( - --ha-favorite-color-button-border-radius - ); - --_container-shape-end-start: var( - --ha-favorite-color-button-border-radius - ); - --_container-shape-end-end: var( - --ha-favorite-color-button-border-radius - ); - } - :host([disabled]) { - pointer-events: none; - } - ha-outlined-icon-button[disabled] { - filter: grayscale(1) opacity(0.5); - } - `, - ]; - } + static readonly styles = css` + :host { + display: block; + width: var(--ha-favorite-color-button-size, 40px); + height: var(--ha-favorite-color-button-size, 40px); + } + button { + background-color: var(--color); + position: relative; + display: block; + width: 100%; + height: 100%; + border: 1px solid transparent; + border-radius: var( + --ha-favorite-color-button-border-radius, + var(--ha-border-radius-pill) + ); + padding: 0; + margin: 0; + cursor: pointer; + outline: none; + transition: + box-shadow 180ms ease-in-out, + transform 180ms ease-in-out; + } + button:focus-visible { + box-shadow: 0 0 0 2px var(--focus-color); + } + button:active { + transform: scale(1.1); + } + :host([disabled]) { + pointer-events: none; + } + button:disabled { + filter: grayscale(1) opacity(0.5); + } + `; } declare global { interface HTMLElementTagNameMap { - "ha-favorite-color-button": MoreInfoViewLightColorPicker; + "ha-favorite-color-button": HaFavoriteColorButton; } } diff --git a/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts b/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts index d7b8d7872a..aed41f0d2f 100644 --- a/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts @@ -1,6 +1,7 @@ +import { ResizeController } from "@lit-labs/observers/resize-controller"; import type { PropertyValues } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { css, html, LitElement, nothing, unsafeCSS } from "lit"; +import { customElement, property, state } from "lit/decorators"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { computeDomain } from "../../../common/entity/compute_domain"; import { UNAVAILABLE } from "../../../data/entity/entity"; @@ -23,7 +24,9 @@ import { } from "../../../data/entity/entity_registry"; import "../../../dialogs/more-info/components/lights/ha-favorite-color-button"; import { actionHandler } from "../common/directives/action-handler-directive"; -import { debounce } from "../../../common/util/debounce"; + +const PILL_GAP = 8; +const PILL_MIN_SIZE = 32; export const supportsLightColorFavoritesCardFeature = ( hass: HomeAssistant, @@ -52,24 +55,26 @@ class HuiLightColorFavoritesCardFeature @state() private _favoriteColors: LightColor[] = []; - @state() private _maxVisible = 0; - - @query(".container") private _container!: HTMLDivElement; - - private _resizeObserver?: ResizeObserver; - private _unsubEntityRegistry?: UnsubscribeFunc; + private _resizeController = new ResizeController(this, { + callback: (entries) => { + const width = entries[0]?.contentRect.width; + if (width) { + return Math.floor((width + PILL_GAP) / (PILL_MIN_SIZE + PILL_GAP)); + } + return 0; + }, + }); + public connectedCallback() { super.connectedCallback(); this._subscribeEntityEntry(); - this.updateComplete.then(() => this._attachObserver()); } public disconnectedCallback() { super.disconnectedCallback(); this._unsubscribeEntityRegistry(); - this._resizeObserver?.disconnect(); } private _unsubscribeEntityRegistry() { @@ -98,19 +103,8 @@ class HuiLightColorFavoritesCardFeature } } - private _measure() { - const w = this._container.clientWidth; - const pillMin = 32 + 8; - this._maxVisible = Math.floor(w / pillMin); - } - - private _attachObserver(): void { - if (!this._resizeObserver) { - this._resizeObserver = new ResizeObserver( - debounce(() => this._measure(), 250, false) - ); - } - this._resizeObserver.observe(this._container); + private get _maxVisible() { + return this._resizeController.value ?? 0; } private get _stateObj() { @@ -126,19 +120,13 @@ class HuiLightColorFavoritesCardFeature this._subscribeEntityEntry(); } - if (changedProps.has("_entry") || changedProps.has("_maxVisible")) { - if (this._entry) { - if (this._entry.options?.light?.favorite_colors) { - this._favoriteColors = - this._entry.options.light.favorite_colors.slice( - 0, - this._maxVisible - ); - } else if (this._stateObj) { - this._favoriteColors = computeDefaultFavoriteColors( - this._stateObj - ).slice(0, this._maxVisible); - } + if (changedProps.has("_entry")) { + if (this._entry?.options?.light?.favorite_colors) { + this._favoriteColors = this._entry.options.light.favorite_colors; + } else if (this._entry && this._stateObj) { + this._favoriteColors = computeDefaultFavoriteColors(this._stateObj); + } else { + this._favoriteColors = []; } } } @@ -167,27 +155,26 @@ class HuiLightColorFavoritesCardFeature return nothing; } + const visibleColors = this._favoriteColors.slice(0, this._maxVisible); + return html`
- ${this._favoriteColors.map( + ${visibleColors.map( (color, index) => html` -
- - -
+ + ` )}
@@ -209,24 +196,29 @@ class HuiLightColorFavoritesCardFeature return [ cardFeatureStyles, css` + :host { + display: block; + --min-width: ${unsafeCSS(PILL_MIN_SIZE)}px; + --gap: ${unsafeCSS(PILL_GAP)}px; + } .container { position: relative; display: flex; user-select: none; flex-wrap: nowrap; align-items: center; - gap: 8px; + gap: var(--gap); + height: var(--feature-height); } - .color { - position: relative; - display: block; - flex: 1 1 32px; - min-width: 32px; - height: 40px; - } ha-favorite-color-button { - --ha-favorite-color-button-border-radius: var(--ha-border-radius-md); + --ha-favorite-color-button-border-radius: var( + --feature-border-radius + ); + height: 100%; + min-width: var(--min-width); + width: 100%; + flex: 1 1 var(--min-width); } `, ];