Enable custom themes to theme Compound (#12240)
* Enable custom themes to theme Compound * Remove the now redundant username color variables They are replaced by the Compound theming options (specifically, username colors can be themed by changing the color of Compound's decorative color tokens).pull/28217/head
parent
203c15f205
commit
8bbad9f653
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
|
46
src/theme.ts
46
src/theme.ts
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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 = [
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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");
|
||||
|
||||
|
|
|
@ -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); } }"`;
|
|
@ -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);
|
||||
|
@ -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", () => {
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
Loading…
Reference in New Issue