Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Commit f242b54

Browse files
committed
Customize the Element theme with configurable colors
Signed-off-by: Dominik Henneke <[email protected]>
1 parent 383769a commit f242b54

File tree

11 files changed

+175
-4
lines changed

11 files changed

+175
-4
lines changed

.changeset/empty-seas-search.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@nordeck/element-web-opendesk-module': patch
3+
---
4+
5+
Customize the Element theme with configurable colors.

e2e/src/deploy/elementWeb/config.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@
5454
"ics_navigation_json_url": "https://example.com/navigation.json",
5555
"ics_silent_url": "https://example.com/silent",
5656
"portal_logo_svg_url": "https://example.com/logo.svg",
57-
"portal_url": "https://example.com"
57+
"portal_url": "https://example.com",
58+
"custom_css_variables": {
59+
"--cpd-color-text-action-accent": "purple"
60+
}
5861
}
5962
},
6063
"net.nordeck.element_web.module.widget_lifecycle": {

e2e/src/navbar.spec.ts e2e/src/opendesk.spec.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,20 @@ import { expect } from '@playwright/test';
1818
import { test } from './fixtures';
1919
import { getElementWebUrl } from './util';
2020

21-
test.describe('Navbar Module', () => {
21+
test.describe('OpenDesk Module', () => {
2222
test('renders the link to the portal', async ({ page }) => {
2323
await page.goto(getElementWebUrl());
2424
const navigation = page.getByRole('navigation');
2525
const link = navigation.getByRole('link', { name: 'Show portal' });
2626
await expect(link).toHaveAttribute('href', 'https://example.com');
2727
});
28+
29+
test('uses a custom primary color from the configuration', async ({
30+
alicePage,
31+
aliceElementWebPage,
32+
}) => {
33+
await expect(
34+
alicePage.getByRole('button', { name: 'Send a Direct Message' }),
35+
).toHaveCSS('background-color', 'rgb(128, 0, 128)'); // -> purple
36+
});
2837
});

packages/element-web-opendesk-module/README.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ It uses the [Module API](https://www.npmjs.com/package/@matrix-org/react-sdk-mod
66

77
<img src="./docs/navbar.png" alt="openDesk navbar" />
88

9+
Features:
10+
11+
- Add a navigation bar to Element.
12+
- Customize the theme colors of Element.
13+
914
## Requirements
1015

1116
The minimal Element version to use this module is `1.11.41`.
@@ -32,6 +37,10 @@ The module provides required configuration options:
3237
- `portal_logo_svg_url` - The URL of the portal `logo.svg` file.
3338
- `portal_url` - The URL of the portal.
3439

40+
There are also other optional configuration options:
41+
42+
- `custom_css_variables` - a configuration of `--cpd-color-*` css variables to override selected colors in the Element theme. The [Element Compound](https://compound.element.io/?path=/docs/tokens-semantic-colors--docs) documentation has a list of all available options.
43+
3544
Example configuration:
3645

3746
```json
@@ -41,7 +50,12 @@ Example configuration:
4150
"ics_navigation_json_url": "https://example.com/navigation.json",
4251
"ics_silent_url": "https://example.com/silent",
4352
"portal_logo_svg_url": "https://example.com/logo.svg",
44-
"portal_url": "https://example.com"
53+
"portal_url": "https://example.com",
54+
55+
// ... add more optional configurations
56+
"custom_css_variables": {
57+
"--cpd-color-text-action-accent": "purple"
58+
}
4559
}
4660
}
4761
}

packages/element-web-opendesk-module/jest.config.js

+4
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@ module.exports = {
2222
preset: 'ts-jest',
2323
testEnvironment: 'jsdom',
2424
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
25+
// TODO: jest doesn't support prettier 3.0 for inline snapshots in Jest <30.
26+
// Remove this line when it is released and updated.
27+
// See https://github.com/jestjs/jest/issues/14305
28+
prettierPath: null,
2529
};

packages/element-web-opendesk-module/src/OpenDeskModule.test.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import {
2222
import { render, screen } from '@testing-library/react';
2323
import { Fragment } from 'react';
2424
import { OpenDeskModule } from './OpenDeskModule';
25+
import { applyStyles } from './utils/applyStyles';
26+
27+
jest.mock('./utils/applyStyles');
2528

2629
describe('OpenDeskModule', () => {
2730
let moduleApi: jest.Mocked<ModuleApi>;
@@ -41,6 +44,7 @@ describe('OpenDeskModule', () => {
4144

4245
it('should register custom translations', () => {
4346
new OpenDeskModule(moduleApi);
47+
4448
expect(moduleApi.registerTranslations).toBeCalledWith({
4549
'Portal logo': {
4650
en: 'Portal logo',
@@ -57,6 +61,22 @@ describe('OpenDeskModule', () => {
5761
});
5862
});
5963

64+
it('should apply custom styles if configured', () => {
65+
moduleApi.getConfigValue.mockReturnValue({
66+
ics_navigation_json_url: 'https://example.com/navigation.json',
67+
ics_silent_url: 'https://example.com/silent',
68+
portal_logo_svg_url: 'https://example.com/logo.svg',
69+
portal_url: 'https://example.com',
70+
custom_css_variables: { '--cpd-color-text-action-accent': 'purple' },
71+
});
72+
73+
new OpenDeskModule(moduleApi);
74+
75+
expect(applyStyles).toBeCalledWith({
76+
'--cpd-color-text-action-accent': 'purple',
77+
});
78+
});
79+
6080
it('should react to the WrapperLifecycle.Wrapper lifecycle', () => {
6181
const module = new OpenDeskModule(moduleApi);
6282

packages/element-web-opendesk-module/src/OpenDeskModule.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
assertValidOpenDeskModuleConfig,
3333
} from './config';
3434
import { theme } from './theme';
35+
import { applyStyles } from './utils/applyStyles';
3536

3637
export class OpenDeskModule extends RuntimeModule {
3738
private readonly config: OpenDeskModuleConfig;
@@ -64,6 +65,10 @@ export class OpenDeskModule extends RuntimeModule {
6465

6566
this.config = config;
6667

68+
if (config.custom_css_variables) {
69+
applyStyles(config.custom_css_variables);
70+
}
71+
6772
// TODO: This should be a functional component. Element calls `ReactDOM.render` and uses the
6873
// return value as a reference to the MatrixChat component. Then they call a function on this
6974
// reference. This is deprecated behavior and only works if the root component is a class. Since

packages/element-web-opendesk-module/src/config.test.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ describe('assertValidOpenDeskModuleConfig', () => {
3232
expect(() => assertValidOpenDeskModuleConfig(config)).not.toThrow();
3333
});
3434

35+
it('should accept optional properties', () => {
36+
expect(() =>
37+
assertValidOpenDeskModuleConfig({
38+
...config,
39+
custom_css_variables: { '--cpd-color-text-action-accent': 'purple' },
40+
additional: 'foo',
41+
}),
42+
).not.toThrow();
43+
});
44+
3545
it('should accept additional properties', () => {
3646
expect(() =>
3747
assertValidOpenDeskModuleConfig({ ...config, additional: 'foo' }),
@@ -55,9 +65,15 @@ describe('assertValidOpenDeskModuleConfig', () => {
5565
{ portal_url: null },
5666
{ portal_url: 123 },
5767
{ portal_url: 'no-uri' },
68+
{ custom_css_variables: { '--other-name': 'purple' } },
69+
{ custom_css_variables: { '--cpd-color-blub': null } },
70+
{ custom_css_variables: { '--cpd-color-blub': 123 } },
71+
{ custom_css_variables: { '--cpd-color-blub': '' } },
5872
])('should reject wrong configuration permissions %j', (patch) => {
5973
expect(() =>
6074
assertValidOpenDeskModuleConfig({ ...config, ...patch }),
61-
).toThrow(/is required|must be a string|must be a valid uri/);
75+
).toThrow(
76+
/is required|must be a string|must be a valid uri|to be empty|is not allowed/,
77+
);
6278
});
6379
});

packages/element-web-opendesk-module/src/config.ts

+14
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,27 @@ export interface OpenDeskModuleConfig {
4545
* @example `https://example.com`
4646
*/
4747
portal_url: string;
48+
49+
/**
50+
* Set custom values to compound color css variables in the theme.
51+
* Reference: https://compound.element.io/?path=/docs/tokens-semantic-colors--docs
52+
*
53+
* @example `{ "--cpd-color-text-action-accent": "purple" }`
54+
*/
55+
custom_css_variables?: { [key: string]: string };
4856
}
4957

5058
const openDeskModuleConfigSchema = Joi.object<OpenDeskModuleConfig, true>({
5159
ics_navigation_json_url: Joi.string().uri().required(),
5260
ics_silent_url: Joi.string().uri().required(),
5361
portal_logo_svg_url: Joi.string().uri().required(),
5462
portal_url: Joi.string().uri().required(),
63+
custom_css_variables: Joi.object().pattern(
64+
Joi.string()
65+
.pattern(/^--cpd-color-/)
66+
.required(),
67+
Joi.string(),
68+
),
5569
})
5670
.unknown()
5771
.required();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2023 Nordeck IT + Consulting GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { applyStyles } from './applyStyles';
18+
19+
describe('applyStyles', () => {
20+
beforeEach(() => {
21+
document.head.innerHTML = '';
22+
});
23+
24+
it('should apply the styles to the head', () => {
25+
applyStyles({
26+
'--cpd-color-text-action-accent': 'green',
27+
'--cpd-color-text-critical-primary': 'red',
28+
});
29+
30+
expect(document.getElementsByTagName('style').item(0)?.outerHTML)
31+
.toMatchInlineSnapshot(`
32+
"<style type="text/css">
33+
.cpd-theme-light.cpd-theme-light.cpd-theme-light.cpd-theme-light,
34+
.cpd-theme-dark.cpd-theme-dark.cpd-theme-dark.cpd-theme-dark,
35+
.cpd-theme-light-hc.cpd-theme-light-hc.cpd-theme-light-hc.cpd-theme-light-hc,
36+
.cpd-theme-dark-hc.cpd-theme-dark-hc.cpd-theme-dark-hc.cpd-theme-dark-hc {
37+
--cpd-color-text-action-accent: green; --cpd-color-text-critical-primary: red
38+
}
39+
</style>"
40+
`);
41+
});
42+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2023 Nordeck IT + Consulting GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Append extra styles to the `<head>` of Element. This should only be used for
19+
* setting compound css variables (ex: `--cpd-color-*`).
20+
*
21+
* @param cssVariableColors - a map of css variables (example: `{"--cpd-color-text-action-accent": "red"}`)
22+
*/
23+
export function applyStyles(cssVariableColors: Record<string, string>) {
24+
const colorsString = Object.entries(cssVariableColors)
25+
.map(([variable, value]) => `${variable}: ${value}`)
26+
.join('; ');
27+
28+
const styles = document.createElement('style');
29+
styles.setAttribute('type', 'text/css');
30+
styles.innerHTML = `
31+
.cpd-theme-light.cpd-theme-light.cpd-theme-light.cpd-theme-light,
32+
.cpd-theme-dark.cpd-theme-dark.cpd-theme-dark.cpd-theme-dark,
33+
.cpd-theme-light-hc.cpd-theme-light-hc.cpd-theme-light-hc.cpd-theme-light-hc,
34+
.cpd-theme-dark-hc.cpd-theme-dark-hc.cpd-theme-dark-hc.cpd-theme-dark-hc {
35+
${colorsString}
36+
}
37+
`;
38+
document.head.appendChild(styles);
39+
}

0 commit comments

Comments
 (0)