Skip to content

[WEB-4980] dev: propel modal portal component#7851

Merged
pushya22 merged 7 commits intopreviewfrom
dev-propel-modal-portal-component
Sep 25, 2025
Merged

[WEB-4980] dev: propel modal portal component#7851
pushya22 merged 7 commits intopreviewfrom
dev-propel-modal-portal-component

Conversation

@anmolsinghbhatia
Copy link
Collaborator

@anmolsinghbhatia anmolsinghbhatia commented Sep 25, 2025

Description

This PR includes propel modal portal component.

Media

Media
media

Summary by CodeRabbit

  • New Features

    • Added ModalPortal (configurable modal: width, position, fullscreen, overlay, close behaviors) and an SSR-safe PortalWrapper; exposed a single public portal entry.
  • Documentation

    • Added Storybook examples demonstrating usage, positions, widths, and interactive controls.
  • Chores

    • Updated build config and published a new public export path for the portal module.

@anmolsinghbhatia anmolsinghbhatia self-assigned this Sep 25, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 25, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a new portal feature under packages/propel: PortalWrapper and ModalPortal components with types, constants, Storybook stories, a src aggregator, a new public export "./portal" in package.json, and includes src/portal/index.ts in tsdown build entries.

Changes

Cohort / File(s) Summary
Build & Exports
packages/propel/package.json, packages/propel/tsdown.config.ts
Adds public export ./portal -> ./dist/portal/index.js and adds src/portal/index.ts to tsdown build entries.
Portal API & Types
packages/propel/src/portal/constants.ts, packages/propel/src/portal/types.ts
Introduces enums EPortalWidth, EPortalPosition, mappings PORTAL_WIDTH_CLASSES, PORTAL_POSITION_CLASSES, constants DEFAULT_PORTAL_ID, MODAL_Z_INDEX, and typed interfaces for PortalWrapper and ModalPortal props/handlers.
Portal Components
packages/propel/src/portal/portal-wrapper.tsx, packages/propel/src/portal/modal-portal.tsx, packages/propel/src/portal/index.ts
Adds PortalWrapper (SSR-safe portal mount/unmount) and ModalPortal (overlay, width/position, fullscreen, escape/overlay-to-close) plus an index that re-exports portal modules.
Stories / Examples
packages/propel/src/portal/portal.stories.tsx
Adds Storybook stories demonstrating ModalPortal (positions, widths, fullscreen, overlay, close behaviors) and a basic PortalWrapper example.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App
  participant ModalPortal
  participant PortalWrapper
  participant DOM

  App->>ModalPortal: render(isOpen, width, position, ...)
  alt isOpen = false
    ModalPortal-->>App: returns null
  else isOpen = true
    ModalPortal->>ModalPortal: compute classes, setup refs & listeners
    ModalPortal->>PortalWrapper: mount overlay + content into portalId
    PortalWrapper->>DOM: find or create container (DEFAULT_PORTAL_ID)
    PortalWrapper-->>ModalPortal: container ready -> portal mounted
    par User interactions
      App->>ModalPortal: press Escape
      ModalPortal->>App: call onClose() if closeOnEscape
      App->>ModalPortal: click overlay
      ModalPortal->>App: call onClose() if closeOnOverlayClick
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • vamsikrishnamathala
  • sriramveeraghanta

Poem

In a burrowed branch of code I leap and play,
A portal opens bright to show the way.
With width and place and gentle overlay,
I hop, I nudge—escape sends me away.
A little rabbit cheers the modal day. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description only includes a brief description and media but omits several required sections from the repository template, such as the 'Type of Change' checklist, 'Test Scenarios', and 'References', making it incomplete. Please update the pull request description to include the 'Type of Change' section with the appropriate checkbox marked, add a 'Test Scenarios' section describing how you validated the changes, include a 'References' section linking related issues, and adjust the header to 'Screenshots and Media' to match the template.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly identifies the addition of a “propel modal portal” component and includes the ticket reference, providing a concise and accurate summary of the main change in this PR.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev-propel-modal-portal-component

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@makeplane
Copy link

makeplane bot commented Sep 25, 2025

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@anmolsinghbhatia anmolsinghbhatia marked this pull request as ready for review September 25, 2025 09:51
Copilot AI review requested due to automatic review settings September 25, 2025 09:51
@cursor
Copy link

