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

feat: add theme management tools #2037

Merged
merged 6 commits into from
Feb 15, 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
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