Beginning of new settings dialog

This commit is contained in:
Adam Gastineau 2024-12-12 07:58:26 -08:00
parent 733f9a2926
commit f12164c295
8 changed files with 254 additions and 10 deletions

View 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' }]}
/>
);
};

View 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;
}

View 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>
</>
);
};

View file

@ -16,7 +16,7 @@
.settings-view {
flex: none;
margin-top: 4px;
user-select: none;
}
.settings-view .setting label {
@ -24,6 +24,7 @@
flex-direction: row;
align-items: center;
margin: 4px 2px;
cursor: pointer;
}
.settings-view .setting:first-of-type label {

View 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,
};
}
};

View file

@ -28,7 +28,6 @@ import { ToolbarButton } from '@web/components/toolbarButton';
import { Toolbar } from '@web/components/toolbar';
import type { XtermDataSource } from '@web/components/xtermWrapper';
import { XtermWrapper } from '@web/components/xtermWrapper';
import { useDarkModeSetting } from '@web/theme';
import { clsx, settings, useSetting } from '@web/uiUtils';
import { statusEx, TestTree } from '@testIsomorphic/testTree';
import type { TreeItem } from '@testIsomorphic/testTree';
@ -37,6 +36,7 @@ import { FiltersView } from './uiModeFiltersView';
import { TestListView } from './uiModeTestListView';
import { TraceView } from './uiModeTraceView';
import { SettingsView } from './settingsView';
import { DefaultSettingsView } from './defaultSettingsView';
let xtermSize = { cols: 80, rows: 24 };
const xtermDataSource: XtermDataSource = {
@ -106,7 +106,6 @@ export const UIModeView: React.FC<{}> = ({
const [singleWorker, setSingleWorker] = React.useState(false);
const [showBrowser, setShowBrowser] = React.useState(false);
const [updateSnapshots, setUpdateSnapshots] = React.useState(false);
const [darkMode, setDarkMode] = useDarkModeSetting();
const inputRef = React.useRef<HTMLInputElement>(null);
@ -523,9 +522,7 @@ export const UIModeView: React.FC<{}> = ({
/>
<div className='section-title'>Settings</div>
</Toolbar>
{settingsVisible && <SettingsView settings={[
{ value: darkMode, set: setDarkMode, title: 'Dark mode' },
]} />}
{settingsVisible && <DefaultSettingsView />}
</div>
}
/>

View file

@ -22,6 +22,7 @@ import './workbenchLoader.css';
import { toggleTheme } from '@web/theme';
import { Workbench } from './workbench';
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
import { SettingsToolbar } from './settingsToolbar';
export const WorkbenchLoader: React.FunctionComponent<{
}> = () => {
@ -161,7 +162,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
<div className='product'>Playwright</div>
{model.title && <div className='title'>{model.title}</div>}
<div className='spacer'></div>
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
<SettingsToolbar />
</div>
<div className='progress'>
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>

View file

@ -31,7 +31,7 @@ export interface ToolbarButtonProps {
ariaLabel?: string,
}
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
export const ToolbarButton = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<ToolbarButtonProps>>(function ToolbarButton({
children,
title = '',
icon,
@ -42,8 +42,9 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
testId,
className,
ariaLabel,
}) => {
}, ref) {
return <button
ref={ref}
className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
onMouseDown={preventDefault}
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>}
{children}
</button>;
};
});
export const ToolbarSeparator: React.FC<{ style?: React.CSSProperties }> = ({
style,