Beginning of new settings dialog
This commit is contained in:
parent
733f9a2926
commit
f12164c295
29
packages/trace-viewer/src/ui/defaultSettingsView.tsx
Normal file
29
packages/trace-viewer/src/ui/defaultSettingsView.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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 * as React from 'react';
|
||||||
|
import { SettingsView } from './settingsView';
|
||||||
|
import { useDarkModeSetting } from '@web/theme';
|
||||||
|
|
||||||
|
export const DefaultSettingsView: React.FC<{}> = () => {
|
||||||
|
const [darkMode, setDarkMode] = useDarkModeSetting();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsView
|
||||||
|
settings={[{ value: darkMode, set: setDarkMode, title: 'Dark mode' }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
21
packages/trace-viewer/src/ui/settingsToolbar.css
Normal file
21
packages/trace-viewer/src/ui/settingsToolbar.css
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.settings-toolbar-dialog {
|
||||||
|
background-color: var(--vscode-sideBar-background);
|
||||||
|
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
47
packages/trace-viewer/src/ui/settingsToolbar.tsx
Normal file
47
packages/trace-viewer/src/ui/settingsToolbar.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
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 * as React from 'react';
|
||||||
|
import { Dialog } from './shared/dialog';
|
||||||
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
|
import { DefaultSettingsView } from './defaultSettingsView';
|
||||||
|
import './settingsToolbar.css';
|
||||||
|
|
||||||
|
export const SettingsToolbar: React.FC<{}> = () => {
|
||||||
|
const hostingRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolbarButton
|
||||||
|
ref={hostingRef}
|
||||||
|
icon='settings-gear'
|
||||||
|
title='Settings'
|
||||||
|
onClick={() => setOpen(current => !current)}
|
||||||
|
/>
|
||||||
|
<Dialog
|
||||||
|
className='settings-toolbar-dialog'
|
||||||
|
open={open}
|
||||||
|
width={150}
|
||||||
|
requestClose={() => setOpen(false)}
|
||||||
|
hostingElement={hostingRef}
|
||||||
|
>
|
||||||
|
<DefaultSettingsView />
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
.settings-view {
|
.settings-view {
|
||||||
flex: none;
|
flex: none;
|
||||||
margin-top: 4px;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-view .setting label {
|
.settings-view .setting label {
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 4px 2px;
|
margin: 4px 2px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-view .setting:first-of-type label {
|
.settings-view .setting:first-of-type label {
|
||||||
|
|
|
||||||
147
packages/trace-viewer/src/ui/shared/dialog.tsx
Normal file
147
packages/trace-viewer/src/ui/shared/dialog.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
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 * as React from 'react';
|
||||||
|
|
||||||
|
export interface DialogProps {
|
||||||
|
className?: string;
|
||||||
|
|
||||||
|
open: boolean;
|
||||||
|
width: number;
|
||||||
|
|
||||||
|
requestClose?: () => void;
|
||||||
|
|
||||||
|
hostingElement?: React.RefObject<HTMLElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
|
||||||
|
className,
|
||||||
|
open,
|
||||||
|
width,
|
||||||
|
requestClose,
|
||||||
|
hostingElement,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const dialogRef = React.useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
|
let style: React.CSSProperties | undefined = undefined;
|
||||||
|
|
||||||
|
if (hostingElement?.current) {
|
||||||
|
// For now, always place dialog below hosting element
|
||||||
|
const bounds = hostingElement.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
style = {
|
||||||
|
// Override default `<dialog>` positioning
|
||||||
|
margin: 0,
|
||||||
|
top: bounds.bottom,
|
||||||
|
left: buildTopLeftCoord(bounds, width),
|
||||||
|
width,
|
||||||
|
// For some reason the dialog is placed behind the timeline, but there's a stacking context that allows the dialog to be placed above
|
||||||
|
zIndex: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onClick = (event: MouseEvent) => {
|
||||||
|
if (!dialogRef.current || !(event.target instanceof Node))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!dialogRef.current.contains(event.target)) {
|
||||||
|
// Click outside of dialog bounds
|
||||||
|
requestClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape')
|
||||||
|
requestClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
document.addEventListener('mousedown', onClick);
|
||||||
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', onClick);
|
||||||
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
}, [open, requestClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
open && (
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
style={style}
|
||||||
|
className={className}
|
||||||
|
open
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</dialog>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTopLeftCoord = (bounds: DOMRect, width: number): number => {
|
||||||
|
// Default to left aligned
|
||||||
|
const leftAlignCoord = buildTopLeftCoordWithAlignment(bounds, width, 'left');
|
||||||
|
|
||||||
|
if (leftAlignCoord.inBounds)
|
||||||
|
return leftAlignCoord.value;
|
||||||
|
|
||||||
|
const rightAlignCoord = buildTopLeftCoordWithAlignment(
|
||||||
|
bounds,
|
||||||
|
width,
|
||||||
|
'right'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rightAlignCoord.inBounds)
|
||||||
|
return rightAlignCoord.value;
|
||||||
|
|
||||||
|
// Fallback to left align, even if it will go off screen
|
||||||
|
return leftAlignCoord.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTopLeftCoordWithAlignment = (
|
||||||
|
bounds: DOMRect,
|
||||||
|
width: number,
|
||||||
|
alignment: 'left' | 'right'
|
||||||
|
): {
|
||||||
|
value: number;
|
||||||
|
inBounds: boolean;
|
||||||
|
} => {
|
||||||
|
const maxLeft = document.documentElement.clientWidth;
|
||||||
|
|
||||||
|
if (alignment === 'left') {
|
||||||
|
const value = bounds.left;
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
// Would extend off of right side of screen
|
||||||
|
inBounds: value + width <= maxLeft,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const value = bounds.right - width;
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
// Would extend off of left side of screen
|
||||||
|
inBounds: bounds.right - width >= 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -28,7 +28,6 @@ import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
import { Toolbar } from '@web/components/toolbar';
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
import type { XtermDataSource } from '@web/components/xtermWrapper';
|
import type { XtermDataSource } from '@web/components/xtermWrapper';
|
||||||
import { XtermWrapper } from '@web/components/xtermWrapper';
|
import { XtermWrapper } from '@web/components/xtermWrapper';
|
||||||
import { useDarkModeSetting } from '@web/theme';
|
|
||||||
import { clsx, settings, useSetting } from '@web/uiUtils';
|
import { clsx, settings, useSetting } from '@web/uiUtils';
|
||||||
import { statusEx, TestTree } from '@testIsomorphic/testTree';
|
import { statusEx, TestTree } from '@testIsomorphic/testTree';
|
||||||
import type { TreeItem } from '@testIsomorphic/testTree';
|
import type { TreeItem } from '@testIsomorphic/testTree';
|
||||||
|
|
@ -37,6 +36,7 @@ import { FiltersView } from './uiModeFiltersView';
|
||||||
import { TestListView } from './uiModeTestListView';
|
import { TestListView } from './uiModeTestListView';
|
||||||
import { TraceView } from './uiModeTraceView';
|
import { TraceView } from './uiModeTraceView';
|
||||||
import { SettingsView } from './settingsView';
|
import { SettingsView } from './settingsView';
|
||||||
|
import { DefaultSettingsView } from './defaultSettingsView';
|
||||||
|
|
||||||
let xtermSize = { cols: 80, rows: 24 };
|
let xtermSize = { cols: 80, rows: 24 };
|
||||||
const xtermDataSource: XtermDataSource = {
|
const xtermDataSource: XtermDataSource = {
|
||||||
|
|
@ -106,7 +106,6 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
const [singleWorker, setSingleWorker] = React.useState(false);
|
const [singleWorker, setSingleWorker] = React.useState(false);
|
||||||
const [showBrowser, setShowBrowser] = React.useState(false);
|
const [showBrowser, setShowBrowser] = React.useState(false);
|
||||||
const [updateSnapshots, setUpdateSnapshots] = React.useState(false);
|
const [updateSnapshots, setUpdateSnapshots] = React.useState(false);
|
||||||
const [darkMode, setDarkMode] = useDarkModeSetting();
|
|
||||||
|
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|
@ -523,9 +522,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
/>
|
/>
|
||||||
<div className='section-title'>Settings</div>
|
<div className='section-title'>Settings</div>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{settingsVisible && <SettingsView settings={[
|
{settingsVisible && <DefaultSettingsView />}
|
||||||
{ value: darkMode, set: setDarkMode, title: 'Dark mode' },
|
|
||||||
]} />}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import './workbenchLoader.css';
|
||||||
import { toggleTheme } from '@web/theme';
|
import { toggleTheme } from '@web/theme';
|
||||||
import { Workbench } from './workbench';
|
import { Workbench } from './workbench';
|
||||||
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
|
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
|
||||||
|
import { SettingsToolbar } from './settingsToolbar';
|
||||||
|
|
||||||
export const WorkbenchLoader: React.FunctionComponent<{
|
export const WorkbenchLoader: React.FunctionComponent<{
|
||||||
}> = () => {
|
}> = () => {
|
||||||
|
|
@ -161,7 +162,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
|
||||||
<div className='product'>Playwright</div>
|
<div className='product'>Playwright</div>
|
||||||
{model.title && <div className='title'>{model.title}</div>}
|
{model.title && <div className='title'>{model.title}</div>}
|
||||||
<div className='spacer'></div>
|
<div className='spacer'></div>
|
||||||
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
<SettingsToolbar />
|
||||||
</div>
|
</div>
|
||||||
<div className='progress'>
|
<div className='progress'>
|
||||||
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
|
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export interface ToolbarButtonProps {
|
||||||
ariaLabel?: string,
|
ariaLabel?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
|
export const ToolbarButton = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<ToolbarButtonProps>>(function ToolbarButton({
|
||||||
children,
|
children,
|
||||||
title = '',
|
title = '',
|
||||||
icon,
|
icon,
|
||||||
|
|
@ -42,8 +42,9 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
||||||
testId,
|
testId,
|
||||||
className,
|
className,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
}) => {
|
}, ref) {
|
||||||
return <button
|
return <button
|
||||||
|
ref={ref}
|
||||||
className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
|
className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
|
||||||
onMouseDown={preventDefault}
|
onMouseDown={preventDefault}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|
@ -57,7 +58,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
||||||
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
||||||
{children}
|
{children}
|
||||||
</button>;
|
</button>;
|
||||||
};
|
});
|
||||||
|
|
||||||
export const ToolbarSeparator: React.FC<{ style?: React.CSSProperties }> = ({
|
export const ToolbarSeparator: React.FC<{ style?: React.CSSProperties }> = ({
|
||||||
style,
|
style,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue