From 02acd2996c76f9c585da4112b22d741f57b7a22f Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 23 Mar 2026 18:32:53 +0100 Subject: [PATCH] Allow boolean option to section background (#30289) --- src/data/lovelace/config/section.ts | 11 ++++++- .../hui-section-settings-editor.ts | 21 ++++++------ .../sections/hui-section-background.ts | 32 ++++++++++++------- .../lovelace/views/hui-sections-view.ts | 2 +- .../sections-background-alignment.test.ts | 24 +++++++------- 5 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/data/lovelace/config/section.ts b/src/data/lovelace/config/section.ts index a944dda728..a342778f84 100644 --- a/src/data/lovelace/config/section.ts +++ b/src/data/lovelace/config/section.ts @@ -14,7 +14,7 @@ export interface LovelaceBaseSectionConfig { disabled?: boolean; column_span?: number; row_span?: number; - background?: LovelaceSectionBackgroundConfig; + background?: boolean | LovelaceSectionBackgroundConfig; /** * @deprecated Use heading card instead. */ @@ -34,6 +34,15 @@ export type LovelaceSectionRawConfig = | LovelaceSectionConfig | LovelaceStrategySectionConfig; +export function resolveSectionBackground( + background: boolean | LovelaceSectionBackgroundConfig | undefined +): LovelaceSectionBackgroundConfig | undefined { + if (typeof background === "boolean") { + return background ? {} : undefined; + } + return background; +} + export function isStrategySection( section: LovelaceSectionRawConfig ): section is LovelaceStrategySectionConfig { diff --git a/src/panels/lovelace/editor/section-editor/hui-section-settings-editor.ts b/src/panels/lovelace/editor/section-editor/hui-section-settings-editor.ts index 56e15e3188..5c733c8090 100644 --- a/src/panels/lovelace/editor/section-editor/hui-section-settings-editor.ts +++ b/src/panels/lovelace/editor/section-editor/hui-section-settings-editor.ts @@ -3,17 +3,18 @@ import { LitElement, html } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; +import type { LocalizeFunc } from "../../../../common/translations/localize"; +import "../../../../components/ha-form/ha-form"; import type { HaFormSchema, SchemaUnion, } from "../../../../components/ha-form/types"; -import "../../../../components/ha-form/ha-form"; import { DEFAULT_SECTION_BACKGROUND_OPACITY, + resolveSectionBackground, type LovelaceSectionRawConfig, } from "../../../../data/lovelace/config/section"; import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; -import type { LocalizeFunc } from "../../../../common/translations/localize"; import type { HomeAssistant } from "../../../../types"; interface SettingsData { @@ -95,13 +96,14 @@ export class HuiDialogEditSection extends LitElement { render() { const backgroundEnabled = this.config.background !== undefined; + const background = resolveSectionBackground(this.config.background); const data: SettingsData = { column_span: this.config.column_span || 1, background_enabled: backgroundEnabled, - background_color: this.config.background?.color ?? "default", + background_color: background?.color ?? "default", background_opacity: - this.config.background?.opacity ?? DEFAULT_SECTION_BACKGROUND_OPACITY, + background?.opacity ?? DEFAULT_SECTION_BACKGROUND_OPACITY, }; const schema = this._schema( @@ -146,12 +148,13 @@ export class HuiDialogEditSection extends LitElement { }; if (newData.background_enabled) { + const hasCustomColor = + newData.background_color !== undefined && + newData.background_color !== "default"; + newConfig.background = { - ...(newData.background_color && newData.background_color !== "default" - ? { color: newData.background_color } - : {}), - opacity: - newData.background_opacity ?? DEFAULT_SECTION_BACKGROUND_OPACITY, + ...(hasCustomColor ? { color: newData.background_color } : {}), + opacity: newData.background_opacity!, }; } else { delete newConfig.background; diff --git a/src/panels/lovelace/sections/hui-section-background.ts b/src/panels/lovelace/sections/hui-section-background.ts index 1515066729..5d686d3020 100644 --- a/src/panels/lovelace/sections/hui-section-background.ts +++ b/src/panels/lovelace/sections/hui-section-background.ts @@ -1,16 +1,17 @@ -import { css, LitElement, nothing } from "lit"; +import { css, LitElement, nothing, unsafeCSS } from "lit"; import type { PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; import { computeCssColor } from "../../../common/color/compute-color"; import { DEFAULT_SECTION_BACKGROUND_OPACITY, + resolveSectionBackground, type LovelaceSectionBackgroundConfig, } from "../../../data/lovelace/config/section"; @customElement("hui-section-background") export class HuiSectionBackground extends LitElement { @property({ attribute: false }) - public background?: LovelaceSectionBackgroundConfig; + public background?: boolean | LovelaceSectionBackgroundConfig; protected render() { return nothing; @@ -18,14 +19,15 @@ export class HuiSectionBackground extends LitElement { protected willUpdate(changedProperties: PropertyValues) { super.willUpdate(changedProperties); - if (changedProperties.has("background") && this.background) { - const color = this.background.color - ? computeCssColor(this.background.color) - : "var(--ha-section-background-color, var(--secondary-background-color))"; - this.style.setProperty("--section-background", color); - const opacity = - this.background.opacity ?? DEFAULT_SECTION_BACKGROUND_OPACITY; - this.style.setProperty("--section-background-opacity", `${opacity}%`); + if (changedProperties.has("background")) { + const resolved = resolveSectionBackground(this.background); + if (resolved) { + const color = resolved.color ? computeCssColor(resolved.color) : null; + this.style.setProperty("--section-background-color", color); + const opacity = + resolved.opacity !== undefined ? `${resolved.opacity}%` : null; + this.style.setProperty("--section-background-opacity", opacity); + } } } @@ -34,8 +36,14 @@ export class HuiSectionBackground extends LitElement { position: absolute; inset: 0; border-radius: inherit; - background-color: var(--section-background, none); - opacity: var(--section-background-opacity, 100%); + background-color: var( + --section-background-color, + var(--ha-section-background-color, var(--secondary-background-color)) + ); + opacity: var( + --section-background-opacity, + ${unsafeCSS(DEFAULT_SECTION_BACKGROUND_OPACITY)}% + ); z-index: 0; pointer-events: none; } diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts index 0877b23cdd..94c229f2d0 100644 --- a/src/panels/lovelace/views/hui-sections-view.ts +++ b/src/panels/lovelace/views/hui-sections-view.ts @@ -30,11 +30,11 @@ import { import type { HuiSection } from "../sections/hui-section"; import "../sections/hui-section-background"; import type { Lovelace } from "../types"; -import { computeSectionsBackgroundAlignment } from "./sections-background-alignment"; import { generateDefaultSection } from "./default-section"; import "./hui-view-footer"; import "./hui-view-header"; import "./hui-view-sidebar"; +import { computeSectionsBackgroundAlignment } from "./sections-background-alignment"; export const DEFAULT_MAX_COLUMNS = 4; diff --git a/test/panels/lovelace/views/sections-background-alignment.test.ts b/test/panels/lovelace/views/sections-background-alignment.test.ts index 7c271dd7cb..ebb52c3379 100644 --- a/test/panels/lovelace/views/sections-background-alignment.test.ts +++ b/test/panels/lovelace/views/sections-background-alignment.test.ts @@ -4,7 +4,7 @@ import { computeSectionsBackgroundAlignment } from "../../../../src/panels/lovel function mockSection( opts: { - background?: {}; + background?: boolean; column_span?: number; hidden?: boolean; } = {} @@ -20,7 +20,7 @@ function mockSection( describe("computeSectionsBackgroundAlignment", () => { it("returns empty set for single column layout", () => { - const sections = [mockSection(), mockSection({ background: {} })]; + const sections = [mockSection(), mockSection({ background: true })]; const result = computeSectionsBackgroundAlignment(sections, 1); expect(result.size).toBe(0); }); @@ -33,15 +33,15 @@ describe("computeSectionsBackgroundAlignment", () => { it("returns empty set when all sections have background", () => { const sections = [ - mockSection({ background: {} }), - mockSection({ background: {} }), + mockSection({ background: true }), + mockSection({ background: true }), ]; const result = computeSectionsBackgroundAlignment(sections, 2); expect(result.size).toBe(0); }); it("marks section without background on same row as one with background", () => { - const sections = [mockSection(), mockSection({ background: {} })]; + const sections = [mockSection(), mockSection({ background: true })]; const result = computeSectionsBackgroundAlignment(sections, 2); expect(result.has(0)).toBe(true); expect(result.has(1)).toBe(false); @@ -49,7 +49,7 @@ describe("computeSectionsBackgroundAlignment", () => { it("does not mark sections on different rows", () => { // Row 1: section 0 (no bg), Row 2: section 1 (bg) - const sections = [mockSection(), mockSection({ background: {} })]; + const sections = [mockSection(), mockSection({ background: true })]; const result = computeSectionsBackgroundAlignment(sections, 1); expect(result.size).toBe(0); }); @@ -59,7 +59,7 @@ describe("computeSectionsBackgroundAlignment", () => { // section 2 (span 1, no bg) = row 2 const sections = [ mockSection({ column_span: 2 }), - mockSection({ column_span: 2, background: {} }), + mockSection({ column_span: 2, background: true }), mockSection(), ]; const result = computeSectionsBackgroundAlignment(sections, 4); @@ -71,7 +71,7 @@ describe("computeSectionsBackgroundAlignment", () => { it("wraps to new row when column_span exceeds remaining space", () => { // 2 columns: section 0 (span 1, bg) on row 1, section 1 (span 2, no bg) on row 2 const sections = [ - mockSection({ background: {} }), + mockSection({ background: true }), mockSection({ column_span: 2 }), ]; const result = computeSectionsBackgroundAlignment(sections, 2); @@ -81,7 +81,7 @@ describe("computeSectionsBackgroundAlignment", () => { it("skips hidden sections", () => { // section 0 (hidden, bg) should not cause section 1 to need margin const sections = [ - mockSection({ hidden: true, background: {} }), + mockSection({ hidden: true, background: true }), mockSection(), ]; const result = computeSectionsBackgroundAlignment(sections, 2); @@ -94,7 +94,7 @@ describe("computeSectionsBackgroundAlignment", () => { // Row 2: section 2 (no bg) + section 3 (no bg) -> no margin needed const sections = [ mockSection(), - mockSection({ background: {} }), + mockSection({ background: true }), mockSection(), mockSection(), ]; @@ -114,7 +114,7 @@ describe("computeSectionsBackgroundAlignment", () => { // section with span 10 in a 2-column layout should be treated as span 2 // Row 1: section 0 (span clamped to 2, bg), Row 2: section 1 (no bg) const sections = [ - mockSection({ column_span: 10, background: {} }), + mockSection({ column_span: 10, background: true }), mockSection(), ]; const result = computeSectionsBackgroundAlignment(sections, 2); @@ -125,7 +125,7 @@ describe("computeSectionsBackgroundAlignment", () => { // 3 columns: section 0 (no bg) + section 1 (bg) + section 2 (no bg) const sections = [ mockSection(), - mockSection({ background: {} }), + mockSection({ background: true }), mockSection(), ]; const result = computeSectionsBackgroundAlignment(sections, 3);