diff --git a/Dnn.AdminExperience/ClientSide/Dnn.React.Common/.storybook/main.ts b/Dnn.AdminExperience/ClientSide/Dnn.React.Common/.storybook/main.ts index a9b77b043d8..dace5bab0b3 100644 --- a/Dnn.AdminExperience/ClientSide/Dnn.React.Common/.storybook/main.ts +++ b/Dnn.AdminExperience/ClientSide/Dnn.React.Common/.storybook/main.ts @@ -13,9 +13,23 @@ const config : StorybookConfig = { "@storybook/addon-onboarding", "@storybook/addon-docs", ], - framework: 'storybook-react-rsbuild', + framework: { + name: 'storybook-react-rsbuild', + options: { + builder: { + lazyCompilation: false, + }, + }, + }, rsbuildFinal: (config) => { return mergeRsbuildConfig(config, { + tools: { + rspack: { + output: { + globalObject: "self", + }, + }, + }, plugins: [ pluginReact({ swcReactOptions: { diff --git a/Dnn.AdminExperience/ClientSide/Dnn.React.Common/package.json b/Dnn.AdminExperience/ClientSide/Dnn.React.Common/package.json index 9549d955881..8a1c9df08bf 100644 --- a/Dnn.AdminExperience/ClientSide/Dnn.React.Common/package.json +++ b/Dnn.AdminExperience/ClientSide/Dnn.React.Common/package.json @@ -35,7 +35,6 @@ "react-custom-scrollbars": "^4.2.1", "react-day-picker": "^7.4.10", "react-height": "^3.0.2", - "react-modal": "^3.16.3", "react-motion": "^0.5.2", "react-scrollbar": "^0.5.6", "react-slider": "2.0.6", diff --git a/Dnn.AdminExperience/ClientSide/Dnn.React.Common/rsbuild.config.ts b/Dnn.AdminExperience/ClientSide/Dnn.React.Common/rsbuild.config.ts index 68064c83282..74aae6a1f9a 100644 --- a/Dnn.AdminExperience/ClientSide/Dnn.React.Common/rsbuild.config.ts +++ b/Dnn.AdminExperience/ClientSide/Dnn.React.Common/rsbuild.config.ts @@ -10,22 +10,20 @@ const packageJson = requireModule("./package.json"); const isProduction = process.env.npm_lifecycle_event === "build"; -const externalizeNodeModules = ({ request }: { request?: string }) => { - if (!request) { - return undefined; - } - - if (request.startsWith(".") || path.isAbsolute(request)) { - return undefined; - } - - // Keep loader/runtime virtual requests bundled. - if (request.includes("!") || request.includes("?")) { - return undefined; - } - - return request; -}; +// Modules explicitly provided by Persona Bar runtime globals. +const runtimeProvidedExternals = new Set([ + "react", + "prop-types", + "redux", + "react-redux", + "react-dom", + "redux-immutable-state-invariant", + "redux-thunk", + "react-collapse", + "react-custom-scrollbars", + "react-widgets", + "es6-promise", +]); export default defineConfig({ source: { @@ -70,11 +68,18 @@ export default defineConfig({ globalObject: "this", }, externals: [ - ({ request }) => { - if (request === "react" || request === "prop-types") { + ({ request }: { request?: string }) => { + if (!request) { + return undefined; + } + + if (runtimeProvidedExternals.has(request)) { return request; } - return externalizeNodeModules({ request }); + + // Bundle everything else in dnn-react-common so runtime-only globals + // do not break components (for example react-modal internals). + return undefined; }, ], resolve: { diff --git a/Dnn.AdminExperience/ClientSide/Dnn.React.Common/src/Modal/index.jsx b/Dnn.AdminExperience/ClientSide/Dnn.React.Common/src/Modal/index.jsx index 4b6f22fd8af..1e024e2b322 100644 --- a/Dnn.AdminExperience/ClientSide/Dnn.React.Common/src/Modal/index.jsx +++ b/Dnn.AdminExperience/ClientSide/Dnn.React.Common/src/Modal/index.jsx @@ -1,12 +1,61 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import ReactModal from "react-modal"; +import ReactDOM from "react-dom"; import {Scrollbars} from "react-custom-scrollbars"; import { XThinIcon } from "../SvgIcons"; import "./style.less"; class Modal extends Component { + componentDidMount() { + if (this.props.isOpen) { + this.handleModalOpened(); + } + } + + componentDidUpdate(prevProps) { + if (!prevProps.isOpen && this.props.isOpen) { + this.handleModalOpened(); + } else if (prevProps.isOpen && !this.props.isOpen) { + this.handleModalClosed(); + } + } + + componentWillUnmount() { + this.handleModalClosed(); + } + + handleModalOpened() { + if (document && document.body) { + document.body.classList.add("ReactModal__Body--open"); + } + if (this.props.onAfterOpen) { + this.props.onAfterOpen(); + } + } + + handleModalClosed() { + if (document && document.body) { + document.body.classList.remove("ReactModal__Body--open"); + } + } + + onOverlayMouseDown() { + if (this.props.shouldCloseOnOverlayClick && this.props.onRequestClose) { + this.props.onRequestClose(); + } + } + + onContentMouseDown(event) { + event.stopPropagation(); + } + + onPortalKeyDown(event) { + if (event.key === "Escape" && this.props.onRequestClose) { + this.props.onRequestClose(); + } + } + getScrollbarStyle(props) { return { width: "100%", @@ -24,14 +73,21 @@ class Modal extends Component { if (document.getElementsByClassName("dnn-persona-bar-page-header") && document.getElementsByClassName("dnn-persona-bar-page-header").length > 0 && !props.modalHeight) { modalTopMargin = document.getElementsByClassName("dnn-persona-bar-page-header")[0].offsetHeight; } - return (props.style || { + const defaultStyles = { overlay: { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, zIndex: "99999", backgroundColor: "rgba(0,0,0,0.6)" }, content: { top: modalTopMargin + props.dialogVerticalMargin, left: props.dialogHorizontalMargin + 85, + right: "auto", + bottom: "auto", padding: 0, borderRadius: 0, border: "none", @@ -39,42 +95,70 @@ class Modal extends Component { height: props.modalHeight || "60%", backgroundColor: "#FFFFFF", position: "absolute", + overflow: "auto", + WebkitOverflowScrolling: "touch", + outline: "none", userSelect: "none", WebkitUserSelect: "none", MozUserSelect: "none", MsUserSelect: "none", boxSizing: "border-box" } - }); + }; + + const customOverlay = props.style && props.style.overlay ? props.style.overlay : {}; + const customContent = props.style && props.style.content ? props.style.content : {}; + + return { + overlay: { + ...defaultStyles.overlay, + ...customOverlay + }, + content: { + ...defaultStyles.content, + ...customContent + } + }; } render() { const {props} = this; + if (!props.isOpen || !document || !document.body) { + return null; + } + const modalStyles = this.getModalStyles(props); const scrollBarStyle = this.getScrollbarStyle(props); - return ( - - {props.header && -
-

{props.header}

- {props.headerChildren} -
- -
-
- } - -
- {props.children} + return ReactDOM.createPortal( +
+
+
+ {props.header && +
+

{props.header}

+ {props.headerChildren} +
+ +
+
+ } + +
+ {props.children} +
+
- - +
+
, + document.body ); } } diff --git a/Dnn.AdminExperience/ClientSide/Dnn.React.Common/src/Modal/modal.stories.js b/Dnn.AdminExperience/ClientSide/Dnn.React.Common/src/Modal/modal.stories.js new file mode 100644 index 00000000000..ff6e8deeaa9 --- /dev/null +++ b/Dnn.AdminExperience/ClientSide/Dnn.React.Common/src/Modal/modal.stories.js @@ -0,0 +1,157 @@ +import React, { Component } from "react"; +import Modal from "./index"; + +const storyStyle = { + overlay: { + zIndex: "99999", + backgroundColor: "rgba(0,0,0,0.6)", + position: "fixed", + inset: 0, + display: "flex", + alignItems: "flex-start", + justifyContent: "center", + padding: "48px", + boxSizing: "border-box", + }, + content: { + inset: "auto", + padding: 0, + borderRadius: 0, + border: "none", + backgroundColor: "#FFFFFF", + position: "relative", + userSelect: "none", + boxSizing: "border-box", + width: "760px", + maxWidth: "calc(100vw - 96px)", + height: "420px", + maxHeight: "calc(100vh - 96px)", + }, +}; + +export default { + component: Modal, + args: { + isOpen: true, + header: "Example Modal", + shouldCloseOnOverlayClick: true, + dialogVerticalMargin: 25, + dialogHorizontalMargin: 30, + modalWidth: 861, + modalTopMargin: 100, + style: storyStyle, + contentStyle: { padding: "25px 30px" }, + closeTimeoutMS: 0, + children: "This is the modal body content.", + }, + argTypes: { + isOpen: { control: "boolean" }, + header: { control: "text" }, + shouldCloseOnOverlayClick: { control: "boolean" }, + dialogVerticalMargin: { control: "number" }, + dialogHorizontalMargin: { control: "number" }, + modalWidth: { control: "number" }, + modalHeight: { control: "number" }, + modalTopMargin: { control: "number" }, + closeTimeoutMS: { control: "number" }, + children: { control: "text" }, + style: { control: false }, + contentStyle: { control: false }, + headerChildren: { control: false }, + onRequestClose: { action: "onRequestClose" }, + onAfterOpen: { action: "onAfterOpen" }, + }, +}; + +const renderModal = (args) => ( + +

{args.children}

+
+); + +export const WithHeader = { + render: renderModal, + args: { + header: "Example Modal", + children: "This is the modal body content.", + }, +}; + +export const WithoutHeader = { + render: renderModal, + args: { + header: "", + children: "This modal has no header. The close button is omitted and the scrollable area fills the full height.", + }, +}; + +export const WithHeaderChildren = { + render: renderModal, + args: { + header: "Modal with Extra Header Controls", + headerChildren: , + children: "This modal has additional children rendered inside the header bar.", + }, +}; + +class ControlledModal extends Component { + constructor(props) { + super(props); + this.state = { isOpen: props.isOpen }; + this.onOpen = this.onOpen.bind(this); + this.onClose = this.onClose.bind(this); + } + + componentDidUpdate(prevProps) { + if (prevProps.isOpen !== this.props.isOpen) { + this.setState({ isOpen: this.props.isOpen }); + } + } + + onOpen() { + this.setState({ isOpen: true }); + } + + onClose() { + this.setState({ isOpen: false }); + if (this.props.onRequestClose) { + this.props.onRequestClose(); + } + } + + render() { + const { + buttonLabel, + children, + isOpen, + onRequestClose, + ...modalProps + } = this.props; + + return ( +
+ + +

{children}

+
+
+ ); + } +} + +export const Controlled = { + render: (args) => , + args: { + isOpen: false, + header: "Controlled Modal", + buttonLabel: "Open Modal", + children: "Click the X button or the overlay to close this modal.", + }, + argTypes: { + buttonLabel: { control: "text" }, + }, +}; diff --git a/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageSettings/style.module.less b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageSettings/style.module.less index dfd9cb8b245..6f5bd50d149 100644 --- a/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageSettings/style.module.less +++ b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageSettings/style.module.less @@ -13,6 +13,9 @@ .dnn-simple-tab-item-modules { display:grid; + .moduleContainer{ + padding: 0; + } } .permission-tab > div:first-child { diff --git a/yarn.lock b/yarn.lock index 30f767498ae..3f629c7fff0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -550,7 +550,6 @@ __metadata: react-dom: "npm:^16.14.0" react-height: "npm:^3.0.2" react-hot-loader: "npm:^4.13.1" - react-modal: "npm:^3.16.3" react-motion: "npm:^0.5.2" react-scrollbar: "npm:^0.5.6" react-slider: "npm:2.0.6" @@ -14029,7 +14028,7 @@ __metadata: languageName: node linkType: hard -"react-modal@npm:3.16.3, react-modal@npm:^3.16.3": +"react-modal@npm:3.16.3": version: 3.16.3 resolution: "react-modal@npm:3.16.3" dependencies: