Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Enable custom themes to theme Compound #12240

Merged
merged 2 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion res/css/_common.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css");
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound);
@import url("@vector-im/compound-web/dist/style.css");
@import "./_font-sizes.pcss";
@import "./_animations.pcss";
Expand Down
2 changes: 0 additions & 2 deletions res/css/views/rooms/_JumpToBottomButton.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

@charset "utf-8";

.mx_JumpToBottomButton {
z-index: 1000;
position: absolute;
Expand Down
2 changes: 0 additions & 2 deletions res/css/views/rooms/_TopUnreadMessagesBar.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

@charset "utf-8";

.mx_TopUnreadMessagesBar {
z-index: 1000;
position: absolute;
Expand Down
11 changes: 0 additions & 11 deletions res/themes/light-custom/css/_custom.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ $background: var(--background, $background);
$panels: var(--panels, var(--cpd-color-gray-600));
$panel-actions: var(--panels-actions, var(--cpd-color-gray-300));

/* --accent-color */
$username-variant3-color: var(--accent-color);

/* --timeline-background-color */
$button-secondary-bg-color: var(--timeline-background-color);
$lightbox-border-color: var(--timeline-background-color);
Expand Down Expand Up @@ -110,14 +107,6 @@ $accent-alt: var(--primary-color);
/* --warning-color */
$button-danger-disabled-bg-color: var(--warning-color-50pct); /* still needs alpha at 0.5 */

/* --username colors (which use a 0-based index) */
$username-variant1-color: var(--username-colors_0, $username-variant1-color);
$username-variant2-color: var(--username-colors_1, $username-variant2-color);
$username-variant3-color: var(--username-colors_2, $username-variant3-color);
$username-variant4-color: var(--username-colors_3, $username-variant4-color);
$username-variant5-color: var(--username-colors_4, $username-variant5-color);
$username-variant6-color: var(--username-colors_5, $username-variant6-color);

/* --timeline-highlights-color */
$event-selected-color: var(--timeline-highlights-color);
$event-highlight-bg-color: var(--timeline-highlights-color);
Expand Down
2 changes: 0 additions & 2 deletions res/themes/light/css/light.pcss
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css");

@import "../../../../res/css/_font-sizes.pcss";
@import "_paths.pcss";
@import "_fonts.pcss";
Expand Down
46 changes: 36 additions & 10 deletions src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,22 @@ interface IFontFaces extends Omit<Record<(typeof allowedFontFaceProps)[number],
}[];
}

interface CompoundTheme {
[token: string]: string;
}

