Skip to content

Commit

Permalink
Redo react 17 compatibility (#2433)
Browse files Browse the repository at this point in the history
* Revert "React 17 compatibility (as patch to 1.28) (#2431)"

This reverts commit 8a51b78.

* fix: move UiShell, renderReactInWebComponent into separate export for React 17 compatibility

* fix: related storybook updates
  • Loading branch information
benschell-okta authored and bryancunningham-okta committed Dec 3, 2024
1 parent 00d13c9 commit e5e61a7
Show file tree
Hide file tree
Showing 25 changed files with 100 additions and 91 deletions.
7 changes: 7 additions & 0 deletions packages/odyssey-react-mui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
"labs": [
"./dist/src/labs/index.d.ts"
],
"ui-shell": [
"./dist/src/ui-shell/index.d.ts"
],
"test-selectors": [
"./dist/src/test-selectors/index.d.ts"
]
Expand All @@ -33,6 +36,10 @@
"types": "./dist/src/labs/index.d.ts",
"default": "./dist/labs/index.js"
},
"./ui-shell": {
"types": "./dist/src/ui-shell/index.d.ts",
"default": "./dist/ui-shell/index.js"
},
"./test-selectors": {
"types": "./dist/src/test-selectors/index.d.ts",
"default": "./dist/test-selectors/index.js"
Expand Down
2 changes: 1 addition & 1 deletion packages/odyssey-react-mui/src/labs/PageTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
import { DocumentationIcon } from "../icons.generated";
import { Heading4, Subordinate } from "../Typography";
import { Link } from "../Link";
import { useHasUiShell } from "./UiShell";
import { useHasUiShell } from "../ui-shell/UiShell/useHasUiShell";

export type PageTemplateProps = {
/**
Expand Down
1 change: 0 additions & 1 deletion packages/odyssey-react-mui/src/labs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,3 @@ export * from "./AppSwitcher";
export * from "./SideNav/NavAccordion";
export * from "./SideNav";
export * from "./TopNav";
export * from "./UiShell";
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import styled from "@emotion/styled";
import { memo, type ReactElement, type ReactNode } from "react";
import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary";

import { AppSwitcher, type AppSwitcherProps } from "../AppSwitcher";
import { SideNav, type SideNavProps } from "../SideNav";
import { TopNav, type TopNavProps } from "../TopNav";
import { AppSwitcher, type AppSwitcherProps } from "../../labs/AppSwitcher";
import { SideNav, type SideNavProps } from "../../labs/SideNav";
import { TopNav, type TopNavProps } from "../../labs/TopNav";
import {
useOdysseyDesignTokens,
type DesignTokens,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import { act, waitFor } from "@testing-library/react";
import { act } from "@testing-library/react";

import { renderUiShell } from "./renderUiShell";
import {
ReactInWebComponentElement,
reactWebComponentElementName,
} from "../../web-component/renderReactInWebComponent";
} from "../renderReactInWebComponent";

describe("renderUiShell", () => {
afterEach(() => {
Expand Down Expand Up @@ -126,11 +126,9 @@ describe("renderUiShell", () => {
});
});

await waitFor(() => {
expect(
rootElement.querySelector(reactWebComponentElementName)!.shadowRoot,
).toHaveTextContent(appName);
});
expect(
rootElement.querySelector(reactWebComponentElementName)!.shadowRoot,
).toHaveTextContent(appName);
});

test("renders `UiShell` with immediately updated props", async () => {
Expand All @@ -155,11 +153,9 @@ describe("renderUiShell", () => {
});
});

await waitFor(() => {
expect(
rootElement.querySelector(reactWebComponentElementName)!.shadowRoot,
).toHaveTextContent(appName);
});
expect(
rootElement.querySelector(reactWebComponentElementName)!.shadowRoot,
).toHaveTextContent(appName);
});

test("renders `<slot>` in the event of an error", async () => {
Expand Down Expand Up @@ -188,16 +184,14 @@ describe("renderUiShell", () => {
);
});

await waitFor(() => {
expect(onError).toHaveBeenCalledTimes(1);
expect(consoleError).toHaveBeenCalledTimes(1);
expect(
rootElement
.querySelector(reactWebComponentElementName)!
.shadowRoot?.querySelector("slot"),
).toBeInstanceOf(HTMLSlotElement);
});

consoleErrorSpy.mockRestore();

expect(onError).toHaveBeenCalledTimes(1);
expect(consoleError).toHaveBeenCalledTimes(1);
expect(
rootElement
.querySelector(reactWebComponentElementName)!
.shadowRoot?.querySelector("slot"),
).toBeInstanceOf(HTMLSlotElement);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ import { ErrorBoundary } from "react-error-boundary";
import { bufferLatest } from "./bufferLatest";
import { createMessageBus } from "./createMessageBus";
import { UiShell, UiShellProps } from "./UiShell";
import { renderReactInWebComponent } from "../../web-component/renderReactInWebComponent";
import { renderReactInWebComponent } from "../renderReactInWebComponent";
import { type UiShellNavComponentProps } from "./UiShellContent";

export const uiShellDataAttribute = "data-unified-ui-shell";
import { uiShellDataAttribute } from "./useHasUiShell";

export const optionalComponentSlotNames: Record<
keyof Required<UiShellProps>["optionalComponents"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import { useEffect, useState } from "react";

import { uiShellDataAttribute } from "./renderUiShell";
export const uiShellDataAttribute = "data-unified-ui-shell";

export const useHasUiShell = () => {
const [hasUiShell, setHasUiShell] = useState(false);
Expand Down
14 changes: 14 additions & 0 deletions packages/odyssey-react-mui/src/ui-shell/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*!
* Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/

export * from "./UiShell";
export * from "./renderReactInWebComponent";
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

import { waitFor } from "@testing-library/dom";

import { createReactRootElements } from "../web-component";
import {
createReactRootElements,
ReactInWebComponentElement,
reactWebComponentElementName,
renderReactInWebComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,11 @@
*/

import { type ReactNode } from "react";
import type { Root } from "react-dom/client";

/**
* 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>;
import { createRoot, type Root } from "react-dom/client";
import {
createReactRootElements,
type ReactRootElements,
} from "../web-component";

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

Expand All @@ -56,8 +25,8 @@ export type GetReactComponentInWebComponent = (

export class ReactInWebComponentElement extends HTMLElement {
getReactComponent: GetReactComponentInWebComponent;
reactRoot: Root;
reactRootElements: ReactRootElements;
reactRootPromise: Promise<Root>;

constructor(getReactComponent: GetReactComponentInWebComponent) {
super();
Expand All @@ -81,20 +50,15 @@ export class ReactInWebComponentElement extends HTMLElement {
shadowRoot.appendChild(this.reactRootElements.stylesRootElement);
shadowRoot.appendChild(this.reactRootElements.appRootElement);

// If we want to support React v17 in the future, we can use a try-catch on the import to grab the old `ReactDOM.render` function if `react-dom/client` errors. --Kevin Ghadyani
this.reactRootPromise = import("react-dom/client").then(({ createRoot }) =>
createRoot(this.reactRootElements.appRootElement),
);
this.reactRoot = createRoot(this.reactRootElements.appRootElement);
}

connectedCallback() {
this.reactRootPromise.then((reactRoot) =>
reactRoot.render(this.getReactComponent(this.reactRootElements)),
);
this.reactRoot.render(this.getReactComponent(this.reactRootElements));
}

disconnectedCallback() {
this.reactRootPromise.then((reactRoot) => reactRoot.unmount());
this.reactRoot.unmount();
}
}

Expand Down
1 change: 0 additions & 1 deletion packages/odyssey-react-mui/src/web-component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

export * from "./renderReactInWebComponent";
export * from "./shadow-dom";
39 changes: 36 additions & 3 deletions packages/odyssey-react-mui/src/web-component/shadow-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,43 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import { createReactRootElements } from "./renderReactInWebComponent";
/**
* 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>;

/**
* @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.
* @deprecated Use `renderReactInWebComponent` from `@okta/odyssey-react-mui/ui-shell` 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.
*/
export const createShadowDomElements = (containerElement: HTMLElement) => {
const shadowRoot = containerElement.attachShadow({ mode: "open" });
Expand All @@ -32,7 +65,7 @@ export const createShadowDomElements = (containerElement: HTMLElement) => {

/**
* @deprecated Use `createShadowDomElements` instead which returns an object instead of an array. It's otherwise the same.
* @deprecated Ideally, 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. */
* @deprecated Ideally, use `renderReactInWebComponent` from `@okta/odyssey-react-mui/ui-shell` 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. */
export const createShadowRootElement = (
containerElement: HTMLElement,
): [HTMLStyleElement, HTMLDivElement] => {
Expand Down
1 change: 1 addition & 0 deletions packages/odyssey-storybook/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const preview: Preview = {
"Contributing",
"MUI Components",
"Labs Components",
"UI Shell Components",
"Customization",
],
locales: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@

import { Meta, StoryObj } from "@storybook/react";
import { MuiThemeDecorator } from "../../../../.storybook/components";
import {
PageTemplate,
UiShell,
uiShellDataAttribute,
UserProfile,
type UiShellNavComponentProps,
type UiShellProps,
} from "@okta/odyssey-react-mui/labs";
import {
Banner,
Button,
Expand All @@ -28,14 +20,21 @@ import {
SearchField,
Surface,
} from "@okta/odyssey-react-mui";
import { PageTemplate, UserProfile } from "@okta/odyssey-react-mui/labs";
import {
UiShell,
uiShellDataAttribute,
type UiShellNavComponentProps,
type UiShellProps,
} from "@okta/odyssey-react-mui/ui-shell";
import {
AddCircleIcon,
HomeIcon,
UserIcon,
} from "@okta/odyssey-react-mui/icons";

const storybookMeta: Meta<UiShellProps> = {
title: "Labs Components/UI Shell",
title: "UI Shell Components/UI Shell",
component: UiShell,
argTypes: {
appComponent: {
Expand Down
2 changes: 1 addition & 1 deletion packages/odyssey-storybook/src/installation/ShadowDom.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ To sandbox your application's styles, you need to use a Web Component which also
To create a Shadow DOM, simply use the `renderReactInWebComponent` in your app's top-level file (typically `index.ts`):

```tsx
import { renderReactInWebComponent } from "@okta/odyssey-react-mui/labs";
import { renderReactInWebComponent } from "@okta/odyssey-react-mui/ui-shell";

const appRootElement = document.createElement("div");
const containerRootElement = document.getElementById("container-root");
Expand Down

0 comments on commit e5e61a7

Please sign in to comment.