Skip to content

Commit

Permalink
Customize the Element theme with configurable colors
Browse files Browse the repository at this point in the history
Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>
  • Loading branch information
dhenneke committed Oct 19, 2023
1 parent 383769a commit ed6c283
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-seas-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@nordeck/element-web-opendesk-module': patch
---

Customize the Element theme with configurable colors.
5 changes: 4 additions & 1 deletion e2e/src/deploy/elementWeb/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@
"ics_navigation_json_url": "https://example.com/navigation.json",
"ics_silent_url": "https://example.com/silent",
"portal_logo_svg_url": "https://example.com/logo.svg",
"portal_url": "https://example.com"
"portal_url": "https://example.com",
"colors": {
"--cpd-color-text-action-accent": "purple"
}
}
},
"net.nordeck.element_web.module.widget_lifecycle": {
Expand Down
11 changes: 10 additions & 1 deletion e2e/src/navbar.spec.ts → e2e/src/opendesk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,20 @@ import { expect } from '@playwright/test';
import { test } from './fixtures';
import { getElementWebUrl } from './util';

test.describe('Navbar Module', () => {
test.describe('OpenDesk Module', () => {
test('renders the link to the portal', async ({ page }) => {
await page.goto(getElementWebUrl());
const navigation = page.getByRole('navigation');
const link = navigation.getByRole('link', { name: 'Show portal' });
await expect(link).toHaveAttribute('href', 'https://example.com');
});

test('uses a custom primary color from the configuration', async ({
alicePage,
aliceElementWebPage,
}) => {
await expect(
alicePage.getByRole('button', { name: 'Send a Direct Message' }),
).toHaveCSS('background-color', 'rgb(128, 0, 128)'); // -> purple
});
});
16 changes: 15 additions & 1 deletion packages/element-web-opendesk-module/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ It uses the [Module API](https://www.npmjs.com/package/@matrix-org/react-sdk-mod

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

Features:

- Add a navigation bar to Element.
- Customize the theme colors of Element.

## Requirements

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

There are also other optional configuration options:

- `colors` - 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.

Example configuration:

```json
Expand All @@ -41,7 +50,12 @@ Example configuration:
"ics_navigation_json_url": "https://example.com/navigation.json",
"ics_silent_url": "https://example.com/silent",
"portal_logo_svg_url": "https://example.com/logo.svg",
"portal_url": "https://example.com"
"portal_url": "https://example.com",
// ... add more optional configurations
"colors": {
"--cpd-color-text-action-accent": "purple"
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/element-web-opendesk-module/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
// TODO: jest doesn't support prettier 3.0 for inline snapshots in Jest <30.
// Remove this line when it is released and updated.
// See https://github.com/jestjs/jest/issues/14305
prettierPath: null,
};
20 changes: 20 additions & 0 deletions packages/element-web-opendesk-module/src/OpenDeskModule.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import {
import { render, screen } from '@testing-library/react';
import { Fragment } from 'react';
import { OpenDeskModule } from './OpenDeskModule';
import { applyStyles } from './utils/applyStyles';

jest.mock('./utils/applyStyles');

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

it('should register custom translations', () => {
new OpenDeskModule(moduleApi);

expect(moduleApi.registerTranslations).toBeCalledWith({
'Portal logo': {
en: 'Portal logo',
Expand All @@ -57,6 +61,22 @@ describe('OpenDeskModule', () => {
});
});

it('should apply custom styles if configured', () => {
moduleApi.getConfigValue.mockReturnValue({
ics_navigation_json_url: 'https://example.com/navigation.json',
ics_silent_url: 'https://example.com/silent',
portal_logo_svg_url: 'https://example.com/logo.svg',
portal_url: 'https://example.com',
colors: { '--cpd-color-text-action-accent': 'purple' },
});

new OpenDeskModule(moduleApi);

expect(applyStyles).toBeCalledWith({
'--cpd-color-text-action-accent': 'purple',
});
});

it('should react to the WrapperLifecycle.Wrapper lifecycle', () => {
const module = new OpenDeskModule(moduleApi);

Expand Down
5 changes: 5 additions & 0 deletions packages/element-web-opendesk-module/src/OpenDeskModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
assertValidOpenDeskModuleConfig,
} from './config';
import { theme } from './theme';
import { applyStyles } from './utils/applyStyles';

export class OpenDeskModule extends RuntimeModule {
private readonly config: OpenDeskModuleConfig;
Expand Down Expand Up @@ -64,6 +65,10 @@ export class OpenDeskModule extends RuntimeModule {

this.config = config;

if (config.colors) {
applyStyles(config.colors);
}

// TODO: This should be a functional component. Element calls `ReactDOM.render` and uses the
// return value as a reference to the MatrixChat component. Then they call a function on this
// reference. This is deprecated behavior and only works if the root component is a class. Since
Expand Down
18 changes: 17 additions & 1 deletion packages/element-web-opendesk-module/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ describe('assertValidOpenDeskModuleConfig', () => {
expect(() => assertValidOpenDeskModuleConfig(config)).not.toThrow();
});

it('should accept optional properties', () => {
expect(() =>
assertValidOpenDeskModuleConfig({
...config,
colors: { '--cpd-color-text-action-accent': 'purple' },
additional: 'foo',
}),
).not.toThrow();
});

it('should accept additional properties', () => {
expect(() =>
assertValidOpenDeskModuleConfig({ ...config, additional: 'foo' }),
Expand All @@ -55,9 +65,15 @@ describe('assertValidOpenDeskModuleConfig', () => {
{ portal_url: null },
{ portal_url: 123 },
{ portal_url: 'no-uri' },
{ colors: { '--other-name': 'purple' } },
{ colors: { '--cpd-color-blub': null } },
{ colors: { '--cpd-color-blub': 123 } },
{ colors: { '--cpd-color-blub': '' } },
])('should reject wrong configuration permissions %j', (patch) => {
expect(() =>
assertValidOpenDeskModuleConfig({ ...config, ...patch }),
).toThrow(/is required|must be a string|must be a valid uri/);
).toThrow(
/is required|must be a string|must be a valid uri|to be empty|is not allowed/,
);
});
});
14 changes: 14 additions & 0 deletions packages/element-web-opendesk-module/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,27 @@ export interface OpenDeskModuleConfig {
* @example `https://example.com`
*/
portal_url: string;

/**
* Set custom values to compound color css variables in the theme.
* Reference: https://compound.element.io/?path=/docs/tokens-semantic-colors--docs
*
* @example `{ "--cpd-color-text-action-accent": "purple" }`
*/
colors?: { [key: string]: string };
}

const openDeskModuleConfigSchema = Joi.object<OpenDeskModuleConfig, true>({
ics_navigation_json_url: Joi.string().uri().required(),
ics_silent_url: Joi.string().uri().required(),
portal_logo_svg_url: Joi.string().uri().required(),
portal_url: Joi.string().uri().required(),
colors: Joi.object().pattern(
Joi.string()
.pattern(/^--cpd-color-/)
.required(),
Joi.string(),
),
})
.unknown()
.required();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2023 Nordeck IT + Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { applyStyles } from './applyStyles';

describe('applyStyles', () => {
beforeEach(() => {
document.head.innerHTML = '';
});

it('should apply the styles to the head', () => {
applyStyles({
'--cpd-color-text-action-accent': 'green',
'--cpd-color-text-critical-primary': 'red',
});

expect(document.getElementsByTagName('style').item(0)?.outerHTML)
.toMatchInlineSnapshot(`
"<style type="text/css">
.cpd-theme-light.cpd-theme-light.cpd-theme-light.cpd-theme-light,
.cpd-theme-dark.cpd-theme-dark.cpd-theme-dark.cpd-theme-dark,
.cpd-theme-light-hc.cpd-theme-light-hc.cpd-theme-light-hc.cpd-theme-light-hc,
.cpd-theme-dark-hc.cpd-theme-dark-hc.cpd-theme-dark-hc.cpd-theme-dark-hc {
--cpd-color-text-action-accent: green; --cpd-color-text-critical-primary: red
}
</style>"
`);
});
});
39 changes: 39 additions & 0 deletions packages/element-web-opendesk-module/src/utils/applyStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2023 Nordeck IT + Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Append extra styles to the `<head>` of Element. This should only be used for
* setting compound css variables (ex: `--cpd-color-*`).
*
* @param cssVariableColors - a map of css variables (example: `{"--cpd-color-text-action-accent": "red"}`)
*/
export function applyStyles(cssVariableColors: Record<string, string>) {
const colorsString = Object.entries(cssVariableColors)
.map(([variable, value]) => `${variable}: ${value}`)
.join('; ');

const styles = document.createElement('style');
styles.setAttribute('type', 'text/css');
styles.innerHTML = `
.cpd-theme-light.cpd-theme-light.cpd-theme-light.cpd-theme-light,
.cpd-theme-dark.cpd-theme-dark.cpd-theme-dark.cpd-theme-dark,
.cpd-theme-light-hc.cpd-theme-light-hc.cpd-theme-light-hc.cpd-theme-light-hc,
.cpd-theme-dark-hc.cpd-theme-dark-hc.cpd-theme-dark-hc.cpd-theme-dark-hc {
${colorsString}
}
`;
document.head.appendChild(styles);
}

0 comments on commit ed6c283

Please sign in to comment.