export type CustomTheme = {
name: string;
colors: {
is_dark?: boolean; // eslint-disable-line camelcase
colors?: {
[key: string]: string;
};
fonts: {
fonts?: {
faces: IFontFaces[];
general: string;
monospace: string;
};
is_dark?: boolean; // eslint-disable-line camelcase
compound?: CompoundTheme;
};

/**
Expand Down Expand Up @@ -120,10 +125,10 @@ function clearCustomTheme(): void {
document.body.style.removeProperty(prop);
}
}
const customFontFaceStyle = document.querySelector("head > style[title='custom-theme-font-faces']");
if (customFontFaceStyle) {
customFontFaceStyle.remove();
}

// remove the custom style sheets
document.querySelector("head > style[title='custom-theme-font-faces']")?.remove();
document.querySelector("head > style[title='custom-theme-compound']")?.remove();
}

const allowedFontFaceProps = [
Expand Down Expand Up @@ -177,6 +182,22 @@ function generateCustomFontFaceCSS(faces: IFontFaces[]): string {
.join("\n");
}

const COMPOUND_TOKEN = /^--cpd-[a-z0-9-]+$/;

/**
* Generates a style sheet to override Compound design tokens as specified in
* the given theme.
*/
function generateCustomCompoundCSS(theme: CompoundTheme): string {
const properties: string[] = [];
for (const [token, value] of Object.entries(theme))
if (COMPOUND_TOKEN.test(token)) properties.push(`${token}: ${value};`);
else logger.warn(`'${token}' is not a valid Compound token`);
// Insert the design token overrides into the 'custom' cascade layer as
// documented at https://compound.element.io/?path=/docs/develop-theming--docs
return `@layer compound.custom { :root, [class*="cpd-theme-"] { ${properties.join(" ")} } }`;
}

function setCustomThemeVars(customTheme: CustomTheme): void {
const { style } = document.body;

Expand Down Expand Up @@ -218,6 +239,14 @@ function setCustomThemeVars(customTheme: CustomTheme): void {
style.setProperty("--font-family-monospace", fonts.monospace);
}
}
if (customTheme.compound) {
const css = generateCustomCompoundCSS(customTheme.compound);
const style = document.createElement("style");
style.setAttribute("title", "custom-theme-compound");
style.setAttribute("type", "text/css");
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
}

export function getCustomTheme(themeName: string): CustomTheme {
Expand Down Expand Up @@ -284,9 +313,6 @@ export async function setTheme(theme?: string): Promise<void> {
* Adds the Compound theme class to the top-most element in the document
* This will automatically refresh the colour scales based on the OS or user
* preferences
*
* Note: Theming through Compound is not yet established. Brand theming should
* be done in a similar manner as it used to be done.
*/
document.body.classList.remove("cpd-theme-light", "cpd-theme-dark", "cpd-theme-light-hc", "cpd-theme-dark-hc");

Expand Down
3 changes: 3 additions & 0 deletions test/__snapshots__/theme-test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`theme setTheme applies a custom Compound theme 1`] = `"@layer compound.custom { :root, [class*="cpd-theme-"] { --cpd-color-icon-accent-tertiary: var(--cpd-color-blue-800); --cpd-color-text-action-accent: var(--cpd-color-blue-900); } }"`;
52 changes: 34 additions & 18 deletions test/theme-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,26 @@ describe("theme", () => {
describe("setTheme", () => {
let lightTheme: HTMLStyleElement;
let darkTheme: HTMLStyleElement;
let lightCustomTheme: HTMLStyleElement;

let spyQuerySelectorAll: jest.MockInstance<NodeListOf<Element>, [selectors: string]>;
let spyClassList: jest.SpyInstance<void, string[], any>;

beforeEach(() => {
const styles = [
{
dataset: {
mxTheme: "light",
},
disabled: true,
href: "urlLight",
onload: (): void => void 0,
} as unknown as HTMLStyleElement,
{
dataset: {
mxTheme: "dark",
},
disabled: true,
href: "urlDark",
onload: (): void => void 0,
} as unknown as HTMLStyleElement,
];
const styles = ["light", "dark", "light-custom", "dark-custom"].map(
(theme) =>
({
dataset: {
mxTheme: theme,
},
disabled: true,
href: "fake URL",
onload: (): void => void 0,
}) as unknown as HTMLStyleElement,
);
lightTheme = styles[0];
darkTheme = styles[1];
lightCustomTheme = styles[2];

jest.spyOn(document.body, "style", "get").mockReturnValue([] as any);
spyQuerySelectorAll = jest.spyOn(document, "querySelectorAll").mockReturnValue(styles as any);
Expand Down Expand Up @@ -124,6 +119,27 @@ describe("theme", () => {
jest.advanceTimersByTime(200 * 10);
});
});

it("applies a custom Compound theme", async () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue([
{
name: "blue",
compound: {
"--cpd-color-icon-accent-tertiary": "var(--cpd-color-blue-800)",
"--cpd-color-text-action-accent": "var(--cpd-color-blue-900)",
},
},
]);

const spy = jest.spyOn(document.head, "appendChild").mockImplementation();
await new Promise((resolve) => {
setTheme("custom-blue").then(resolve);
lightCustomTheme.onload!({} as Event);
});
expect(spy).toHaveBeenCalled();
expect(spy.mock.calls[0][0].textContent).toMatchSnapshot();
spy.mockRestore();
});
});

describe("enumerateThemes", () => {
Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3123,9 +3123,9 @@
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==

"@vector-im/compound-design-tokens@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.0.0.tgz#4fe7744bbe0bd093b064d42ca8bb475862bb2ce7"
integrity sha512-/hKAxE/WsmnNZamlSmLoFeAhNDhRpFdJYuY8NrPLaS/dKS/QRnty6UYzs9yWOVNFeiBfkNsrb7wYIFMrYWSRJw==
version "1.1.0"
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.1.0.tgz#9b1a91317c404a1cd0d76d2fd5a7f2df5f1bf0a6"
integrity sha512-1HcCm6YsOda98rGXO4fg0WjEdrMnx/0tdtFmYIlnYkDYTbnfpFg+ffIDY7jgammWbOYwUZpZhM5q9ofb7/EgkA==
dependencies:
svg2vectordrawable "^2.9.1"

Expand Down
Loading