Move Sidebar to flipper-plugin

Summary:
This diff deprecates the Sidebar concept, and copies the implementation to Sandy (tried moving first, but since existing plugins use the Sidebar in non-flex (Layout) contexts, that the layout of several plugins, so rather deprecated the old implementation.

Instead of exposing `Sidebar` explicitly, one can now put the `resizable` flag on a Layout.Top/Left/Bottom/Right, which makes building layouts even simpler, see demo.

The gutter logic was moved to the new implementation, since that was only used by the Sandy chrome anyway.

Changelog: Layout.Top / Left / Bottom / Right now support a resizable option

Reviewed By: passy

Differential Revision: D27233899

fbshipit-source-id: fbbdeb2ebf30d49d0837705a00ea86bb07fc2ba2
This commit is contained in:
Michel Weststrate
2021-03-23 12:54:16 -07:00
committed by Facebook GitHub Bot
parent 0bf786544a
commit dd1f2fdeaa
7 changed files with 384 additions and 96 deletions

View File

@@ -9,11 +9,10 @@
import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import Sidebar from '../ui/components/Sidebar';
import {toggleRightSidebarAvailable} from '../reducers/application';
import {useDispatch, useStore} from '../utils/useStore';
import {ContentContainer} from '../sandy-chrome/ContentContainer';
import {Layout} from '../ui';
import {Layout, _Sidebar} from 'flipper-plugin';
type OwnProps = {
children: any;
@@ -66,7 +65,7 @@ export default function DetailSidebar({children, width, minWidth}: OwnProps) {
rightSidebarVisible &&
domNode &&
ReactDOM.createPortal(
<Sidebar
<_Sidebar
minWidth={minWidth}
width={width || 300}
position="right"
@@ -74,7 +73,7 @@ export default function DetailSidebar({children, width, minWidth}: OwnProps) {
<ContentContainer>
<Layout.ScrollContainer vertical>{children}</Layout.ScrollContainer>
</ContentContainer>
</Sidebar>,
</_Sidebar>,
domNode,
)) ||
null

View File

@@ -230,6 +230,16 @@ const demos: PreviewProps[] = [
'true / number (0)',
'Set the spacing between children. If just set, theme.space.small will be used.',
],
[
'resizable',
'true / undefined',
'If set, this split container will be resizable by the user. It is recommend to set width, maxWidth, minWidth respectively height, maxHeight, minHeight properties as well.',
],
[
'width / height / minWidth / minHeight / maxWidth / maxHeight',
'number / undefined',
'These dimensions in pixels will be used for clamping if the layout is marked as resizable',
],
],
demos: {
'Layout.Top': (
@@ -272,19 +282,19 @@ const demos: PreviewProps[] = [
</Layout.Left>
</Layout.Container>
),
'Layout.Right + Layout.ScrollContainer': (
'Layout.Right resizable + Layout.ScrollContainer': (
<Layout.Container style={{height: 150}}>
<Layout.Right>
<Layout.Right resizable>
<Layout.ScrollContainer>{largeChild}</Layout.ScrollContainer>
{aFixedWidthBox}
{aDynamicBox}
</Layout.Right>
</Layout.Container>
),
'Layout.Bottom + Layout.ScrollContainer': (
'Layout.Bottom resizable + Layout.ScrollContainer': (
<Layout.Container style={{height: 150}}>
<Layout.Bottom>
<Layout.Bottom resizable height={50} minHeight={20}>
<Layout.ScrollContainer>{largeChild}</Layout.ScrollContainer>
{aFixedHeightBox}
{aDynamicBox}
</Layout.Bottom>
</Layout.Container>
),

View File

@@ -8,9 +8,8 @@
*/
import React, {useEffect, useState, useCallback} from 'react';
import {TrackingScope, useLogger} from 'flipper-plugin';
import {TrackingScope, useLogger, _Sidebar, Layout} from 'flipper-plugin';
import {Link, styled} from '../ui';
import {Layout, Sidebar} from '../ui';
import {theme} from 'flipper-plugin';
import {ipcRenderer} from 'electron';
import {Logger} from '../fb-interfaces/Logger';
@@ -148,13 +147,13 @@ export function SandyApp() {
toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection}
/>
<Sidebar width={250} minWidth={220} maxWidth={800} gutter>
<_Sidebar width={250} minWidth={220} maxWidth={800} gutter>
{leftMenuContent && (
<TrackingScope scope={toplevelSelection!}>
{leftMenuContent}
</TrackingScope>
)}
</Sidebar>
</_Sidebar>
</Layout.Horizontal>
<MainContainer>
{outOfContentsContainer}

View File

@@ -7,16 +7,13 @@
* @format
*/
import {_Interactive, _InteractiveProps} from 'flipper-plugin';
import {theme, _Interactive, _InteractiveProps} from 'flipper-plugin';
import FlexColumn from './FlexColumn';
import {colors} from './colors';
import {Component, ReactNode} from 'react';
import {Component} from 'react';
import styled from '@emotion/styled';
import {Property} from 'csstype';
import React from 'react';
import FlexRow from './FlexRow';
import {MoreOutlined} from '@ant-design/icons';
import {theme} from 'flipper-plugin';
const SidebarInteractiveContainer = styled(_Interactive)<_InteractiveProps>({
flex: 'none',
@@ -25,6 +22,8 @@ SidebarInteractiveContainer.displayName = 'Sidebar:SidebarInteractiveContainer';
type SidebarPosition = 'left' | 'top' | 'right' | 'bottom';
const borderStyle = '1px solid ' + theme.dividerColor;
const SidebarContainer = styled(FlexColumn)<{
position: 'right' | 'top' | 'left' | 'bottom';
backgroundColor?: Property.BackgroundClip;
@@ -36,10 +35,10 @@ const SidebarContainer = styled(FlexColumn)<{
: {
backgroundColor:
props.backgroundColor || colors.macOSTitleBarBackgroundBlur,
borderLeft: props.position === 'right' ? '1px solid #b3b3b3' : 'none',
borderTop: props.position === 'bottom' ? '1px solid #b3b3b3' : 'none',
borderRight: props.position === 'left' ? '1px solid #b3b3b3' : 'none',
borderBottom: props.position === 'top' ? '1px solid #b3b3b3' : 'none',
borderLeft: props.position === 'right' ? borderStyle : 'none',
borderTop: props.position === 'bottom' ? borderStyle : 'none',
borderRight: props.position === 'left' ? borderStyle : 'none',
borderBottom: props.position === 'top' ? borderStyle : 'none',
}),
height: '100%',
overflowX: 'hidden',
@@ -97,10 +96,6 @@ type SidebarProps = {
* Class name to customise styling.
*/
className?: string;
/**
* use a Sandy themed large gutter
*/
gutter?: boolean;
};
type SidebarState = {
@@ -111,6 +106,7 @@ type SidebarState = {
/**
* A resizable sidebar.
* @deprecated use Layout.Top / Right / Bottom / Left from flipper-plugin instead
*/
export default class Sidebar extends Component<SidebarProps, SidebarState> {
constructor(props: SidebarProps, context: Object) {
@@ -146,7 +142,7 @@ export default class Sidebar extends Component<SidebarProps, SidebarState> {
};
render() {
const {backgroundColor, onResize, position, children, gutter} = this.props;
const {backgroundColor, onResize, position, children} = this.props;
let height: number | undefined;
let minHeight: number | undefined;
let maxHeight: number | undefined;
@@ -170,7 +166,7 @@ export default class Sidebar extends Component<SidebarProps, SidebarState> {
}
const horizontal = position === 'left' || position === 'right';
const gutterWidth = gutter ? theme.space.large : 0;
const gutterWidth = 0;
if (horizontal) {
width = width == null ? 200 : width;
@@ -198,71 +194,14 @@ export default class Sidebar extends Component<SidebarProps, SidebarState> {
minHeight={minHeight}
maxHeight={maxHeight}
height={
!horizontal
? onResize
? height
: this.state.height
: gutter /*TODO: should use isSandy check*/
? undefined
: '100%'
!horizontal ? (onResize ? height : this.state.height) : undefined
}
resizable={resizable}
onResize={this.onResize}
gutterWidth={gutter ? theme.space.large : undefined}>
<SidebarContainer
position={position}
backgroundColor={backgroundColor}
unstyled={gutter}>
{gutter ? (
<GutterWrapper position={position}>{children}</GutterWrapper>
) : (
children
)}
onResize={this.onResize}>
<SidebarContainer position={position} backgroundColor={backgroundColor}>
{children}
</SidebarContainer>
</SidebarInteractiveContainer>
);
}
}
const GutterWrapper = ({
position,
children,
}: {
position: SidebarPosition;
children: ReactNode;
}) => {
return position === 'right' ? (
<FlexRow grow>
<VerticalGutter enabled={!!children} />
{children}
</FlexRow>
) : (
<FlexRow grow>
{children}
<VerticalGutter enabled={!!children} />
</FlexRow>
); // TODO: support top / bottom
};
const VerticalGutterContainer = styled('div')<{enabled: boolean}>(
({enabled}) => ({
width: theme.space.large,
minWidth: theme.space.large,
height: '100%',
cursor: enabled ? undefined : 'default', // hide cursor from interactive container
color: enabled ? theme.textColorPlaceholder : theme.backgroundWash,
fontSize: '16px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
background: theme.backgroundWash,
':hover': {
background: enabled ? theme.dividerColor : undefined,
},
}),
);
const VerticalGutter = ({enabled}: {enabled: boolean}) => (
<VerticalGutterContainer enabled={enabled}>
<MoreOutlined />
</VerticalGutterContainer>
);

View File

@@ -55,6 +55,7 @@ export {
NuxManagerContext as _NuxManagerContext,
createNuxManager as _createNuxManager,
} from './ui/NUX';
export {Sidebar as _Sidebar} from './ui/Sidebar';
export {renderReactRoot} from './utils/renderReactRoot';
export {

View File

@@ -141,14 +141,79 @@ type SplitLayoutProps = {
gap?: Spacing;
children: [React.ReactNode, React.ReactNode];
style?: CSSProperties;
};
} & SplitHorizontalResizableProps &
SplitVerticalResizableProps;
type SplitHorizontalResizableProps =
| {
resizable: true;
/**
* Width describes the width of the resizable pane. To set a global width use the style attribute.
*/
width?: number;
minWidth?: number;
maxWidth?: number;
}
| {};
type SplitVerticalResizableProps =
| {
resizable: true;
/**
* Width describes the width of the resizable pane. To set a global width use the style attribute.
*/
height?: number;
minHeight?: number;
maxHeight?: number;
}
| {};
function renderSplitLayout(
props: SplitLayoutProps,
direction: 'column' | 'row',
grow: 1 | 2,
) {
const [child1, child2] = props.children;
let [child1, child2] = props.children;
if ('resizable' in props && props.resizable) {
const {
width,
height,
minHeight,
minWidth,
maxHeight,
maxWidth,
} = props as any;
const sizeProps =
direction === 'column'
? ({
minHeight,
height: height ?? 300,
maxHeight,
} as const)
: ({
minWidth,
width: width ?? 300,
maxWidth,
} as const);
const Sidebar = require('./Sidebar').Sidebar;
if (grow === 2) {
child1 = (
<Sidebar
position={direction === 'column' ? 'top' : 'left'}
{...sizeProps}>
{child1}
</Sidebar>
);
} else {
child2 = (
<Sidebar
position={direction === 'column' ? 'bottom' : 'right'}
{...sizeProps}>
{child2}
</Sidebar>
);
}
}
return (
<SandySplitContainer {...props} flexDirection={direction} grow={grow}>
{child1}
@@ -169,16 +234,16 @@ function renderSplitLayout(
* Use Layout.Top / Right / Bottom / Left to indicate where the fixed element should live.
*/
export const Layout = {
Top(props: SplitLayoutProps) {
Top(props: SplitLayoutProps & SplitVerticalResizableProps) {
return renderSplitLayout(props, 'column', 2);
},
Bottom(props: SplitLayoutProps) {
Bottom(props: SplitLayoutProps & SplitVerticalResizableProps) {
return renderSplitLayout(props, 'column', 1);
},
Left(props: SplitLayoutProps) {
Left(props: SplitLayoutProps & SplitHorizontalResizableProps) {
return renderSplitLayout(props, 'row', 2);
},
Right(props: SplitLayoutProps) {
Right(props: SplitLayoutProps & SplitHorizontalResizableProps) {
return renderSplitLayout(props, 'row', 1);
},
Container,

View File

@@ -0,0 +1,275 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Layout} from './Layout';
import {Component, ReactNode} from 'react';
import styled from '@emotion/styled';
import {Property} from 'csstype';
import React from 'react';
import {MoreOutlined} from '@ant-design/icons';
import {Interactive, InteractiveProps} from './Interactive';
import {theme} from './theme';
const SidebarInteractiveContainer = styled(Interactive)<InteractiveProps>({
display: 'flex',
flex: 1,
});
SidebarInteractiveContainer.displayName = 'Sidebar:SidebarInteractiveContainer';
type SidebarPosition = 'left' | 'top' | 'right' | 'bottom';
const borderStyle = `1px solid ${theme.dividerColor}`;
const SidebarContainer = styled(Layout.Container)<{
position: 'right' | 'top' | 'left' | 'bottom';
overflow?: boolean;
unstyled?: boolean;
}>((props) => ({
...(props.unstyled
? undefined
: {
borderLeft: props.position === 'right' ? borderStyle : 'none',
borderTop: props.position === 'bottom' ? borderStyle : 'none',
borderRight: props.position === 'left' ? borderStyle : 'none',
borderBottom: props.position === 'top' ? borderStyle : 'none',
backgroundColor: theme.backgroundDefault,
}),
flex: 1,
}));
SidebarContainer.displayName = 'Sidebar:SidebarContainer';
type SidebarProps = {
/**
* Position of the sidebar.
*/
position: SidebarPosition;
/**
* Default width of the sidebar. Only used for left/right sidebars.
*/
width?: number;
/**
* Minimum sidebar width. Only used for left/right sidebars.
*/
minWidth?: number;
/**
* Maximum sidebar width. Only used for left/right sidebars.
*/
maxWidth?: number;
/**
* Default height of the sidebar.
*/
height?: number;
/**
* Minimum sidebar height. Only used for top/bottom sidebars.
*/
minHeight?: number;
/**
* Maximum sidebar height. Only used for top/bottom sidebars.
*/
maxHeight?: number;
/**
* Callback when the sidebar size ahs changed.
*/
onResize?: (width: number, height: number) => void;
/**
* Contents of the sidebar.
*/
children?: React.ReactNode;
/**
* Class name to customise styling.
*/
className?: string;
/**
* use a Sandy themed large gutter
*/
gutter?: boolean;
};
type SidebarState = {
width?: Property.Width<number>;
height?: Property.Height<number>;
userChange: boolean;
};
/**
* A resizable sidebar.
*/
export class Sidebar extends Component<SidebarProps, SidebarState> {
constructor(props: SidebarProps, context: Object) {
super(props, context);
this.state = {
userChange: false,
width: props.width,
height: props.height,
};
}
static defaultProps = {
position: 'left',
};
static getDerivedStateFromProps(
nextProps: SidebarProps,
state: SidebarState,
) {
if (!state.userChange) {
return {width: nextProps.width, height: nextProps.height};
}
return null;
}
onResize = (width: number, height: number) => {
const {onResize} = this.props;
if (onResize) {
onResize(width, height);
} else {
this.setState({userChange: true, width, height});
}
};
render() {
const {onResize, position, children, gutter} = this.props;
let height: number | undefined;
let minHeight: number | undefined;
let maxHeight: number | undefined;
let width: number | undefined;
let minWidth: number | undefined;
let maxWidth: number | undefined;
const resizable: {[key: string]: boolean} = {};
if (position === 'left') {
resizable.right = true;
({width, minWidth, maxWidth} = this.props);
} else if (position === 'top') {
resizable.bottom = true;
({height, minHeight, maxHeight} = this.props);
} else if (position === 'right') {
resizable.left = true;
({width, minWidth, maxWidth} = this.props);
} else if (position === 'bottom') {
resizable.top = true;
({height, minHeight, maxHeight} = this.props);
}
const horizontal = position === 'left' || position === 'right';
const gutterWidth = gutter ? theme.space.large : 0;
if (horizontal) {
width = width == null ? 200 : width;
minWidth = (minWidth == null ? 100 : minWidth) + gutterWidth;
maxWidth = maxWidth == null ? 600 : maxWidth;
} else {
height = height == null ? 200 : height;
minHeight = minHeight == null ? 100 : minHeight;
maxHeight = maxHeight == null ? 600 : maxHeight;
}
return (
<SidebarInteractiveContainer
className={this.props.className}
minWidth={minWidth}
maxWidth={maxWidth}
width={
horizontal
? !children
? gutterWidth
: onResize
? width
: this.state.width
: undefined
}
minHeight={minHeight}
maxHeight={maxHeight}
height={
!horizontal
? onResize
? height
: this.state.height
: gutter
? undefined
: '100%'
}
resizable={resizable}
onResize={this.onResize}
gutterWidth={gutter ? theme.space.large : undefined}>
<SidebarContainer position={position} unstyled={gutter}>
{gutter ? (
<GutterWrapper position={position}>{children}</GutterWrapper>
) : (
children
)}
</SidebarContainer>
</SidebarInteractiveContainer>
);
}
}
const GutterWrapper = ({
position,
children,
}: {
position: SidebarPosition;
children: ReactNode;
}) => {
switch (position) {
case 'right':
return (
<Layout.Left>
<VerticalGutter enabled={!!children} />
{children}
</Layout.Left>
);
case 'left':
return (
<Layout.Right>
{children}
<VerticalGutter enabled={!!children} />
</Layout.Right>
);
case 'bottom':
// TODO: needs rotated styling
return (
<Layout.Top>
<VerticalGutter enabled={!!children} />
{children}
</Layout.Top>
);
case 'top':
// TODO: needs rotated styling
return (
<Layout.Bottom>
{children}
<VerticalGutter enabled={!!children} />
</Layout.Bottom>
);
}
};
const VerticalGutterContainer = styled('div')<{enabled: boolean}>(
({enabled}) => ({
width: theme.space.large,
minWidth: theme.space.large,
cursor: enabled ? undefined : 'default', // hide cursor from interactive container
color: enabled ? theme.textColorPlaceholder : theme.backgroundWash,
fontSize: '16px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
background: theme.backgroundWash,
':hover': {
background: enabled ? theme.dividerColor : undefined,
},
}),
);
const VerticalGutter = ({enabled}: {enabled: boolean}) => (
<VerticalGutterContainer enabled={enabled}>
<MoreOutlined />
</VerticalGutterContainer>
);