Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/portal-css-containment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Added `primer_react_css_contain_portal` feature flag that applies CSS containment (`contain: layout style`) to Portal elements when enabled. This can improve rendering performance by isolating layout and style calculations within portals without clipping overflow content.
101 changes: 99 additions & 2 deletions packages/react/src/Portal/Portal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {describe, expect, it} from 'vitest'
import Portal, {registerPortalRoot, PortalContext} from '../Portal/index'
import {describe, expect, it, vi, beforeEach} from 'vitest'
import Portal, {registerPortalRoot, PortalContext, resetCSSContainmentTracking} from '../Portal/index'

import {render} from '@testing-library/react'
import BaseStyles from '../BaseStyles'
import React from 'react'
import {FeatureFlags} from '../FeatureFlags'

describe('Portal', () => {
it('renders a default portal into document.body (no BaseStyles present)', () => {
Expand Down Expand Up @@ -183,4 +184,100 @@
document.body.removeChild(contextPortalRoot)
document.body.removeChild(propPortalRoot)
})

describe('CSS containment feature flag', () => {
beforeEach(() => {
// Reset containment tracking between tests
resetCSSContainmentTracking()
})

it('does not apply CSS containment by default', () => {
const {baseElement} = render(<Portal>test-content</Portal>)
const generatedRoot = baseElement.querySelector('#__primerPortalRoot__') as HTMLElement

expect(generatedRoot).toBeInstanceOf(HTMLElement)
expect(generatedRoot.style.contain).toBe('')

baseElement.innerHTML = ''
})

it('applies CSS containment when primer_react_css_contain_portal flag is enabled', () => {
const toRender = (
<FeatureFlags flags={{primer_react_css_contain_portal: true}}>
<Portal>contained-content</Portal>
</FeatureFlags>
)

const {baseElement} = render(toRender)
const generatedRoot = baseElement.querySelector('#__primerPortalRoot__') as HTMLElement

expect(generatedRoot).toBeInstanceOf(HTMLElement)
expect(generatedRoot.style.contain).toBe('layout style')

baseElement.innerHTML = ''
})

it('does not apply CSS containment when flag is explicitly disabled', () => {
const toRender = (
<FeatureFlags flags={{primer_react_css_contain_portal: false}}>
<Portal>uncontained-content</Portal>
</FeatureFlags>
)

const {baseElement} = render(toRender)
const generatedRoot = baseElement.querySelector('#__primerPortalRoot__') as HTMLElement

expect(generatedRoot).toBeInstanceOf(HTMLElement)
expect(generatedRoot.style.contain).toBe('')

baseElement.innerHTML = ''
})

it('applies CSS containment to pre-existing portal root', () => {
// Create a pre-existing portal root declaratively
const existingRoot = document.createElement('div')
existingRoot.id = '__primerPortalRoot__'
document.body.appendChild(existingRoot)

const toRender = (
<FeatureFlags flags={{primer_react_css_contain_portal: true}}>
<Portal>content-in-existing-root</Portal>
</FeatureFlags>
)

const {baseElement} = render(toRender)
const generatedRoot = baseElement.querySelector('#__primerPortalRoot__') as HTMLElement

expect(generatedRoot).toBeInstanceOf(HTMLElement)
expect(generatedRoot.style.contain).toBe('layout style')

baseElement.innerHTML = ''
document.body.removeChild(existingRoot)

Check failure on line 255 in packages/react/src/Portal/Portal.test.tsx

View workflow job for this annotation

GitHub Actions / test (react-19)

[@primer/react (chromium)] src/Portal/Portal.test.tsx > Portal > CSS containment feature flag > applies CSS containment to pre-existing portal root

NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node. ❯ removeChild src/Portal/Portal.test.tsx:255:20 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 8, INDEX_SIZE_ERR: 1, DOMSTRING_SIZE_ERR: 2, HIERARCHY_REQUEST_ERR: 3, WRONG_DOCUMENT_ERR: 4, INVALID_CHARACTER_ERR: 5, NO_DATA_ALLOWED_ERR: 6, NO_MODIFICATION_ALLOWED_ERR: 7, NOT_FOUND_ERR: 8, NOT_SUPPORTED_ERR: 9, INUSE_ATTRIBUTE_ERR: 10, INVALID_STATE_ERR: 11, SYNTAX_ERR: 12, INVALID_MODIFICATION_ERR: 13, NAMESPACE_ERR: 14, INVALID_ACCESS_ERR: 15, VALIDATION_ERR: 16, TYPE_MISMATCH_ERR: 17, SECURITY_ERR: 18, NETWORK_ERR: 19, ABORT_ERR: 20, URL_MISMATCH_ERR: 21, QUOTA_EXCEEDED_ERR: 22, TIMEOUT_ERR: 23, INVALID_NODE_TYPE_ERR: 24, DATA_CLONE_ERR: 25 }

Check failure on line 255 in packages/react/src/Portal/Portal.test.tsx

View workflow job for this annotation

GitHub Actions / test (react-18)

[@primer/react (chromium)] src/Portal/Portal.test.tsx > Portal > CSS containment feature flag > applies CSS containment to pre-existing portal root

NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node. ❯ removeChild src/Portal/Portal.test.tsx:255:20 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 8, INDEX_SIZE_ERR: 1, DOMSTRING_SIZE_ERR: 2, HIERARCHY_REQUEST_ERR: 3, WRONG_DOCUMENT_ERR: 4, INVALID_CHARACTER_ERR: 5, NO_DATA_ALLOWED_ERR: 6, NO_MODIFICATION_ALLOWED_ERR: 7, NOT_FOUND_ERR: 8, NOT_SUPPORTED_ERR: 9, INUSE_ATTRIBUTE_ERR: 10, INVALID_STATE_ERR: 11, SYNTAX_ERR: 12, INVALID_MODIFICATION_ERR: 13, NAMESPACE_ERR: 14, INVALID_ACCESS_ERR: 15, VALIDATION_ERR: 16, TYPE_MISMATCH_ERR: 17, SECURITY_ERR: 18, NETWORK_ERR: 19, ABORT_ERR: 20, URL_MISMATCH_ERR: 21, QUOTA_EXCEEDED_ERR: 22, TIMEOUT_ERR: 23, INVALID_NODE_TYPE_ERR: 24, DATA_CLONE_ERR: 25 }
})

it('warns when overriding existing contain value', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

// Create a pre-existing portal root with a different contain value
const existingRoot = document.createElement('div')
existingRoot.id = '__primerPortalRoot__'
existingRoot.style.contain = 'size'
document.body.appendChild(existingRoot)

const toRender = (
<FeatureFlags flags={{primer_react_css_contain_portal: true}}>
<Portal>content-with-override</Portal>
</FeatureFlags>
)

render(toRender)

expect(warnSpy).toHaveBeenCalledWith(
'Portal root already has contain: "size". Overriding with "layout style" due to primer_react_css_contain_portal flag.',
)

warnSpy.mockRestore()
document.body.removeChild(existingRoot)
})
})
})
34 changes: 32 additions & 2 deletions packages/react/src/Portal/Portal.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import React, {useContext} from 'react'
import {createPortal} from 'react-dom'
import useLayoutEffect from '../utils/useIsomorphicLayoutEffect'
import {useFeatureFlag} from '../FeatureFlags'