cursor bot commented Sep 25, 2025

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on October 20.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new portal component system for the Propel UI library, enabling modal and overlay functionality that renders outside the normal DOM tree. The portal system provides reusable components for creating modals, overlays, and other UI elements that need to appear above other content.

  • Adds ModalPortal component with configurable positioning, sizing, and overlay behavior
  • Implements PortalWrapper as a low-level portal utility for DOM manipulation
  • Includes comprehensive Storybook documentation with interactive examples

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/propel/tsdown.config.ts Adds portal module to build configuration
packages/propel/package.json Exports portal components in package.json exports
packages/propel/src/portal/types.ts Defines TypeScript interfaces for portal components
packages/propel/src/portal/constants.ts Defines enums and styling constants for portal positioning and sizing
packages/propel/src/portal/portal-wrapper.tsx Implements low-level portal wrapper component
packages/propel/src/portal/modal-portal.tsx Implements modal portal component with accessibility features
packages/propel/src/portal/portal.stories.tsx Provides Storybook documentation and interactive examples
packages/propel/src/portal/index.ts Exports all portal-related components and types

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (8)
packages/propel/src/portal/constants.ts (1)

27-29: Overlay z-index constant is unused.

Consider using OVERLAY_Z_INDEX in ModalPortal to make stacking explicit and consistent with MODAL_Z_INDEX.

packages/propel/src/portal/portal-wrapper.tsx (3)

1-12: Unify props typing and avoid SSR warnings from useLayoutEffect.

  • Don’t redeclare PortalWrapperProps locally; import it from ./types to keep a single source of truth.
  • Using useLayoutEffect on the server emits warnings. Prefer an isomorphic layout effect.

Apply:

-import React, { useLayoutEffect, useState, useMemo } from "react";
+import React, { useEffect, useLayoutEffect, useState, useMemo } from "react";
 import { createPortal } from "react-dom";
 import { DEFAULT_PORTAL_ID } from "./constants";
+import type { PortalWrapperProps } from "./types";
 
-type PortalWrapperProps = {
-  children: React.ReactNode;
-  portalId?: string;
-  fallbackToDocument?: boolean;
-  className?: string;
-  onMount?: () => void;
-  onUnmount?: () => void;
-};
+const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;

36-61: Swap to isomorphic layout effect.

This removes the “useLayoutEffect does nothing on the server” warning.

-  useLayoutEffect(() => {
+  useIsomorphicLayoutEffect(() => {
     // Ensure we're in browser environment
     if (typeof window === "undefined") return;
@@
-  }, [portalId, onMount, onUnmount]);
+  }, [portalId, onMount, onUnmount]);

79-81: Avoid unsafe cast to ReactElement.

content can be non-element nodes. Prefer returning content directly (ReactNode) or ensure wrapping.

-  if (fallbackToDocument) {
-    return content as React.ReactElement;
-  }
+  if (fallbackToDocument) {
+    return <>{content}</>;
+  }
packages/propel/src/portal/portal.stories.tsx (2)

192-207: Mixing stories for different components under one default meta.

BasicPortal renders PortalWrapper but the file’s default meta is for ModalPortal. Split into portal-wrapper.stories.tsx or set a separate default export for PortalWrapper in a new file to keep controls/docs accurate.


171-172: String transform nit.

EPortalWidth values are hyphenated (e.g., "three-quarter"); replace("_", " ") is no-op. Consider normalizing labels with a dedicated util.

Also applies to: 180-181

packages/propel/src/portal/modal-portal.tsx (2)

13-27: Duplicate props type with ./types.

This local ModalPortalProps duplicates the public interface in types.ts. Import and reuse to avoid drift.

-type ModalPortalProps = {
+import type { ModalPortalProps } from "./types";
+type ModalPortalProps = {
   children: React.ReactNode;
   isOpen: boolean;
   onClose?: () => void;
   portalId?: string;
   className?: string;
   overlayClassName?: string;
   contentClassName?: string;
   width?: EPortalWidth;
   position?: EPortalPosition;
   fullScreen?: boolean;
   showOverlay?: boolean;
   closeOnOverlayClick?: boolean;
   closeOnEscape?: boolean;
 };

If you prefer not to re-export, at least keep it in sync with the public type.


60-60: contentRef is unused without focus features.

If you don’t implement focus management, remove the ref to avoid dead code.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 586a7a4 and a256c97.

📒 Files selected for processing (8)
  • packages/propel/package.json (1 hunks)
  • packages/propel/src/portal/constants.ts (1 hunks)
  • packages/propel/src/portal/index.ts (1 hunks)
  • packages/propel/src/portal/modal-portal.tsx (1 hunks)
  • packages/propel/src/portal/portal-wrapper.tsx (1 hunks)
  • packages/propel/src/portal/portal.stories.tsx (1 hunks)
  • packages/propel/src/portal/types.ts (1 hunks)
  • packages/propel/tsdown.config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/propel/src/portal/portal-wrapper.tsx (2)
packages/propel/src/portal/types.ts (1)
  • PortalWrapperProps (8-13)
packages/propel/src/portal/constants.ts (1)
  • DEFAULT_PORTAL_ID (27-27)
packages/propel/src/portal/portal.stories.tsx (1)
packages/propel/src/portal/portal-wrapper.tsx (1)
  • PortalWrapper (25-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and lint web apps
🔇 Additional comments (4)
packages/propel/tsdown.config.ts (1)

24-24: Build entry for portal added — looks good.

Including "src/portal/index.ts" ensures the new module is built and published.

packages/propel/package.json (1)

38-38: New export "./portal" — OK.

Matches the added tsdown entry and index re-exports. Should resolve to dist/portal/index.(js|d.ts).

packages/propel/src/portal/constants.ts (1)

14-25: Class tokens: verify Tailwind presets for min/max width utilities.

min-w-80 / min-w-96 etc. require custom Tailwind config. Confirm these exist in @plane/tailwind-config to avoid missing styles.

packages/propel/src/portal/index.ts (1)

1-4: Public aggregator is clean and cohesive.

Re-exports provide a tidy surface for consumers.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/propel/src/portal/portal-wrapper.tsx (1)

1-1: Prefer useEffect here to stay SSR-safe.

Using useLayoutEffect in a component that can render during SSR will emit warnings even though the body short-circuits in browsers. Swapping to useEffect keeps the behavior while avoiding the server-side complaint.

-import React, { useLayoutEffect, useState, useMemo } from "react";
+import React, { useEffect, useState, useMemo } from "react";
@@
-  useLayoutEffect(() => {
+  useEffect(() => {
     // Ensure we're in browser environment
     if (typeof window === "undefined") return;
@@
-  }, [portalId, onMount, onUnmount]);
+  }, [portalId, onMount, onUnmount]);

Also applies to: 28-53

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a256c97 and 1742b17.

📒 Files selected for processing (3)
  • packages/propel/src/portal/constants.ts (1 hunks)
  • packages/propel/src/portal/modal-portal.tsx (1 hunks)
  • packages/propel/src/portal/portal-wrapper.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/propel/src/portal/constants.ts
🧰 Additional context used
🧬 Code graph analysis (2)
packages/propel/src/portal/modal-portal.tsx (3)
packages/propel/src/portal/types.ts (1)
  • ModalPortalProps (15-27)
packages/propel/src/portal/constants.ts (4)
  • DEFAULT_PORTAL_ID (27-27)
  • PORTAL_WIDTH_CLASSES (14-19)
  • PORTAL_POSITION_CLASSES (21-25)
  • MODAL_Z_INDEX (28-28)
packages/propel/src/portal/portal-wrapper.tsx (1)
  • PortalWrapper (17-76)
packages/propel/src/portal/portal-wrapper.tsx (2)
packages/propel/src/portal/types.ts (1)
  • PortalWrapperProps (8-13)
packages/propel/src/portal/constants.ts (1)
  • DEFAULT_PORTAL_ID (27-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (1)
packages/propel/src/portal/modal-portal.tsx (1)

66-72: Escape handler should only exist while the modal is open.

handleEscape stays attached even after the modal is closed, so pressing Escape anywhere keeps firing onClose. Please gate the listener behind isOpen.

-  useEffect(() => {
-    document.addEventListener("keydown", handleEscape);
-    return () => {
-      document.removeEventListener("keydown", handleEscape);
-    };
-  }, [handleEscape]);
+  useEffect(() => {
+    if (!isOpen) {
+      return;
+    }
+    document.addEventListener("keydown", handleEscape);
+    return () => {
+      document.removeEventListener("keydown", handleEscape);
+    };
+  }, [isOpen, handleEscape]);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
packages/propel/src/portal/portal.stories.tsx (4)

82-83: Avoid overstating a11y; remove “Tab/focus management” claims or implement trap.

ModalPortal doesn’t implement focus trap or managed focus/return-to-trigger. Adjust story copy to avoid misleading guidance.

Apply:

-  description = "This is a modal portal component with full accessibility support. Try pressing Tab to navigate through elements or Escape to close.",
+  description = "Accessible modal portal demo. Supports closing via Escape key and clicking the overlay.",
-        description="A standard modal with all default settings. Demonstrates focus management, keyboard navigation, and accessibility features."
+        description="A standard modal with default settings. Supports closing via Escape key and overlay click."

Also applies to: 121-124


62-64: Use stable enum type for buttonVariant instead of inferring from Button.

Using Parameters is brittle (forwardRefs/HOCs can break it). Prefer the enum you already export.

 }: Omit<Parameters<typeof ModalPortal>[0], "isOpen" | "onClose"> & {
   buttonText?: string;
-  buttonVariant?: Parameters<typeof Button>[0]["variant"];
+  buttonVariant?: EButtonVariant;
 }) => {

69-71: Add ARIA to the trigger button.

Expose dialog intent and expanded state.

-      <Button variant={buttonVariant} onClick={() => setIsOpen(true)}>
+      <Button
+        variant={buttonVariant}
+        onClick={() => setIsOpen(true)}
+        aria-haspopup="dialog"
+        aria-expanded={isOpen}
+      >

191-206: PortalWrapper story won’t appear under its own “Components/Portal/PortalWrapper” title.

Storybook uses a single default meta per file; per‑story title override isn’t supported. Move PortalWrapper into its own CSF file with its own default export to get the desired sidebar grouping.

Follow-up: create packages/propel/src/portal/portal-wrapper.stories.tsx with default meta for PortalWrapper and move BasicPortal there.

packages/propel/src/portal/types.ts (1)

16-28: Add ARIA props to support accessible labeling.

Enable consumers to set aria-modal, aria-labelledby, and aria-describedby on the dialog wrapper.

 export interface ModalPortalProps extends BasePortalProps {
   isOpen: boolean;
   onClose?: () => void;
   portalId?: string;
   overlayClassName?: string;
   contentClassName?: string;
   width?: EPortalWidth;
   position?: EPortalPosition;
   fullScreen?: boolean;
   showOverlay?: boolean;
   closeOnOverlayClick?: boolean;
   closeOnEscape?: boolean;
+  ariaModal?: boolean;
+  ariaLabelledby?: string;
+  ariaDescribedby?: string;
 }

Follow-up: pass these through to the element with role="dialog" in modal-portal.tsx.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1742b17 and b896894.

📒 Files selected for processing (3)
  • packages/propel/src/portal/modal-portal.tsx (1 hunks)
  • packages/propel/src/portal/portal.stories.tsx (1 hunks)
  • packages/propel/src/portal/types.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/propel/src/portal/modal-portal.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
packages/propel/src/portal/portal.stories.tsx (2)
packages/propel/src/portal/modal-portal.tsx (1)
  • ModalPortal (29-110)
packages/propel/src/portal/portal-wrapper.tsx (1)
  • PortalWrapper (17-76)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and lint web apps
🔇 Additional comments (1)
packages/propel/src/portal/types.ts (1)

1-1: Good fix: type-only React imports.

Resolves previous React namespace/type issues.

Please run type-check to confirm no downstream breakages.

@pushya22 pushya22 merged commit 7f28cbe into preview Sep 25, 2025
6 of 7 checks passed
@pushya22 pushya22 deleted the dev-propel-modal-portal-component branch September 25, 2025 10:26
yarikoptic pushed a commit to yarikoptic/plane that referenced this pull request Oct 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants