Skip to content

Commit

Permalink
feat: add theme management tools (#2037)
Browse files Browse the repository at this point in the history
* feat: theme toggle control

adds a new component to allow switching in the ui between light and dark modes
adds a theme toggle to all stories by default
updates developer test page to include toggle
adds helper functions to assist with theme changing using fluentui web components theming tools
  • Loading branch information
gavinbarron authored and musale committed Feb 21, 2023
1 parent d801658 commit 19339d7
Show file tree
Hide file tree
Showing 16 changed files with 503 additions and 16 deletions.
57 changes: 43 additions & 14 deletions .storybook/addons/codeEditorAddon/codeAddon.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export const withCodeEditor = makeDecorator({
name: `withCodeEditor`,
parameterName: 'myParameter',
skipIfNoParametersOrOptions: false,
wrapper: (getStory, context, { parameters }) => {
wrapper: (getStory, context, { options }) => {
const disableThemeToggle = options ? options.disableThemeToggle : false;
let story = getStory(context);

let storyHtml;
Expand Down Expand Up @@ -153,7 +154,29 @@ export const withCodeEditor = makeDecorator({
}
}
}

const themeToggleCss = disableThemeToggle
? ''
: `
body {
background-color: var(--neutral-fill-rest);
color: var(--neutral-foreground-rest);
font-family: var(--body-font);
padding: 0 12px;
}
header {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding: 0 0 12px 0;
}
`;
const themeToggle = disableThemeToggle
? ''
: `
<header>
<mgt-theme-toggle mode="light"></mgt-theme-toggle>
</header>
`;
const loadEditorContent = () => {
let providerInitCode = `
import {Providers, MockProvider} from "${mgtScriptName}";
Expand All @@ -162,16 +185,18 @@ export const withCodeEditor = makeDecorator({

const storyElement = document.createElement('iframe');

storyElement.addEventListener('load', () => {
let doc = storyElement.contentDocument;
storyElement.addEventListener(
'load',
() => {
let doc = storyElement.contentDocument;

let { html, css, js } = editor.files;
js = js.replace(
/import \{([^\}]+)\}\s+from\s+['"]@microsoft\/mgt['"];/gm,
`import {$1} from '${mgtScriptName}';`
);
let { html, css, js } = editor.files;
js = js.replace(
/import \{([^\}]+)\}\s+from\s+['"]@microsoft\/mgt['"];/gm,
`import {$1} from '${mgtScriptName}';`
);

const docContent = `
const docContent = `
<html>
<head>
<script type="module" src="${mgtScriptName}"></script>
Expand All @@ -183,10 +208,12 @@ export const withCodeEditor = makeDecorator({
html, body {
height: 100%;
}
${themeToggleCss}
${css}
</style>
</head>
<body>
${themeToggle}
${html}
<script type="module">
${js}
Expand All @@ -195,10 +222,12 @@ export const withCodeEditor = makeDecorator({
</html>
`;

doc.open();
doc.write(docContent);
doc.close();
}, {once:true});
doc.open();
doc.write(docContent);
doc.close();
},
{ once: true }
);

storyElement.className = 'story-mgt-preview';
storyElement.setAttribute('title', 'preview');
Expand Down
18 changes: 17 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@
<!-- <script src="./packages/mgt/dist/bundle/mgt-loader.js"></script> -->

<script type="module" src="./packages/mgt/dist/es6/index.js"></script>
<style>
header {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
body {
background-color: var(--neutral-fill-rest);
color: var(--neutral-foreground-rest);
font-family: var(--body-font);
padding: 24px 12px;
}
</style>
</head>

<body>
Expand Down Expand Up @@ -51,6 +64,9 @@
></mgt-msal2-provider> -->

<mgt-mock-provider></mgt-mock-provider>
<header>
<mgt-theme-toggle></mgt-theme-toggle>
</header>

<h1>Developer test page</h1>
<main>
Expand All @@ -77,4 +93,4 @@ <h2>mgt-picker</h2>
<mgt-picker resource="me/todo/lists" scopes="tasks.read, tasks.readwrite"></mgt-picker>
</main>
</body>
</html>
</html>
8 changes: 7 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
// These files are know to be ESM and should be transformed by ts-jest
// These packages are know to be ESM and should be transformed by ts-jest
const esModules = [
'msal',
'@open-wc',
'@lit',
'lit',
'@fluentui/web-components',
'testing-library__dom',
'@microsoft/fast-color',
'@microsoft/fast-element',
'@microsoft/fast-foundation',
'@microsoft/fast-web-utilities',
'exenv-es6',
'@microsoft/microsoft-graph-client',
'@microsoft/mgt-element',
'@microsoft/mgt-components',
Expand Down
2 changes: 2 additions & 0 deletions packages/mgt-components/src/components/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import './mgt-person/mgt-person-types';
import './mgt-tasks/mgt-tasks';
import './mgt-teams-channel-picker/mgt-teams-channel-picker';
import './mgt-todo/mgt-todo';
import './mgt-theme-toggle/mgt-theme-toggle';

export * from './mgt-agenda/mgt-agenda';
export * from './mgt-file/mgt-file';
Expand All @@ -34,3 +35,4 @@ export * from './mgt-person/mgt-person-types';
export * from './mgt-tasks/mgt-tasks';
export * from './mgt-teams-channel-picker/mgt-teams-channel-picker';
export * from './mgt-todo/mgt-todo';
export * from './mgt-theme-toggle/mgt-theme-toggle';
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/

/**
* Enumeration to define what parts of the person component render
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
// import the mock for media match first to ensure it's hoisted and available for our dependencies
import './mock-media-match';
import { screen } from 'testing-library__dom';
import { fixture } from '@open-wc/testing-helpers';
import './mgt-theme-toggle';

describe('mgt-theme-toggle - tests', () => {
it('should render', async () => {
await fixture('<mgt-theme-toggle></mgt-theme-toggle>');
const toggle = await screen.findByRole('switch');
expect(toggle).not.toBeNull();
});
it("should emit darkmodechanged with the current 'checked' state on click", async () => {
let darkModeState = false;
const element = await fixture('<mgt-theme-toggle></mgt-theme-toggle>');
const toggle: HTMLInputElement = await screen.findByRole('switch');
expect(toggle).not.toBeNull();
element.addEventListener('darkmodechanged', (e: CustomEvent<boolean>) => {
darkModeState = e.detail;
});
expect(darkModeState).toBe(false);
expect(toggle.checked).toBe(false);
toggle.click();
expect(darkModeState).toBe(true);
expect(toggle.checked).toBe(true);
toggle.click();
expect(darkModeState).toBe(false);
expect(toggle.checked).toBe(false);
});

it('should have a checked switch if mode is dark', async () => {
await fixture('<mgt-theme-toggle mode="dark"></mgt-theme-toggle>');
const toggle = await screen.findByRole('switch');
expect(toggle).not.toBeNull();
expect(toggle.getAttribute('aria-checked')).toBe('true');
expect(toggle.getAttribute('checked')).toBe('true');
});

it('should not have a checked switch if mode is light', async () => {
await fixture('<mgt-theme-toggle></mgt-theme-toggle>');
const toggle = await screen.findByRole('switch');
expect(toggle).not.toBeNull();
expect(toggle.getAttribute('aria-checked')).toBe('false');
expect(toggle.getAttribute('checked')).toBe('false');
});

it('should have a checked switch if user prefers dark mode and no mode is set', async () => {
// redefine matchMedia to return true
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: true,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
}))
});
await fixture('<mgt-theme-toggle></mgt-theme-toggle>');
const toggle = await screen.findByRole('switch');
expect(toggle).not.toBeNull();
expect(toggle.getAttribute('aria-checked')).toBe('true');
expect(toggle.getAttribute('checked')).toBe('true');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/

import { html, TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import { customElement, MgtBaseComponent } from '@microsoft/mgt-element';
import { fluentSwitch } from '@fluentui/web-components/dist/esm/switch';
import { registerFluentComponents } from '../../utils/FluentComponents';
import { applyTheme } from '../../styles/theme-manager';
import { strings } from './strings';

registerFluentComponents(fluentSwitch);

/**
* Toggle to switch between light and dark mode
* Will detect browser preference and set accordingly or dark mode can be forced
*
* @fires {CustomEvent<boolean>} darkmodechanged - Fired when dark mode is toggled by a user action
*
* @class MgtDarkToggle
* @extends {MgtBaseComponent}
*/
@customElement('theme-toggle')
class MgtThemeToggle extends MgtBaseComponent {
constructor() {
super();
const prefersDarkMode = window.matchMedia('(prefers-color-scheme:dark)').matches;
this.darkModeActive = prefersDarkMode;
this.applyTheme(this.darkModeActive);
}
/**
* Provides strings for localization
*
* @readonly
* @protected
* @memberof MgtDarkToggle
*/
protected get strings() {
return strings;
}

/**
* Controls whether dark mode is active
*
* @type {boolean}
* @memberof MgtDarkToggle
*/
@property({
attribute: 'mode',
reflect: true,
type: String,
converter: {
fromAttribute(value: string) {
return value === 'dark';
},
toAttribute(value: boolean) {
return value ? 'dark' : 'light';
}
}
})
public darkModeActive: boolean;

/**
* Fires after a component is updated.
* Allows a component to trigger side effects after updating.
*
* @param {Map<string, any>} changedProperties
* @memberof MgtDarkToggle
*/
updated(changedProperties: Map<string, any>): void {
if (changedProperties.has('darkModeActive')) {
this.applyTheme(this.darkModeActive);
}
}

/**
* renders the component
*
* @return {TemplateResult}
* @memberof MgtDarkToggle
*/
render(): TemplateResult {
return html`
<fluent-switch checked=${this.darkModeActive} @change=${this.onSwitchChanged}>
<span slot="checked-message">${strings.on}</span>
<span slot="unchecked-message">${strings.off}</span>
<label for="direction-switch">${strings.label}</label>
</fluent-switch>
`;
}

private onSwitchChanged(e: Event) {
this.darkModeActive = (e.target as HTMLInputElement).checked;
this.fireCustomEvent('darkmodechanged', this.darkModeActive);
}

private applyTheme(active: boolean) {
const targetTheme = active ? 'dark' : 'light';
applyTheme(targetTheme);

document.body.classList.remove('mgt-dark-mode', 'mgt-light-mode');
document.body.classList.add(`mgt-${targetTheme}-mode`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/

Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
}))
});
12 changes: 12 additions & 0 deletions packages/mgt-components/src/components/mgt-theme-toggle/strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/

export const strings = {
label: 'Color mode:',
on: 'Dark',
off: 'Light'
};
Loading

0 comments on commit 19339d7

Please sign in to comment.