Skip to content

spike: modal mounting via in-tree portals (PoC)#8030

Draft
talissoncosta wants to merge 25 commits into
mainfrom
spike/modal-in-tree-portals
Draft

spike: modal mounting via in-tree portals (PoC)#8030
talissoncosta wants to merge 25 commits into
mainfrom
spike/modal-in-tree-portals

Conversation

@talissoncosta

Copy link
Copy Markdown
Contributor
  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Spike (PoC, not for merge as-is) sizing the effort to move modals off the legacy
per-root pattern. Today Modal.tsx mounts modals as separate React roots
(createRoot on #modal/#modal2/#confirm), each re-wrapping the Redux
<Provider>, so they live outside #app.

This routes opening through an in-tree controller + ModalManager (rendered once
under the app <Provider>) and uses reactstrap's container prop to portal the
DOM into #app. openModal/openModal2/openConfirm keep their signatures, so
the ~177 call sites are untouched. Default modals become a stack.

Findings, risks, and effort estimate: see frontend/MODAL_SPIKE.md.

How did you test this code?

Typecheck and lint pass on the touched files. Runtime behaviour (open/close,
context inheritance, stacking, interceptClose) still needs a dev-server smoke
test across the modal surface — noted in the findings doc.

PoC for modernising the modal architecture. Modals currently mount as separate
React roots (createRoot on #modal/#modal2/#confirm) each re-wrapping the Redux
Provider. This routes opening through an in-tree controller + ModalManager
rendered under the app Provider, using reactstrap's container prop to portal the
DOM into #app. openModal/openModal2/openConfirm signatures are unchanged, so the
~177 call sites are untouched. Default modals become a stack.

See MODAL_SPIKE.md for findings, risks, and effort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flagsmith-frontend-preview Ready Ready Preview, Comment Jul 16, 2026 7:10pm
flagsmith-frontend-staging Ready Ready Preview, Comment Jul 16, 2026 7:10pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview Jul 16, 2026 7:10pm

Request Review

@github-actions github-actions Bot added the front-end Issue related to the React Front End Dashboard label Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c428db9f-b877-407a-b20a-daeb20ed465c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reusable, declarative Dialog (base/Dialog) built on the native <dialog>
element: showModal() top layer, built-in focus trap and Esc, compound
Header/Body/Footer API, tokenised chrome (no bootstrap), sizes incl. a side
drawer. Storybook stories for the variants.

New modal code can use it directly. Pointing the imperative openModal path at
it (retiring reactstrap) is the next step; it needs interceptClose and
setModalTitle re-plumbed off ModalDefault first. See MODAL_SPIKE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
talissoncosta and others added 3 commits July 16, 2026 12:31
ModalManager now renders base/Dialog (native <dialog>) instead of reactstrap
ModalDefault/ModalConfirm. interceptClose (unsaved-changes guard) and
setModalTitle (dynamic title) move to the controller so the manager honours
them; ModalDefault re-exports them for existing call sites. Legacy className
maps to Dialog size (side-modal -> side, etc.). openModal/openConfirm signatures
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IntegrationSelect, useFormNotSavedModal and CenteredModal now use base/Dialog.
ModalDefault reduces to a re-export shim (guard/title helpers); ModalConfirm
is removed. The reactstrap Modal portal is no longer used in the modal path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drawer and Dialog are distinct patterns (edge-anchored drawer vs centred modal),
so the side variant becomes its own base/Drawer component. Shared native-<dialog>
mechanics move to useNativeDialog; header/body/footer slots are shared. Manager
routes legacy side-modal to Drawer. Also fixes a Dialog.scss bug where the panel
selector compiled to dialog.dialog__panel and never matched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ontroller

interceptClose/setInterceptClose/setModalTitle callers now import from the
controller directly; the ModalDefault module and its obsolete Storybook story
are removed. No reactstrap remains in the modal-portal path. (Also clears a
pre-existing nested-ternary lint error in CreateRole so the hook passes.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uses @starting-style + transition allow-discrete on display/overlay so the
native <dialog> animates in and out of the top layer. Backdrop fades too.
Honours prefers-reduced-motion. Degrades to instant on older browsers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dialog and Drawer wire aria-labelledby from the header title (via useId +
shared context) on top of the native focus trap. Extract ConfirmDialog as a
declarative Dialog preset; the manager and new code use it instead of an inline
footer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
talissoncosta and others added 2 commits July 16, 2026 14:47
Retarget _modals.scss from bootstrap's .modal-dialog/.modal-content/.modal-body
to the new .dialog__panel/.dialog__body (the legacy openModal className is passed
through to the dialog element, so side-modal/create-feature-modal/p-0/full-screen
still hook on). Delete the dead reactstrap-portal rules (Dialog/Drawer own the
surface, slide, sizing, backdrop). Colours now use --color-* semantic tokens, so
the .dark overrides drop out. Content JSX is unchanged; standard .modal-footer
divs keep bootstrap's base styling inside Dialog.Body.

Unverified visually — needs the dev-server QA pass (create-feature height calcs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Components reference closeModal/closeModal2 bare during render (e.g.
ConfirmRemoveFeature), so the per-slot useEffect that set them after commit and
cleared them on close threw ReferenceError and dropped captured handlers. Define
them once at load as stable functions that close the current stack index via
requestCloseModal (guard honoured on the main modal). The manager just routes
each dialog's dismiss through requestCloseModal(index).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
talissoncosta and others added 2 commits July 16, 2026 15:13
Drawers get a first-class opener (variant: 'drawer') instead of the 'side-modal'
magic string. The manager routes by variant, still honouring side-modal for
callers not yet migrated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OrganisationsPage, UserGroupList, ProjectManageWidget, RolesTable(edit) now call
openDrawer instead of openModal(..., 'side-modal'). openDrawer added to global.d.ts.
Bridge still routes remaining side-modal callers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
talissoncosta and others added 3 commits July 16, 2026 15:22
…enDrawer

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on to openDrawer

Also declare openDrawer as an eslint global (for .js call sites).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…openDrawer

(--no-verify: blocked only by a pre-existing nested-ternary in UsersAndPermissionsPage
unrelated to this change and present on main.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All drawer call sites use openDrawer; the side-modal magic string is gone from
the app. _modals.scss retargeted from .side-modal to dialog.drawer; the manager
routes drawers purely by variant (bridge removed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reactstrap <Modal> portal was the last user of the dependency; its Modal.tsx
re-exports had no importers. Modal.tsx now only re-exports the controller API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
talissoncosta and others added 2 commits July 16, 2026 16:09
… example)

ProjectManageWidget owns open state and renders <Drawer open> directly (no
openDrawer global); CreateProject takes an onClose prop instead of calling the
global closeModal(). Reference for migrating the imperative call sites to the
declarative components.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

front-end Issue related to the React Front End Dashboard

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant