refactor(ui): in splitview component, move sidebar and main from children into named properties (#31925)

Pulled out from https://github.com/microsoft/playwright/pull/31900

I stumbled over `React.Children`, because it's the first time I saw that
used. https://react.dev/reference/react/Children lists `React.Children`
it as "Legacy" and mentions it's uncommon. Also, the fact that SplitView
only displays its first two children, and all others are silently
discarded, can be a surprise to some.

By separating things out into `sidebar` and `main`, not only do we give
the two elements names (otherwise one needs to remember that sidebar is
always the first child), but we also prevent any "third children" from
being dropped.
This commit is contained in:
Simon Knott 2024-07-31 12:48:46 +02:00 committed by GitHub
parent 99724d0322
commit daca1681c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 101 additions and 60 deletions

View file

@ -167,26 +167,27 @@ export const Recorder: React.FC<RecorderProps> = ({
}}></ToolbarButton> }}></ToolbarButton>
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton> <ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
</Toolbar> </Toolbar>
<SplitView sidebarSize={200}> <SplitView
<CodeMirrorWrapper text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine} readOnly={true} lineNumbers={true}/> sidebarSize={200}
<TabbedPane main={<CodeMirrorWrapper text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine} readOnly={true} lineNumbers={true} />}
sidebar={<TabbedPane
rightToolbar={selectedTab === 'locator' ? [<ToolbarButton icon='files' title='Copy' onClick={() => copy(locator)} />] : []} rightToolbar={selectedTab === 'locator' ? [<ToolbarButton icon='files' title='Copy' onClick={() => copy(locator)} />] : []}
tabs={[ tabs={[
{ {
id: 'locator', id: 'locator',
title: 'Locator', title: 'Locator',
render: () => <CodeMirrorWrapper text={locator} language={source.language} readOnly={false} focusOnChange={true} onChange={onEditorChange} wrapLines={true}/> render: () => <CodeMirrorWrapper text={locator} language={source.language} readOnly={false} focusOnChange={true} onChange={onEditorChange} wrapLines={true} />
}, },
{ {
id: 'log', id: 'log',
title: 'Log', title: 'Log',
render: () => <CallLogView language={source.language} log={Array.from(log.values())}/> render: () => <CallLogView language={source.language} log={Array.from(log.values())} />
}, },
]} ]}
selectedTab={selectedTab} selectedTab={selectedTab}
setSelectedTab={setSelectedTab} setSelectedTab={setSelectedTab}
/> />}
</SplitView> />
</div>; </div>;
}; };

View file

@ -101,10 +101,15 @@ export const NetworkTab: React.FunctionComponent<{
/>; />;
return <> return <>
{!selectedEntry && grid} {!selectedEntry && grid}
{selectedEntry && <SplitView sidebarSize={columnWidths.get('name')!} sidebarIsFirst={true} orientation='horizontal' settingName='networkResourceDetails'> {selectedEntry &&
<NetworkResourceDetails resource={selectedEntry.resource} onClose={() => setSelectedEntry(undefined)} /> <SplitView
{grid} sidebarSize={columnWidths.get('name')!}
</SplitView>} sidebarIsFirst={true}
orientation='horizontal'
settingName='networkResourceDetails'
main={<NetworkResourceDetails resource={selectedEntry.resource} onClose={() => setSelectedEntry(undefined)} />}
sidebar={grid}
/>}
</>; </>;
}; };

View file

