Browse Source

Change Dialog to accept React instead of Phosphor

Change the Dialog widget to accept ReactElement's instead of Phosphor
VirtualElement's.

Also add a helper class that creates a Widget out of a ReactElement.
Saul Shanabrook 7 years ago
parent
commit
5bf2cc9663

+ 1 - 1
packages/application-extension/package.json

@@ -32,7 +32,7 @@
     "@jupyterlab/application": "^0.15.4",
     "@jupyterlab/apputils": "^0.15.5",
     "@jupyterlab/coreutils": "^1.0.10",
-    "@phosphor/virtualdom": "^1.1.2"
+    "react": "~16.2.0"
   },
   "devDependencies": {
     "rimraf": "~2.6.2",

+ 14 - 12
packages/application-extension/src/index.ts → packages/application-extension/src/index.tsx

@@ -13,9 +13,7 @@ import {
   IStateDB, PageConfig
 } from '@jupyterlab/coreutils';
 
-import {
-  h
-} from '@phosphor/virtualdom';
+import * as React from 'react';
 
 
 /**
@@ -69,7 +67,11 @@ const main: JupyterLabPlugin<void> = {
   activate: (app: JupyterLab, palette: ICommandPalette) => {
     // If there were errors registering plugins, tell the user.
     if (app.registerPluginErrors.length !== 0) {
-      const body = h.pre(app.registerPluginErrors.map(e => e.message).join('\n'));
+      const body = (
+        <pre>
+          {app.registerPluginErrors.map(e => e.message).join('\n')}
+        </pre>
+      );
       let options = {
         title: 'Error Registering Plugins',
         body,
@@ -104,7 +106,7 @@ const main: JupyterLabPlugin<void> = {
       }).catch(err => {
         showDialog({
           title: 'Build Failed',
-          body: h.pre(err.message)
+          body: (<pre>{err.message}</pre>)
         });
       });
     };
@@ -117,13 +119,13 @@ const main: JupyterLabPlugin<void> = {
         if (response.status !== 'needed') {
           return;
         }
-        let body = h.div(
-          h.p(
-            'JupyterLab build is suggested:',
-            h.br(),
-            h.pre(response.message)
-          )
-        );
+        let body = (<div>
+          <p>
+            JupyterLab build is suggested:
+            <br />
+            <pre>{response.message}</pre>
+          </p>
+        </div>);
         showDialog({
           title: 'Build Recommended',
           body,

+ 2 - 1
packages/application-extension/tsconfig.json

@@ -9,7 +9,8 @@
     "target": "ES5",
     "outDir": "./lib",
     "lib": ["ES5", "ES2015.Promise", "DOM", "ES2015.Collection"],
-    "types": []
+    "types": [],
+    "jsx": "react"
   },
   "include": ["src/*"]
 }

+ 3 - 5
packages/apputils/src/clientsession.ts → packages/apputils/src/clientsession.tsx

@@ -25,14 +25,12 @@ import {
   ISignal, Signal
 } from '@phosphor/signaling';
 
-import {
-  h
-} from '@phosphor/virtualdom';
-
 import {
   Widget
 } from '@phosphor/widgets';
 
+import * as React from 'react';
+
 import {
   showDialog, Dialog
 } from './dialog';
@@ -684,7 +682,7 @@ class ClientSession implements IClientSession {
       }
       let dialog = this._dialog = new Dialog({
         title: 'Error Starting Kernel',
-        body: h.pre(message),
+        body: <pre>{message}</pre>,
         buttons: [Dialog.okButton()]
       });
       return dialog.launch();

+ 31 - 26
packages/apputils/src/dialog.ts

@@ -13,14 +13,16 @@ import {
   Message
 } from '@phosphor/messaging';
 
-import {
-  VirtualDOM, VirtualElement, h
-} from '@phosphor/virtualdom';
-
 import {
   PanelLayout, Panel, Widget
 } from '@phosphor/widgets';
 
+import * as React from 'react';
+
+import {
+  ReactElementWidget
+} from './vdom';
+
 import {
   Styling
 } from './styling';
@@ -444,7 +446,7 @@ namespace Dialog {
    * The header input types.
    */
   export
-  type HeaderType = VirtualElement | string;
+  type HeaderType = React.ReactElement<any> | string;
 
   /**
    * The result of a dialog.
@@ -477,7 +479,7 @@ namespace Dialog {
    * The body input types.
    */
   export
-  type BodyType<T> = IBodyWidget<T> | VirtualElement | string;
+  type BodyType<T> = IBodyWidget<T> | React.ReactElement<any> | string;
 
   /**
    * Create an accept button.
@@ -584,7 +586,7 @@ namespace Dialog {
         header = new Widget({ node: document.createElement('span') });
         header.node.textContent = title;
       } else {
-        header = new Widget({ node: VirtualDOM.realize(title) });
+        header = new ReactElementWidget(title);
       }
       header.addClass('jp-Dialog-header');
       Styling.styleNode(header.node);
@@ -606,7 +608,7 @@ namespace Dialog {
       } else if (value instanceof Widget) {
         body = value;
       } else {
-        body = new Widget({ node: VirtualDOM.realize(value) });
+        body = new ReactElementWidget(value);
       }
       body.addClass('jp-Dialog-body');
       Styling.styleNode(body.node);
@@ -638,16 +640,13 @@ namespace Dialog {
      * @returns A node for the button.
      */
     createButtonNode(button: IButton): HTMLElement {
-      let className = this.createItemClass(button);
-      // We use realize here instead of creating
-      // nodes with document.createElement as a
-      // shorthand, and only because this is not
-      // called often.
-      return VirtualDOM.realize(
-        h.button({ className },
-              this.renderIcon(button),
-              this.renderLabel(button))
+      const e = document.createElement(
+        'button'
       );
+      e.className = this.createItemClass(button);
+      e.appendChild(this.renderIcon(button));
+      e.appendChild(this.renderLabel(button));
+      return e;
     }
 
     /**
@@ -686,11 +685,15 @@ namespace Dialog {
      *
      * @param data - The data to use for rendering the icon.
      *
-     * @returns A virtual element representing the icon.
+     * @returns An HTML element representing the icon.
      */
-    renderIcon(data: IButton): VirtualElement {
-      return h.div({ className: this.createIconClass(data) },
-                   data.iconLabel);
+    renderIcon(data: IButton): HTMLElement {
+      const e = document.createElement('div');
+      e.className = this.createIconClass(data);
+      e.appendChild(
+        document.createTextNode(data.iconLabel)
+      );
+      return e;
     }
 
     /**
@@ -711,12 +714,14 @@ namespace Dialog {
      *
      * @param data - The data to use for rendering the label.
      *
-     * @returns A virtual element representing the item label.
+     * @returns An HTML element representing the item label.
      */
-    renderLabel(data: IButton): VirtualElement {
-      let className = 'jp-Dialog-buttonLabel';
-      let title = data.caption;
-      return h.div({ className, title }, data.label);
+    renderLabel(data: IButton): HTMLElement {
+      const e = document.createElement('div');
+      e.className = 'jp-Dialog-buttonLabel';
+      e.title = data.caption;
+      e.appendChild(document.createTextNode(data.label));
+      return e;
     }
   }
 

+ 22 - 0
packages/apputils/src/vdom.ts

@@ -106,6 +106,28 @@ abstract class VDomRenderer<T extends VDomRenderer.IModel | null> extends Widget
   private _modelChanged = new Signal<this, void>(this);
 }
 
+/**
+ * Phosphor widget that renders React Element(s).
+ *
+ * All messages will re-render the element.
+ */
+export
+class ReactElementWidget extends VDomRenderer<any> {
+  /**
+   * Creates a Phosphor widget that renders the element(s) `es`.
+   */
+  constructor(es: Array<React.ReactElement<any>> | React.ReactElement<any> | null) {
+    super();
+    this._es = es;
+  }
+
+  render():  Array<React.ReactElement<any>> | React.ReactElement<any> | null {
+    return this._es;
+  }
+
+  private _es: Array<React.ReactElement<any>> | React.ReactElement<any> | null;
+}
+
 
 /**
  * The namespace for VDomRenderer statics.

+ 1 - 0
packages/apputils/tsconfig.json

@@ -9,6 +9,7 @@
     "target": "ES5",
     "outDir": "./lib",
     "lib": ["ES5", "ES2015.Promise", "DOM", "ES2015.Collection"],
+    "jsx": "react",
     "types": []
   },
   "include": ["src/*"]

+ 2 - 2
packages/help-extension/package.json

@@ -36,8 +36,8 @@
     "@jupyterlab/mainmenu": "^0.4.4",
     "@jupyterlab/services": "^1.1.4",
     "@phosphor/messaging": "^1.2.2",
-    "@phosphor/virtualdom": "^1.1.2",
-    "@phosphor/widgets": "^1.5.0"
+    "@phosphor/widgets": "^1.5.0",
+    "react": "~16.2.0"
   },
   "devDependencies": {
     "rimraf": "~2.6.2",

+ 51 - 40
packages/help-extension/src/index.ts → packages/help-extension/src/index.tsx

@@ -25,14 +25,12 @@ import {
   Message
 } from '@phosphor/messaging';
 
-import {
-  h
-} from '@phosphor/virtualdom';
-
 import {
   Menu, PanelLayout, Widget
 } from '@phosphor/widgets';
 
+import * as React from 'react';
+
 import '../style/index.css';
 
 /**
@@ -201,7 +199,6 @@ function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette
   const resourcesGroup = RESOURCES
     .map(args => ({ args, command: CommandIDs.open }));
   helpMenu.addGroup(resourcesGroup, 10);
-  helpMenu.addGroup([{ command: 'apputils:reset' }], 20);
 
   // Generate a cache of the kernel help links.
   const kernelInfoCache = new Map<string, KernelMessage.IInfoReply>();
@@ -261,14 +258,18 @@ function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette
           isEnabled: usesKernel,
           execute: () => {
             // Create the header of the about dialog
-            let headerLogo = h.img({ src: kernelIconUrl});
-            let title = h.span({className: 'jp-About-header'},
-              headerLogo,
-              h.div({className: 'jp-About-header-info'}, kernelName)
+            let headerLogo = (<img src={kernelIconUrl} />);
+            let title = (
+              <span className='jp-About-header'>,
+                {headerLogo},
+                <div className='jp-About-header-info'>{kernelName}</div>
+              </span>
             );
-            const banner = h.pre({}, kernelInfo.banner);
-            let body = h.div({ className: 'jp-About-body' },
-              banner
+            const banner = (<pre>{kernelInfo.banner}</pre>);
+            let body = (
+              <div className='jp-About-body'>
+                {banner}
+              </div>
             );
 
             showDialog({
@@ -310,43 +311,53 @@ function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette
     execute: () => {
 
       // Create the header of the about dialog
-      let headerLogo = h.div({className: 'jp-About-header-logo'});
-      let headerWordmark = h.div({className: 'jp-About-header-wordmark'});
+      let headerLogo = (<div className='jp-About-header-logo'/>);
+      let headerWordmark = (<div className='jp-About-header-wordmark'/>);
       let release = 'Beta Release Series';
       let versionNumber = `Version ${info.version}`;
-      let versionInfo = h.span({className: 'jp-About-version-info'},
-        h.span({className: 'jp-About-release'}, release),
-        h.span({className: 'jp-About-version'}, versionNumber)
+      let versionInfo = (
+        <span className='jp-About-version-info'>
+          <span className='jp-About-release'>{release}</span>
+          <span className='jp-About-version'>{versionNumber}</span>
+        </span>
       );
-      let title = h.span({className: 'jp-About-header'},
-        headerLogo,
-        h.div({className: 'jp-About-header-info'},
-          headerWordmark,
-          versionInfo
-        )
+      let title = (
+        <span className='jp-About-header'>
+          {headerLogo},
+          <div className='jp-About-header-info'>
+            {headerWordmark}
+            {versionInfo}
+          </div>
+        </span>
       );
 
       // Create the body of the about dialog
       let jupyterURL = 'https://jupyter.org/about.html';
       let contributorsURL = 'https://github.com/jupyterlab/jupyterlab/graphs/contributors';
-      let externalLinks = h.span({className: 'jp-About-externalLinks'},
-        h.a({
-          href: contributorsURL,
-          target: '_blank',
-          className: 'jp-Button-flat'
-        }, 'CONTRIBUTOR LIST'),
-        h.a({
-          href: jupyterURL,
-          target: '_blank',
-          className: 'jp-Button-flat'
-        }, 'ABOUT PROJECT JUPYTER')
+      let externalLinks = (
+        <span className='jp-About-externalLinks'>
+          <a
+            href={contributorsURL}
+            target='_blank'
+            className='jp-Button-flat'
+          >CONTRIBUTOR LIST</a>
+          <a
+            href={jupyterURL}
+            target='_blank'
+            className='jp-Button-flat'
+          >ABOUT PROJECT JUPYTER</a>
+        </span>
+      );
+      let copyright = (
+        <span
+          className='jp-About-copyright'
+        >© 2018 Project Jupyter</span>
       );
-      let copyright = h.span({
-        className: 'jp-About-copyright'
-      }, '© 2018 Project Jupyter');
-      let body = h.div({ className: 'jp-About-body' },
-        externalLinks,
-        copyright
+      let body = (
+        <div className='jp-About-body'>
+          {externalLinks}
+          {copyright}
+        </div>
       );
 
       showDialog({

+ 2 - 1
packages/help-extension/tsconfig.json

@@ -9,7 +9,8 @@
     "target": "ES5",
     "outDir": "./lib",
     "lib": ["ES5", "ES2015.Promise", "DOM", "ES2015.Collection"],
-    "types": []
+    "types": [],
+    "jsx": "react"
   },
   "include": ["src/*"]
 }

+ 2 - 1
packages/notebook/package.json

@@ -46,7 +46,8 @@
     "@phosphor/properties": "^1.1.2",
     "@phosphor/signaling": "^1.2.2",
     "@phosphor/virtualdom": "^1.1.2",
-    "@phosphor/widgets": "^1.5.0"
+    "@phosphor/widgets": "^1.5.0",
+    "react": "~16.2.0"
   },
   "devDependencies": {
     "rimraf": "~2.6.2",

+ 12 - 12
packages/notebook/src/actions.ts → packages/notebook/src/actions.tsx

@@ -26,9 +26,7 @@ import {
   ElementExt
 } from '@phosphor/domutils';
 
-import {
-  h
-} from '@phosphor/virtualdom';
+import * as React from 'react';
 
 import {
   INotebookModel
@@ -40,15 +38,17 @@ import {
 
 
 // The message to display to the user when prompting to trust the notebook.
-const TRUST_MESSAGE = h.p(
-  'A trusted Jupyter notebook may execute hidden malicious code when you ',
-  'open it.',
-  h.br(),
-  'Selecting trust will re-render this notebook in a trusted state.',
-  h.br(),
-  'For more information, see the',
-  h.a({ href: 'https://jupyter-notebook.readthedocs.io/en/stable/security.html' },
-      'Jupyter security documentation'),
+const TRUST_MESSAGE = (
+  <p>
+    A trusted Jupyter notebook may execute hidden malicious code when you
+    open it.
+    <br />
+    Selecting trust will re-render this notebook in a trusted state.
+    <br />
+    For more information, see the
+    <a href='https://jupyter-notebook.readthedocs.io/en/stable/security.html'>
+      Jupyter security documentation</a>
+  </p>
 );
 
 

+ 2 - 1
packages/notebook/tsconfig.json

@@ -9,7 +9,8 @@
     "target": "ES5",
     "outDir": "./lib",
     "lib": ["ES5", "ES2015.Promise", "DOM", "ES2015.Collection"],
-    "types": []
+    "types": [],
+    "jsx": "react"
   },
   "include": ["src/*"]
 }

+ 7 - 9
tests/test-apputils/src/dialog.spec.ts → tests/test-apputils/src/dialog.spec.tsx

@@ -13,10 +13,6 @@ import {
   Message
 } from '@phosphor/messaging';
 
-import {
-  VirtualDOM, h
-} from '@phosphor/virtualdom';
-
 import {
   Widget
 } from '@phosphor/widgets';
@@ -29,6 +25,8 @@ import {
   Dialog, showDialog
 } from '@jupyterlab/apputils';
 
+import * as React from 'react';
+
 import {
   acceptDialog, dismissDialog, waitForDialog
 } from '../../utils';
@@ -98,7 +96,7 @@ describe('@jupyterlab/apputils', () => {
     });
 
     it('should accept a virtualdom body', () => {
-      let body = h.div([h.input(), h.select()]);
+      let body = (<div><input /><select /></div>);
       let promise = showDialog({ body }).then(result => {
         expect(result.button.accept).to.equal(true);
         expect(result.value).to.equal(null);
@@ -378,7 +376,7 @@ describe('@jupyterlab/apputils', () => {
         });
 
         it('should focus the primary element', () => {
-          let body = h.div([h.input()]);
+          let body = (<div><input /></div>);
           dialog = new TestDialog({ body, focusNodeSelector: 'input' });
           Widget.attach(dialog, document.body);
           expect((document.activeElement as HTMLElement).localName).to.equal('input');
@@ -465,7 +463,7 @@ describe('@jupyterlab/apputils', () => {
           });
 
           it('should create the body from a virtual node', () => {
-            let vnode = h.div({}, [h.input(), h.select(), h.button()]);
+            let vnode = (<div><input /><select /><button /></div>);
             let widget = renderer.createBody(vnode);
             let button = widget.node.querySelector('button');
             expect(button.className).to.contain('jp-mod-styled');
@@ -520,7 +518,7 @@ describe('@jupyterlab/apputils', () => {
         describe('#renderIcon()', () => {
 
           it('should render an icon element for a dialog item', () => {
-            let node = VirtualDOM.realize(renderer.renderIcon(data));
+            let node = renderer.renderIcon(data);
             expect(node.className).to.contain('jp-Dialog-buttonIcon');
             expect(node.textContent).to.equal('foo');
           });
@@ -551,7 +549,7 @@ describe('@jupyterlab/apputils', () => {
         describe('#renderLabel()', () => {
 
           it('should render a label element for a button', () => {
-            let node = VirtualDOM.realize(renderer.renderLabel(data));
+            let node = renderer.renderLabel(data);
             expect(node.className).to.equal('jp-Dialog-buttonLabel');
             expect(node.title).to.equal(data.caption);
             expect(node.textContent).to.equal(data.label);

+ 2 - 1
tests/test-apputils/tsconfig.json

@@ -9,6 +9,7 @@
     "outDir": "./build",
     "lib": [
       "ES5", "ES2015.Promise", "DOM", "ES2015.Collection", "ES2016", "ES6"
-    ]
+    ],
+    "jsx": "react"
   }
 }