Sheet component for plugins

Summary:
Exposing a `<Sheet>` component to plugins. This allows plugins to create sheets, by returning something like this from their render function:

```js
return <div>
  contents of my plugin
  <Sheet>{() => <div>content of my sheet</div>}
</div>
```

Reviewed By: passy

Differential Revision: D13597251

fbshipit-source-id: 9da6ba6d2036243ddd2d05b73d392bf24be8d375
This commit is contained in:
Daniel Büchele
2019-01-09 10:45:22 -08:00
committed by Facebook Github Bot
parent 0048fc6e4a
commit 384529e97f
7 changed files with 151 additions and 17 deletions

View File

@@ -17,6 +17,10 @@ import PluginContainer from './PluginContainer.js';
import Sheet from './chrome/Sheet.js'; import Sheet from './chrome/Sheet.js';
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import PluginDebugger from './chrome/PluginDebugger.js'; import PluginDebugger from './chrome/PluginDebugger.js';
import {
ACTIVE_SHEET_BUG_REPORTER,
ACTIVE_SHEET_PLUGIN_DEBUGGER,
} from './reducers/application.js';
import type Logger from './fb-stubs/Logger.js'; import type Logger from './fb-stubs/Logger.js';
import type BugReporter from './fb-stubs/BugReporter.js'; import type BugReporter from './fb-stubs/BugReporter.js';
@@ -49,16 +53,17 @@ export class App extends React.Component<Props> {
} }
getSheet = (onHide: () => mixed) => { getSheet = (onHide: () => mixed) => {
if (this.props.activeSheet === 'BUG_REPORTER') { if (this.props.activeSheet === ACTIVE_SHEET_BUG_REPORTER) {
return ( return (
<BugReporterDialog <BugReporterDialog
bugReporter={this.props.bugReporter} bugReporter={this.props.bugReporter}
onHide={onHide} onHide={onHide}
/> />
); );
} else if (this.props.activeSheet === 'PLUGIN_DEBUGGER') { } else if (this.props.activeSheet === ACTIVE_SHEET_PLUGIN_DEBUGGER) {
return <PluginDebugger onHide={onHide} />; return <PluginDebugger onHide={onHide} />;
} else { } else {
// contents are added via React.Portal
return null; return null;
} }
}; };

View File

@@ -5,7 +5,7 @@ exports[`Empty app state matches snapshot 1`] = `
className="css-1si6n3e" className="css-1si6n3e"
> >
<div <div
className="toolbar css-1cn9bxd" className="toolbar css-78eqwq"
> >
<div <div
className="css-q7gju5" className="css-q7gju5"
@@ -109,6 +109,18 @@ exports[`Empty app state matches snapshot 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div
className="css-6xnt2s"
>
<div
id="pluginSheetContents"
style={
Object {
"display": "none",
}
}
/>
</div>
<div <div
className="css-9qtipk" className="css-9qtipk"
> >

View File

@@ -10,18 +10,23 @@ import {Transition} from 'react-transition-group';
import {setActiveSheet} from '../reducers/application.js'; import {setActiveSheet} from '../reducers/application.js';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {styled} from 'flipper'; import {styled} from 'flipper';
import {PLUGIN_SHEET_ELEMENT_ID} from '../ui/components/Sheet';
import {ACTIVE_SHEET_PLUGIN_SHEET} from '../reducers/application';
import type {ActiveSheet} from '../reducers/application';
const DialogContainer = styled('div')(({state}) => ({ const DialogContainer = styled('div')(({state}) => ({
transform: `translate(-50%, ${ transform: `translate(-50%, ${
state === 'entering' || state === 'exiting' || state === 'exited' state === 'entering' || state === 'exiting' || state === 'exited'
? '-110' ? 'calc(-100% - 20px)'
: '0' : '0%'
}%)`, })`,
opacity: state === 'exited' ? 0 : 1,
transition: '.3s transform', transition: '.3s transform',
position: 'absolute', position: 'absolute',
left: '50%', left: '50%',
top: 38, top: 38,
zIndex: 2, zIndex: 3,
backgroundColor: '#EFEEEF', backgroundColor: '#EFEEEF',
border: '1px solid #C6C6C6', border: '1px solid #C6C6C6',
borderTop: 'none', borderTop: 'none',
@@ -31,7 +36,7 @@ const DialogContainer = styled('div')(({state}) => ({
})); }));
type Props = {| type Props = {|
sheetVisible: boolean, activeSheet: ActiveSheet,
onHideSheet: () => void, onHideSheet: () => void,
children: (onHide: () => mixed) => any, children: (onHide: () => mixed) => any,
|}; |};
@@ -42,11 +47,11 @@ type State = {|
class Sheet extends Component<Props, State> { class Sheet extends Component<Props, State> {
state = { state = {
isVisible: this.props.sheetVisible, isVisible: Boolean(this.props.activeSheet),
}; };
static getDerivedStateFromProps(props: Props, state: State) { static getDerivedStateFromProps(props: Props, state: State) {
if (!props.sheetVisible) { if (!props.activeSheet) {
return { return {
isVisible: true, isVisible: true,
}; };
@@ -76,12 +81,23 @@ class Sheet extends Component<Props, State> {
render() { render() {
return ( return (
<Transition <Transition
in={this.props.sheetVisible && this.state.isVisible} in={Boolean(this.props.activeSheet) && this.state.isVisible}
timeout={300} timeout={300}
onExited={() => this.props.onHideSheet()} onExited={() => this.props.onHideSheet()}>
unmountOnExit>
{state => ( {state => (
<DialogContainer state={state}> <DialogContainer state={state}>
<div
/* This is the target for React.portal, it should not be
* unmounted, therefore it's hidden, when another sheet
* is presented. */
id={PLUGIN_SHEET_ELEMENT_ID}
style={{
display:
this.props.activeSheet === ACTIVE_SHEET_PLUGIN_SHEET
? 'block'
: 'none',
}}
/>
{this.props.children(this.onHide)} {this.props.children(this.onHide)}
</DialogContainer> </DialogContainer>
)} )}
@@ -95,7 +111,7 @@ class Sheet extends Component<Props, State> {
* run Flow. */ * run Flow. */
export default connect( export default connect(
({application: {activeSheet}}) => ({ ({application: {activeSheet}}) => ({
sheetVisible: Boolean(activeSheet), activeSheet,
}), }),
{ {
onHideSheet: () => setActiveSheet(null), onHideSheet: () => setActiveSheet(null),

View File

@@ -21,6 +21,7 @@ import {
setActiveSheet, setActiveSheet,
toggleLeftSidebarVisible, toggleLeftSidebarVisible,
toggleRightSidebarVisible, toggleRightSidebarVisible,
ACTIVE_SHEET_BUG_REPORTER,
} from '../reducers/application.js'; } from '../reducers/application.js';
import DevicesButton from './DevicesButton.js'; import DevicesButton from './DevicesButton.js';
import ScreenCaptureButtons from './ScreenCaptureButtons.js'; import ScreenCaptureButtons from './ScreenCaptureButtons.js';
@@ -44,7 +45,7 @@ const AppTitleBar = styled(FlexRow)(({focused}) => ({
paddingRight: 10, paddingRight: 10,
justifyContent: 'space-between', justifyContent: 'space-between',
WebkitAppRegion: 'drag', WebkitAppRegion: 'drag',
zIndex: 3, zIndex: 4,
})); }));
type Props = {| type Props = {|
@@ -68,7 +69,7 @@ class TitleBar extends Component<Props> {
{config.bugReportButtonVisible && ( {config.bugReportButtonVisible && (
<Button <Button
compact={true} compact={true}
onClick={() => this.props.setActiveSheet('BUG_REPORTER')} onClick={() => this.props.setActiveSheet(ACTIVE_SHEET_BUG_REPORTER)}
title="Report Bug" title="Report Bug"
icon="bug" icon="bug"
/> />

View File

@@ -7,7 +7,16 @@
import {remote} from 'electron'; import {remote} from 'electron';
export type ActiveSheet = 'BUG_REPORTER' | 'PLUGIN_DEBUGGER' | null; export const ACTIVE_SHEET_PLUGIN_SHEET: 'PLUGIN_SHEET' = 'PLUGIN_SHEET';
export const ACTIVE_SHEET_BUG_REPORTER: 'BUG_REPORTER' = 'BUG_REPORTER';
export const ACTIVE_SHEET_PLUGIN_DEBUGGER: 'PLUGIN_DEBUGGER' =
'PLUGIN_DEBUGGER';
export type ActiveSheet =
| typeof ACTIVE_SHEET_PLUGIN_SHEET
| typeof ACTIVE_SHEET_BUG_REPORTER
| typeof ACTIVE_SHEET_PLUGIN_DEBUGGER
| null;
export type State = { export type State = {
leftSidebarVisible: boolean, leftSidebarVisible: boolean,

View File

@@ -0,0 +1,89 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import {Component} from 'react';
import {createPortal} from 'react-dom';
import {connect} from 'react-redux';
import {setActiveSheet} from '../../reducers/application.js';
import type {ActiveSheet} from '../../reducers/application';
export const PLUGIN_SHEET_ELEMENT_ID = 'pluginSheetContents';
type Props = {
/**
* Function as child component (FaCC) to render the contents of the sheet.
* A `onHide` function is passed as argument, that can be called to remove
* the sheet.
*/
children: (onHide: () => void) => ?React.Node,
setActiveSheet: (sheet: ActiveSheet) => any,
activeSheet: ActiveSheet,
};
type State = {
content: ?React.Node,
};
/**
* Usage: <Sheet>{onHide => <YourSheetContent onHide={onHide} />}</Sheet>
*/
class Sheet extends Component<Props, State> {
static getDerivedStateFromProps(props: Props) {
if (props.activeSheet === 'PLUGIN_SHEET') {
return {
content: props.children(() => {
props.setActiveSheet(null);
}),
};
}
return null;
}
state = {
content: this.props.children(() => {
this.props.setActiveSheet(null);
}),
};
componentDidMount() {
this.showSheetIfContentsAvailable();
}
componentDidUpdate(prevProps: Props, prevState: State) {
if (prevState.content !== this.state.content) {
this.showSheetIfContentsAvailable();
}
}
showSheetIfContentsAvailable = () => {
if (this.state.content) {
this.props.setActiveSheet('PLUGIN_SHEET');
} else {
this.props.setActiveSheet(null);
}
};
render() {
const container = document.getElementById(PLUGIN_SHEET_ELEMENT_ID);
if (this.state.content && container) {
return createPortal(this.state.content, container);
}
if (this.state.content) {
console.warn(
`The <Sheet> could not be displayed, because there was not element#${PLUGIN_SHEET_ELEMENT_ID}.`,
);
}
return null;
}
}
// $FlowFixMe
export default connect(
({application: {activeSheet}}) => ({activeSheet}),
{setActiveSheet},
)(Sheet);

View File

@@ -165,3 +165,5 @@ export {
export {InspectorSidebar} from './components/elements-inspector/sidebar.js'; export {InspectorSidebar} from './components/elements-inspector/sidebar.js';
export {Console} from './components/console.js'; export {Console} from './components/console.js';
export {default as Sheet} from './components/Sheet.js';