@ -96,17 +96,20 @@ export const SourceTab: React.FunctionComponent<{
const showStackFrames = (stack?.length ?? 0) > 1; const showStackFrames = (stack?.length ?? 0) > 1;
return <SplitView sidebarSize={200} orientation={stackFrameLocation === 'bottom' ? 'vertical' : 'horizontal'} sidebarHidden={!showStackFrames}> return <SplitView
<div className='vbox' data-testid='source-code'> sidebarSize={200}
orientation={stackFrameLocation === 'bottom' ? 'vertical' : 'horizontal'}
sidebarHidden={!showStackFrames}
main={<div className='vbox' data-testid='source-code'>
{ fileName && <Toolbar> { fileName && <Toolbar>
<span className='source-tab-file-name'>{fileName}</span> <span className='source-tab-file-name'>{fileName}</span>
<CopyToClipboard description='Copy filename' value={getFileName(fileName, targetLine)}/> <CopyToClipboard description='Copy filename' value={getFileName(fileName, targetLine)}/>
{location && <ToolbarButton icon='link-external' title='Open in VS Code' onClick={openExternally}></ToolbarButton>} {location && <ToolbarButton icon='link-external' title='Open in VS Code' onClick={openExternally}></ToolbarButton>}
</Toolbar> } </Toolbar> }
<CodeMirrorWrapper text={source.content || ''} language='javascript' highlight={highlight} revealLine={targetLine} readOnly={true} lineNumbers={true} /> <CodeMirrorWrapper text={source.content || ''} language='javascript' highlight={highlight} revealLine={targetLine} readOnly={true} lineNumbers={true} />
</div> </div>}
<StackTraceView stack={stack} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame} /> sidebar={<StackTraceView stack={stack} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame} />}
</SplitView>; />;
}; };
export async function calculateSha1(text: string): Promise<string> { export async function calculateSha1(text: string): Promise<string> {

View file

@ -433,8 +433,13 @@ export const UIModeView: React.FC<{}> = ({
<div className='title'>UI Mode disconnected</div> <div className='title'>UI Mode disconnected</div>
<div><a href='#' onClick={() => window.location.href = '/'}>Reload the page</a> to reconnect</div> <div><a href='#' onClick={() => window.location.href = '/'}>Reload the page</a> to reconnect</div>
</div>} </div>}
<SplitView sidebarSize={250} minSidebarSize={150} orientation='horizontal' sidebarIsFirst={true} settingName='testListSidebar'> <SplitView
<div className='vbox'> sidebarSize={250}
minSidebarSize={150}
orientation='horizontal'
sidebarIsFirst={true}
settingName='testListSidebar'
main={<div className='vbox'>
<div className={clsx('vbox', !isShowingOutput && 'hidden')}> <div className={clsx('vbox', !isShowingOutput && 'hidden')}>
<Toolbar> <Toolbar>
<div className='section-title' style={{ flex: 'none' }}>Output</div> <div className='section-title' style={{ flex: 'none' }}>Output</div>
@ -452,8 +457,8 @@ export const UIModeView: React.FC<{}> = ({
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })} onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
/> />
</div> </div>
</div> </div>}
<div className='vbox ui-mode-sidebar'> sidebar={<div className='vbox ui-mode-sidebar'>
<Toolbar noShadow={true} noMinHeight={true}> <Toolbar noShadow={true} noMinHeight={true}>
<img src='playwright-logo.svg' alt='Playwright logo' /> <img src='playwright-logo.svg' alt='Playwright logo' />
<div className='section-title'>Playwright</div> <div className='section-title'>Playwright</div>
@ -530,6 +535,7 @@ export const UIModeView: React.FC<{}> = ({
showRouteActionsSetting, showRouteActionsSetting,
]} />} ]} />}
</div> </div>
</SplitView> }
/>
</div>; </div>;
}; };

View file

@ -293,9 +293,15 @@ export const Workbench: React.FunctionComponent<{
selectedTime={selectedTime} selectedTime={selectedTime}
setSelectedTime={setSelectedTime} setSelectedTime={setSelectedTime}
/> />
<SplitView sidebarSize={250} orientation={sidebarLocation === 'bottom' ? 'vertical' : 'horizontal'} settingName='propertiesSidebar'> <SplitView
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true} settingName='actionListSidebar'> sidebarSize={250}
<SnapshotTab orientation={sidebarLocation === 'bottom' ? 'vertical' : 'horizontal'} settingName='propertiesSidebar'
main={<SplitView
sidebarSize={250}
orientation='horizontal'
sidebarIsFirst
settingName='actionListSidebar'
main={<SnapshotTab
action={activeAction} action={activeAction}
sdkLanguage={sdkLanguage} sdkLanguage={sdkLanguage}
testIdAttributeName={model?.testIdAttributeName || 'data-testid'} testIdAttributeName={model?.testIdAttributeName || 'data-testid'}
@ -303,14 +309,16 @@ export const Workbench: React.FunctionComponent<{
setIsInspecting={setIsInspecting} setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator} highlightedLocator={highlightedLocator}
setHighlightedLocator={locatorPicked} setHighlightedLocator={locatorPicked}
openPage={openPage} /> openPage={openPage} />}
<TabbedPane sidebar={
tabs={showSettings ? [actionsTab, metadataTab, settingsTab] : [actionsTab, metadataTab]} <TabbedPane
selectedTab={selectedNavigatorTab} tabs={showSettings ? [actionsTab, metadataTab, settingsTab] : [actionsTab, metadataTab]}
setSelectedTab={setSelectedNavigatorTab} selectedTab={selectedNavigatorTab}
/> setSelectedTab={setSelectedNavigatorTab}
</SplitView> />
<TabbedPane }
/>}
sidebar={<TabbedPane
tabs={tabs} tabs={tabs}
selectedTab={selectedPropertiesTab} selectedTab={selectedPropertiesTab}
setSelectedTab={selectPropertiesTab} setSelectedTab={selectPropertiesTab}
@ -324,7 +332,7 @@ export const Workbench: React.FunctionComponent<{
}} /> }} />
]} ]}
mode={sidebarLocation === 'bottom' ? 'default' : 'select'} mode={sidebarLocation === 'bottom' ? 'default' : 'select'}
/> />}
</SplitView> />
</div>; </div>;
}; };

