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

refactor(core/theme): add new theme util #271

Merged
merged 7 commits into from Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Expand Up @@ -92,10 +92,9 @@ Contains the wrapper components for angular.

### Package `html-test-app`, `react-test-app` and `angular-test-app`

Contains a playground application to explore and test `ix` components.
Inside the `x-test-app`'s are also the preview source code for the documentation. (`src/preview-examples`)

This preview-examples will be generated into markdown files and will copied into `./packages/documentation/docs/auto-generated/previews`.
These packages contain playground applications to explore and test the respective `ix` components.
The preview source code for the documentation is also located inside the `x-test-app`'s. (`src/preview-examples`)
These preview-examples will be translated to markdown files and get copied into `./packages/documentation/docs/auto-generated/previews`.

**_Not published_**

Expand Down
2 changes: 2 additions & 0 deletions packages/angular-test-app/src/app/app-routing.module.ts
Expand Up @@ -68,6 +68,7 @@ import TabsRounded from 'src/preview-examples/tabs-rounded';
import Textarea from 'src/preview-examples/textarea';
import TextareaDisabled from 'src/preview-examples/textarea-disabled';
import TextareaReadonly from 'src/preview-examples/textarea-readonly';
import ThemeService from 'src/preview-examples/theme-switcher';
import Tile from 'src/preview-examples/tile';
import Timepicker from 'src/preview-examples/timepicker';
import Toast from 'src/preview-examples/toast';
Expand Down Expand Up @@ -245,6 +246,7 @@ const routes: Routes = [
{ path: 'textarea', component: Textarea },
{ path: 'textarea-disabled', component: TextareaDisabled },
{ path: 'textarea-readonly', component: TextareaReadonly },
{ path: 'theme-service', component: ThemeService },
{ path: 'tile', component: Tile },
{ path: 'timepicker', component: Timepicker },
{ path: 'toggle-color', component: ToggleColor },
Expand Down
2 changes: 2 additions & 0 deletions packages/angular-test-app/src/app/app.module.ts
Expand Up @@ -17,6 +17,7 @@ import { FormsModule } from '@angular/forms';
import { IxModule } from '@siemens/ix-angular';
import { AgGridModule } from 'ag-grid-angular';

import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import AboutAndLegal from 'src/preview-examples/about-and-legal';
import AGGrid from 'src/preview-examples/aggrid';
import BasicNavigation from 'src/preview-examples/basic-navigation';
Expand Down Expand Up @@ -183,5 +184,6 @@ import { NavigationTestComponent } from './components/navigation-test.component'
],
providers: [],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
danielleroux marked this conversation as resolved.
Show resolved Hide resolved
})
export class AppModule {}
33 changes: 33 additions & 0 deletions packages/angular-test-app/src/preview-examples/theme-switcher.ts
@@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2022 Siemens AG
*
* SPDX-License-Identifier: MIT
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { Component } from '@angular/core';
import { ThemeService } from '@siemens/ix-angular';

@Component({
selector: 'app-example',
template: `
<ix-button class="mb-2" (click)="themeService.toggleMode()">
Toggle mode
</ix-button>
<ix-select (itemSelectionChange)="themeService.setTheme($event)">
<ix-select-item
label="Classic light"
value="theme-classic-light"
></ix-select-item>
<ix-select-item
label="Classic dark"
value="theme-classic-dark"
></ix-select-item>
</ix-select>
`,
})
export default class Modal {
constructor(private readonly themeService: ThemeService) {}
}
1 change: 1 addition & 0 deletions packages/angular/src/index.ts
Expand Up @@ -12,5 +12,6 @@ export * from './components';
export * from './dropdown/trigger.directive';
export * from './modal';
export * from './module';
export * from './theme';
export * from './toast';
export * from './tree';
2 changes: 2 additions & 0 deletions packages/angular/src/module.ts
Expand Up @@ -18,6 +18,7 @@ import { appInitialize } from './app-initialize';
import { DIRECTIVES } from './declare-components';
import { IxDropdownTriggerDirective } from './dropdown/trigger.directive';
import { ModalService } from './modal';
import { ThemeService } from './theme';
import { ToastService } from './toast';
import * as tree from './tree';

Expand All @@ -39,6 +40,7 @@ export class IxModule {
deps: [DOCUMENT, NgZone],
},
ModalService,
ThemeService,
ToastService,
],
};
Expand Down
10 changes: 10 additions & 0 deletions packages/angular/src/theme/index.ts
@@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: 2022 Siemens AG
*
* SPDX-License-Identifier: MIT
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

export * from './theme.service';
34 changes: 34 additions & 0 deletions packages/angular/src/theme/theme.service.ts
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2022 Siemens AG
*
* SPDX-License-Identifier: MIT
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { EventEmitter, Injectable, Output } from '@angular/core';
import { ThemeSwitcher } from '@siemens/ix';

@Injectable({
providedIn: 'root',
})
export class ThemeService {
@Output() themeChanged = new EventEmitter<string>();

private themeSwitcher = new ThemeSwitcher();

constructor() {
this.themeSwitcher.themeChanged.on((theme: string) =>
this.themeChanged.emit(theme)
);
}

toggleMode(): void {
this.themeSwitcher.toggleMode();
}

setTheme(themeName: string): void {
this.themeSwitcher.setTheme(themeName);
}
}
6 changes: 4 additions & 2 deletions packages/core/src/components/menu/menu.tsx
Expand Up @@ -21,7 +21,7 @@ import {
} from '@stencil/core';
import { Popover } from '../utils/popover.util';
import { convertToRemString } from '../utils/rwd.util';
import { toggleVariant } from '../utils/toggle-theme';
import { ThemeSwitcher } from '../utils/theme-switcher';

@Component({
tag: 'ix-menu',
Expand Down Expand Up @@ -123,6 +123,8 @@ export class Menu {

@State() isMoreTabEmpty = false;

private themeSwitcher = new ThemeSwitcher();

private readonly domObserver = new MutationObserver(
this.onDomChange.bind(this)
);
Expand Down Expand Up @@ -825,7 +827,7 @@ export class Menu {
{this.enableToggleTheme ? (
<ix-menu-item
id="toggleTheme"
onClick={() => toggleVariant()}
onClick={() => this.themeSwitcher.toggleMode()}
class="internal-tab bottom-tab"
tabIcon="bulb"
>
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/components/utils/theme-switcher.e2e.ts
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2022 Siemens AG
*
* SPDX-License-Identifier: MIT
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { expect } from '@playwright/test';
import { regressionTest } from '@utils/test';
import { ThemeSwitcher } from './theme-switcher';

regressionTest.fixme('theme-switcher', () => {
regressionTest('basic', async ({ page }) => {
await page.goto('toggle/test/basic');
await page.evaluate(async () => {
const themeLight = 'theme-test-light';
const themeDark = 'theme-test-dark';
document.body.classList.add(themeLight);
const themeSwitcher = new ThemeSwitcher();
themeSwitcher.setTheme(themeDark);
expect(!document.body.classList.contains(themeLight));
expect(document.body.classList.contains(themeDark));
});
});
});
84 changes: 84 additions & 0 deletions packages/core/src/components/utils/theme-switcher.ts
@@ -0,0 +1,84 @@
import { TypedEvent } from './typed-event';

export class ThemeSwitcher {
danielleroux marked this conversation as resolved.
Show resolved Hide resolved
readonly prefixTheme = 'theme-';
readonly suffixLight = '-light';
readonly suffixDark = '-dark';

mutationObserver: MutationObserver;

themeChanged = new TypedEvent<string>();

private isThemeClass(className: string) {
return (
className.startsWith(this.prefixTheme) &&
(className.endsWith(this.suffixDark) ||
className.endsWith(this.suffixLight))
);
}

public setTheme(themeName: string) {
const oldThemes: string[] = [];

document.body.classList.forEach((className) => {
if (this.isThemeClass(className)) {
oldThemes.push(className);
}
});

document.body.classList.remove(...oldThemes);
document.body.classList.add(themeName);
}

public toggleMode() {
const oldThemes: string[] = [];

document.body.classList.forEach((className) => {
if (this.isThemeClass(className)) {
oldThemes.push(className);
}
});

oldThemes.forEach((themeName) => {
document.body.classList.replace(
themeName,
this.getOppositeMode(themeName)
);
});
}

private getOppositeMode(themeName: string) {
if (themeName.endsWith(this.suffixDark)) {
return themeName.replace(/-dark$/g, this.suffixLight);
}

if (themeName.endsWith(this.suffixLight)) {
return themeName.replace(/-light$/g, this.suffixDark);
}
}

private handleMutations(mutations: MutationRecord[]) {
return mutations.forEach((mutation) => {
const { target } = mutation;
(target as HTMLElement).classList.forEach((className) => {
if (
this.isThemeClass(className) &&
!mutation.oldValue?.includes(className)
) {
this.themeChanged.emit(className);
}
});
});
}

public constructor() {
this.mutationObserver = new MutationObserver((mutations) => {
this.handleMutations(mutations);
});

this.mutationObserver.observe(document.body, {
attributeFilter: ['class'],
attributeOldValue: true,
});
}
}
30 changes: 0 additions & 30 deletions packages/core/src/components/utils/toggle-theme.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/core/src/exports.ts
Expand Up @@ -14,3 +14,4 @@ export * from './components/toast/toast-utils';
export * from './components/tree-item/default-tree-item';
export * from './components/tree/tree-model';
export * from './components/upload/upload-file-state';
export * from './components/utils/theme-switcher';
16 changes: 16 additions & 0 deletions packages/documentation/docs/theming/theme.md
Expand Up @@ -2,6 +2,12 @@
sidebar_position: 2
---

import Playground from '@site/src/components/Playground';

import SourceWebComponent from './../auto-generated/previews/web-component/theme-switcher.md'
import ReactComponent from './../auto-generated/previews/angular/theme-switcher.md'
import AngularComponent from './../auto-generated/previews/angular/theme-switcher.md'

# Themes

Siemens Industrial Experience supports theming for all of its components.
Expand All @@ -27,6 +33,16 @@ The default theme is `theme-classic-dark`. To set a different theme change the `
</html>
```

## Working with themes during runtime

<Playground
name="theme-utils" height="10rem" noMargin
frameworks={{
react: ReactComponent,
angular: AngularComponent,
javascript: SourceWebComponent
}}></Playground>

## Siemens AG Corporate Brand Theme

<div class="siemens-brand-section">
Expand Down