const PRIMER_PORTAL_ROOT_ID = '__primerPortalRoot__'
const DEFAULT_PORTAL_CONTAINER_NAME = '__default__'

const portalRootRegistry: Partial<Record<string, Element>> = {}

// Track which portal roots have had CSS containment applied (auto-cleans when element is GC'd)
const cssContainmentApplied = new WeakMap<Element, boolean>()

// Reset containment tracking (exported for testing)
export function resetCSSContainmentTracking(): void {
// WeakMap doesn't have a clear method, but we can reset by deleting the current portal root entry
const portalRoot = portalRootRegistry[DEFAULT_PORTAL_CONTAINER_NAME]
if (portalRoot) {
cssContainmentApplied.delete(portalRoot)
}
}

/**
* Register a container to serve as a portal root.
* @param root The element that will be the root for portals created in this container
Expand All @@ -20,7 +33,7 @@ export function registerPortalRoot(root: Element, name = DEFAULT_PORTAL_CONTAINE
// Ensures that a default portal root exists and is registered. If a DOM element exists
// with id __primerPortalRoot__, allow that element to serve as the default portal root.
// Otherwise, create that element and attach it to the end of document.body.
function ensureDefaultPortal() {
function ensureDefaultPortal(enableCSSContainment = false) {
const existingDefaultPortalContainer = portalRootRegistry[DEFAULT_PORTAL_CONTAINER_NAME]
if (!existingDefaultPortalContainer || !document.body.contains(existingDefaultPortalContainer)) {
let defaultPortalContainer = document.getElementById(PRIMER_PORTAL_ROOT_ID)
Expand All @@ -41,6 +54,22 @@ function ensureDefaultPortal() {

registerPortalRoot(defaultPortalContainer)
}

// Apply CSS containment to the portal root if enabled (only once per root)
if (enableCSSContainment) {
const portalRoot = portalRootRegistry[DEFAULT_PORTAL_CONTAINER_NAME]
if (portalRoot instanceof HTMLElement && !cssContainmentApplied.has(portalRoot)) {
const existingContain = portalRoot.style.contain
if (existingContain && existingContain !== 'layout style') {
// eslint-disable-next-line no-console
console.warn(
`Portal root already has contain: "${existingContain}". Overriding with "layout style" due to primer_react_css_contain_portal flag.`,
)
}
portalRoot.style.contain = 'layout style'
cssContainmentApplied.set(portalRoot, true)
}
}
}

/**
Expand Down Expand Up @@ -75,6 +104,7 @@ export const Portal: React.FC<React.PropsWithChildren<PortalProps>> = ({
containerName: _containerName,
}) => {
const {portalContainerName} = useContext(PortalContext)
const enableCSSContainment = useFeatureFlag('primer_react_css_contain_portal')
const elementRef = React.useRef<HTMLDivElement | null>(null)
if (!elementRef.current) {
const div = document.createElement('div')
Expand All @@ -92,7 +122,7 @@ export const Portal: React.FC<React.PropsWithChildren<PortalProps>> = ({
let containerName = _containerName ?? portalContainerName
if (containerName === undefined) {
containerName = DEFAULT_PORTAL_CONTAINER_NAME
ensureDefaultPortal()
ensureDefaultPortal(enableCSSContainment)
}
const parentElement = portalRootRegistry[containerName]

Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/Portal/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {PortalProps} from './Portal'
import {Portal, registerPortalRoot, PortalContext} from './Portal'
import {Portal, registerPortalRoot, PortalContext, resetCSSContainmentTracking} from './Portal'

export default Portal
export {registerPortalRoot}
export {registerPortalRoot, resetCSSContainmentTracking}
export type {PortalProps}
export {PortalContext}
Loading