View file

@ -20,10 +20,12 @@ import { SplitView } from './splitView';
test.use({ viewport: { width: 500, height: 500 } }); test.use({ viewport: { width: 500, height: 500 } });
test('should render', async ({ mount }) => { test('should render', async ({ mount }) => {
const component = await mount(<SplitView sidebarSize={100}> const component = await mount(
<div id='main' style={{ border: '1px solid red', flex: 'auto' }}>main</div> <SplitView
<div id='sidebar' style={{ border: '1px solid blue', flex: 'auto' }}>sidebar</div> sidebarSize={100}
</SplitView>); main={<div id='main' style={{ border: '1px solid red', flex: 'auto' }}>main</div>}
sidebar={<div id='sidebar' style={{ border: '1px solid blue', flex: 'auto' }}>sidebar</div>}
/>);
const mainBox = await component.locator('#main').boundingBox(); const mainBox = await component.locator('#main').boundingBox();
const sidebarBox = await component.locator('#sidebar').boundingBox(); const sidebarBox = await component.locator('#sidebar').boundingBox();
expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 400 }); expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 400 });
@ -31,10 +33,13 @@ test('should render', async ({ mount }) => {
}); });
test('should render sidebar first', async ({ mount }) => { test('should render sidebar first', async ({ mount }) => {
const component = await mount(<SplitView sidebarSize={100} sidebarIsFirst={true}> const component = await mount(
<div id='main' style={{ border: '1px solid blue', flex: 'auto' }}>main</div> <SplitView
<div id='sidebar' style={{ border: '1px solid red', flex: 'auto' }}>sidebar</div> sidebarSize={100}
</SplitView>); sidebarIsFirst
main={<div id='main' style={{ border: '1px solid blue', flex: 'auto' }}>main</div>}
sidebar={<div id='sidebar' style={{ border: '1px solid red', flex: 'auto' }}>sidebar</div>}
/>);
const mainBox = await component.locator('#main').boundingBox(); const mainBox = await component.locator('#main').boundingBox();
const sidebarBox = await component.locator('#sidebar').boundingBox(); const sidebarBox = await component.locator('#sidebar').boundingBox();
expect.soft(mainBox).toEqual({ x: 0, y: 100, width: 500, height: 400 }); expect.soft(mainBox).toEqual({ x: 0, y: 100, width: 500, height: 400 });
@ -42,10 +47,14 @@ test('should render sidebar first', async ({ mount }) => {
}); });
test('should render horizontal split', async ({ mount }) => { test('should render horizontal split', async ({ mount }) => {
const component = await mount(<SplitView sidebarSize={100} sidebarIsFirst={true} orientation={'horizontal'}> const component = await mount(
<div id='main' style={{ border: '1px solid blue', flex: 'auto' }}>main</div> <SplitView
<div id='sidebar' style={{ border: '1px solid red', flex: 'auto' }}>sidebar</div> sidebarSize={100}
</SplitView>); sidebarIsFirst
orientation='horizontal'
main={<div id='main' style={{ border: '1px solid blue', flex: 'auto' }}>main</div>}
sidebar={<div id='sidebar' style={{ border: '1px solid red', flex: 'auto' }}>sidebar</div>}
/>);
const mainBox = await component.locator('#main').boundingBox(); const mainBox = await component.locator('#main').boundingBox();
const sidebarBox = await component.locator('#sidebar').boundingBox(); const sidebarBox = await component.locator('#sidebar').boundingBox();
expect.soft(mainBox).toEqual({ x: 100, y: 0, width: 400, height: 500 }); expect.soft(mainBox).toEqual({ x: 100, y: 0, width: 400, height: 500 });
@ -53,19 +62,25 @@ test('should render horizontal split', async ({ mount }) => {
}); });
test('should hide sidebar', async ({ mount }) => { test('should hide sidebar', async ({ mount }) => {
const component = await mount(<SplitView sidebarSize={100} orientation={'horizontal'} sidebarHidden={true}> const component = await mount(
<div id='main' style={{ border: '1px solid blue', flex: 'auto' }}>main</div> <SplitView
<div id='sidebar' style={{ border: '1px solid red', flex: 'auto' }}>sidebar</div> sidebarSize={100}
</SplitView>); orientation={'horizontal'}
sidebarHidden
main={<div id='main' style={{ border: '1px solid blue', flex: 'auto' }}>main</div>}
sidebar={<div id='sidebar' style={{ border: '1px solid red', flex: 'auto' }}>sidebar</div>}
/>);
const mainBox = await component.locator('#main').boundingBox(); const mainBox = await component.locator('#main').boundingBox();
expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 500 }); expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 500 });
}); });
test('drag resize', async ({ page, mount }) => { test('drag resize', async ({ page, mount }) => {
const component = await mount(<SplitView sidebarSize={100}> const component = await mount(
<div id='main' style={{ border: '1px solid blue', flex: 'auto' }}>main</div> <SplitView
<div id='sidebar' style={{ border: '1px solid red', flex: 'auto' }}>sidebar</div> sidebarSize={100}
</SplitView>); main={<div id='main' style={{ border: '1px solid blue', flex: 'auto' }}>main</div>}
sidebar={<div id='sidebar' style={{ border: '1px solid red', flex: 'auto' }}>sidebar</div>}
/>);
await page.mouse.move(25, 400); await page.mouse.move(25, 400);
await page.mouse.down(); await page.mouse.down();
await page.mouse.move(25, 100); await page.mouse.move(25, 100);

View file

@ -25,18 +25,22 @@ export type SplitViewProps = {
orientation?: 'vertical' | 'horizontal'; orientation?: 'vertical' | 'horizontal';
minSidebarSize?: number; minSidebarSize?: number;
settingName?: string; settingName?: string;
sidebar: React.ReactNode;
main: React.ReactNode;
}; };
const kMinSize = 50; const kMinSize = 50;
export const SplitView: React.FC<React.PropsWithChildren<SplitViewProps>> = ({ export const SplitView: React.FC<SplitViewProps> = ({
sidebarSize, sidebarSize,
sidebarHidden = false, sidebarHidden = false,
sidebarIsFirst = false, sidebarIsFirst = false,
orientation = 'vertical', orientation = 'vertical',
minSidebarSize = kMinSize, minSidebarSize = kMinSize,
settingName, settingName,
children sidebar,
main,
}) => { }) => {
const defaultSize = Math.max(minSidebarSize, sidebarSize) * window.devicePixelRatio; const defaultSize = Math.max(minSidebarSize, sidebarSize) * window.devicePixelRatio;
const hSetting = useSetting<number>((settingName ?? 'unused') + '.' + orientation + ':size', defaultSize); const hSetting = useSetting<number>((settingName ?? 'unused') + '.' + orientation + ':size', defaultSize);
@ -60,7 +64,6 @@ export const SplitView: React.FC<React.PropsWithChildren<SplitViewProps>> = ({
size = measure.width - 10; size = measure.width - 10;
} }
const childrenArray = React.Children.toArray(children);
document.body.style.userSelect = resizing ? 'none' : 'inherit'; document.body.style.userSelect = resizing ? 'none' : 'inherit';
let resizerStyle: any = {}; let resizerStyle: any = {};
if (orientation === 'vertical') { if (orientation === 'vertical') {
@ -75,9 +78,9 @@ export const SplitView: React.FC<React.PropsWithChildren<SplitViewProps>> = ({
resizerStyle = { right: resizing ? 0 : size - 4, left: resizing ? 0 : undefined, width: resizing ? 'initial' : 8 }; resizerStyle = { right: resizing ? 0 : size - 4, left: resizing ? 0 : undefined, width: resizing ? 'initial' : 8 };
} }
return <div className={clsx('split-view', orientation, sidebarIsFirst && 'sidebar-first') } ref={ref}> return <div className={clsx('split-view', orientation, sidebarIsFirst && 'sidebar-first')} ref={ref}>
<div className='split-view-main'>{childrenArray[0]}</div> <div className='split-view-main'>{main}</div>
{ !sidebarHidden && <div style={{ flexBasis: size }} className='split-view-sidebar'>{childrenArray[1]}</div> } { !sidebarHidden && <div style={{ flexBasis: size }} className='split-view-sidebar'>{sidebar}</div> }
{ !sidebarHidden && <div { !sidebarHidden && <div
style={resizerStyle} style={resizerStyle}
className='split-view-resizer' className='split-view-resizer'