Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize the Element theme with configurable colors #44

Merged
merged 3 commits into from
Oct 20, 2023
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
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: 5 additions & 0 deletions .changeset/tender-papayas-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@nordeck/element-web-opendesk-module': patch
---

Add support for future Element versions.
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",
"custom_css_variables": {
"--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:

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

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
"custom_css_variables": {
"--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,
};
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',
custom_css_variables: { '--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
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.custom_css_variables) {
applyStyles(config.custom_css_variables);
}

// 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 All @@ -83,7 +88,9 @@ export class OpenDeskModule extends RuntimeModule {
// Add the ref to our only children -> the MatrixChat component
const children =
React.Children.only(this.props.children) &&
React.isValidElement<{ ref: unknown }>(this.props.children)
React.isValidElement<{ ref: unknown }>(this.props.children) &&
'ref' in this.props.children &&
!this.props.children.ref
? React.cloneElement(this.props.children, { ref: this.ref })
: this.props.children;

Expand Down
17 changes: 16 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,15 @@ describe('assertValidOpenDeskModuleConfig', () => {
expect(() => assertValidOpenDeskModuleConfig(config)).not.toThrow();
});

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

it('should accept additional properties', () => {
expect(() =>
assertValidOpenDeskModuleConfig({ ...config, additional: 'foo' }),
Expand All @@ -55,9 +64,15 @@ describe('assertValidOpenDeskModuleConfig', () => {
{ portal_url: null },
{ portal_url: 123 },
{ portal_url: 'no-uri' },
{ custom_css_variables: { '--other-name': 'purple' } },
{ custom_css_variables: { '--cpd-color-blub': null } },
{ custom_css_variables: { '--cpd-color-blub': 123 } },
{ custom_css_variables: { '--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" }`
*/
custom_css_variables?: { [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(),
custom_css_variables: Joi.object().pattern(
Joi.string()
.pattern(/^--cpd-color-/)
charlynguyen marked this conversation as resolved.
Show resolved Hide resolved
.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);
}
Loading