Skip to content

Conversation

@Parsium
Copy link
Contributor

@Parsium Parsium commented Dec 23, 2025

Proposed behaviour

For Carbon components involving React Portals, resolve inconsistencies in rendering behaviour:

  • Portal - when unmounting, properly clean up DOM node created as the container for the portal content
  • Popover - always render portal content in document body or nearest Carbon modal, to avoid the need to create and cleanup a DOM node.
  • CarbonProvider - ensure global used in updating topmost modal is cleaned up properly.

Current behaviour

Since #7148 was released, several Carbon components that use React Portals show unexpected behaviour when React Strict Mode is enabled, such as modals and popups not appearing at all. This occurs because these components have rendering inconsistencies that are surfaced by the stricter Strict Mode checks introduced in React v18.

Before upgrading Carbon to support newer React versions, we should ensure all components behave correctly under Strict Mode to prepare for future React changes.

Checklist

  • Commits follow our style guide
  • Related issues linked in commit messages if required
  • Screenshots are included in the PR if useful
  • All themes are supported if required
  • Unit tests added or updated if required
  • Playwright automation tests added or updated if required
  • Storybook added or updated if required
  • Translations added or updated (including creating or amending translation keys table in storybook) if required
  • Typescript d.ts file added or updated if required
  • Related docs have been updated if required

QA

  • Tested in provided StackBlitz sandbox/Storybook
  • Add new Playwright test coverage if required
  • Carbon implementation matches Design System/designs
  • UI Tests GitHub check reviewed if required

Additional context

Testing instructions

  1. Run Storybook with React Strict Mode enabled: npm run start:strict-mode
  2. Check the following components and compare behaviour with their production docs counterparts:
  • AdvancedColorPicker
  • Alert
  • Confirm
  • Dialog
  • Sidebar
  • MenuFullscreen
  • ResponsiveVerticalMenu
  • Toast
  • Tooltip
  • VerticalMenuFullscreen

PortalContext was added to ensure Portal was captured by Chromatic properly, by
attaching the React portal to Storybook's root element.

Storybook no longer uses the referenced root, so this context is unused.
export const TopModalProvider = ({ children }: { children: ReactNode }) => {
const [topModal, setTopModal] = useState<HTMLElement | null>(null);

// can't add the setter to the global list inside useEffect because that doesn't run until
Copy link
Contributor

Choose a reason for hiding this comment

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

question(non-blocking): Are we all good to remove this ref and condition for first render? The comment states that we can't add the setter to the global list inside a useEffect and we're now doing that.

Copy link
Contributor Author

@Parsium Parsium Jan 9, 2026

Choose a reason for hiding this comment

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

This should be okay now, since Modal no longer attempts to access this global on initial render.

Portal now creates its attachment node on a separate render to when the portal content is displayed. Because of this change, Modal has been re-structured (see below) so its internal hooks, like useModalManager, are only called once the modal content is rendered.

// src/__internal__/modal/modal.component.tsx#L179

const Modal = (props: ModalProps) => (
  <Portal>
   {/* 👇 `Portal` only renders this once its attachment node is present */}
    <ModalRoot {...props} />
  </Portal>
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants