diff --git a/src/annotation-context/annotation/open-annotation.tsx b/src/annotation-context/annotation/open-annotation.tsx
index 181bf9aad2..ab91c711c6 100644
--- a/src/annotation-context/annotation/open-annotation.tsx
+++ b/src/annotation-context/annotation/open-annotation.tsx
@@ -2,8 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
import React, { useRef } from 'react';
+import { Portal } from '@cloudscape-design/component-toolkit/internal';
+
import { HotspotProps } from '../../hotspot/interfaces';
-import Portal from '../../internal/components/portal';
import { AnnotationContextProps } from '../interfaces';
import { AnnotationPopover } from './annotation-popover';
import AnnotationTrigger from './annotation-trigger';
diff --git a/src/button-dropdown/tooltip.tsx b/src/button-dropdown/tooltip.tsx
index 6a92cddff3..32bc8c1e42 100644
--- a/src/button-dropdown/tooltip.tsx
+++ b/src/button-dropdown/tooltip.tsx
@@ -2,9 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
import React, { KeyboardEventHandler, useRef, useState } from 'react';
-import { useReducedMotion } from '@cloudscape-design/component-toolkit/internal';
+import { Portal, useReducedMotion } from '@cloudscape-design/component-toolkit/internal';
-import Portal from '../internal/components/portal';
import { usePortalModeClasses } from '../internal/hooks/use-portal-mode-classes';
import Arrow from '../popover/arrow';
import PopoverBody from '../popover/body';
diff --git a/src/internal/components/drag-handle-wrapper/__tests__/portal-overlay.test.tsx b/src/internal/components/drag-handle-wrapper/__tests__/portal-overlay.test.tsx
index f606d0afde..a543e937ea 100644
--- a/src/internal/components/drag-handle-wrapper/__tests__/portal-overlay.test.tsx
+++ b/src/internal/components/drag-handle-wrapper/__tests__/portal-overlay.test.tsx
@@ -11,6 +11,7 @@ import styles from '../../../../../lib/components/internal/components/drag-handl
let isRtl = false;
jest.mock('@cloudscape-design/component-toolkit/internal', () => ({
+ ...jest.requireActual('@cloudscape-design/component-toolkit/internal'),
getIsRtl: jest.fn(() => isRtl),
getLogicalBoundingClientRect: jest.fn().mockReturnValue({
insetInlineStart: 2,
diff --git a/src/internal/components/drag-handle-wrapper/portal-overlay.tsx b/src/internal/components/drag-handle-wrapper/portal-overlay.tsx
index 65485f67fa..67ae8123f8 100644
--- a/src/internal/components/drag-handle-wrapper/portal-overlay.tsx
+++ b/src/internal/components/drag-handle-wrapper/portal-overlay.tsx
@@ -6,10 +6,9 @@ import {
getIsRtl,
getLogicalBoundingClientRect,
getScrollInlineStart,
+ Portal,
} from '@cloudscape-design/component-toolkit/internal';
-import Portal from '../portal';
-
import styles from './styles.css.js';
export default function PortalOverlay({
diff --git a/src/internal/components/portal/__tests__/portal.test.tsx b/src/internal/components/portal/__tests__/portal.test.tsx
deleted file mode 100644
index 5fc80546e3..0000000000
--- a/src/internal/components/portal/__tests__/portal.test.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-import React, { useState } from 'react';
-import { act, fireEvent, render, screen } from '@testing-library/react';
-
-import { warnOnce } from '@cloudscape-design/component-toolkit/internal';
-
-import Portal, { PortalProps } from '../../../../../lib/components/internal/components/portal';
-
-function renderPortal(props: PortalProps) {
- const { rerender, unmount } = render( );
- return { unmount, rerender: (props: PortalProps) => rerender( ) };
-}
-
-jest.mock('@cloudscape-design/component-toolkit/internal', () => ({
- ...jest.requireActual('@cloudscape-design/component-toolkit/internal'),
- warnOnce: jest.fn(),
-}));
-
-afterEach(() => {
- expect(warnOnce).not.toHaveBeenCalled();
- jest.clearAllMocks();
-});
-
-describe('Portal', () => {
- describe('when container is provided', () => {
- let container: Element | undefined;
-
- beforeEach(() => {
- container = document.createElement('div');
- document.body.appendChild(container);
- });
-
- afterEach(() => {
- container?.remove();
- });
-
- test('renders to the container', () => {
- renderPortal({ children:
Hello!
, container });
- expect(container).toContainHTML('Hello!
');
- });
-
- test('ignores getContainer property', () => {
- const getContainer = jest.fn();
- const removeContainer = jest.fn();
- renderPortal({ children: Hello!
, container, getContainer, removeContainer });
- expect(container).toContainHTML('Hello!
');
- expect(getContainer).not.toHaveBeenCalled();
- expect(removeContainer).not.toHaveBeenCalled();
- });
-
- test('cleans up react content inside container when unmounted', () => {
- const { unmount } = renderPortal({ children: Hello!
, container });
- unmount();
- expect(container).toBeEmptyDOMElement();
- });
-
- test('cleans up react content inside container if an explicit container is no longer provided', () => {
- const { rerender } = renderPortal({ children: Hello!
, container });
- rerender({ children: Hello!
});
- expect(container).toBeEmptyDOMElement();
- expect(document.body).toHaveTextContent('Hello!');
- });
- });
-
- describe('when getContainer/removeContainer property is provided', () => {
- test('falls back to default if only getContainer is provided', () => {
- const getContainer = jest.fn();
- renderPortal({ children: Hello!
, getContainer });
- expect(getContainer).not.toHaveBeenCalled();
- expect(warnOnce).toHaveBeenCalledWith('portal', '`removeContainer` is required when `getContainer` is provided');
- jest.mocked(warnOnce).mockReset();
- });
-
- test('falls back to default if only removeContainer is provided', () => {
- const removeContainer = jest.fn();
- renderPortal({ children: Hello!
, removeContainer });
- expect(removeContainer).not.toHaveBeenCalled();
- expect(warnOnce).toHaveBeenCalledWith('portal', '`getContainer` is required when `removeContainer` is provided');
- jest.mocked(warnOnce).mockReset();
- });
-
- test('renders and cleans up async container', async () => {
- const container = document.createElement('div');
- const getContainer = jest.fn(async () => {
- await Promise.resolve();
- document.body.appendChild(container);
- return container;
- });
- const removeContainer = jest.fn(element => document.body.removeChild(element));
- const { unmount } = renderPortal({
- children: Hello!
,
- getContainer,
- removeContainer,
- });
- expect(screen.queryByTestId('portal-content')).toBeFalsy();
- expect(container).not.toBeInTheDocument();
- expect(getContainer).toHaveBeenCalled();
-
- // wait a tick to resolve pending promises
- await act(() => Promise.resolve());
- expect(container).toBeInTheDocument();
- expect(container).toContainElement(screen.queryByTestId('portal-content'));
-
- unmount();
- expect(removeContainer).toHaveBeenCalledWith(container);
- expect(screen.queryByTestId('portal-content')).toBeFalsy();
- expect(container).not.toBeInTheDocument();
- });
-
- test('allows conditional change of getContainer/removeContainer', async () => {
- function MovablePortal({ getContainer, removeContainer }: Pick) {
- const [visible, setVisible] = useState(false);
- return (
- <>
- setVisible(!visible)}>
- Toggle
-
-
- portal content
-
- >
- );
- }
-
- const iframe = document.createElement('iframe');
- document.body.appendChild(iframe);
- const externalDocument = iframe.contentDocument!;
-
- const getContainer = jest.fn(() => {
- const container = externalDocument.createElement('div');
- container.setAttribute('data-testid', 'dynamic-container');
- externalDocument.body.appendChild(container);
- return Promise.resolve(container);
- });
-
- const removeContainer = jest.fn(() => {
- const allContainers = externalDocument.querySelectorAll('[data-testid="dynamic-container"]');
- expect(allContainers).toHaveLength(1);
- allContainers[0].remove();
- });
-
- render( );
- expect(screen.getByTestId('portal-content')).toBeInTheDocument();
- expect(getContainer).not.toHaveBeenCalled();
- expect(removeContainer).not.toHaveBeenCalled();
-
- fireEvent.click(screen.getByTestId('toggle-portal'));
- // wait a tick to resolve pending promises
- await act(() => Promise.resolve());
- expect(screen.queryByTestId('portal-content')).toBeFalsy();
- expect(externalDocument.querySelector('[data-testid="portal-content"]')).toBeTruthy();
- expect(externalDocument.querySelectorAll('[data-testid="dynamic-container"]')).toHaveLength(1);
- expect(getContainer).toHaveBeenCalledTimes(1);
- expect(removeContainer).toHaveBeenCalledTimes(0);
-
- fireEvent.click(screen.getByTestId('toggle-portal'));
- // wait a tick to resolve pending promises
- await act(() => Promise.resolve());
- expect(screen.getByTestId('portal-content')).toBeInTheDocument();
- expect(externalDocument.querySelector('[data-testid="portal-content"]')).toBeFalsy();
- expect(externalDocument.querySelectorAll('[data-testid="dynamic-container"]')).toHaveLength(0);
- expect(getContainer).toHaveBeenCalledTimes(1);
- expect(removeContainer).toHaveBeenCalledTimes(1);
- });
-
- describe('console logging', () => {
- beforeEach(() => {
- jest.spyOn(console, 'warn').mockImplementation(() => {});
- });
-
- test('prints a warning if getContainer rejects a promise', async () => {
- const getContainer = jest.fn(() => Promise.reject('Error for testing'));
- const removeContainer = jest.fn(() => {});
- renderPortal({
- children: Hello!
,
- getContainer,
- removeContainer,
- });
- expect(screen.queryByTestId('portal-content')).toBeFalsy();
- expect(getContainer).toHaveBeenCalled();
- expect(console.warn).not.toHaveBeenCalled();
-
- await Promise.resolve();
- expect(screen.queryByTestId('portal-content')).toBeFalsy();
- expect(removeContainer).not.toHaveBeenCalled();
- expect(console.warn).toHaveBeenCalledWith('[AwsUi] [portal]: failed to load portal root', 'Error for testing');
- });
- });
- });
-
- describe('when a container is not provided', () => {
- test('renders to a div under body', () => {
- renderPortal({ children: Hello!
});
- expect(document.querySelector('body > div > p')).toHaveTextContent('Hello!');
- });
-
- test('removes container element when unmounted', () => {
- const { unmount } = renderPortal({ children: Hello!
});
- // The extra is a wrapper element that react-testing-library creates.
- expect(document.querySelectorAll('body > div').length).toBe(2);
- unmount();
- expect(document.querySelectorAll('body > div').length).toBe(1);
- expect(document.querySelector('body > div')).toBeEmptyDOMElement();
- });
- });
-});
diff --git a/src/internal/components/portal/index.tsx b/src/internal/components/portal/index.tsx
deleted file mode 100644
index 7001cc40c7..0000000000
--- a/src/internal/components/portal/index.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-import React, { useLayoutEffect, useState } from 'react';
-import { createPortal } from 'react-dom';
-
-import { warnOnce } from '@cloudscape-design/component-toolkit/internal';
-
-import { isDevelopment } from '../../is-development';
-
-export interface PortalProps {
- container?: null | Element;
- getContainer?: () => Promise;
- removeContainer?: (container: HTMLElement) => void;
- children: React.ReactNode;
-}
-
-function manageDefaultContainer(setState: React.Dispatch>) {
- const newContainer = document.createElement('div');
- document.body.appendChild(newContainer);
- setState(newContainer);
- return () => {
- document.body.removeChild(newContainer);
- };
-}
-
-function manageAsyncContainer(
- getContainer: () => Promise,
- removeContainer: (container: HTMLElement) => void,
- setState: React.Dispatch>
-) {
- let newContainer: HTMLElement;
- getContainer().then(
- container => {
- newContainer = container;
- setState(container);
- },
- error => {
- console.warn('[AwsUi] [portal]: failed to load portal root', error);
- }
- );
- return () => {
- removeContainer(newContainer);
- };
-}
-
-/**
- * A safe react portal component that renders to a provided node.
- * If a node isn't provided, it creates one under document.body.
- */
-export default function Portal({ container, getContainer, removeContainer, children }: PortalProps) {
- const [activeContainer, setActiveContainer] = useState(container ?? null);
-
- useLayoutEffect(() => {
- if (container) {
- setActiveContainer(container);
- return;
- }
- if (isDevelopment) {
- if (getContainer && !removeContainer) {
- warnOnce('portal', '`removeContainer` is required when `getContainer` is provided');
- }
- if (!getContainer && removeContainer) {
- warnOnce('portal', '`getContainer` is required when `removeContainer` is provided');
- }
- }
- if (getContainer && removeContainer) {
- return manageAsyncContainer(getContainer, removeContainer, setActiveContainer);
- }
- return manageDefaultContainer(setActiveContainer);
- }, [container, getContainer, removeContainer]);
-
- return activeContainer && createPortal(children, activeContainer);
-}
diff --git a/src/internal/components/sortable-area/index.tsx b/src/internal/components/sortable-area/index.tsx
index 54e1fd50a5..0eb2dbeb49 100644
--- a/src/internal/components/sortable-area/index.tsx
+++ b/src/internal/components/sortable-area/index.tsx
@@ -7,7 +7,8 @@ import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy }
import { CSS } from '@dnd-kit/utilities';
import clsx from 'clsx';
-import Portal from '../../components/portal';
+import { Portal } from '@cloudscape-design/component-toolkit/internal';
+
import { fireNonCancelableEvent } from '../../events';
import { joinStrings } from '../../utils/strings';
import { SortableAreaProps } from './interfaces';
diff --git a/src/internal/components/tooltip/index.tsx b/src/internal/components/tooltip/index.tsx
index 59305467e5..82135e403f 100644
--- a/src/internal/components/tooltip/index.tsx
+++ b/src/internal/components/tooltip/index.tsx
@@ -2,11 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect } from 'react';
+import { Portal } from '@cloudscape-design/component-toolkit/internal';
+
import PopoverArrow from '../../../popover/arrow';
import PopoverBody from '../../../popover/body';
import PopoverContainer from '../../../popover/container';
import { PopoverProps } from '../../../popover/interfaces';
-import Portal from '../portal';
import { Transition } from '../transition';
import styles from './styles.css.js';
diff --git a/src/internal/hooks/use-base-component/__tests__/use-base-component.test.tsx b/src/internal/hooks/use-base-component/__tests__/use-base-component.test.tsx
index c4dae62f64..3cd93f7aca 100644
--- a/src/internal/hooks/use-base-component/__tests__/use-base-component.test.tsx
+++ b/src/internal/hooks/use-base-component/__tests__/use-base-component.test.tsx
@@ -3,10 +3,9 @@
import React, { useState } from 'react';
import { render } from '@testing-library/react';
-import { COMPONENT_METADATA_KEY } from '@cloudscape-design/component-toolkit/internal';
+import { COMPONENT_METADATA_KEY, Portal } from '@cloudscape-design/component-toolkit/internal';
import { Button } from '../../../../../lib/components';
-import Portal from '../../../../../lib/components/internal/components/portal';
import { PACKAGE_VERSION } from '../../../../../lib/components/internal/environment';
import useBaseComponent, {
InternalBaseComponentProps,
diff --git a/src/modal/internal.tsx b/src/modal/internal.tsx
index c7a0d218a2..2b9d65ccf8 100644
--- a/src/modal/internal.tsx
+++ b/src/modal/internal.tsx
@@ -4,6 +4,7 @@ import React, { useEffect, useRef } from 'react';
import clsx from 'clsx';
import { useContainerQuery } from '@cloudscape-design/component-toolkit';
+import { Portal } from '@cloudscape-design/component-toolkit/internal';
import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
import { InternalButton } from '../button/internal';
@@ -18,7 +19,6 @@ import {
import { FunnelProps, useFunnel, useFunnelStep, useFunnelSubStep } from '../internal/analytics/hooks/use-funnel';
import { getBaseProps } from '../internal/base-component';
import FocusLock from '../internal/components/focus-lock';
-import Portal from '../internal/components/portal';
import { ButtonContext, ButtonContextProps } from '../internal/context/button-context';
import { ModalContext } from '../internal/context/modal-context';
import ResetContextsForModal from '../internal/context/reset-contexts-for-modal';
diff --git a/src/popover/internal.tsx b/src/popover/internal.tsx
index 3e22356a03..118807ccb9 100644
--- a/src/popover/internal.tsx
+++ b/src/popover/internal.tsx
@@ -3,10 +3,11 @@
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import clsx from 'clsx';
+import { Portal } from '@cloudscape-design/component-toolkit/internal';
+
import { useInternalI18n } from '../i18n/context';
import { getBaseProps } from '../internal/base-component';
import { getFirstFocusable } from '../internal/components/focus-lock/utils';
-import Portal from '../internal/components/portal';
import { LinkDefaultVariantContext } from '../internal/context/link-default-variant-context';
import ResetContextsForModal from '../internal/context/reset-contexts-for-modal';
import { useSingleTabStopNavigation } from '../internal/context/single-tab-stop-navigation-context';
diff --git a/src/table/body-cell/disabled-inline-editor.tsx b/src/table/body-cell/disabled-inline-editor.tsx
index b6b1830a68..382d2ae6f2 100644
--- a/src/table/body-cell/disabled-inline-editor.tsx
+++ b/src/table/body-cell/disabled-inline-editor.tsx
@@ -3,8 +3,9 @@
import React, { useRef } from 'react';
import clsx from 'clsx';
+import { Portal } from '@cloudscape-design/component-toolkit/internal';
+
import Icon from '../../icon/internal';
-import Portal from '../../internal/components/portal';
import { useSingleTabStopNavigation } from '../../internal/context/single-tab-stop-navigation-context';
import useHiddenDescription from '../../internal/hooks/use-hidden-description';
import { usePortalModeClasses } from '../../internal/hooks/use-portal-mode-classes';
diff --git a/src/top-navigation/1.0-beta/internal.tsx b/src/top-navigation/1.0-beta/internal.tsx
index 34f1ab3bb7..640d321447 100644
--- a/src/top-navigation/1.0-beta/internal.tsx
+++ b/src/top-navigation/1.0-beta/internal.tsx
@@ -3,8 +3,9 @@
import React from 'react';
import clsx from 'clsx';
+import { Portal } from '@cloudscape-design/component-toolkit/internal';
+
import { getBaseProps } from '../../internal/base-component';
-import Portal from '../../internal/components/portal';
import VisualContext from '../../internal/components/visual-context';
import { fireCancelableEvent, isPlainLeftClick } from '../../internal/events';
import { InternalBaseComponentProps } from '../../internal/hooks/use-base-component';