Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 packages/html-reporter/src/headerView.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
line-height: 1.25;
}

.header-setting-theme {
display: grid;
margin-left: 22px
}

@media only screen and (max-width: 600px) {
.header-view {
padding: 0;
Expand Down
49 changes: 25 additions & 24 deletions packages/html-reporter/src/headerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { statusIcon } from './statusIcon';
import { filterWithQuery } from './filter';
import { linkifyText } from '@web/renderUtils';
import { Dialog } from '@web/shared/dialog';
import { useDarkModeSetting } from '@web/theme';
import { kThemeOptions, type Theme, useThemeSetting } from '@web/theme';
import { useSetting } from '@web/uiUtils';

export const HeaderView: React.FC<{
Expand Down Expand Up @@ -132,7 +132,7 @@ const NavLink: React.FC<{
const SettingsButton: React.FC = () => {
const settingsRef = React.useRef<HTMLDivElement>(null);
const [settingsOpen, setSettingsOpen] = React.useState(false);
const [darkMode, setDarkMode] = useDarkModeSetting();
const [theme, setTheme] = useThemeSetting();
const [mergeFiles, setMergeFiles] = useSetting('mergeFiles', false);

return <>
Expand All @@ -148,33 +148,34 @@ const SettingsButton: React.FC = () => {
}}
onMouseDown={preventDefault}>
{icons.settings()}
<Dialog
open={settingsOpen}
minWidth={150}
verticalOffset={4}
requestClose={() => setSettingsOpen(false)}
anchor={settingsRef}
dataTestId='settings-dialog'
>
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4 }} onClick={stopPropagation}>
<input type='checkbox' checked={darkMode} onChange={() => setDarkMode(!darkMode)}></input>
Dark mode
</label>
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4 }} onClick={stopPropagation}>
<input type='checkbox' checked={mergeFiles} onChange={() => setMergeFiles(!mergeFiles)}></input>
Merge files
</label>
</Dialog>
</div>

<Dialog
open={settingsOpen}
minWidth={150}
verticalOffset={4}
requestClose={() => setSettingsOpen(false)}
anchor={settingsRef}
dataTestId='settings-dialog'
>
<label className='header-setting-theme'>
Theme:
<select value={theme} onChange={e => setTheme(e.target.value as Theme)}>
{kThemeOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</label>

<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4 }}>
<input type='checkbox' checked={mergeFiles} onChange={() => setMergeFiles(!mergeFiles)}></input>
Merge files
</label>
</Dialog>
</>;
};

const preventDefault = (e: any) => {
e.stopPropagation();
e.preventDefault();
};

const stopPropagation = (e: any) => {
e.stopPropagation();
e.stopImmediatePropagation();
};
5 changes: 5 additions & 0 deletions packages/recorder/src/recorder.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@
align-items: center;
}

.setting-theme {
display: grid;
margin-left: 22px
}

