Skip to content

Commit

Permalink
fix: refactors where we create react root elements
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinGhadyani-Okta committed Oct 23, 2024
1 parent dc18d41 commit bd67db8
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 73 deletions.
4 changes: 2 additions & 2 deletions packages/odyssey-react-mui/src/ui-shell/UiShell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ describe("UiShell", () => {
<UiShell
appComponent={<div />}
appRootElement={document.createElement("div")}
emotionRootElement={document.createElement("div")}
onSubscriptionCreated={() => {}}
stylesRootElement={document.createElement("div")}
subscribeToPropChanges={subscribeToPropChanges}
/>,
);
Expand Down Expand Up @@ -228,8 +228,8 @@ describe("UiShell", () => {
<UiShell
appComponent={<div />}
appRootElement={document.createElement("div")}
emotionRootElement={document.createElement("div")}
onSubscriptionCreated={() => {}}
stylesRootElement={document.createElement("div")}
subscribeToPropChanges={subscribeToPropChanges}
/>,
);
Expand Down
8 changes: 4 additions & 4 deletions packages/odyssey-react-mui/src/ui-shell/UiShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary";
import { SideNav, type SideNavProps } from "../labs/SideNav";
import { TopNav, type TopNavProps } from "../labs/TopNav";
import { OdysseyProvider } from "../OdysseyProvider";
import { ShadowDomElements } from "../web-component/shadow-dom";
import { type ReactRootElements } from "../web-component";

const appContainerStyles = {
flexGrow: "1",
Expand Down Expand Up @@ -79,7 +79,7 @@ export type UiShellProps = {
subscribeToPropChanges: (
subscriber: (componentProps: SetStateAction<UiShellComponentProps>) => void,
) => () => void;
} & ShadowDomElements;
} & ReactRootElements;

/**
* Our new Unified Platform UI Shell.
Expand All @@ -91,10 +91,10 @@ export type UiShellProps = {
const UiShell = ({
appComponent,
appRootElement,
emotionRootElement,
onError = console.error,
onSubscriptionCreated,
optionalComponents,
stylesRootElement,
subscribeToPropChanges,
}: UiShellProps) => {
const [componentProps, setComponentProps] = useState(defaultComponentProps);
Expand All @@ -115,7 +115,7 @@ const UiShell = ({
return (
<ErrorBoundary fallback={appComponent} onError={onError}>
<OdysseyProvider
emotionRootElement={emotionRootElement}
emotionRootElement={stylesRootElement}
shadowRootElement={appRootElement}
>
<div style={uiShellContainerStyles}>
Expand Down
2 changes: 1 addition & 1 deletion packages/odyssey-react-mui/src/ui-shell/renderUiShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ export const renderUiShell = ({
<UiShell
appComponent={<slot />}
appRootElement={shadowDomElements.appRootElement}
emotionRootElement={shadowDomElements.emotionRootElement}
onError={onError}
onSubscriptionCreated={publishSubscriptionCreated}
optionalComponents={Object.fromEntries(
Expand All @@ -106,6 +105,7 @@ export const renderUiShell = ({
],
),
)}
stylesRootElement={shadowDomElements.stylesRootElement}
subscribeToPropChanges={subscribeToPropChanges}
/>
</ErrorBoundary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,39 @@
import { waitFor } from "@testing-library/dom";

import {
ReactInWebComponentElement,
createReactRootElements,
reactWebComponentElementName,
renderReactInWebComponent,
type ReactInWebComponentElement,
} from "./renderReactInWebComponent";

describe("createReactRootElements", () => {
test("returns two elements at attach to a Shadow DOM", () => {
const { appRootElement, stylesRootElement } = createReactRootElements();

expect(appRootElement).toBeInstanceOf(HTMLDivElement);
expect(stylesRootElement).toBeInstanceOf(HTMLDivElement);
});

test("App root element has the correct attributes", () => {
const { appRootElement } = createReactRootElements();

expect(appRootElement).toHaveAttribute("id", "app-root");
expect(appRootElement).toHaveAttribute("style", "height: inherit;");
});

test("Emotion root element has the correct attributes", () => {
const nonce = "hello-world";

window.cspNonce = nonce;

const { stylesRootElement } = createReactRootElements();

expect(stylesRootElement).toHaveAttribute("id", "style-root");
expect(stylesRootElement).toHaveAttribute("nonce", nonce);
});
});

describe("renderReactInWebComponent", () => {
afterEach(() => {
// Remove any appended elements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,57 @@
import { type ReactNode } from "react";
import { createRoot, type Root } from "react-dom/client";

import {
createUnattachedShadowDomElements,
type ShadowDomElements,
} from "./shadow-dom";
/**
* Creates elements for a Shadow DOM that Odyssey will render into.
* The Emotion root is for `<style>` tags and the app root is for an app to render into.
* These are bare elements that
*/
export const createReactRootElements = () => {
const appRootElement = document.createElement("div");
const stylesRootElement = document.createElement("div");

// This `div` may cause layout issues unless it inherits the parent's height.
appRootElement.style.setProperty("height", "inherit");

appRootElement.setAttribute("id", "app-root");
stylesRootElement.setAttribute("id", "style-root");
stylesRootElement.setAttribute("nonce", window.cspNonce);

return {
/**
* The element your React root component renders into.
* React has to render or portal somewhere, and this element can be used for that root element.
*
* In the case of a web component, there is no defined root element, so you have to define it yourself.
*/
appRootElement,
/**
* In React apps, your styles typically go in `document.head`, but you may want to render them somewhere else.
*
* Specifically when rendering in a web component, there is no `<head>`, so you have to create a spot for styles to render.
*/
stylesRootElement,
};
};

export type ReactRootElements = ReturnType<typeof createReactRootElements>;

export const reactWebComponentElementName = "odyssey-react-web-component";

export type GetReactComponentInWebComponent = (
shadowDomElements: ShadowDomElements,
reactRootElements: ReactRootElements,
) => ReactNode;

export class ReactInWebComponentElement extends HTMLElement {
getReactComponent: GetReactComponentInWebComponent;
shadowDomElements: ShadowDomElements;
reactRoot: Root;
reactRootElements: ReactRootElements;

constructor(getReactComponent: GetReactComponentInWebComponent) {
super();

this.getReactComponent = getReactComponent;
this.shadowDomElements = createUnattachedShadowDomElements();
this.reactRootElements = createReactRootElements();

const styleElement = document.createElement("style");
const shadowRoot = this.attachShadow({ mode: "open" });
Expand All @@ -47,15 +77,15 @@ export class ReactInWebComponentElement extends HTMLElement {

styleElement.setAttribute("nonce", window.cspNonce);

this.shadowDomElements.emotionRootElement.appendChild(styleElement);
shadowRoot.appendChild(this.shadowDomElements.emotionRootElement);
shadowRoot.appendChild(this.shadowDomElements.appRootElement);
this.reactRootElements.stylesRootElement.appendChild(styleElement);
shadowRoot.appendChild(this.reactRootElements.stylesRootElement);
shadowRoot.appendChild(this.reactRootElements.appRootElement);

this.reactRoot = createRoot(this.shadowDomElements.appRootElement);
this.reactRoot = createRoot(this.reactRootElements.appRootElement);
}

connectedCallback() {
this.reactRoot.render(this.getReactComponent(this.shadowDomElements));
this.reactRoot.render(this.getReactComponent(this.reactRootElements));
}

disconnectedCallback() {
Expand Down
33 changes: 1 addition & 32 deletions packages/odyssey-react-mui/src/web-component/shadow-dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,7 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import {
createShadowDomElements,
createUnattachedShadowDomElements,
} from "./shadow-dom";

describe("createUnattachedShadowDomElements", () => {
test("returns two elements at attach to a Shadow DOM", () => {
const { appRootElement, emotionRootElement } =
createUnattachedShadowDomElements();

expect(appRootElement).toBeInstanceOf(HTMLDivElement);
expect(emotionRootElement).toBeInstanceOf(HTMLDivElement);
});

test("App root element has the correct attributes", () => {
const { appRootElement } = createUnattachedShadowDomElements();

expect(appRootElement).toHaveAttribute("id", "app-root");
expect(appRootElement).toHaveAttribute("style", "height: inherit;");
});

test("Emotion root element has the correct attributes", () => {
const nonce = "hello-world";

window.cspNonce = nonce;

const { emotionRootElement } = createUnattachedShadowDomElements();

expect(emotionRootElement).toHaveAttribute("id", "style-root");
expect(emotionRootElement).toHaveAttribute("nonce", nonce);
});
});
import { createShadowDomElements } from "./shadow-dom";

describe("createShadowDomElements", () => {
test("returns two elements attached to a Shadow DOM", () => {
Expand Down
28 changes: 7 additions & 21 deletions packages/odyssey-react-mui/src/web-component/shadow-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,7 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

export const createUnattachedShadowDomElements = () => {
const appRootElement = document.createElement("div");
const emotionRootElement = document.createElement("div");

// This `div` may cause layout issues unless it inherits the parent's height.
appRootElement.style.setProperty("height", "inherit");

appRootElement.setAttribute("id", "app-root");
emotionRootElement.setAttribute("id", "style-root");
emotionRootElement.setAttribute("nonce", window.cspNonce);

return { appRootElement, emotionRootElement };
};

export type ShadowDomElements = ReturnType<
typeof createUnattachedShadowDomElements
>;
import { createReactRootElements } from "./renderReactInWebComponent";

/**
* @deprecated Use `renderReactInWebComponent` instead. This function was necessary when using bare Shadow DOM, but with UI Shell rendering in a Web Component, you won't be able to render your Shadow DOM in its Shadow DOM without using a Web Component.
Expand All @@ -35,13 +19,15 @@ export const createShadowDomElements = (containerElement: HTMLElement) => {
const shadowRoot = containerElement.attachShadow({ mode: "open" });

// Container for Emotion `<style>` elements.
const { appRootElement, emotionRootElement } =
createUnattachedShadowDomElements();
const { appRootElement, stylesRootElement } = createReactRootElements();

shadowRoot.appendChild(appRootElement);
shadowRoot.appendChild(emotionRootElement);
shadowRoot.appendChild(stylesRootElement);

return { emotionRootElement, shadowRootElement: appRootElement };
return {
emotionRootElement: stylesRootElement,
shadowRootElement: appRootElement,
};
};

/**
Expand Down

0 comments on commit bd67db8

Please sign in to comment.