From 1430fd5af619f2367e3c2efde7a433e6b2ef4908 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Apr 2025 09:31:21 +0100 Subject: [PATCH] Fix custom theme support for short hex & rgba hex strings (#29726) * Fix custom theme support for hex colours other than 6-char Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/theme.ts | 43 +++++++++++++++++++++++++++---- test/unit-tests/theme-test.ts | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/theme.ts b/src/theme.ts index de384021aa..bfc471544c 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -129,7 +129,7 @@ function clearCustomTheme(): void { // remove all css variables, we assume these are there because of the custom theme const inlineStyleProps = Object.values(document.body.style); for (const prop of inlineStyleProps) { - if (prop.startsWith("--")) { + if (typeof prop === "string" && prop.startsWith("--")) { document.body.style.removeProperty(prop); } } @@ -206,16 +206,49 @@ function generateCustomCompoundCSS(theme: CompoundTheme): string { return `@layer compound.custom { :root, [class*="cpd-theme-"] { ${properties.join(" ")} } }`; } +/** + * Normalizes the hex colour to 8 characters (including alpha) + * @param hexColor the hex colour to normalize + */ +function normalizeHexColour(hexColor: string): string { + switch (hexColor.length) { + case 4: + case 5: + // Short RGB or RGBA hex + return `#${hexColor + .slice(1) + .split("") + .map((c) => c + c) + .join("")}`; + case 7: + // Long RGB hex + return `${hexColor}ff`; + default: + return hexColor; + } +} + +function setHexAlpha(normalizedHexColor: string, alpha: number): string { + return normalizeHexColour(normalizedHexColor).slice(0, 7) + Math.round(alpha).toString(16).padStart(2, "0"); +} + +function parseAlpha(normalizedHexColor: string): number { + return parseInt(normalizedHexColor.slice(7), 16); +} + function setCustomThemeVars(customTheme: CustomTheme): void { const { style } = document.body; function setCSSColorVariable(name: string, hexColor: string, doPct = true): void { style.setProperty(`--${name}`, hexColor); + const normalizedHexColor = normalizeHexColour(hexColor); + const baseAlpha = parseAlpha(normalizedHexColor); + if (doPct) { - // uses #rrggbbaa to define the color with alpha values at 0%, 15% and 50% - style.setProperty(`--${name}-0pct`, hexColor + "00"); - style.setProperty(`--${name}-15pct`, hexColor + "26"); - style.setProperty(`--${name}-50pct`, hexColor + "7F"); + // uses #rrggbbaa to define the color with alpha values at 0%, 15% and 50% (relative to base alpha channel) + style.setProperty(`--${name}-0pct`, setHexAlpha(normalizedHexColor, 0)); + style.setProperty(`--${name}-15pct`, setHexAlpha(normalizedHexColor, baseAlpha * 0.15)); + style.setProperty(`--${name}-50pct`, setHexAlpha(normalizedHexColor, baseAlpha * 0.5)); } } diff --git a/test/unit-tests/theme-test.ts b/test/unit-tests/theme-test.ts index 6c2f247e4e..b9c7e9341c 100644 --- a/test/unit-tests/theme-test.ts +++ b/test/unit-tests/theme-test.ts @@ -135,6 +135,54 @@ describe("theme", () => { expect(spy.mock.calls[0][0].textContent).toMatchSnapshot(); spy.mockRestore(); }); + + it("should handle 4-char rgba hex strings", async () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue([ + { + name: "blue", + colors: { + "sidebar-color": "#abcd", + }, + }, + ]); + + const spy = jest.fn(); + jest.spyOn(document.body, "style", "get").mockReturnValue({ + setProperty: spy, + } as any); + await new Promise((resolve) => { + setTheme("custom-blue").then(resolve); + lightCustomTheme.onload!({} as Event); + }); + expect(spy).toHaveBeenCalledWith("--sidebar-color", "#abcd"); + expect(spy).toHaveBeenCalledWith("--sidebar-color-0pct", "#aabbcc00"); + expect(spy).toHaveBeenCalledWith("--sidebar-color-15pct", "#aabbcc21"); + expect(spy).toHaveBeenCalledWith("--sidebar-color-50pct", "#aabbcc6f"); + }); + + it("should handle 6-char rgb hex strings", async () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue([ + { + name: "blue", + colors: { + "sidebar-color": "#abcdef", + }, + }, + ]); + + const spy = jest.fn(); + jest.spyOn(document.body, "style", "get").mockReturnValue({ + setProperty: spy, + } as any); + await new Promise((resolve) => { + setTheme("custom-blue").then(resolve); + lightCustomTheme.onload!({} as Event); + }); + expect(spy).toHaveBeenCalledWith("--sidebar-color", "#abcdef"); + expect(spy).toHaveBeenCalledWith("--sidebar-color-0pct", "#abcdef00"); + expect(spy).toHaveBeenCalledWith("--sidebar-color-15pct", "#abcdef26"); + expect(spy).toHaveBeenCalledWith("--sidebar-color-50pct", "#abcdef80"); + }); }); describe("enumerateThemes", () => {