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:
committed by
Facebook GitHub Bot
parent
0bf786544a
commit
dd1f2fdeaa
@@ -9,11 +9,10 @@
|
|||||||
|
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Sidebar from '../ui/components/Sidebar';
|
|
||||||
import {toggleRightSidebarAvailable} from '../reducers/application';
|
import {toggleRightSidebarAvailable} from '../reducers/application';
|
||||||
import {useDispatch, useStore} from '../utils/useStore';
|
import {useDispatch, useStore} from '../utils/useStore';
|
||||||
import {ContentContainer} from '../sandy-chrome/ContentContainer';
|
import {ContentContainer} from '../sandy-chrome/ContentContainer';
|
||||||
import {Layout} from '../ui';
|
import {Layout, _Sidebar} from 'flipper-plugin';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
children: any;
|
children: any;
|
||||||
@@ -66,7 +65,7 @@ export default function DetailSidebar({children, width, minWidth}: OwnProps) {
|
|||||||
rightSidebarVisible &&
|
rightSidebarVisible &&
|
||||||
domNode &&
|
domNode &&
|
||||||
ReactDOM.createPortal(
|
ReactDOM.createPortal(
|
||||||
<Sidebar
|
<_Sidebar
|
||||||
minWidth={minWidth}
|
minWidth={minWidth}
|
||||||
width={width || 300}
|
width={width || 300}
|
||||||
position="right"
|
position="right"
|
||||||
@@ -74,7 +73,7 @@ export default function DetailSidebar({children, width, minWidth}: OwnProps) {
|
|||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
<Layout.ScrollContainer vertical>{children}</Layout.ScrollContainer>
|
<Layout.ScrollContainer vertical>{children}</Layout.ScrollContainer>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Sidebar>,
|
</_Sidebar>,
|
||||||
domNode,
|
domNode,
|
||||||
)) ||
|
)) ||
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -230,6 +230,16 @@ const demos: PreviewProps[] = [
|
|||||||
'true / number (0)',
|
'true / number (0)',
|
||||||
'Set the spacing between children. If just set, theme.space.small will be used.',
|
'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: {
|
demos: {
|
||||||
'Layout.Top': (
|
'Layout.Top': (
|
||||||
@@ -272,19 +282,19 @@ const demos: PreviewProps[] = [
|
|||||||
</Layout.Left>
|
</Layout.Left>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
),
|
),
|
||||||
'Layout.Right + Layout.ScrollContainer': (
|
'Layout.Right resizable + Layout.ScrollContainer': (
|
||||||
<Layout.Container style={{height: 150}}>
|
<Layout.Container style={{height: 150}}>
|
||||||
<Layout.Right>
|
<Layout.Right resizable>
|
||||||
<Layout.ScrollContainer>{largeChild}</Layout.ScrollContainer>
|
<Layout.ScrollContainer>{largeChild}</Layout.ScrollContainer>
|
||||||
{aFixedWidthBox}
|
{aDynamicBox}
|
||||||
</Layout.Right>
|
</Layout.Right>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
),
|
),
|
||||||
'Layout.Bottom + Layout.ScrollContainer': (
|
'Layout.Bottom resizable + Layout.ScrollContainer': (
|
||||||
<Layout.Container style={{height: 150}}>
|
<Layout.Container style={{height: 150}}>
|
||||||
<Layout.Bottom>
|
<Layout.Bottom resizable height={50} minHeight={20}>
|
||||||
<Layout.ScrollContainer>{largeChild}</Layout.ScrollContainer>
|
<Layout.ScrollContainer>{largeChild}</Layout.ScrollContainer>
|
||||||
{aFixedHeightBox}
|
{aDynamicBox}
|
||||||
</Layout.Bottom>
|
</Layout.Bottom>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,9 +8,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useEffect, useState, useCallback} from 'react';
|
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 {Link, styled} from '../ui';
|
||||||
import {Layout, Sidebar} from '../ui';
|
|
||||||
import {theme} from 'flipper-plugin';
|
import {theme} from 'flipper-plugin';
|
||||||
import {ipcRenderer} from 'electron';
|
import {ipcRenderer} from 'electron';
|
||||||
import {Logger} from '../fb-interfaces/Logger';
|
import {Logger} from '../fb-interfaces/Logger';
|
||||||
@@ -148,13 +147,13 @@ export function SandyApp() {
|
|||||||
toplevelSelection={toplevelSelection}
|
toplevelSelection={toplevelSelection}
|
||||||
setToplevelSelection={setToplevelSelection}
|
setToplevelSelection={setToplevelSelection}
|
||||||
/>
|
/>
|
||||||
<Sidebar width={250} minWidth={220} maxWidth={800} gutter>
|
<_Sidebar width={250} minWidth={220} maxWidth={800} gutter>
|
||||||
{leftMenuContent && (
|
{leftMenuContent && (
|
||||||
<TrackingScope scope={toplevelSelection!}>
|
<TrackingScope scope={toplevelSelection!}>
|
||||||
{leftMenuContent}
|
{leftMenuContent}
|
||||||
</TrackingScope>
|
</TrackingScope>
|
||||||
)}
|
)}
|
||||||
</Sidebar>
|
</_Sidebar>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
{outOfContentsContainer}
|
{outOfContentsContainer}
|
||||||
|
|||||||
@@ -7,16 +7,13 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {_Interactive, _InteractiveProps} from 'flipper-plugin';
|
import {theme, _Interactive, _InteractiveProps} from 'flipper-plugin';
|
||||||
import FlexColumn from './FlexColumn';
|
import FlexColumn from './FlexColumn';
|
||||||
import {colors} from './colors';
|
import {colors} from './colors';
|
||||||
import {Component, ReactNode} from 'react';
|
import {Component} from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {Property} from 'csstype';
|
import {Property} from 'csstype';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FlexRow from './FlexRow';
|
|
||||||
import {MoreOutlined} from '@ant-design/icons';
|
|
||||||
import {theme} from 'flipper-plugin';
|
|
||||||
|
|
||||||
const SidebarInteractiveContainer = styled(_Interactive)<_InteractiveProps>({
|
const SidebarInteractiveContainer = styled(_Interactive)<_InteractiveProps>({
|
||||||
flex: 'none',
|
flex: 'none',
|
||||||
@@ -25,6 +22,8 @@ SidebarInteractiveContainer.displayName = 'Sidebar:SidebarInteractiveContainer';
|
|||||||
|
|
||||||
type SidebarPosition = 'left' | 'top' | 'right' | 'bottom';
|
type SidebarPosition = 'left' | 'top' | 'right' | 'bottom';
|
||||||
|
|
||||||
|
const borderStyle = '1px solid ' + theme.dividerColor;
|
||||||
|
|
||||||
const SidebarContainer = styled(FlexColumn)<{
|
const SidebarContainer = styled(FlexColumn)<{
|
||||||
position: 'right' | 'top' | 'left' | 'bottom';
|
position: 'right' | 'top' | 'left' | 'bottom';
|
||||||
backgroundColor?: Property.BackgroundClip;
|
backgroundColor?: Property.BackgroundClip;
|
||||||
@@ -36,10 +35,10 @@ const SidebarContainer = styled(FlexColumn)<{
|
|||||||
: {
|
: {
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
props.backgroundColor || colors.macOSTitleBarBackgroundBlur,
|
props.backgroundColor || colors.macOSTitleBarBackgroundBlur,
|
||||||
borderLeft: props.position === 'right' ? '1px solid #b3b3b3' : 'none',
|
borderLeft: props.position === 'right' ? borderStyle : 'none',
|
||||||
borderTop: props.position === 'bottom' ? '1px solid #b3b3b3' : 'none',
|
borderTop: props.position === 'bottom' ? borderStyle : 'none',
|
||||||
borderRight: props.position === 'left' ? '1px solid #b3b3b3' : 'none',
|
borderRight: props.position === 'left' ? borderStyle : 'none',
|
||||||
borderBottom: props.position === 'top' ? '1px solid #b3b3b3' : 'none',
|
borderBottom: props.position === 'top' ? borderStyle : 'none',
|
||||||
}),
|
}),
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
@@ -97,10 +96,6 @@ type SidebarProps = {
|
|||||||
* Class name to customise styling.
|
* Class name to customise styling.
|
||||||
*/
|
*/
|
||||||
className?: string;
|
className?: string;
|
||||||
/**
|
|
||||||
* use a Sandy themed large gutter
|
|
||||||
*/
|
|
||||||
gutter?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type SidebarState = {
|
type SidebarState = {
|
||||||
@@ -111,6 +106,7 @@ type SidebarState = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A resizable sidebar.
|
* A resizable sidebar.
|
||||||
|
* @deprecated use Layout.Top / Right / Bottom / Left from flipper-plugin instead
|
||||||
*/
|
*/
|
||||||
export default class Sidebar extends Component<SidebarProps, SidebarState> {
|
export default class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
constructor(props: SidebarProps, context: Object) {
|
constructor(props: SidebarProps, context: Object) {
|
||||||
@@ -146,7 +142,7 @@ export default class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {backgroundColor, onResize, position, children, gutter} = this.props;
|
const {backgroundColor, onResize, position, children} = this.props;
|
||||||
let height: number | undefined;
|
let height: number | undefined;
|
||||||
let minHeight: number | undefined;
|
let minHeight: number | undefined;
|
||||||
let maxHeight: 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 horizontal = position === 'left' || position === 'right';
|
||||||
const gutterWidth = gutter ? theme.space.large : 0;
|
const gutterWidth = 0;
|
||||||
|
|
||||||
if (horizontal) {
|
if (horizontal) {
|
||||||
width = width == null ? 200 : width;
|
width = width == null ? 200 : width;
|
||||||
@@ -198,71 +194,14 @@ export default class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||||||
minHeight={minHeight}
|
minHeight={minHeight}
|
||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
height={
|
height={
|
||||||
!horizontal
|
!horizontal ? (onResize ? height : this.state.height) : undefined
|
||||||
? onResize
|
|
||||||
? height
|
|
||||||
: this.state.height
|
|
||||||
: gutter /*TODO: should use isSandy check*/
|
|
||||||
? undefined
|
|
||||||
: '100%'
|
|
||||||
}
|
}
|
||||||
resizable={resizable}
|
resizable={resizable}
|
||||||
onResize={this.onResize}
|
onResize={this.onResize}>
|
||||||
gutterWidth={gutter ? theme.space.large : undefined}>
|
<SidebarContainer position={position} backgroundColor={backgroundColor}>
|
||||||
<SidebarContainer
|
{children}
|
||||||
position={position}
|
|
||||||
backgroundColor={backgroundColor}
|
|
||||||
unstyled={gutter}>
|
|
||||||
{gutter ? (
|
|
||||||
<GutterWrapper position={position}>{children}</GutterWrapper>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</SidebarContainer>
|
</SidebarContainer>
|
||||||
</SidebarInteractiveContainer>
|
</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>
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export {
|
|||||||
NuxManagerContext as _NuxManagerContext,
|
NuxManagerContext as _NuxManagerContext,
|
||||||
createNuxManager as _createNuxManager,
|
createNuxManager as _createNuxManager,
|
||||||
} from './ui/NUX';
|
} from './ui/NUX';
|
||||||
|
export {Sidebar as _Sidebar} from './ui/Sidebar';
|
||||||
|
|
||||||
export {renderReactRoot} from './utils/renderReactRoot';
|
export {renderReactRoot} from './utils/renderReactRoot';
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -141,14 +141,79 @@ type SplitLayoutProps = {
|
|||||||
gap?: Spacing;
|
gap?: Spacing;
|
||||||
children: [React.ReactNode, React.ReactNode];
|
children: [React.ReactNode, React.ReactNode];
|
||||||
style?: CSSProperties;
|
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(
|
function renderSplitLayout(
|
||||||
props: SplitLayoutProps,
|
props: SplitLayoutProps,
|
||||||
direction: 'column' | 'row',
|
direction: 'column' | 'row',
|
||||||
grow: 1 | 2,
|
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 (
|
return (
|
||||||
<SandySplitContainer {...props} flexDirection={direction} grow={grow}>
|
<SandySplitContainer {...props} flexDirection={direction} grow={grow}>
|
||||||
{child1}
|
{child1}
|
||||||
@@ -169,16 +234,16 @@ function renderSplitLayout(
|
|||||||
* Use Layout.Top / Right / Bottom / Left to indicate where the fixed element should live.
|
* Use Layout.Top / Right / Bottom / Left to indicate where the fixed element should live.
|
||||||
*/
|
*/
|
||||||
export const Layout = {
|
export const Layout = {
|
||||||
Top(props: SplitLayoutProps) {
|
Top(props: SplitLayoutProps & SplitVerticalResizableProps) {
|
||||||
return renderSplitLayout(props, 'column', 2);
|
return renderSplitLayout(props, 'column', 2);
|
||||||
},
|
},
|
||||||
Bottom(props: SplitLayoutProps) {
|
Bottom(props: SplitLayoutProps & SplitVerticalResizableProps) {
|
||||||
return renderSplitLayout(props, 'column', 1);
|
return renderSplitLayout(props, 'column', 1);
|
||||||
},
|
},
|
||||||
Left(props: SplitLayoutProps) {
|
Left(props: SplitLayoutProps & SplitHorizontalResizableProps) {
|
||||||
return renderSplitLayout(props, 'row', 2);
|
return renderSplitLayout(props, 'row', 2);
|
||||||
},
|
},
|
||||||
Right(props: SplitLayoutProps) {
|
Right(props: SplitLayoutProps & SplitHorizontalResizableProps) {
|
||||||
return renderSplitLayout(props, 'row', 1);
|
return renderSplitLayout(props, 'row', 1);
|
||||||
},
|
},
|
||||||
Container,
|
Container,
|
||||||
|
|||||||
275
desktop/flipper-plugin/src/ui/Sidebar.tsx
Normal file
275
desktop/flipper-plugin/src/ui/Sidebar.tsx
Normal 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>
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user