Browse Source

Refactor EditorGuideModal and dynamic loading to improve structure and functionality

Yuki Takei 5 months ago
parent
commit
a68c846dad

+ 41 - 19
apps/app/src/client/components/PageEditor/EditorGuideModal/EditorGuideModal.tsx

@@ -1,12 +1,21 @@
-import { useState, useEffect, type JSX } from 'react';
+import {
+  useState, useEffect, type JSX, type CSSProperties,
+} from 'react';
 
 import { useEditorGuideModalStatus, useEditorGuideModalActions } from '@growi/editor/dist/states/modal/editor-guide';
+import { createPortal } from 'react-dom';
+
+type SubstanceProps = {
+  rect: DOMRectReadOnly,
+  close: () => void,
+};
 
 /**
  * EditorGuideModalSubstance - The actual modal content
- * Only rendered when isOpened is true
+ * Renders backdrop and modal content over the specified area
+ * Uses position:fixed to prevent scrolling with the container
  */
-const EditorGuideModalSubstance = ({ close }: { close: () => void }): JSX.Element => {
+const EditorGuideModalSubstance = ({ rect, close }: SubstanceProps): JSX.Element => {
   const [isShown, setIsShown] = useState(false);
 
   // Trigger fade-in after mount
@@ -18,20 +27,31 @@ const EditorGuideModalSubstance = ({ close }: { close: () => void }): JSX.Elemen
     return () => cancelAnimationFrame(frameId);
   }, []);
 
-  return (
+  // Fixed positioning style based on container's viewport position
+  const fixedStyle: CSSProperties = {
+    position: 'fixed',
+    top: rect.top,
+    left: rect.left,
+    width: rect.width,
+    height: rect.height,
+  };
+
+  return createPortal(
     <>
       {/* Editor Guide Modal Overlay - covers only the preview area */}
       <div
-        className={`position-absolute w-100 h-100 modal-backdrop fade z-2 ${isShown ? 'show' : ''}`}
+        className={`modal-backdrop fade z-2 ${isShown ? 'show' : ''}`}
+        style={fixedStyle}
         onClick={close}
         aria-hidden="true"
       />
 
       {/* Editor Guide Modal Content */}
       <div
-        className={`position-fixed top-0 bottom-0 start-50 end-0 d-flex align-items-center justify-content-center z-3 pe-none fade ${isShown ? 'show' : ''}`}
+        className={`d-flex align-items-center justify-content-center z-3 pe-none fade ${isShown ? 'show' : ''}`}
+        style={fixedStyle}
       >
-        <div className="px-3">
+        <div className="px-3 pe-auto">
           <div className="card shadow-lg">
             <div className="card-header d-flex justify-content-between align-items-center">
               <h5 className="mb-0">Editor Guide</h5>
@@ -51,26 +71,28 @@ const EditorGuideModalSubstance = ({ close }: { close: () => void }): JSX.Elemen
           </div>
         </div>
       </div>
-    </>
+    </>,
+    document.body,
   );
 };
 
+type Props = {
+  rect: DOMRectReadOnly,
+};
+
 /**
  * EditorGuideModal (Container)
  *
- * This modal is rendered within the Preview component and overlays only the preview area,
- * not the entire screen. The backdrop covers the preview area only.
- *
- * The container div is always rendered (for fade transitions),
- * but the actual content (Substance) is only rendered when isOpened is true.
+ * This modal overlays only the preview area (specified by rect),
+ * not the entire screen. Uses createPortal to render into document.body.
  */
-export const EditorGuideModal = (): JSX.Element => {
+export const EditorGuideModal = ({ rect }: Props): JSX.Element => {
   const { isOpened } = useEditorGuideModalStatus();
   const { close } = useEditorGuideModalActions();
 
-  return (
-    <div className={`${isOpened ? 'show' : ''}`}>
-      {isOpened && <EditorGuideModalSubstance close={close} />}
-    </div>
-  );
+  if (!isOpened) {
+    return <></>;
+  }
+
+  return <EditorGuideModalSubstance rect={rect} close={close} />;
 };

+ 8 - 2
apps/app/src/client/components/PageEditor/EditorGuideModal/dynamic.tsx

@@ -4,7 +4,11 @@ import { useEditorGuideModalStatus } from '@growi/editor/dist/states/modal/edito
 
 import { useLazyLoader } from '~/components/utils/use-lazy-loader';
 
-export const EditorGuideModalLazyLoaded = (): JSX.Element => {
+type Props = {
+  rect: DOMRectReadOnly | undefined,
+};
+
+export const EditorGuideModalLazyLoaded = ({ rect }: Props): JSX.Element => {
   const { isOpened } = useEditorGuideModalStatus();
 
   const EditorGuideModal = useLazyLoader(
@@ -13,5 +17,7 @@ export const EditorGuideModalLazyLoaded = (): JSX.Element => {
     isOpened,
   );
 
-  return EditorGuideModal ? <EditorGuideModal /> : <></>;
+  return (EditorGuideModal != null && rect != null)
+    ? <EditorGuideModal rect={rect} />
+    : <></>;
 };

+ 34 - 30
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -54,6 +54,7 @@ import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { usePreviewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
+import { EditorGuideModalLazyLoaded } from './EditorGuideModal';
 import { EditorNavbar } from './EditorNavbar';
 import { EditorNavbarBottom } from './EditorNavbarBottom';
 import Preview from './Preview';
@@ -371,37 +372,40 @@ export const PageEditorSubstance = (props: Props): JSX.Element => {
   }
 
   return (
-    <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
-      <div className="page-editor-editor-container flex-expand-vert border-end">
-        <CodeMirrorEditorMain
-          enableUnifiedMergeView={isEnableUnifiedMergeView}
-          enableCollaboration={editorMode === EditorMode.Editor}
-          onSave={saveWithShortcut}
-          onUpload={uploadHandler}
-          acceptedUploadFileType={acceptedUploadFileType}
-          onScroll={scrollEditorHandlerThrottle}
-          indentSize={currentIndentSize ?? defaultIndentSize}
-          user={user ?? undefined}
-          pageId={pageId ?? undefined}
-          editorSettings={editorSettings}
-          onEditorsUpdated={setEditingClients}
-          cmProps={cmProps}
-        />
+    <>
+      <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
+        <div className="page-editor-editor-container flex-expand-vert border-end">
+          <CodeMirrorEditorMain
+            enableUnifiedMergeView={isEnableUnifiedMergeView}
+            enableCollaboration={editorMode === EditorMode.Editor}
+            onSave={saveWithShortcut}
+            onUpload={uploadHandler}
+            acceptedUploadFileType={acceptedUploadFileType}
+            onScroll={scrollEditorHandlerThrottle}
+            indentSize={currentIndentSize ?? defaultIndentSize}
+            user={user ?? undefined}
+            pageId={pageId ?? undefined}
+            editorSettings={editorSettings}
+            onEditorsUpdated={setEditingClients}
+            cmProps={cmProps}
+          />
+        </div>
+        <div
+          ref={previewRef}
+          onScroll={scrollPreviewHandlerThrottle}
+          className="page-editor-preview-container flex-expand-vert overflow-y-auto d-none d-lg-flex position-relative"
+        >
+          <Preview
+            rendererOptions={rendererOptions}
+            markdown={markdownToPreview}
+            pagePath={currentPagePath}
+            expandContentWidth={shouldExpandContent}
+            style={pastEndStyle}
+          />
+        </div>
       </div>
-      <div
-        ref={previewRef}
-        onScroll={scrollPreviewHandlerThrottle}
-        className="page-editor-preview-container flex-expand-vert overflow-y-auto d-none d-lg-flex"
-      >
-        <Preview
-          rendererOptions={rendererOptions}
-          markdown={markdownToPreview}
-          pagePath={currentPagePath}
-          expandContentWidth={shouldExpandContent}
-          style={pastEndStyle}
-        />
-      </div>
-    </div>
+      <EditorGuideModalLazyLoaded rect={previewRect} />
+    </>
   );
 };
 

+ 1 - 5
apps/app/src/client/components/PageEditor/Preview.tsx

@@ -8,8 +8,6 @@ import { useRendererConfig } from '~/states/server-configurations';
 
 import { SlideRenderer } from '../Page/SlideRenderer';
 
-import { EditorGuideModalLazyLoaded } from './EditorGuideModal';
-
 import styles from './Preview.module.scss';
 
 const moduleClass = styles['page-editor-preview-body'] ?? '';
@@ -41,11 +39,9 @@ const Preview = (props: Props): JSX.Element => {
   return (
     <div
       data-testid="page-editor-preview-body"
-      className={`${moduleClass} ${fluidLayoutClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''} position-relative`}
+      className={`${moduleClass} ${fluidLayoutClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
       style={style}
     >
-      <EditorGuideModalLazyLoaded />
-
       { markdown != null
         && (
           isSlide != null