From c056730a5882f0983af44080770cf6b8e3c22e90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:40:59 +0000 Subject: [PATCH 1/4] Initial plan From 276593387ec84f72aeba02a940b5e0a4d0a026a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:47:42 +0000 Subject: [PATCH 2/4] Detach confirm() host element from body on close to fix leak Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com> --- .changeset/confirm-dialog-host-cleanup.md | 5 ++++ .../ConfirmationDialog.test.tsx | 26 ++++++++++++++++++- .../ConfirmationDialog/ConfirmationDialog.tsx | 6 ++--- 3 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 .changeset/confirm-dialog-host-cleanup.md diff --git a/.changeset/confirm-dialog-host-cleanup.md b/.changeset/confirm-dialog-host-cleanup.md new file mode 100644 index 00000000000..27b2c8114e6 --- /dev/null +++ b/.changeset/confirm-dialog-host-cleanup.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +ConfirmationDialog: `useConfirm`/`confirm` now removes its host element from `document.body` after the dialog is closed, and uses a fresh host element per call, so the empty container no longer lingers or leaks into other components and tests diff --git a/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx b/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx index eeb91d7e9fd..bc43ee511c3 100644 --- a/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx +++ b/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx @@ -1,4 +1,4 @@ -import {render, fireEvent} from '@testing-library/react' +import {render, fireEvent, waitFor} from '@testing-library/react' import {describe, it, expect, vi} from 'vitest' import type React from 'react' import {useCallback, useRef, useState} from 'react' @@ -310,4 +310,28 @@ describe('ConfirmationDialog', () => { }) implementsClassName(ConfirmationDialog, dialogClasses.Dialog) + + describe('useConfirm', () => { + it('removes the host element from the document body when the dialog is closed', async () => { + const initialBodyChildCount = document.body.childElementCount + const {getByText, getByRole} = render() + + fireEvent.click(getByText('Show menu')) + fireEvent.click(getByText('Show dialog')) + + // The dialog is rendered into a host element appended to + expect(getByRole('alertdialog')).toBeInTheDocument() + expect(document.body.childElementCount).toBeGreaterThan(initialBodyChildCount + 1) + + fireEvent.click(getByRole('button', {name: 'Secondary'})) + + // After closing, neither the dialog nor its host element should linger in the DOM + await waitFor(() => { + expect(document.querySelector('[role="alertdialog"]')).toBeNull() + }) + // The host element appended for the dialog must be detached from , leaving + // only the testing-library render container behind. + expect(document.body.childElementCount).toBe(initialBodyChildCount + 1) + }) + }) }) diff --git a/packages/react/src/ConfirmationDialog/ConfirmationDialog.tsx b/packages/react/src/ConfirmationDialog/ConfirmationDialog.tsx index c13fe632cad..e6693771b5d 100644 --- a/packages/react/src/ConfirmationDialog/ConfirmationDialog.tsx +++ b/packages/react/src/ConfirmationDialog/ConfirmationDialog.tsx @@ -138,16 +138,16 @@ export const ConfirmationDialog: React.FC & {content: React.ReactNode} async function confirm(options: ConfirmOptions): Promise { const {content, ...confirmationDialogProps} = options return new Promise(resolve => { - hostElement ||= document.createElement('div') - if (!hostElement.isConnected) document.body.append(hostElement) + const hostElement = document.createElement('div') + document.body.append(hostElement) const root = createRoot(hostElement) const onClose: ConfirmationDialogProps['onClose'] = gesture => { root.unmount() + hostElement.remove() if (gesture === 'confirm') { resolve(true) } else { From 87ab9c38e7b666208dd866d25a0b8ec342bcd19a Mon Sep 17 00:00:00 2001 From: Matthew Costabile Date: Tue, 9 Jun 2026 19:43:54 -0400 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../ConfirmationDialog.test.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx b/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx index bc43ee511c3..1e0524228cb 100644 --- a/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx +++ b/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx @@ -313,25 +313,26 @@ describe('ConfirmationDialog', () => { describe('useConfirm', () => { it('removes the host element from the document body when the dialog is closed', async () => { - const initialBodyChildCount = document.body.childElementCount const {getByText, getByRole} = render() fireEvent.click(getByText('Show menu')) + + // Capture children after the menu opens so we can reliably detect the confirm() host element + const bodyChildrenBeforeDialog = Array.from(document.body.children) + fireEvent.click(getByText('Show dialog')) - // The dialog is rendered into a host element appended to expect(getByRole('alertdialog')).toBeInTheDocument() - expect(document.body.childElementCount).toBeGreaterThan(initialBodyChildCount + 1) + + const hostElement = Array.from(document.body.children).find(el => !bodyChildrenBeforeDialog.includes(el)) + if (!hostElement) throw new Error('Expected confirm() to append a host element to ') fireEvent.click(getByRole('button', {name: 'Secondary'})) // After closing, neither the dialog nor its host element should linger in the DOM await waitFor(() => { expect(document.querySelector('[role="alertdialog"]')).toBeNull() + expect(hostElement).not.toBeConnected() }) - // The host element appended for the dialog must be detached from , leaving - // only the testing-library render container behind. - expect(document.body.childElementCount).toBe(initialBodyChildCount + 1) - }) }) }) From 56cd164bdf7033051aa64f4257b88668e2d2bc24 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:55:19 +0000 Subject: [PATCH 4/4] Fix missing closing bracket and invalid matcher in ConfirmationDialog test Co-authored-by: siddharthkp <1863771+siddharthkp@users.noreply.github.com> --- .../react/src/ConfirmationDialog/ConfirmationDialog.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx b/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx index 1e0524228cb..4939c2a03ea 100644 --- a/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx +++ b/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx @@ -332,7 +332,8 @@ describe('ConfirmationDialog', () => { // After closing, neither the dialog nor its host element should linger in the DOM await waitFor(() => { expect(document.querySelector('[role="alertdialog"]')).toBeNull() - expect(hostElement).not.toBeConnected() + expect(hostElement.isConnected).toBe(false) }) + }) }) })