From ea8297b6eb07cc833c9302959c2ac5ef65687163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Valli=C3=A8res?= Date: Wed, 11 Oct 2023 02:50:02 +0000 Subject: [PATCH 1/7] add multi-path icon support and path attributes --- src/components/ha-control-select-menu.ts | 2 +- src/components/ha-icon-next.ts | 5 +- src/components/ha-icon-prev.ts | 5 +- src/components/ha-icon.ts | 52 ++++++++++++++------ src/components/ha-svg-icon.ts | 62 ++++++++++++++++++++++-- src/data/custom_icons.ts | 3 ++ 6 files changed, 105 insertions(+), 24 deletions(-) diff --git a/src/components/ha-control-select-menu.ts b/src/components/ha-control-select-menu.ts index 04422d890e57..b9f526f46e52 100644 --- a/src/components/ha-control-select-menu.ts +++ b/src/components/ha-control-select-menu.ts @@ -127,7 +127,7 @@ export class HaControlSelectMenu extends SelectBase { return html`
${icon && "path" in icon - ? html`` + ? html`` : icon && "icon" in icon ? html`` : html``} diff --git a/src/components/ha-icon-next.ts b/src/components/ha-icon-next.ts index 607f4242fc25..d92d63e4a207 100644 --- a/src/components/ha-icon-next.ts +++ b/src/components/ha-icon-next.ts @@ -4,8 +4,9 @@ import { HaSvgIcon } from "./ha-svg-icon"; @customElement("ha-icon-next") export class HaIconNext extends HaSvgIcon { - @property() public override path = - document.dir === "ltr" ? mdiChevronRight : mdiChevronLeft; + @property() public override get path() { + return document.dir === "ltr" ? mdiChevronRight : mdiChevronLeft; + } } declare global { diff --git a/src/components/ha-icon-prev.ts b/src/components/ha-icon-prev.ts index da93f628dadc..154761196c1f 100644 --- a/src/components/ha-icon-prev.ts +++ b/src/components/ha-icon-prev.ts @@ -4,8 +4,9 @@ import { HaSvgIcon } from "./ha-svg-icon"; @customElement("ha-icon-prev") export class HaIconPrev extends HaSvgIcon { - @property() public override path = - document.dir === "ltr" ? mdiChevronLeft : mdiChevronRight; + @property() public override get path() { + return document.dir === "ltr" ? mdiChevronLeft : mdiChevronRight; + } } declare global { diff --git a/src/components/ha-icon.ts b/src/components/ha-icon.ts index 0aa89e643563..bdbd96e75111 100644 --- a/src/components/ha-icon.ts +++ b/src/components/ha-icon.ts @@ -1,9 +1,9 @@ import { - css, CSSResultGroup, - html, LitElement, PropertyValues, + css, + html, nothing, } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -11,15 +11,15 @@ import { fireEvent } from "../common/dom/fire_event"; import { debounce } from "../common/util/debounce"; import { CustomIcon, customIcons } from "../data/custom_icons"; import { - checkCacheVersion, Chunks, - findIconChunk, - getIcon, Icons, MDI_PREFIXES, + checkCacheVersion, + findIconChunk, + getIcon, writeCache, } from "../data/iconsets"; -import "./ha-svg-icon"; +import { SvgPath } from "./ha-svg-icon"; interface DeprecatedIcon { [key: string]: { @@ -39,22 +39,40 @@ if (!__SUPERVISOR__) { const debouncedWriteCache = debounce(() => writeCache(chunks), 2000); -const cachedIcons: Record = {}; +const cachedIcons: Record = {}; @customElement("ha-icon") export class HaIcon extends LitElement { @property() public icon?: string; - @state() private _path?: string; + @state() private _paths?: SvgPath[]; @state() private _viewBox?: string; @state() private _legacy = false; + // For backward compatibility with unique path icons + public set _path(path: string | undefined) { + if (path !== undefined) { + this._paths = [ + { + value: path, + } as SvgPath, + ]; + } else { + this._paths = undefined; + } + } + + // For backward compatibility with unique path icons + public get _path(): string | undefined { + return this._paths?.map((path) => path.value).join(" "); + } + public willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); if (changedProps.has("icon")) { - this._path = undefined; + this._paths = undefined; this._viewBox = undefined; this._loadIcon(); } @@ -69,7 +87,7 @@ export class HaIcon extends LitElement { `; } return html``; } @@ -120,7 +138,7 @@ export class HaIcon extends LitElement { } if (iconName in cachedIcons) { - this._path = cachedIcons[iconName]; + this._paths = cachedIcons[iconName]; return; } @@ -131,7 +149,7 @@ export class HaIcon extends LitElement { if (this.icon === requestedIcon) { this._path = icon; } - cachedIcons[iconName] = icon; + cachedIcons[iconName] = this._paths; return; } @@ -148,7 +166,7 @@ export class HaIcon extends LitElement { if (this.icon === requestedIcon) { this._path = databaseIcon; } - cachedIcons[iconName] = databaseIcon; + cachedIcons[iconName] = this._paths!; return; } const chunk = findIconChunk(iconName); @@ -174,7 +192,11 @@ export class HaIcon extends LitElement { if (this.icon !== requestedIcon) { return; } - this._path = icon.path; + if (icon.paths !== undefined) { + this._paths = icon.paths; + } else { + this._path = icon.path; + } this._viewBox = icon.viewBox; } @@ -187,7 +209,7 @@ export class HaIcon extends LitElement { if (this.icon === requestedIcon) { this._path = iconPack[iconName]; } - cachedIcons[iconName] = iconPack[iconName]; + cachedIcons[iconName] = this._paths; } static get styles(): CSSResultGroup { diff --git a/src/components/ha-svg-icon.ts b/src/components/ha-svg-icon.ts index 5810dd6bb8c5..f04114ddc675 100644 --- a/src/components/ha-svg-icon.ts +++ b/src/components/ha-svg-icon.ts @@ -1,27 +1,61 @@ -import { css, CSSResultGroup, LitElement, svg, SVGTemplateResult } from "lit"; +import { + css, + CSSResultGroup, + LitElement, + nothing, + svg, + SVGTemplateResult, +} from "lit"; import { customElement, property } from "lit/decorators"; +import { DirectiveResult } from "lit/directive"; +import { unsafeSVG } from "lit/directives/unsafe-svg.js"; @customElement("ha-svg-icon") export class HaSvgIcon extends LitElement { - @property() public path?: string; + @property() public paths?: SvgPath[]; @property() public viewBox?: string; + // For backward compatibility with unique path icons + @property() + public set path(path: string | undefined) { + if (path !== undefined) { + this.paths = [ + { + value: path, + } as SvgPath, + ]; + } else { + this.paths = undefined; + } + } + + // For backward compatibility with unique path icons + public get path(): string | undefined { + return this.paths?.map((path) => path.value).join("\n"); + } + protected render(): SVGTemplateResult { return svg` `; } + protected renderPaths(): DirectiveResult[] | SVGTemplateResult { + return this.paths !== undefined + ? this.paths.map((path) => this.renderPath(path)) + : svg`${nothing}`; + } + static get styles(): CSSResultGroup { return css` :host { @@ -42,9 +76,29 @@ export class HaSvgIcon extends LitElement { } `; } + + protected renderPath(path: SvgPath): DirectiveResult { + return unsafeSVG( + ` `${key}="${value}"`) + .join(" ") + : "" + }>` + ); + } } declare global { interface HTMLElementTagNameMap { "ha-svg-icon": HaSvgIcon; } } + +// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path#attributes +export interface SvgPath { + value: string; + attributes?: { + [key: string]: string; + }; +} diff --git a/src/data/custom_icons.ts b/src/data/custom_icons.ts index 8728116a0e6a..04d45cfa6c23 100644 --- a/src/data/custom_icons.ts +++ b/src/data/custom_icons.ts @@ -1,7 +1,10 @@ +import { SvgPath } from "../components/ha-svg-icon"; import { customIconsets } from "./custom_iconsets"; export interface CustomIcon { + // For backward compatibility with unique path icons path: string; + paths: SvgPath[]; viewBox?: string; } From 5c57854ff79eb54da4f74a85d37b35fadf39d973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Valli=C3=A8res?= Date: Fri, 13 Oct 2023 03:27:05 +0000 Subject: [PATCH 2/7] refactoring using 'innerSvg' property instead of 'paths' --- src/components/ha-icon.ts | 47 ++++++++--------------- src/components/ha-svg-icon.ts | 70 ++++++----------------------------- src/data/custom_icons.ts | 6 +-- 3 files changed, 29 insertions(+), 94 deletions(-) diff --git a/src/components/ha-icon.ts b/src/components/ha-icon.ts index bdbd96e75111..e8fb0385e5c8 100644 --- a/src/components/ha-icon.ts +++ b/src/components/ha-icon.ts @@ -19,7 +19,7 @@ import { getIcon, writeCache, } from "../data/iconsets"; -import { SvgPath } from "./ha-svg-icon"; +import "./ha-svg-icon"; interface DeprecatedIcon { [key: string]: { @@ -39,40 +39,25 @@ if (!__SUPERVISOR__) { const debouncedWriteCache = debounce(() => writeCache(chunks), 2000); -const cachedIcons: Record = {}; +const cachedIcons: Record = {}; @customElement("ha-icon") export class HaIcon extends LitElement { @property() public icon?: string; - @state() private _paths?: SvgPath[]; + @state() private _path?: string; + + @state() private _innerSvg?: string; @state() private _viewBox?: string; @state() private _legacy = false; - // For backward compatibility with unique path icons - public set _path(path: string | undefined) { - if (path !== undefined) { - this._paths = [ - { - value: path, - } as SvgPath, - ]; - } else { - this._paths = undefined; - } - } - - // For backward compatibility with unique path icons - public get _path(): string | undefined { - return this._paths?.map((path) => path.value).join(" "); - } - public willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); if (changedProps.has("icon")) { - this._paths = undefined; + this._path = undefined; + this._innerSvg = undefined; this._viewBox = undefined; this._loadIcon(); } @@ -87,7 +72,8 @@ export class HaIcon extends LitElement { `; } return html``; } @@ -138,7 +124,7 @@ export class HaIcon extends LitElement { } if (iconName in cachedIcons) { - this._paths = cachedIcons[iconName]; + this._path = cachedIcons[iconName]; return; } @@ -149,7 +135,7 @@ export class HaIcon extends LitElement { if (this.icon === requestedIcon) { this._path = icon; } - cachedIcons[iconName] = this._paths; + cachedIcons[iconName] = icon; return; } @@ -166,7 +152,7 @@ export class HaIcon extends LitElement { if (this.icon === requestedIcon) { this._path = databaseIcon; } - cachedIcons[iconName] = this._paths!; + cachedIcons[iconName] = databaseIcon; return; } const chunk = findIconChunk(iconName); @@ -192,11 +178,8 @@ export class HaIcon extends LitElement { if (this.icon !== requestedIcon) { return; } - if (icon.paths !== undefined) { - this._paths = icon.paths; - } else { - this._path = icon.path; - } + this._path = icon.path; + this._innerSvg = icon.innerSvg; this._viewBox = icon.viewBox; } @@ -209,7 +192,7 @@ export class HaIcon extends LitElement { if (this.icon === requestedIcon) { this._path = iconPack[iconName]; } - cachedIcons[iconName] = this._paths; + cachedIcons[iconName] = iconPack[iconName]; } static get styles(): CSSResultGroup { diff --git a/src/components/ha-svg-icon.ts b/src/components/ha-svg-icon.ts index f04114ddc675..127503175752 100644 --- a/src/components/ha-svg-icon.ts +++ b/src/components/ha-svg-icon.ts @@ -1,39 +1,15 @@ -import { - css, - CSSResultGroup, - LitElement, - nothing, - svg, - SVGTemplateResult, -} from "lit"; +import { css, CSSResultGroup, LitElement, svg, SVGTemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; -import { DirectiveResult } from "lit/directive"; +// eslint-disable-next-line import/extensions import { unsafeSVG } from "lit/directives/unsafe-svg.js"; @customElement("ha-svg-icon") export class HaSvgIcon extends LitElement { - @property() public paths?: SvgPath[]; + @property() public path?: string; - @property() public viewBox?: string; - - // For backward compatibility with unique path icons - @property() - public set path(path: string | undefined) { - if (path !== undefined) { - this.paths = [ - { - value: path, - } as SvgPath, - ]; - } else { - this.paths = undefined; - } - } + @property() public innerSvg?: string; - // For backward compatibility with unique path icons - public get path(): string | undefined { - return this.paths?.map((path) => path.value).join("\n"); - } + @property() public viewBox?: string; protected render(): SVGTemplateResult { return svg` @@ -44,18 +20,16 @@ export class HaSvgIcon extends LitElement { role="img" aria-hidden="true" > - - ${this.renderPaths()} - + ${ + this.innerSvg + ? svg`${unsafeSVG(this.innerSvg)}` + : this.path + ? svg`` + : "" + } `; } - protected renderPaths(): DirectiveResult[] | SVGTemplateResult { - return this.paths !== undefined - ? this.paths.map((path) => this.renderPath(path)) - : svg`${nothing}`; - } - static get styles(): CSSResultGroup { return css` :host { @@ -76,29 +50,9 @@ export class HaSvgIcon extends LitElement { } `; } - - protected renderPath(path: SvgPath): DirectiveResult { - return unsafeSVG( - ` `${key}="${value}"`) - .join(" ") - : "" - }>` - ); - } } declare global { interface HTMLElementTagNameMap { "ha-svg-icon": HaSvgIcon; } } - -// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path#attributes -export interface SvgPath { - value: string; - attributes?: { - [key: string]: string; - }; -} diff --git a/src/data/custom_icons.ts b/src/data/custom_icons.ts index 04d45cfa6c23..6b98dd360385 100644 --- a/src/data/custom_icons.ts +++ b/src/data/custom_icons.ts @@ -1,10 +1,8 @@ -import { SvgPath } from "../components/ha-svg-icon"; import { customIconsets } from "./custom_iconsets"; export interface CustomIcon { - // For backward compatibility with unique path icons - path: string; - paths: SvgPath[]; + path?: string; + innerSvg?: string; viewBox?: string; } From 2790ad7b2dedbe1d95ef9ac622a0ae90b012b59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Valli=C3=A8res?= Date: Fri, 13 Oct 2023 03:34:05 +0000 Subject: [PATCH 3/7] clean up after refactoring --- src/components/ha-control-select-menu.ts | 2 +- src/components/ha-icon-next.ts | 5 ++--- src/components/ha-icon-prev.ts | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/ha-control-select-menu.ts b/src/components/ha-control-select-menu.ts index b9f526f46e52..04422d890e57 100644 --- a/src/components/ha-control-select-menu.ts +++ b/src/components/ha-control-select-menu.ts @@ -127,7 +127,7 @@ export class HaControlSelectMenu extends SelectBase { return html`
${icon && "path" in icon - ? html`` + ? html`` : icon && "icon" in icon ? html`` : html``} diff --git a/src/components/ha-icon-next.ts b/src/components/ha-icon-next.ts index d92d63e4a207..607f4242fc25 100644 --- a/src/components/ha-icon-next.ts +++ b/src/components/ha-icon-next.ts @@ -4,9 +4,8 @@ import { HaSvgIcon } from "./ha-svg-icon"; @customElement("ha-icon-next") export class HaIconNext extends HaSvgIcon { - @property() public override get path() { - return document.dir === "ltr" ? mdiChevronRight : mdiChevronLeft; - } + @property() public override path = + document.dir === "ltr" ? mdiChevronRight : mdiChevronLeft; } declare global { diff --git a/src/components/ha-icon-prev.ts b/src/components/ha-icon-prev.ts index 154761196c1f..da93f628dadc 100644 --- a/src/components/ha-icon-prev.ts +++ b/src/components/ha-icon-prev.ts @@ -4,9 +4,8 @@ import { HaSvgIcon } from "./ha-svg-icon"; @customElement("ha-icon-prev") export class HaIconPrev extends HaSvgIcon { - @property() public override get path() { - return document.dir === "ltr" ? mdiChevronLeft : mdiChevronRight; - } + @property() public override path = + document.dir === "ltr" ? mdiChevronLeft : mdiChevronRight; } declare global { From 5731c197372a334a1e8740c70a9572a8cebc1e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Valli=C3=A8res?= Date: Fri, 13 Oct 2023 16:31:22 +0000 Subject: [PATCH 4/7] secondaryPath implementation --- src/components/ha-icon.ts | 8 ++++---- src/components/ha-svg-icon.ts | 31 +++++++++++++++++++++---------- src/data/custom_icons.ts | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/components/ha-icon.ts b/src/components/ha-icon.ts index e8fb0385e5c8..4077da8f7bed 100644 --- a/src/components/ha-icon.ts +++ b/src/components/ha-icon.ts @@ -47,7 +47,7 @@ export class HaIcon extends LitElement { @state() private _path?: string; - @state() private _innerSvg?: string; + @state() private _secondaryPath?: string; @state() private _viewBox?: string; @@ -57,7 +57,7 @@ export class HaIcon extends LitElement { super.willUpdate(changedProps); if (changedProps.has("icon")) { this._path = undefined; - this._innerSvg = undefined; + this._secondaryPath = undefined; this._viewBox = undefined; this._loadIcon(); } @@ -73,7 +73,7 @@ export class HaIcon extends LitElement { } return html``; } @@ -179,7 +179,7 @@ export class HaIcon extends LitElement { return; } this._path = icon.path; - this._innerSvg = icon.innerSvg; + this._secondaryPath = icon.secondaryPath; this._viewBox = icon.viewBox; } diff --git a/src/components/ha-svg-icon.ts b/src/components/ha-svg-icon.ts index 127503175752..764d550af69f 100644 --- a/src/components/ha-svg-icon.ts +++ b/src/components/ha-svg-icon.ts @@ -1,13 +1,11 @@ import { css, CSSResultGroup, LitElement, svg, SVGTemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; -// eslint-disable-next-line import/extensions -import { unsafeSVG } from "lit/directives/unsafe-svg.js"; @customElement("ha-svg-icon") export class HaSvgIcon extends LitElement { @property() public path?: string; - @property() public innerSvg?: string; + @property() public secondaryPath?: string; @property() public viewBox?: string; @@ -20,13 +18,18 @@ export class HaSvgIcon extends LitElement { role="img" aria-hidden="true" > - ${ - this.innerSvg - ? svg`${unsafeSVG(this.innerSvg)}` - : this.path - ? svg`` - : "" - } + + ${ + this.path + ? svg`` + : "" + } + ${ + this.secondaryPath + ? svg`` + : "" + } + `; } @@ -48,6 +51,14 @@ export class HaSvgIcon extends LitElement { pointer-events: none; display: block; } + path.primary-path { + fill: var(--icon-primary-color, currentcolor); + opacity: var(--icon-primary-opactity, 1); + } + path.secondary-path { + fill: var(--icon-secondary-color, currentcolor); + opacity: var(--icon-secondary-opactity, 0.5); + } `; } } diff --git a/src/data/custom_icons.ts b/src/data/custom_icons.ts index 6b98dd360385..315609d0c0f8 100644 --- a/src/data/custom_icons.ts +++ b/src/data/custom_icons.ts @@ -2,7 +2,7 @@ import { customIconsets } from "./custom_iconsets"; export interface CustomIcon { path?: string; - innerSvg?: string; + secondaryPath?: string; viewBox?: string; } From cd9387da53b38c7fe8fa28568b1c49c2e19fb577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Valli=C3=A8res?= Date: Tue, 17 Oct 2023 20:29:10 -0400 Subject: [PATCH 5/7] Update src/components/ha-svg-icon.ts Co-authored-by: Bram Kragten --- src/components/ha-svg-icon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-svg-icon.ts b/src/components/ha-svg-icon.ts index 764d550af69f..fb3e721ea3df 100644 --- a/src/components/ha-svg-icon.ts +++ b/src/components/ha-svg-icon.ts @@ -22,7 +22,7 @@ export class HaSvgIcon extends LitElement { ${ this.path ? svg`` - : "" + : nothing } ${ this.secondaryPath From 991bfb59c5bf6a92c4fe9ea8a893c9c89003a7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Valli=C3=A8res?= Date: Wed, 18 Oct 2023 00:43:33 +0000 Subject: [PATCH 6/7] resolution of comments raised by bramkragten --- src/components/ha-svg-icon.ts | 18 +++++++++++------- src/data/custom_icons.ts | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/ha-svg-icon.ts b/src/components/ha-svg-icon.ts index fb3e721ea3df..836a07bf5ca1 100644 --- a/src/components/ha-svg-icon.ts +++ b/src/components/ha-svg-icon.ts @@ -1,4 +1,11 @@ -import { css, CSSResultGroup, LitElement, svg, SVGTemplateResult } from "lit"; +import { + css, + CSSResultGroup, + LitElement, + nothing, + svg, + SVGTemplateResult, +} from "lit"; import { customElement, property } from "lit/decorators"; @customElement("ha-svg-icon") @@ -27,7 +34,7 @@ export class HaSvgIcon extends LitElement { ${ this.secondaryPath ? svg`` - : "" + : nothing } `; @@ -41,7 +48,8 @@ export class HaSvgIcon extends LitElement { justify-content: center; position: relative; vertical-align: middle; - fill: currentcolor; + fill: var(--icon-primary-color, currentcolor); + opacity: var(--icon-primary-opactity, 1); width: var(--mdc-icon-size, 24px); height: var(--mdc-icon-size, 24px); } @@ -51,10 +59,6 @@ export class HaSvgIcon extends LitElement { pointer-events: none; display: block; } - path.primary-path { - fill: var(--icon-primary-color, currentcolor); - opacity: var(--icon-primary-opactity, 1); - } path.secondary-path { fill: var(--icon-secondary-color, currentcolor); opacity: var(--icon-secondary-opactity, 0.5); diff --git a/src/data/custom_icons.ts b/src/data/custom_icons.ts index 315609d0c0f8..397b11984cb9 100644 --- a/src/data/custom_icons.ts +++ b/src/data/custom_icons.ts @@ -1,7 +1,7 @@ import { customIconsets } from "./custom_iconsets"; export interface CustomIcon { - path?: string; + path: string; secondaryPath?: string; viewBox?: string; } From d66e4ab2c89249be9073fc7c4a2eb0b2b2d52f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Valli=C3=A8res?= Date: Wed, 18 Oct 2023 23:10:35 +0000 Subject: [PATCH 7/7] resolution of the issue raised by bramkragten --- src/components/ha-svg-icon.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ha-svg-icon.ts b/src/components/ha-svg-icon.ts index 836a07bf5ca1..2c9405e9e2f2 100644 --- a/src/components/ha-svg-icon.ts +++ b/src/components/ha-svg-icon.ts @@ -49,7 +49,6 @@ export class HaSvgIcon extends LitElement { position: relative; vertical-align: middle; fill: var(--icon-primary-color, currentcolor); - opacity: var(--icon-primary-opactity, 1); width: var(--mdc-icon-size, 24px); height: var(--mdc-icon-size, 24px); } @@ -59,6 +58,9 @@ export class HaSvgIcon extends LitElement { pointer-events: none; display: block; } + path.primary-path { + opacity: var(--icon-primary-opactity, 1); + } path.secondary-path { fill: var(--icon-secondary-color, currentcolor); opacity: var(--icon-secondary-opactity, 0.5);