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

@@ -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>
);