.setting label {
text-overflow: ellipsis;
white-space: nowrap;
Expand Down
14 changes: 9 additions & 5 deletions packages/recorder/src/recorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import * as React from 'react';
import { CallLogView } from './callLog';
import './recorder.css';
import { asLocator } from '@isomorphic/locatorGenerators';
import { useDarkModeSetting } from '@web/theme';
import { kThemeOptions, type Theme, useThemeSetting } from '@web/theme';
import { copy, useSetting } from '@web/uiUtils';
import yaml from 'yaml';
import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot';
Expand All @@ -50,7 +50,7 @@ export const Recorder: React.FC<RecorderProps> = ({
const [ariaSnapshot, setAriaSnapshot] = React.useState<string | undefined>();
const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState<SourceHighlight[]>();
const [settingsOpen, setSettingsOpen] = React.useState(false);
const [darkMode, setDarkMode] = useDarkModeSetting();
const [theme, setTheme] = useThemeSetting();
const [autoExpect, setAutoExpect] = useSetting<boolean>('autoExpect', false);
const settingsButtonRef = React.useRef<HTMLButtonElement>(null);
window.playwrightSelectSource = selectedSourceId => setSelectedFileId(selectedSourceId);
Expand Down Expand Up @@ -203,9 +203,13 @@ export const Recorder: React.FC<RecorderProps> = ({
anchor={settingsButtonRef}
dataTestId='settings-dialog'
>
<div key='dark-mode-setting' className='setting'>
<input type='checkbox' id='dark-mode-setting' checked={darkMode} onChange={() => setDarkMode(!darkMode)} />
<label htmlFor='dark-mode-setting'>Dark mode</label>
<div key='dark-mode-setting' className='setting setting-theme'>
<label htmlFor='dark-mode-setting'>Theme:</label>
<select id='dark-mode-setting' value={theme} onChange={e => setTheme(e.target.value as Theme)}>
{kThemeOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
<div key='auto-expect-setting' className='setting' title='Automatically generate assertions while recording'>
<input type='checkbox' id='auto-expect-setting' checked={autoExpect} onChange={() => {
Expand Down
15 changes: 8 additions & 7 deletions packages/trace-viewer/src/ui/defaultSettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import * as React from 'react';
import { type Setting, SettingsView } from './settingsView';
import { useDarkModeSetting } from '@web/theme';
import { kThemeOptions, type Theme, useThemeSetting } from '@web/theme';
import { useSetting } from '@web/uiUtils';

/**
Expand All @@ -29,18 +29,19 @@ export const DefaultSettingsView: React.FC<{
shouldPopulateCanvasFromScreenshot,
setShouldPopulateCanvasFromScreenshot,
] = useSetting('shouldPopulateCanvasFromScreenshot', false);
const [darkMode, setDarkMode] = useDarkModeSetting();
const [theme, setTheme] = useThemeSetting();
const [mergeFiles, setMergeFiles] = useSetting('mergeFiles', false);

return (
<SettingsView
settings={[
{
type: 'check',
value: darkMode,
set: setDarkMode,
name: 'Dark mode'
},
type: 'select',
value: theme,
set: setTheme,
name: 'Theme',
options: kThemeOptions
} satisfies Setting<Theme>,
...(location === 'ui-mode' ? [{
type: 'check',
value: mergeFiles,
Expand Down
1 change: 1 addition & 0 deletions packages/trace-viewer/src/ui/settingsView.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
grid-template-rows: auto auto;
row-gap: 8px;
margin: 0 16px 0 22px;
line-height: initial;
}

.settings-view .setting-select:not(:first-child) {
Expand Down
18 changes: 9 additions & 9 deletions packages/trace-viewer/src/ui/settingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import * as React from 'react';
import './settingsView.css';

export type Setting = {
export type Setting<Value extends string = string> = {
name: string;
title?: string;
count?: number;
Expand All @@ -27,14 +27,14 @@ export type Setting = {
set: (value: boolean) => void;
} | {
type: 'select',
options: Array<{ label: string, value: string }>;
value: string;
set: (value: string) => void;
options: Array<{ label: string, value: Value }>;
value: Value;
set: (value: Value) => void;
});

export const SettingsView: React.FunctionComponent<{
settings: Setting[];
}> = ({ settings }) => {
export const SettingsView = <Value extends string>(
{ settings }: { settings: Setting<Value>[] }
) => {
return (
<div className='vbox settings-view'>
{settings.map(setting => {
Expand All @@ -50,7 +50,7 @@ export const SettingsView: React.FunctionComponent<{
);
};

const renderSetting = (setting: Setting, labelId: string) => {
const renderSetting = <Value extends string>(setting: Setting<Value>, labelId: string) => {
switch (setting.type) {
case 'check':
return (
Expand All @@ -68,7 +68,7 @@ const renderSetting = (setting: Setting, labelId: string) => {
return (
<>
<label htmlFor={labelId}>{setting.name}:{!!setting.count && <span className='setting-counter'>{setting.count}</span>}</label>
<select id={labelId} value={setting.value} onChange={e => setting.set(e.target.value)}>
<select id={labelId} value={setting.value} onChange={e => setting.set(e.target.value as Value)}>
{setting.options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/components/xtermWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import './xtermWrapper.css';
import type { ITheme, Terminal } from '@xterm/xterm';
import type { FitAddon } from '@xterm/addon-fit';
import type { XtermModule } from './xtermModule';
import { currentTheme, addThemeListener, removeThemeListener } from '../theme';
import { currentDocumentTheme, addThemeListener, removeThemeListener } from '../theme';
import { useMeasure } from '../uiUtils';

export type XtermDataSource = {
Expand All @@ -33,7 +33,7 @@ export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({
source,
}) => {
const [measure, xtermElement] = useMeasure<HTMLDivElement>();
const [theme, setTheme] = React.useState(currentTheme());
const [theme, setTheme] = React.useState(currentDocumentTheme());
const [modulePromise] = React.useState<Promise<XtermModule>>(import('./xtermModule').then(m => m.default));
const terminal = React.useRef<{ terminal: Terminal, fitAddon: FitAddon } | null>(null);

Expand Down
77 changes: 49 additions & 28 deletions packages/web/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ declare global {
}
}

export type Theme = 'dark-mode' | 'light-mode' | 'system';
type DocumentTheme = Exclude<Theme, 'system'>;

const kDefaultTheme: Theme = 'system';
const kThemeSettingsKey = 'theme';
export const kThemeOptions: { label: string; value: Theme }[] = [
{ label: 'Dark mode', value: 'dark-mode' },
{ label: 'Light mode', value: 'light-mode' },
{ label: 'System', value: 'system' },
] as const satisfies { label: string; value: Theme }[];

const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');

export function applyTheme() {
if (document.playwrightThemeInitialized)
return;
Expand All @@ -36,49 +49,57 @@ export function applyTheme() {
document.body.classList.add('inactive');
}, false);

const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const defaultTheme = prefersDarkScheme.matches ? 'dark-mode' : 'light-mode';
const theme = currentTheme();
const documentTheme: DocumentTheme = theme === 'system'
? (prefersDarkScheme.matches ? 'dark-mode' : 'light-mode')
: theme;
document.documentElement.classList.add(documentTheme);

const currentTheme = settings.getString('theme', defaultTheme);
if (currentTheme === 'dark-mode')
document.documentElement.classList.add('dark-mode');
else
document.documentElement.classList.add('light-mode');
prefersDarkScheme.addEventListener('change', e => {
if (currentTheme() === 'system')
updateDocumentTheme(e.matches ? 'dark-mode' : 'light-mode');
});
}

type Theme = 'dark-mode' | 'light-mode';
const listeners = new Set<(theme: DocumentTheme) => void>();
function updateDocumentTheme(newTheme: Theme) {
const oldDocumentTheme = currentDocumentTheme();
const newDocumentTheme = newTheme === 'system'
? (prefersDarkScheme.matches ? 'dark-mode' : 'light-mode')
: newTheme;

const listeners = new Set<(theme: Theme) => void>();
export function toggleTheme() {
const oldTheme = currentTheme();
const newTheme = oldTheme === 'dark-mode' ? 'light-mode' : 'dark-mode';
if (oldDocumentTheme === newDocumentTheme)
return;

if (oldTheme)
document.documentElement.classList.remove(oldTheme);
document.documentElement.classList.add(newTheme);
settings.setString('theme', newTheme);
document.documentElement.classList.remove(oldDocumentTheme);
document.documentElement.classList.add(newDocumentTheme);
for (const listener of listeners)
listener(newTheme);
listener(newDocumentTheme);
}

export function addThemeListener(listener: (theme: 'light-mode' | 'dark-mode') => void) {
export function addThemeListener(listener: (theme: DocumentTheme) => void) {
listeners.add(listener);
}

export function removeThemeListener(listener: (theme: Theme) => void) {
export function removeThemeListener(listener: (theme: DocumentTheme) => void) {
listeners.delete(listener);
}

export function currentTheme(): Theme {
function currentTheme(): Theme {
return settings.getString(kThemeSettingsKey, kDefaultTheme);
}

export function currentDocumentTheme(): DocumentTheme {
return document.documentElement.classList.contains('dark-mode') ? 'dark-mode' : 'light-mode';
}

export function useDarkModeSetting(): [boolean, (value: boolean) => void] {
const [theme, setTheme] = React.useState(currentTheme() === 'dark-mode');
return [theme, (value: boolean) => {
const current = currentTheme() === 'dark-mode';
if (current !== value)
toggleTheme();
setTheme(value);
}];
export function useThemeSetting(): [Theme, (value: Theme) => void] {
const [theme, setTheme] = React.useState<Theme>(currentTheme());

React.useEffect(() => {
settings.setString(kThemeSettingsKey, theme);
updateDocumentTheme(theme);
}, [theme]);

return [theme, setTheme];
}
4 changes: 2 additions & 2 deletions packages/web/src/uiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,11 @@ declare global {
export class Settings {
onChangeEmitter = new EventTarget();

getString(name: string, defaultValue: string): string {
getString<T extends string>(name: string, defaultValue: T): T {
return localStorage[name] || defaultValue;
}

setString(name: string, value: string) {
setString<T extends string>(name: string, value: T) {
localStorage[name] = value;
this.onChangeEmitter.dispatchEvent(new Event(name));
window.saveSettings?.();
Expand Down
4 changes: 2 additions & 2 deletions tests/config/traceViewerFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class TraceViewerPage {
sourceCodeTab: Locator;

settingsDialog: Locator;
darkModeSetting: Locator;
themeSetting: Locator;
displayCanvasContentSetting: Locator;

constructor(public page: Page) {
Expand All @@ -67,7 +67,7 @@ class TraceViewerPage {
this.sourceCodeTab = page.getByRole('tabpanel', { name: 'Source' });

this.settingsDialog = page.getByTestId('settings-toolbar-dialog');
this.darkModeSetting = page.locator('.setting').getByText('Dark mode');
this.themeSetting = this.settingsDialog.getByRole('combobox', { name: 'Theme' });
this.displayCanvasContentSetting = page.locator('.setting').getByText('Display canvas content');
}

Expand Down
Loading