T createRef();
@@ -28,9 +31,9 @@ > ReactElement createElement(
*/
@JsOverlay
public static <
- P extends ReactComponentProps
- > ReactElement createElementWithThemeContext(
- ReactComponentType component,
+ T extends ReactComponentType
, P extends ReactComponentProps
+ > ReactElement, ?> createElementWithThemeContext(
+ ReactComponentType
componentType,
P props
) {
SynapseReactClientFullContextProviderProps emptyContext =
@@ -38,7 +41,7 @@ > ReactElement createElementWithThemeContext(
SynapseContextJsObject.create(null, false, false),
null
);
- return createElementWithSynapseContext(component, props, emptyContext);
+ return createElementWithSynapseContext(componentType, props, emptyContext);
}
/**
@@ -46,7 +49,7 @@ > ReactElement createElementWithThemeContext(
* simplifies creating the wrapper.
*
* For setting props, use {@link SynapseReactClientFullContextPropsProvider}
- * @param component
+ * @param componentType
* @param props
* @param wrapperProps
* @param
@@ -54,13 +57,13 @@ > ReactElement createElementWithThemeContext(
*/
@JsOverlay
public static <
- P extends ReactComponentProps
- > ReactElement createElementWithSynapseContext(
- ReactComponentType
component,
+ T extends ReactComponentType
, P extends ReactComponentProps
+ > ReactElement, ?> createElementWithSynapseContext(
+ T componentType,
P props,
SynapseReactClientFullContextProviderProps wrapperProps
) {
- ReactElement componentElement = createElement(component, props);
+ ReactElement componentElement = createElement(componentType, props);
return createElement(
SRC.SynapseContext.FullContextProvider,
wrapperProps,
diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactComponentProps.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactComponentProps.java
index 33c001237a..84832af49a 100644
--- a/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactComponentProps.java
+++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactComponentProps.java
@@ -3,7 +3,6 @@
import com.google.gwt.dom.client.Element;
import jsinterop.annotations.JsConstructor;
import jsinterop.annotations.JsFunction;
-import jsinterop.annotations.JsOverlay;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;
@@ -18,29 +17,6 @@ public interface CallbackRef {
void run(Element element);
}
- public JsArray> children;
-
// Either a ComponentRef or CallbackRef may be passed. A CallbackRef will be invoked when the ref is set.
public Object ref;
-
- @JsOverlay
- public final void addChild(ReactElement> child) {
- if (children == null) {
- children = new JsArray<>();
- }
- children.push(child);
- }
-
- @JsOverlay
- public final void clearChildren() {
- children = new JsArray<>();
- }
-
- @JsOverlay
- public final JsArray> getChildren() {
- if (children == null) {
- children = new JsArray<>();
- }
- return children;
- }
}
diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactElement.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactElement.java
index ed9e6aa197..fbe486bcb1 100644
--- a/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactElement.java
+++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactElement.java
@@ -5,11 +5,15 @@
import jsinterop.annotations.JsType;
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
-public class ReactElement {
+public class ReactElement<
+ T extends ReactComponentType, P extends ReactComponentProps
+> {
+
+ public T type;
@JsNullable
- public T props;
+ public P props;
@JsNullable
- public ComponentRef ref;
+ public String key;
}
diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/Grid.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/Grid.java
new file mode 100644
index 0000000000..b8848a3e02
--- /dev/null
+++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/Grid.java
@@ -0,0 +1,91 @@
+package org.sagebionetworks.web.client.jsinterop.mui;
+
+import org.sagebionetworks.web.client.jsinterop.ReactComponentType;
+import org.sagebionetworks.web.client.jsinterop.react.HasStyle;
+
+public class Grid extends HasStyle, GridProps> {
+
+ public Grid() {
+ super(MaterialUI.Unstable_Grid2, GridProps.create(false));
+ }
+
+ public void setId(String id) {
+ props.id = id;
+ this.render();
+ }
+
+ public void setContainer(boolean container) {
+ props.container = container;
+ this.render();
+ }
+
+ public void setXs(int xs) {
+ props.xs = xs;
+ this.render();
+ }
+
+ public void setSm(int sm) {
+ props.sm = sm;
+ this.render();
+ }
+
+ public void setMd(int md) {
+ props.md = md;
+ this.render();
+ }
+
+ public void setLg(int lg) {
+ props.lg = lg;
+ this.render();
+ }
+
+ public void setXl(int xl) {
+ props.xl = xl;
+ this.render();
+ }
+
+ public void setXsOffset(int xsOffset) {
+ props.xsOffset = xsOffset;
+ this.render();
+ }
+
+ public void setSmOffset(int smOffset) {
+ props.smOffset = smOffset;
+ this.render();
+ }
+
+ public void setMdOffset(int mdOffset) {
+ props.mdOffset = mdOffset;
+ this.render();
+ }
+
+ public void setLgOffset(int lgOffset) {
+ props.lgOffset = lgOffset;
+ this.render();
+ }
+
+ public void setXlOffset(int xlOffset) {
+ props.xlOffset = xlOffset;
+ this.render();
+ }
+
+ public void setMt(String mt) {
+ props.mt = mt;
+ this.render();
+ }
+
+ public void setPl(String pl) {
+ props.pl = pl;
+ this.render();
+ }
+
+ public void setRowSpacing(String rowSpacing) {
+ props.rowSpacing = rowSpacing;
+ this.render();
+ }
+
+ public void setColumnSpacing(String columnSpacing) {
+ props.columnSpacing = columnSpacing;
+ this.render();
+ }
+}
diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/GridProps.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/GridProps.java
new file mode 100644
index 0000000000..e374a3cd3a
--- /dev/null
+++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/GridProps.java
@@ -0,0 +1,68 @@
+package org.sagebionetworks.web.client.jsinterop.mui;
+
+import jsinterop.annotations.JsNullable;
+import jsinterop.annotations.JsOverlay;
+import jsinterop.annotations.JsPackage;
+import jsinterop.annotations.JsType;
+import org.sagebionetworks.web.client.jsinterop.PropsWithStyle;
+
+@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
+public class GridProps extends PropsWithStyle {
+
+ @JsNullable
+ String id;
+
+ boolean container;
+
+ @JsNullable
+ int xs;
+
+ @JsNullable
+ int sm;
+
+ @JsNullable
+ int md;
+
+ @JsNullable
+ int lg;
+
+ @JsNullable
+ int xl;
+
+ @JsNullable
+ int xsOffset;
+
+ @JsNullable
+ int smOffset;
+
+ @JsNullable
+ int mdOffset;
+
+ @JsNullable
+ int lgOffset;
+
+ @JsNullable
+ int xlOffset;
+
+ @JsNullable
+ String mt;
+
+ @JsNullable
+ String pl;
+
+ @JsNullable
+ String rowSpacing;
+
+ @JsNullable
+ String columnSpacing;
+
+ @JsOverlay
+ public static GridProps create(boolean container) {
+ GridProps props = new GridProps();
+
+ if (container) {
+ props.container = true;
+ }
+ return props;
+ }
+}
diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/MaterialUI.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/MaterialUI.java
new file mode 100644
index 0000000000..8167a7958a
--- /dev/null
+++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/MaterialUI.java
@@ -0,0 +1,11 @@
+package org.sagebionetworks.web.client.jsinterop.mui;
+
+import jsinterop.annotations.JsPackage;
+import jsinterop.annotations.JsType;
+import org.sagebionetworks.web.client.jsinterop.ReactComponentType;
+
+@JsType(isNative = true, namespace = JsPackage.GLOBAL)
+public class MaterialUI {
+
+ public static ReactComponentType Unstable_Grid2;
+}
diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/react/HasStyle.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/react/HasStyle.java
index a4b1db39f4..a7d5bd8f85 100644
--- a/src/main/java/org/sagebionetworks/web/client/jsinterop/react/HasStyle.java
+++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/react/HasStyle.java
@@ -3,18 +3,21 @@
import jsinterop.base.JsPropertyMap;
import org.sagebionetworks.web.client.jsinterop.JsObject;
import org.sagebionetworks.web.client.jsinterop.PropsWithStyle;
-import org.sagebionetworks.web.client.widget.ReactComponent;
+import org.sagebionetworks.web.client.jsinterop.ReactComponentType;
+import org.sagebionetworks.web.client.widget.ReactComponentV2;
/**
* Abstract class for React component widgets that have a style prop. The style prop for the component may be manipulated
* to show/hide the component based on the current state of the widget.
* @param the prop type.
*/
-public abstract class HasStyle
- extends ReactComponent {
+public abstract class HasStyle<
+ T extends ReactComponentType, P extends PropsWithStyle
+>
+ extends ReactComponentV2 {
- public HasStyle() {
- super();
+ public HasStyle(T reactComponentType, P props) {
+ super(reactComponentType, props);
}
public void setStyle(JsPropertyMap style) {
@@ -33,7 +36,11 @@ public void setVisible(boolean visible) {
} else {
// Update the style prop to `display: none`.
if (this.props == null) {
- this.props = (T) JsPropertyMap.of();
+ this.props = (P) JsPropertyMap.of();
+ }
+
+ if (this.props.style == null) {
+ this.props.style = (JsPropertyMap) new JsObject();
}
this.props.style.set("display", "none");
diff --git a/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java
index 32f37cc582..ff28cc3509 100644
--- a/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java
+++ b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java
@@ -1,187 +1,66 @@
package org.sagebionetworks.web.client.widget;
import com.google.gwt.dom.client.DivElement;
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.ui.ComplexPanel;
-import com.google.gwt.user.client.ui.Widget;
-import java.util.ArrayList;
-import java.util.List;
-import jsinterop.base.JsPropertyMap;
-import org.sagebionetworks.web.client.jsinterop.React;
-import org.sagebionetworks.web.client.jsinterop.ReactComponentProps;
-import org.sagebionetworks.web.client.jsinterop.ReactComponentType;
+import com.google.gwt.user.client.ui.FlowPanel;
import org.sagebionetworks.web.client.jsinterop.ReactDOM;
import org.sagebionetworks.web.client.jsinterop.ReactDOMRoot;
import org.sagebionetworks.web.client.jsinterop.ReactElement;
/**
- * Widget that manages the lifecycle of a React component. To use this widget, create a {@link ReactElement} using the
- * {@link React} API and call {@link #render(ReactElement)} to render the React component.
- *
- * This widget also manages appending child elements if the associated React component can contain children. If all
- * child widgets use this class, then the child {@link ReactElement}s will be cloned and passed as children to the React
- * component. If any child of this component is a non-ReactComponent widget, then the child widgets will be injected
- * into the node found using the component's `ref`.
- *
- * The root element defaults to a `div`, but can be changed (e.g. to a `span`) using the {@link #ReactComponent(String)} constructor.
- *
- * This widget automatically unmounts the ReactComponent (if any) when this container is detached/unloaded.
+ * Automatically unmounts the ReactComponent (if any) inside this div when this container is detached/unloaded.
*/
-public class ReactComponent
- extends ComplexPanel
- implements HasClickHandlers {
+public class ReactComponent extends FlowPanel implements HasClickHandlers {
private ReactDOMRoot root;
- private ReactComponentType reactComponentType;
- public T props;
-
- private ReactElement> reactElement;
+ private ReactElement, ?> reactElement;
public ReactComponent() {
- this(DivElement.TAG);
+ super(DivElement.TAG);
}
public ReactComponent(String tag) {
- setElement(Document.get().createElement(tag));
- }
-
- private boolean allChildrenAreReactComponents() {
- boolean allChildrenAreReactComponents = getChildren().size() > 0;
- for (Widget w : getChildren()) {
- if (!(w instanceof ReactComponent)) {
- allChildrenAreReactComponents = false;
- break;
- }
- }
- return allChildrenAreReactComponents;
- }
-
- private boolean isRenderedAsReactComponentChild() {
- return (
- getParent() instanceof ReactComponent &&
- ((ReactComponent>) getParent()).allChildrenAreReactComponents()
- );
- }
-
- /**
- * This method returns the root ReactComponent widget, which is the only place where this React tree is attached to the DOM.
- */
- private ReactComponent> getRootReactComponentWidget() {
- if (isRenderedAsReactComponentChild()) {
- return ((ReactComponent>) getParent()).getRootReactComponentWidget();
- } else {
- return this;
- }
- }
-
- @Override
- public HandlerRegistration addClickHandler(ClickHandler handler) {
- return addDomHandler(handler, ClickEvent.getType());
+ super(tag);
}
- private void maybeCreateRoot() {
- if (root == null && !isRenderedAsReactComponentChild()) {
+ private void createRoot() {
+ if (root == null) {
root = ReactDOM.createRoot(this.getElement());
}
}
- /**
- * Override the current props of the React component.
- * Because re-rendering the component will use `React.cloneElement`, old props must be explicitly set to `undefined`
- * to remove them.
- */
- public void overrideProps(T props) {
- this.props = props;
- this.rerender();
- }
-
- /**
- * Injects the GWT children into the React component. If all children are ReactComponents,
- * they will be cloned and added as React children.
- */
- private void injectChildWidgetsIntoComponent() {
- if (this.allChildrenAreReactComponents()) {
- // If all widget children are ReactElements, clone the React component and add them as children
- List> childWidgets = new ArrayList<>();
- getChildren().forEach(w -> childWidgets.add(((ReactComponent>) w)));
-
- ReactElement>[] childReactElements = childWidgets
- .stream()
- .map(ReactComponent::getReactElement)
- .toArray(ReactElement>[]::new);
-
- this.reactElement =
- React.cloneElement(reactElement, this.props, childReactElements);
- } else if (getChildren().size() > 0) {
- // Create a callback ref that will allow us to inject the GWT children into the DOM
- ReactComponentProps.CallbackRef refCallback = (Element node) -> {
- if (node != null) {
- // Once the DOM node is defined, inject each child
- getChildren().forEach(w -> node.appendChild(w.getElement()));
- }
- };
-
- if (this.props == null) {
- this.props = (T) JsPropertyMap.of();
- }
- this.props.ref = refCallback;
-
- this.reactElement =
- React.cloneElement(
- reactElement,
- // Override the ref
- this.props
- );
- }
- }
-
- public void render(ReactElement> reactElement) {
+ public void render(ReactElement, ?> reactElement) {
this.reactElement = reactElement;
- injectChildWidgetsIntoComponent();
// This component may be a React child of another component. If so, we must rerender the ancestor component(s) so
// that they use the new ReactElement created in this render step.
- ReactComponent> componentToRender = getRootReactComponentWidget();
- if (componentToRender == this) {
- // Asynchronously schedule creating a root in case React is still rendering and may unmount the current root
- Timer t = new Timer() {
- @Override
- public void run() {
- maybeCreateRoot();
- // Resynchronize with the DOM
- root.render(reactElement);
- }
- };
- t.schedule(0);
- } else {
- // Walk up the tree to the parent and repeat rerendering
- componentToRender.rerender();
- }
- }
-
- @Override
- public void setVisible(boolean visible) {
- super.setVisible(visible);
- // Re-render the element
- this.rerender();
+ // Asynchronously schedule creating a root in case React is still rendering and may unmount the current root
+ Timer t = new Timer() {
+ @Override
+ public void run() {
+ createRoot();
+ // Resynchronize with the DOM
+ root.render(reactElement);
+ }
+ };
+ t.schedule(0);
}
@Override
protected void onLoad() {
super.onLoad();
- maybeCreateRoot();
- this.rerender();
+ createRoot();
+ if (reactElement != null) {
+ this.render(reactElement);
+ }
}
@Override
protected void onUnload() {
- super.onUnload();
if (root != null) {
// Asynchronously schedule unmounting the root to allow React to finish the current render cycle.
// https://github.com/facebook/react/issues/25675
@@ -194,66 +73,18 @@ public void run() {
};
t.schedule(0);
}
+ super.onUnload();
}
- /**
- * Adds a child widget.
- *
- * @param child the widget to be added
- * @throws UnsupportedOperationException if this method is not supported (most
- * often this means that a specific overload must be called)
- */
@Override
- public void add(Widget child) {
- // See implementation in com.google.gwt.user.client.ui.ComplexPanel
-
- // Detach new child
- child.removeFromParent();
-
- // Logical attach
- getChildren().add(child);
-
- // Physical attach (via React API!)
- if (reactElement != null) {
- // Rerender if possible
- this.render(reactElement);
- }
-
- // Adopt.
- adopt(child);
+ public void clear() {
+ // clear doesn't typically call onUnload, but we want to for this element.
+ this.onUnload();
+ super.clear();
}
@Override
- public boolean remove(Widget w) {
- // See implementation in ComplexPanel
-
- // Validate.
- if (w.getParent() != this) {
- return false;
- }
- // Orphan.
- try {
- orphan(w);
- } finally {
- // Note - compared to ComplexPanel, we flipped logical and physical detach
- // This is because our render implementation depends on logical attachment
-
- // Logical detach.
- getChildren().remove(w);
-
- // Physical detach (via React API!)
- this.rerender();
- }
- return true;
- }
-
- public ReactElement> getReactElement() {
- return reactElement;
- }
-
- public void rerender() {
- if (reactElement != null) {
- this.render(reactElement);
- }
+ public HandlerRegistration addClickHandler(ClickHandler handler) {
+ return addDomHandler(handler, ClickEvent.getType());
}
}
diff --git a/src/main/java/org/sagebionetworks/web/client/widget/ReactComponentV2.java b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponentV2.java
new file mode 100644
index 0000000000..2d3ec3cc66
--- /dev/null
+++ b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponentV2.java
@@ -0,0 +1,280 @@
+package org.sagebionetworks.web.client.widget;
+
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.HasClickHandlers;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import java.util.ArrayList;
+import java.util.List;
+import jsinterop.base.JsPropertyMap;
+import org.sagebionetworks.web.client.jsinterop.React;
+import org.sagebionetworks.web.client.jsinterop.ReactComponentProps;
+import org.sagebionetworks.web.client.jsinterop.ReactComponentType;
+import org.sagebionetworks.web.client.jsinterop.ReactDOM;
+import org.sagebionetworks.web.client.jsinterop.ReactDOMRoot;
+import org.sagebionetworks.web.client.jsinterop.ReactElement;
+
+/**
+ * Abstract widget that manages the lifecycle of a {@link React} component tree mounted with {@link ReactDOM}.
+ *
+ * To use it, extend the class, supply a {@link ReactComponentType} and {@link ReactComponentProps} to the constructor,
+ * and call {@link #render()} to render the React component.
+ *
+ * This widget also manages appending child elements if the associated React component can contain children. If all
+ * child widgets implement this class, then the child {@link ReactElement}s will be passed as children to the React
+ * component. If any child of this component is a non-ReactComponent widget, then all child widgets (including
+ * implementations of {@link ReactComponentV2}) will be injected into the node found using the component's `ref`.
+ *
+ * The root element defaults to a `div`, but can be changed (e.g. to a `span`) using the
+ * {@link #ReactComponentV2(T, P, String)} constructor.
+ *
+ * This widget automatically unmounts the ReactComponent (if any) when this container is detached/unloaded.
+ */
+public abstract class ReactComponentV2<
+ T extends ReactComponentType
, P extends ReactComponentProps
+>
+ extends ComplexPanel
+ implements HasClickHandlers {
+
+ private ReactDOMRoot root;
+ private final ReactComponentType
reactComponentType;
+ public P props;
+
+ private final ArrayList nonReactChildElements = new ArrayList<>();
+
+ public ReactComponentV2(T reactComponentType, P props) {
+ this(reactComponentType, props, DivElement.TAG);
+ }
+
+ public ReactComponentV2(T reactComponentType, P props, String tag) {
+ this.reactComponentType = reactComponentType;
+ this.props = props;
+ setElement(Document.get().createElement(tag));
+ }
+
+ /**
+ * If any children of this widget do not implement this class, then we will inject them into the DOM using the React
+ * component's `ref` prop. This method will update the props with a callback ref that handles appending the children
+ * to the DOM node on which the ref is forwarded.
+ */
+ private void maybeUpdatePropsWithCallbackRef() {
+ if (!this.allChildrenAreReactComponents() && getChildren().size() > 0) {
+ // Create a callback ref that will allow us to inject the GWT children into the DOM
+ ReactComponentProps.CallbackRef callbackRef = (Element node) -> {
+ if (node != null) {
+ // Once the DOM node is defined, inject each child
+ getChildren()
+ .forEach(w -> {
+ node.appendChild(w.getElement());
+ // Keep track of child elements to ensure they are removed when the component unloads or re-renders
+ nonReactChildElements.add(w.getElement());
+ });
+ }
+ };
+
+ if (this.props == null) {
+ this.props = (P) JsPropertyMap.of();
+ }
+ // Override the ref
+ this.props.ref = callbackRef;
+ }
+ }
+
+ private void createRoot() {
+ if (root == null) {
+ root = ReactDOM.createRoot(this.getElement());
+ }
+ }
+
+ private void destroyRoot() {
+ if (root != null) {
+ // Asynchronously schedule unmounting the root to allow React to finish the current render cycle.
+ // https://github.com/facebook/react/issues/25675
+ Timer t = new Timer() {
+ @Override
+ public void run() {
+ root.unmount();
+ root = null;
+ }
+ };
+ t.schedule(0);
+ }
+ }
+
+ private void detachNonReactChildElements() {
+ if (allChildrenAreReactComponents()) {
+ // No need to remove non-React child elements from this widget
+ // But a descendant may contain non-React children, so recurse!
+ for (Widget w : getChildren()) {
+ ((ReactComponentV2, ?>) w).detachNonReactChildElements();
+ }
+ } else {
+ nonReactChildElements.forEach(Element::removeFromParent);
+ nonReactChildElements.clear();
+ }
+ }
+
+ private ReactElement createReactElement() {
+ detachNonReactChildElements();
+ maybeUpdatePropsWithCallbackRef();
+ return React.createElement(
+ reactComponentType,
+ props,
+ getChildReactElements()
+ );
+ }
+
+ /**
+ * @return true iff there are children, and all children are React components
+ */
+ private boolean allChildrenAreReactComponents() {
+ boolean allChildrenAreReactComponents = getChildren().size() > 0;
+ for (Widget w : getChildren()) {
+ if (!(w instanceof ReactComponentV2)) {
+ allChildrenAreReactComponents = false;
+ break;
+ }
+ }
+ return allChildrenAreReactComponents;
+ }
+
+ private boolean isRenderedAsReactComponentChild() {
+ return (
+ getParent() instanceof ReactComponentV2 &&
+ ((ReactComponentV2, ?>) getParent()).allChildrenAreReactComponents()
+ );
+ }
+
+ /**
+ * This method returns the root ReactComponent widget, which is the only place where this React tree is attached to the DOM.
+ */
+ private ReactComponentV2, ?> getRootReactComponentWidget() {
+ if (isRenderedAsReactComponentChild()) {
+ return (
+ (ReactComponentV2, ?>) getParent()
+ ).getRootReactComponentWidget();
+ } else {
+ return this;
+ }
+ }
+
+ @Override
+ public HandlerRegistration addClickHandler(ClickHandler handler) {
+ return addDomHandler(handler, ClickEvent.getType());
+ }
+
+ private ReactElement, ?>[] getChildReactElements() {
+ if (this.allChildrenAreReactComponents()) {
+ // If all widget children are ReactNodes, get their ReactElements and add them as React children
+ List> childWidgets = new ArrayList<>();
+ getChildren()
+ .forEach(w -> childWidgets.add(((ReactComponentV2, ?>) w)));
+
+ return childWidgets
+ .stream()
+ .map(ReactComponentV2::createReactElement)
+ .toArray(ReactElement, ?>[]::new);
+ } else {
+ return new ReactElement[0];
+ }
+ }
+
+ public void render() {
+ if (isRenderedAsReactComponentChild()) {
+ // This component will be rendered as a child of another React component, so destroy the root if one exists
+ destroyRoot();
+ }
+
+ // This component may be a React child of another component, so retrieve the root widget that renders this component tree.
+ ReactComponentV2, ?> componentToRender = getRootReactComponentWidget();
+
+ // Asynchronously schedule creating a root in case we queued up an unmount of the current root
+ // See https://stackoverflow.com/questions/73459382
+ Timer t = new Timer() {
+ @Override
+ public void run() {
+ componentToRender.createRoot();
+ // Create a fresh ReactElement tree and render it
+ componentToRender.root.render(componentToRender.createReactElement());
+ }
+ };
+ t.schedule(0);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ // Re-render the element
+ this.render();
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+ this.render();
+ }
+
+ @Override
+ protected void onUnload() {
+ super.onUnload();
+
+ // Detach any non-React descendants that were injected into the component tree
+ detachNonReactChildElements();
+
+ destroyRoot();
+ }
+
+ /**
+ * Adds a child widget.
+ *
+ * @param child the widget to be added
+ * @throws UnsupportedOperationException if this method is not supported (most
+ * often this means that a specific overload must be called)
+ */
+ @Override
+ public void add(Widget child) {
+ // See implementation in com.google.gwt.user.client.ui.ComplexPanel
+
+ // Detach new child
+ child.removeFromParent();
+
+ // Logical attach
+ getChildren().add(child);
+
+ // Physical attach (via React API!)
+ this.render();
+
+ // Adopt.
+ adopt(child);
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ // See implementation in ComplexPanel
+
+ // Validate.
+ if (w.getParent() != this) {
+ return false;
+ }
+ // Orphan.
+ try {
+ orphan(w);
+ } finally {
+ // Note - compared to ComplexPanel, we flipped logical and physical detach
+ // This is because our render implementation depends on logical attachment
+
+ // Logical detach.
+ getChildren().remove(w);
+
+ // Physical detach (via React API!)
+ this.render();
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/org/sagebionetworks/web/client/widget/sharing/EntityAccessControlListModalWidgetImpl.java b/src/main/java/org/sagebionetworks/web/client/widget/sharing/EntityAccessControlListModalWidgetImpl.java
index 8803e99e86..02a241c5e5 100644
--- a/src/main/java/org/sagebionetworks/web/client/widget/sharing/EntityAccessControlListModalWidgetImpl.java
+++ b/src/main/java/org/sagebionetworks/web/client/widget/sharing/EntityAccessControlListModalWidgetImpl.java
@@ -13,14 +13,14 @@
public class EntityAccessControlListModalWidgetImpl
implements EntityAccessControlListModalWidget {
- private final ReactComponent reactComponent;
+ private final ReactComponent reactComponent;
private final SynapseReactClientFullContextPropsProvider propsProvider;
private EntityAclEditorModalProps componentProps;
@Inject
EntityAccessControlListModalWidgetImpl(
- ReactComponent reactComponent,
+ ReactComponent reactComponent,
SynapseReactClientFullContextPropsProvider propsProvider
) {
this.reactComponent = reactComponent;
diff --git a/src/main/webapp/Portal.html b/src/main/webapp/Portal.html
index c672c0a307..540d4974d5 100644
--- a/src/main/webapp/Portal.html
+++ b/src/main/webapp/Portal.html
@@ -154,7 +154,6 @@
// The Bootstrap 3 custom theme CSS is loaded through swc.scss
// Note: we alias to bootstrap3 because we rely on Bootstrap 4 to be in node_modules/bootstrap to compile our scss (via synapse-react-client).
loadJs(cdnEndpoint + 'generated/bootstrap/dist/js/bootstrap.min.js')
- loadJs(cdnEndpoint + 'generated/bootstrap/dist/js/npm.js')
loadJs(cdnEndpoint + 'js/back-forward-nav-handler.js')
loadJs(cdnEndpoint + 'generated/moment.min.js')
loadJs(cdnEndpoint + 'generated/jsplumb.min.js')
@@ -185,6 +184,7 @@
loadJs(cdnEndpoint + 'generated/pica.min.js')
loadJs(reactPath)
loadJs(reactDomPath)
+ loadJs(cdnEndpoint + 'generated/material-ui.production.min.js')
loadJs(cdnEndpoint + 'generated/react-transition-group.min.js')
loadJs(cdnEndpoint + 'generated/prop-types.min.js')
loadJs(cdnEndpoint + 'generated/react-measure.js')