From 15db44824f4585354647dac75f668dd93a1aecff Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 21 Mar 2025 15:56:26 -0700 Subject: [PATCH 1/9] Initial refactor to tear out UNSTABLE_portalContainer in favor of the PortalContainer --- packages/@react-aria/overlays/src/Overlay.tsx | 2 +- packages/react-aria-components/package.json | 1 + packages/react-aria-components/src/Modal.tsx | 11 ++-------- .../react-aria-components/src/Popover.tsx | 14 ++++--------- packages/react-aria-components/src/Toast.tsx | 17 ++++++++------- .../react-aria-components/src/Tooltip.tsx | 9 ++------ .../react-aria-components/test/Dialog.test.js | 19 ++++++++++------- .../test/Popover.test.js | 15 +++++++------ .../react-aria-components/test/Toast.test.js | 21 +++++++++++-------- .../test/Tooltip.test.js | 13 +++++++----- 10 files changed, 60 insertions(+), 62 deletions(-) diff --git a/packages/@react-aria/overlays/src/Overlay.tsx b/packages/@react-aria/overlays/src/Overlay.tsx index 61711422c25..a3d8562308f 100644 --- a/packages/@react-aria/overlays/src/Overlay.tsx +++ b/packages/@react-aria/overlays/src/Overlay.tsx @@ -56,7 +56,7 @@ export function Overlay(props: OverlayProps): ReactNode | null { let contextValue = useMemo(() => ({contain, setContain}), [contain, setContain]); let {getContainer} = useUNSTABLE_PortalContext(); - if (!props.portalContainer && getContainer) { + if (!props.portalContainer && getContainer) { portalContainer = getContainer(); } diff --git a/packages/react-aria-components/package.json b/packages/react-aria-components/package.json index aef626ec8c9..6b1cd161e77 100644 --- a/packages/react-aria-components/package.json +++ b/packages/react-aria-components/package.json @@ -45,6 +45,7 @@ "@react-aria/focus": "^3.20.1", "@react-aria/interactions": "^3.24.1", "@react-aria/live-announcer": "^3.4.1", + "@react-aria/overlays": "^3.26.1", "@react-aria/ssr": "^3.9.7", "@react-aria/toolbar": "3.0.0-beta.14", "@react-aria/utils": "^3.28.1", diff --git a/packages/react-aria-components/src/Modal.tsx b/packages/react-aria-components/src/Modal.tsx index 713d458ecb5..d306f92271f 100644 --- a/packages/react-aria-components/src/Modal.tsx +++ b/packages/react-aria-components/src/Modal.tsx @@ -27,11 +27,6 @@ export interface ModalOverlayProps extends AriaModalOverlayProps, OverlayTrigger * Whether the modal is currently performing an exit animation. */ isExiting?: boolean, - /** - * The container element in which the overlay portal will be placed. This may have unknown behavior depending on where it is portalled to. - * @default document.body - */ - UNSTABLE_portalContainer?: Element } interface InternalModalContextValue { @@ -80,7 +75,6 @@ export const Modal = /*#__PURE__*/ (forwardRef as forwardRefType)(function Modal children, isEntering, isExiting, - UNSTABLE_portalContainer, shouldCloseOnInteractOutside, ...otherProps } = props; @@ -94,7 +88,6 @@ export const Modal = /*#__PURE__*/ (forwardRef as forwardRefType)(function Modal onOpenChange={onOpenChange} isEntering={isEntering} isExiting={isExiting} - UNSTABLE_portalContainer={UNSTABLE_portalContainer} shouldCloseOnInteractOutside={shouldCloseOnInteractOutside}> {children} @@ -142,7 +135,7 @@ function ModalOverlayWithForwardRef(props: ModalOverlayProps, ref: ForwardedRef< */ export const ModalOverlay = /*#__PURE__*/ (forwardRef as forwardRefType)(ModalOverlayWithForwardRef); -function ModalOverlayInner({UNSTABLE_portalContainer, ...props}: ModalOverlayInnerProps) { +function ModalOverlayInner(props: ModalOverlayInnerProps) { let modalRef = props.modalRef; let {state} = props; let {modalProps, underlayProps} = useModalOverlay(props, state, modalRef); @@ -165,7 +158,7 @@ function ModalOverlayInner({UNSTABLE_portalContainer, ...props}: ModalOverlayInn }; return ( - +
, Omit(null); @@ -154,7 +148,7 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po ...props, offset: props.offset ?? 8, arrowSize: arrowWidth, - // If this is a submenu/subdialog, use the root popover's container + // If this is a submenu/subdialog, use the root popover's container // to detect outside interaction and add aria-hidden. groupRef: isSubPopover ? groupCtx! : containerRef }, state); @@ -217,7 +211,7 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po // If this is a root popover, render an extra div to act as the portal container for submenus/subdialogs. if (!isSubPopover) { return ( - + {!props.isNonModal && state.isOpen &&
}
@@ -230,7 +224,7 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po // Submenus/subdialogs are mounted into the root popover's container. return ( - + {overlay} ); diff --git a/packages/react-aria-components/src/Toast.tsx b/packages/react-aria-components/src/Toast.tsx index 5a0e9cf9ea0..eea1eb3886f 100644 --- a/packages/react-aria-components/src/Toast.tsx +++ b/packages/react-aria-components/src/Toast.tsx @@ -20,6 +20,7 @@ import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, Rea import {TextContext} from './Text'; import {useIsSSR} from '@react-aria/ssr'; import {useObjectRef} from '@react-aria/utils'; +import {useUNSTABLE_PortalContext} from '@react-aria/overlays'; const ToastStateContext = createContext | null>(null); @@ -42,12 +43,7 @@ export interface ToastRegionProps extends AriaToastRegionProps, StyleRenderPr /** The queue of toasts to display. */ queue: ToastQueue, /** A function to render each toast. */ - children: (renderProps: {toast: QueuedToast}) => ReactElement, - /** - * The container element in which the toast region portal will be placed. - * @default document.body - */ - portalContainer?: Element + children: (renderProps: {toast: QueuedToast}) => ReactElement } /** @@ -71,7 +67,14 @@ export const ToastRegion = /*#__PURE__*/ (forwardRef as forwardRefType)(function } }); - let {portalContainer = isSSR ? null : document.body} = props; + let portalContainer; + let {getContainer} = useUNSTABLE_PortalContext(); + if (!isSSR) { + portalContainer = document.body; + if (getContainer) { + portalContainer = getContainer(); + } + } let region = ( diff --git a/packages/react-aria-components/src/Tooltip.tsx b/packages/react-aria-components/src/Tooltip.tsx index 18eb46f978d..9809daa74c2 100644 --- a/packages/react-aria-components/src/Tooltip.tsx +++ b/packages/react-aria-components/src/Tooltip.tsx @@ -38,11 +38,6 @@ export interface TooltipProps extends PositionProps, Pick) { +export const Tooltip = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tooltip(props: TooltipProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, TooltipContext); let contextState = useContext(TooltipTriggerStateContext); let localState = useTooltipTriggerState(props); @@ -112,7 +107,7 @@ export const Tooltip = /*#__PURE__*/ (forwardRef as forwardRefType)(function Too } return ( - + ); diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js index c2c67f0495a..dc4d21c92db 100644 --- a/packages/react-aria-components/test/Dialog.test.js +++ b/packages/react-aria-components/test/Dialog.test.js @@ -21,7 +21,8 @@ import { Popover } from '../'; import {pointerMap, render, within} from '@react-spectrum/test-utils-internal'; -import React from 'react'; +import React, {useRef} from 'react'; +import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; import userEvent from '@testing-library/user-event'; describe('Dialog', () => { @@ -302,12 +303,12 @@ describe('Dialog', () => { expect(modal).not.toBeInTheDocument(); }); - describe('portalContainer', () => { - function InfoDialog(props) { + describe('portal provider', () => { + function InfoDialog() { return ( - + {({close}) => ( <> @@ -321,15 +322,17 @@ describe('Dialog', () => { ); } function App() { - let [container, setContainer] = React.useState(); + let container = useRef(null); return ( <> - -
+ container.current}> + + +
); } - it('should render the tooltip in the portal container', async () => { + it('should render the tooltip in the portal container provided by the PortalProvider', async () => { let {getByRole, getByTestId} = render(); let button = getByRole('button'); await user.click(button); diff --git a/packages/react-aria-components/test/Popover.test.js b/packages/react-aria-components/test/Popover.test.js index 9697ee58632..df3a94e85d6 100644 --- a/packages/react-aria-components/test/Popover.test.js +++ b/packages/react-aria-components/test/Popover.test.js @@ -12,7 +12,8 @@ import {act, pointerMap, render} from '@react-spectrum/test-utils-internal'; import {Button, Dialog, DialogTrigger, OverlayArrow, Popover, Pressable} from '../'; -import React from 'react'; +import React, {useRef} from 'react'; +import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; import userEvent from '@testing-library/user-event'; let TestPopover = (props) => ( @@ -177,11 +178,11 @@ describe('Popover', () => { }); describe('portalContainer', () => { - function InfoPopover(props) { + function InfoPopover() { return ( )} - + ); } function App() { - let [container, setContainer] = React.useState(); + let container = useRef(null); return ( <> - -
+ container.current}> + + +
); } diff --git a/packages/react-aria-components/test/Tooltip.test.js b/packages/react-aria-components/test/Tooltip.test.js index 91c16871787..1f5dc79c826 100644 --- a/packages/react-aria-components/test/Tooltip.test.js +++ b/packages/react-aria-components/test/Tooltip.test.js @@ -12,7 +12,8 @@ import {act, fireEvent, pointerMap, render} from '@react-spectrum/test-utils-internal'; import {Button, Focusable, OverlayArrow, Pressable, Tooltip, TooltipTrigger} from 'react-aria-components'; -import React from 'react'; +import React, {useRef} from 'react'; +import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; import userEvent from '@testing-library/user-event'; function TestTooltip(props) { @@ -173,7 +174,7 @@ describe('Tooltip', () => { return ( - + @@ -185,11 +186,13 @@ describe('Tooltip', () => { ); } function App() { - let [container, setContainer] = React.useState(); + let container = useRef(null); return ( <> - -
+ container.current}> + + +
); } From e29a277d4becf7d73077f3ddc9b3dfe24fd644ab Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 21 Mar 2025 15:59:25 -0700 Subject: [PATCH 2/9] yarn.lock update --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index 2dbc833d989..099164a555a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29252,6 +29252,7 @@ __metadata: "@react-aria/focus": "npm:^3.20.1" "@react-aria/interactions": "npm:^3.24.1" "@react-aria/live-announcer": "npm:^3.4.1" + "@react-aria/overlays": "npm:^3.26.1" "@react-aria/ssr": "npm:^3.9.7" "@react-aria/toolbar": "npm:3.0.0-beta.14" "@react-aria/utils": "npm:^3.28.1" From 72cec38ac644cb777c3dd5d9e48d6d4ea3ff0ce0 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 21 Mar 2025 16:48:49 -0700 Subject: [PATCH 3/9] switch to deprecating UNSTABLE_portalContainer --- packages/@react-aria/overlays/src/Overlay.tsx | 3 +- .../@react-aria/overlays/src/useModal.tsx | 6 +++ packages/react-aria-components/src/Modal.tsx | 12 +++++- .../react-aria-components/src/Popover.tsx | 13 ++++-- .../react-aria-components/src/Tooltip.tsx | 10 ++++- .../react-aria-components/test/Dialog.test.js | 42 +++++++++++++++++- .../test/Popover.test.js | 40 ++++++++++++++++- .../test/Tooltip.test.js | 43 ++++++++++++++++++- 8 files changed, 157 insertions(+), 12 deletions(-) diff --git a/packages/@react-aria/overlays/src/Overlay.tsx b/packages/@react-aria/overlays/src/Overlay.tsx index a3d8562308f..2bc2e0f7780 100644 --- a/packages/@react-aria/overlays/src/Overlay.tsx +++ b/packages/@react-aria/overlays/src/Overlay.tsx @@ -22,6 +22,7 @@ export interface OverlayProps { /** * The container element in which the overlay portal will be placed. * @default document.body + * @deprecated - Use a parent UNSTABLE_PortalProvider to set your portal container instead. */ portalContainer?: Element, /** The overlay to render in the portal. */ @@ -56,7 +57,7 @@ export function Overlay(props: OverlayProps): ReactNode | null { let contextValue = useMemo(() => ({contain, setContain}), [contain, setContain]); let {getContainer} = useUNSTABLE_PortalContext(); - if (!props.portalContainer && getContainer) { + if (getContainer) { portalContainer = getContainer(); } diff --git a/packages/@react-aria/overlays/src/useModal.tsx b/packages/@react-aria/overlays/src/useModal.tsx index 5360a93cad8..79bbe48654f 100644 --- a/packages/@react-aria/overlays/src/useModal.tsx +++ b/packages/@react-aria/overlays/src/useModal.tsx @@ -14,6 +14,7 @@ import {DOMAttributes} from '@react-types/shared'; import React, {AriaAttributes, ReactNode, useContext, useEffect, useMemo, useState} from 'react'; import ReactDOM from 'react-dom'; import {useIsSSR} from '@react-aria/ssr'; +import {useUNSTABLE_PortalContext} from './PortalProvider'; export interface ModalProviderProps extends DOMAttributes { children: ReactNode @@ -112,6 +113,7 @@ export interface OverlayContainerProps extends ModalProviderProps { /** * The container element in which the overlay portal will be placed. * @default document.body + * @deprecated - Use a parent UNSTABLE_PortalProvider to set your portal container instead. */ portalContainer?: Element } @@ -126,6 +128,10 @@ export interface OverlayContainerProps extends ModalProviderProps { export function OverlayContainer(props: OverlayContainerProps): React.ReactPortal | null { let isSSR = useIsSSR(); let {portalContainer = isSSR ? null : document.body, ...rest} = props; + let {getContainer} = useUNSTABLE_PortalContext(); + if (getContainer) { + portalContainer = getContainer(); + } React.useEffect(() => { if (portalContainer?.closest('[data-overlay-container]')) { diff --git a/packages/react-aria-components/src/Modal.tsx b/packages/react-aria-components/src/Modal.tsx index d306f92271f..3c8624ac29c 100644 --- a/packages/react-aria-components/src/Modal.tsx +++ b/packages/react-aria-components/src/Modal.tsx @@ -27,6 +27,12 @@ export interface ModalOverlayProps extends AriaModalOverlayProps, OverlayTrigger * Whether the modal is currently performing an exit animation. */ isExiting?: boolean, + /** + * The container element in which the overlay portal will be placed. This may have unknown behavior depending on where it is portalled to. + * @default document.body + * @deprecated - Use a parent UNSTABLE_PortalProvider to set your portal container instead. + */ + UNSTABLE_portalContainer?: Element } interface InternalModalContextValue { @@ -75,6 +81,7 @@ export const Modal = /*#__PURE__*/ (forwardRef as forwardRefType)(function Modal children, isEntering, isExiting, + UNSTABLE_portalContainer, shouldCloseOnInteractOutside, ...otherProps } = props; @@ -88,6 +95,7 @@ export const Modal = /*#__PURE__*/ (forwardRef as forwardRefType)(function Modal onOpenChange={onOpenChange} isEntering={isEntering} isExiting={isExiting} + UNSTABLE_portalContainer={UNSTABLE_portalContainer} shouldCloseOnInteractOutside={shouldCloseOnInteractOutside}> {children} @@ -135,7 +143,7 @@ function ModalOverlayWithForwardRef(props: ModalOverlayProps, ref: ForwardedRef< */ export const ModalOverlay = /*#__PURE__*/ (forwardRef as forwardRefType)(ModalOverlayWithForwardRef); -function ModalOverlayInner(props: ModalOverlayInnerProps) { +function ModalOverlayInner({UNSTABLE_portalContainer, ...props}: ModalOverlayInnerProps) { let modalRef = props.modalRef; let {state} = props; let {modalProps, underlayProps} = useModalOverlay(props, state, modalRef); @@ -158,7 +166,7 @@ function ModalOverlayInner(props: ModalOverlayInnerProps) { }; return ( - +
, Omit(null); @@ -211,7 +218,7 @@ function PopoverInner({state, isExiting, ...props}: PopoverInnerProps) { // If this is a root popover, render an extra div to act as the portal container for submenus/subdialogs. if (!isSubPopover) { return ( - + {!props.isNonModal && state.isOpen &&
}
@@ -224,7 +231,7 @@ function PopoverInner({state, isExiting, ...props}: PopoverInnerProps) { // Submenus/subdialogs are mounted into the root popover's container. return ( - + {overlay} ); diff --git a/packages/react-aria-components/src/Tooltip.tsx b/packages/react-aria-components/src/Tooltip.tsx index 9809daa74c2..6ec1bacc05f 100644 --- a/packages/react-aria-components/src/Tooltip.tsx +++ b/packages/react-aria-components/src/Tooltip.tsx @@ -38,6 +38,12 @@ export interface TooltipProps extends PositionProps, Pick) { +export const Tooltip = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tooltip({UNSTABLE_portalContainer, ...props}: TooltipProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, TooltipContext); let contextState = useContext(TooltipTriggerStateContext); let localState = useTooltipTriggerState(props); @@ -107,7 +113,7 @@ export const Tooltip = /*#__PURE__*/ (forwardRef as forwardRefType)(function Too } return ( - + ); diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js index dc4d21c92db..2e411bf56bb 100644 --- a/packages/react-aria-components/test/Dialog.test.js +++ b/packages/react-aria-components/test/Dialog.test.js @@ -303,7 +303,7 @@ describe('Dialog', () => { expect(modal).not.toBeInTheDocument(); }); - describe('portal provider', () => { + describe('portalProvider', () => { function InfoDialog() { return ( @@ -332,7 +332,45 @@ describe('Dialog', () => { ); } - it('should render the tooltip in the portal container provided by the PortalProvider', async () => { + it('should render the dialog in the portal container provided by the PortalProvider', async () => { + let {getByRole, getByTestId} = render(); + let button = getByRole('button'); + await user.click(button); + + expect(getByRole('alertdialog').closest('[data-testid="custom-container"]')).toBe(getByTestId('custom-container')); + await user.click(document.body); + }); + }); + + // TODO: delete this test when we get rid of the deprecated prop + describe('portalContainer', () => { + function InfoDialog(props) { + return ( + + + + + {({close}) => ( + <> + Alert + + + )} + + + + ); + } + function App() { + let [container, setContainer] = React.useState(); + return ( + <> + +
+ + ); + } + it('should render the dialog in the portal container', async () => { let {getByRole, getByTestId} = render(); let button = getByRole('button'); await user.click(button); diff --git a/packages/react-aria-components/test/Popover.test.js b/packages/react-aria-components/test/Popover.test.js index df3a94e85d6..71b18ce784d 100644 --- a/packages/react-aria-components/test/Popover.test.js +++ b/packages/react-aria-components/test/Popover.test.js @@ -177,7 +177,7 @@ describe('Popover', () => { expect(arrow).toHaveAttribute('style', expect.stringContaining('top: 5px')); }); - describe('portalContainer', () => { + describe('portalProvider', () => { function InfoPopover() { return ( @@ -204,6 +204,44 @@ describe('Popover', () => { ); } + it('should render the dialog in the portal container set by the PortalProvider', async () => { + let {getByRole, getByTestId} = render( + + ); + + let button = getByRole('button'); + await user.click(button); + + expect(getByRole('dialog').closest('[data-testid="custom-container"]')).toBe(getByTestId('custom-container')); + }); + }); + + // TODO: delete this test when we get rid of the deprecated prop + describe('portalContainer', () => { + function InfoPopover(props) { + return ( + + + + + + + + + Edit + + + ); + } + function App() { + let [container, setContainer] = React.useState(); + return ( + <> + +
+ + ); + } it('should render the tooltip in the portal container', async () => { let {getByRole, getByTestId} = render(); let button = getByRole('button'); From b9b0995c869a2b23ae7cf0c176871d45b748e320 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 25 Mar 2025 09:55:54 -0700 Subject: [PATCH 4/9] prefer deprecated prop over context to make this a non-breaking change --- packages/@react-aria/overlays/src/Overlay.tsx | 2 +- packages/@react-aria/overlays/src/useModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/overlays/src/Overlay.tsx b/packages/@react-aria/overlays/src/Overlay.tsx index 2bc2e0f7780..f10a6fb16ed 100644 --- a/packages/@react-aria/overlays/src/Overlay.tsx +++ b/packages/@react-aria/overlays/src/Overlay.tsx @@ -57,7 +57,7 @@ export function Overlay(props: OverlayProps): ReactNode | null { let contextValue = useMemo(() => ({contain, setContain}), [contain, setContain]); let {getContainer} = useUNSTABLE_PortalContext(); - if (getContainer) { + if (!props.portalContainer && getContainer) { portalContainer = getContainer(); } diff --git a/packages/@react-aria/overlays/src/useModal.tsx b/packages/@react-aria/overlays/src/useModal.tsx index 79bbe48654f..150dedb95d8 100644 --- a/packages/@react-aria/overlays/src/useModal.tsx +++ b/packages/@react-aria/overlays/src/useModal.tsx @@ -129,7 +129,7 @@ export function OverlayContainer(props: OverlayContainerProps): React.ReactPorta let isSSR = useIsSSR(); let {portalContainer = isSSR ? null : document.body, ...rest} = props; let {getContainer} = useUNSTABLE_PortalContext(); - if (getContainer) { + if (!props.portalContainer && getContainer) { portalContainer = getContainer(); } From 26a5f2fc35b5aa7c51d904f170660b911771ff48 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 25 Mar 2025 17:14:01 -0700 Subject: [PATCH 5/9] add rough docs --- .../overlays/docs/PortalProvider.mdx | 120 ++++++++++++++++++ .../overlays/src/PortalProvider.tsx | 17 ++- packages/@react-aria/overlays/src/index.ts | 1 + 3 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 packages/@react-aria/overlays/docs/PortalProvider.mdx diff --git a/packages/@react-aria/overlays/docs/PortalProvider.mdx b/packages/@react-aria/overlays/docs/PortalProvider.mdx new file mode 100644 index 00000000000..b797266962f --- /dev/null +++ b/packages/@react-aria/overlays/docs/PortalProvider.mdx @@ -0,0 +1,120 @@ +{/* Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '@react-spectrum/docs'; +export default Layout; + +import docs from 'docs:@react-aria/overlays'; +import {HeaderInfo, PropTable, FunctionAPI, PageDescription} from '@react-spectrum/docs'; +import packageData from '@react-aria/overlays/package.json'; + +--- +category: Utilities +keywords: [overlays, portals] +--- + +# PortalProvider + +{docs.exports.UNSTABLE_PortalProvider.description} + + + +## Introduction + +`UNSTABLE_PortalProvider` is a utility wrapper component that can be used to set where components like +Modals, Popovers, Toasts, and Tooltips will portal their overlay element to. This is typically used when +your app is already portalling other elements to a location other than the `document.body` and thus requires +your React Aria components to send their overlays to the same container. + +## Props + + + +## Example + +The example below shows how you can use `UNSTABLE_PortalProvider` to portal your Toasts to an arbitrary container. Note that +the Toast in this example is taken directly from the [React Aria Components Toast documentation](Toast.html#example), please visit that page for +a detailed explanation of its implementation. + +```tsx example +import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; +///- begin collapse -/// +import {UNSTABLE_ToastRegion as ToastRegion, UNSTABLE_Toast as Toast, UNSTABLE_ToastQueue as ToastQueue, UNSTABLE_ToastContent as ToastContent, Button, Text} from 'react-aria-components'; + + +// Define the type for your toast content. +interface MyToastContent { + title: string, + description?: string +} + +// Create a global ToastQueue. +const queue = new ToastQueue(); + +function MyToastRegion() { + return ( + + {({toast}) => ( + + + {toast.content.title} + {toast.content.description} + + + + )} + + + ); +} +///- end collapse -/// + +// See the above Toast docs link for the ToastRegion implementation +function App() { + let container = React.useRef(null); + return ( + <> + container.current}> + + + +
+ Toasts are portalled here! +
+ + ); +} + + +``` + +## Contexts + +The `getContainer` set by the nearest PortalProvider can be accessed by calling `useUNSTABLE_PortalContext`. This can be +used by custom overlay components to ensure that they are also being consistently portalled throughout your app. + + + +```tsx +import {useUNSTABLE_PortalContext} from '@react-aria/overlays'; + +function MyOverlay(props) { + let {children} = props; + let {getContainer} = useUNSTABLE_PortalContext(); + return ReactDOM.createPortal(children, getContainer()); +} +``` diff --git a/packages/@react-aria/overlays/src/PortalProvider.tsx b/packages/@react-aria/overlays/src/PortalProvider.tsx index ab0e59a8c6e..90a92876483 100644 --- a/packages/@react-aria/overlays/src/PortalProvider.tsx +++ b/packages/@react-aria/overlays/src/PortalProvider.tsx @@ -13,13 +13,20 @@ import React, {createContext, ReactNode, useContext} from 'react'; export interface PortalProviderProps { - /* Should return the element where we should portal to. Can clear the context by passing null. */ - getContainer?: () => HTMLElement | null + /** Should return the element where we should portal to. Can clear the context by passing null. */ + getContainer?: () => HTMLElement | null, + /** The content of the PortalProvider. Should contain all children that want to portal their overlays to the element returned by the provided getContainer(). */ + children: ReactNode } -export const PortalContext = createContext({}); +export interface PortalProviderContextValue extends Omit{}; -export function UNSTABLE_PortalProvider(props: PortalProviderProps & {children: ReactNode}): ReactNode { +export const PortalContext = createContext({}); + +/** + * Sets the portal container for all overlay elements rendered by its children. + */ +export function UNSTABLE_PortalProvider(props: PortalProviderProps): ReactNode { let {getContainer} = props; let {getContainer: ctxGetContainer} = useUNSTABLE_PortalContext(); return ( @@ -29,6 +36,6 @@ export function UNSTABLE_PortalProvider(props: PortalProviderProps & {children: ); } -export function useUNSTABLE_PortalContext(): PortalProviderProps { +export function useUNSTABLE_PortalContext(): PortalProviderContextValue { return useContext(PortalContext) ?? {}; } diff --git a/packages/@react-aria/overlays/src/index.ts b/packages/@react-aria/overlays/src/index.ts index d73f2def7eb..0f0fc8bbe62 100644 --- a/packages/@react-aria/overlays/src/index.ts +++ b/packages/@react-aria/overlays/src/index.ts @@ -30,3 +30,4 @@ export type {AriaPopoverProps, PopoverAria} from './usePopover'; export type {AriaModalOverlayProps, ModalOverlayAria} from './useModalOverlay'; export type {OverlayProps} from './Overlay'; export type {Placement, PlacementAxis, PositionProps} from '@react-types/overlays'; +export type {PortalProviderProps, PortalProviderContextValue} from './PortalProvider'; From bf0a29f513820a04775f598c0ef1f85ce78e1720 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 27 Mar 2025 16:03:38 -0700 Subject: [PATCH 6/9] updating copy to include explaination of UNSTABLE --- packages/@react-aria/overlays/docs/PortalProvider.mdx | 9 ++++++++- packages/@react-aria/overlays/src/PortalProvider.tsx | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/overlays/docs/PortalProvider.mdx b/packages/@react-aria/overlays/docs/PortalProvider.mdx index b797266962f..7f51369c6fc 100644 --- a/packages/@react-aria/overlays/docs/PortalProvider.mdx +++ b/packages/@react-aria/overlays/docs/PortalProvider.mdx @@ -34,6 +34,13 @@ Modals, Popovers, Toasts, and Tooltips will portal their overlay element to. Thi your app is already portalling other elements to a location other than the `document.body` and thus requires your React Aria components to send their overlays to the same container. +Please note that `UNSTABLE_PortalProvider` is considered `UNSTABLE` because it is an escape hatch, and there are +many places that an application could portal to. Not all of them will work, either with styling, accessibility, +or for a variety of other reasons. Typically, it is best to portal to the root of the entire application, typically the `body` element, +outside of any possible overflow or stacking contexts. We envision `UNSTABLE_PortalProvider` being used to group all of the portalled +elements into a single container at the root of the app or to control the order of children of the `body` element, but you may have use cases +that need to do otherwise. + ## Props @@ -89,7 +96,7 @@ function App() { title: 'Toast complete!', description: 'Great success.' })}> - Toast + Open Toast
diff --git a/packages/@react-aria/overlays/src/PortalProvider.tsx b/packages/@react-aria/overlays/src/PortalProvider.tsx index 90a92876483..138c07f70f7 100644 --- a/packages/@react-aria/overlays/src/PortalProvider.tsx +++ b/packages/@react-aria/overlays/src/PortalProvider.tsx @@ -15,7 +15,7 @@ import React, {createContext, ReactNode, useContext} from 'react'; export interface PortalProviderProps { /** Should return the element where we should portal to. Can clear the context by passing null. */ getContainer?: () => HTMLElement | null, - /** The content of the PortalProvider. Should contain all children that want to portal their overlays to the element returned by the provided getContainer(). */ + /** The content of the PortalProvider. Should contain all children that want to portal their overlays to the element returned by the provided `getContainer()`. */ children: ReactNode } From 580dbbb33db27625bdf5f8f258ae69607bcc3e5a Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 28 Mar 2025 17:20:14 -0700 Subject: [PATCH 7/9] rename to UNSAFE_PortalProvider --- .../overlays/docs/PortalProvider.mdx | 28 +++++++++---------- packages/@react-aria/overlays/src/Overlay.tsx | 6 ++-- .../overlays/src/PortalProvider.tsx | 6 ++-- packages/@react-aria/overlays/src/index.ts | 2 +- .../@react-aria/overlays/src/useModal.tsx | 6 ++-- .../dialog/test/DialogContainer.test.js | 6 ++-- .../dialog/test/DialogTrigger.test.js | 6 ++-- .../menu/test/MenuTrigger.test.js | 6 ++-- .../@react-spectrum/table/src/Resizer.tsx | 6 ++-- .../table/test/TableSizing.test.tsx | 6 ++-- .../@react-spectrum/toast/src/Toaster.tsx | 4 +-- .../toast/stories/Toast.stories.tsx | 6 ++-- .../tooltip/test/TooltipTrigger.test.js | 14 +++++----- packages/react-aria-components/src/Modal.tsx | 2 +- .../react-aria-components/src/Popover.tsx | 2 +- packages/react-aria-components/src/Toast.tsx | 4 +-- .../react-aria-components/src/Tooltip.tsx | 2 +- .../react-aria-components/test/Dialog.test.js | 6 ++-- .../react-aria-components/test/Menu.test.tsx | 6 ++-- .../test/Popover.test.js | 6 ++-- .../react-aria-components/test/Toast.test.js | 6 ++-- .../test/Tooltip.test.js | 6 ++-- 22 files changed, 71 insertions(+), 71 deletions(-) diff --git a/packages/@react-aria/overlays/docs/PortalProvider.mdx b/packages/@react-aria/overlays/docs/PortalProvider.mdx index 7f51369c6fc..104fb8ae7ab 100644 --- a/packages/@react-aria/overlays/docs/PortalProvider.mdx +++ b/packages/@react-aria/overlays/docs/PortalProvider.mdx @@ -21,38 +21,38 @@ keywords: [overlays, portals] # PortalProvider -{docs.exports.UNSTABLE_PortalProvider.description} +{docs.exports.UNSAFE_PortalProvider.description} + componentNames={['UNSAFE_PortalProvider', 'useUNSAFE_PortalContext']} /> ## Introduction -`UNSTABLE_PortalProvider` is a utility wrapper component that can be used to set where components like +`UNSAFE_PortalProvider` is a utility wrapper component that can be used to set where components like Modals, Popovers, Toasts, and Tooltips will portal their overlay element to. This is typically used when your app is already portalling other elements to a location other than the `document.body` and thus requires your React Aria components to send their overlays to the same container. -Please note that `UNSTABLE_PortalProvider` is considered `UNSTABLE` because it is an escape hatch, and there are +Please note that `UNSAFE_PortalProvider` is considered `UNSTABLE` because it is an escape hatch, and there are many places that an application could portal to. Not all of them will work, either with styling, accessibility, or for a variety of other reasons. Typically, it is best to portal to the root of the entire application, typically the `body` element, -outside of any possible overflow or stacking contexts. We envision `UNSTABLE_PortalProvider` being used to group all of the portalled +outside of any possible overflow or stacking contexts. We envision `UNSAFE_PortalProvider` being used to group all of the portalled elements into a single container at the root of the app or to control the order of children of the `body` element, but you may have use cases that need to do otherwise. ## Props - + ## Example -The example below shows how you can use `UNSTABLE_PortalProvider` to portal your Toasts to an arbitrary container. Note that +The example below shows how you can use `UNSAFE_PortalProvider` to portal your Toasts to an arbitrary container. Note that the Toast in this example is taken directly from the [React Aria Components Toast documentation](Toast.html#example), please visit that page for a detailed explanation of its implementation. ```tsx example -import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; ///- begin collapse -/// import {UNSTABLE_ToastRegion as ToastRegion, UNSTABLE_Toast as Toast, UNSTABLE_ToastQueue as ToastQueue, UNSTABLE_ToastContent as ToastContent, Button, Text} from 'react-aria-components'; @@ -89,7 +89,7 @@ function App() { let container = React.useRef(null); return ( <> - container.current}> + container.current}> - +
Toasts are portalled here!
@@ -111,17 +111,17 @@ function App() { ## Contexts -The `getContainer` set by the nearest PortalProvider can be accessed by calling `useUNSTABLE_PortalContext`. This can be +The `getContainer` set by the nearest PortalProvider can be accessed by calling `useUNSAFE_PortalContext`. This can be used by custom overlay components to ensure that they are also being consistently portalled throughout your app. - + ```tsx -import {useUNSTABLE_PortalContext} from '@react-aria/overlays'; +import {useUNSAFE_PortalContext} from '@react-aria/overlays'; function MyOverlay(props) { let {children} = props; - let {getContainer} = useUNSTABLE_PortalContext(); + let {getContainer} = useUNSAFE_PortalContext(); return ReactDOM.createPortal(children, getContainer()); } ``` diff --git a/packages/@react-aria/overlays/src/Overlay.tsx b/packages/@react-aria/overlays/src/Overlay.tsx index f10a6fb16ed..ee818bcefc6 100644 --- a/packages/@react-aria/overlays/src/Overlay.tsx +++ b/packages/@react-aria/overlays/src/Overlay.tsx @@ -16,13 +16,13 @@ import React, {ReactNode, useContext, useMemo, useState} from 'react'; import ReactDOM from 'react-dom'; import {useIsSSR} from '@react-aria/ssr'; import {useLayoutEffect} from '@react-aria/utils'; -import {useUNSTABLE_PortalContext} from './PortalProvider'; +import {useUNSAFE_PortalContext} from './PortalProvider'; export interface OverlayProps { /** * The container element in which the overlay portal will be placed. * @default document.body - * @deprecated - Use a parent UNSTABLE_PortalProvider to set your portal container instead. + * @deprecated - Use a parent UNSAFE_PortalProvider to set your portal container instead. */ portalContainer?: Element, /** The overlay to render in the portal. */ @@ -56,7 +56,7 @@ export function Overlay(props: OverlayProps): ReactNode | null { let [contain, setContain] = useState(false); let contextValue = useMemo(() => ({contain, setContain}), [contain, setContain]); - let {getContainer} = useUNSTABLE_PortalContext(); + let {getContainer} = useUNSAFE_PortalContext(); if (!props.portalContainer && getContainer) { portalContainer = getContainer(); } diff --git a/packages/@react-aria/overlays/src/PortalProvider.tsx b/packages/@react-aria/overlays/src/PortalProvider.tsx index 138c07f70f7..8721a00df5f 100644 --- a/packages/@react-aria/overlays/src/PortalProvider.tsx +++ b/packages/@react-aria/overlays/src/PortalProvider.tsx @@ -26,9 +26,9 @@ export const PortalContext = createContext({}); /** * Sets the portal container for all overlay elements rendered by its children. */ -export function UNSTABLE_PortalProvider(props: PortalProviderProps): ReactNode { +export function UNSAFE_PortalProvider(props: PortalProviderProps): ReactNode { let {getContainer} = props; - let {getContainer: ctxGetContainer} = useUNSTABLE_PortalContext(); + let {getContainer: ctxGetContainer} = useUNSAFE_PortalContext(); return ( {props.children} @@ -36,6 +36,6 @@ export function UNSTABLE_PortalProvider(props: PortalProviderProps): ReactNode { ); } -export function useUNSTABLE_PortalContext(): PortalProviderContextValue { +export function useUNSAFE_PortalContext(): PortalProviderContextValue { return useContext(PortalContext) ?? {}; } diff --git a/packages/@react-aria/overlays/src/index.ts b/packages/@react-aria/overlays/src/index.ts index 0f0fc8bbe62..cf37e048e7d 100644 --- a/packages/@react-aria/overlays/src/index.ts +++ b/packages/@react-aria/overlays/src/index.ts @@ -19,7 +19,7 @@ export {ariaHideOutside} from './ariaHideOutside'; export {usePopover} from './usePopover'; export {useModalOverlay} from './useModalOverlay'; export {Overlay, useOverlayFocusContain} from './Overlay'; -export {UNSTABLE_PortalProvider, useUNSTABLE_PortalContext} from './PortalProvider'; +export {UNSAFE_PortalProvider, useUNSAFE_PortalContext} from './PortalProvider'; export type {AriaPositionProps, PositionAria} from './useOverlayPosition'; export type {AriaOverlayProps, OverlayAria} from './useOverlay'; diff --git a/packages/@react-aria/overlays/src/useModal.tsx b/packages/@react-aria/overlays/src/useModal.tsx index 150dedb95d8..5a5e8188b8a 100644 --- a/packages/@react-aria/overlays/src/useModal.tsx +++ b/packages/@react-aria/overlays/src/useModal.tsx @@ -14,7 +14,7 @@ import {DOMAttributes} from '@react-types/shared'; import React, {AriaAttributes, ReactNode, useContext, useEffect, useMemo, useState} from 'react'; import ReactDOM from 'react-dom'; import {useIsSSR} from '@react-aria/ssr'; -import {useUNSTABLE_PortalContext} from './PortalProvider'; +import {useUNSAFE_PortalContext} from './PortalProvider'; export interface ModalProviderProps extends DOMAttributes { children: ReactNode @@ -113,7 +113,7 @@ export interface OverlayContainerProps extends ModalProviderProps { /** * The container element in which the overlay portal will be placed. * @default document.body - * @deprecated - Use a parent UNSTABLE_PortalProvider to set your portal container instead. + * @deprecated - Use a parent UNSAFE_PortalProvider to set your portal container instead. */ portalContainer?: Element } @@ -128,7 +128,7 @@ export interface OverlayContainerProps extends ModalProviderProps { export function OverlayContainer(props: OverlayContainerProps): React.ReactPortal | null { let isSSR = useIsSSR(); let {portalContainer = isSSR ? null : document.body, ...rest} = props; - let {getContainer} = useUNSTABLE_PortalContext(); + let {getContainer} = useUNSAFE_PortalContext(); if (!props.portalContainer && getContainer) { portalContainer = getContainer(); } diff --git a/packages/@react-spectrum/dialog/test/DialogContainer.test.js b/packages/@react-spectrum/dialog/test/DialogContainer.test.js index 972055d8105..4fe0a03c01d 100644 --- a/packages/@react-spectrum/dialog/test/DialogContainer.test.js +++ b/packages/@react-spectrum/dialog/test/DialogContainer.test.js @@ -21,7 +21,7 @@ import {Heading, Text} from '@react-spectrum/text'; import {Provider} from '@react-spectrum/provider'; import React, {useRef, useState} from 'react'; import {theme} from '@react-spectrum/theme-default'; -import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import userEvent from '@testing-library/user-event'; describe('DialogContainer', function () { @@ -254,13 +254,13 @@ describe('DialogContainer', function () { return ( setOpen(true)}>Open dialog - container.current}> + container.current}> setOpen(false)} {...props}> {isOpen && } - +
); diff --git a/packages/@react-spectrum/dialog/test/DialogTrigger.test.js b/packages/@react-spectrum/dialog/test/DialogTrigger.test.js index 15ee46ec88f..f0eb6ff8efc 100644 --- a/packages/@react-spectrum/dialog/test/DialogTrigger.test.js +++ b/packages/@react-spectrum/dialog/test/DialogTrigger.test.js @@ -21,7 +21,7 @@ import {Provider} from '@react-spectrum/provider'; import React from 'react'; import {TextField} from '@react-spectrum/textfield'; import {theme} from '@react-spectrum/theme-default'; -import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import userEvent from '@testing-library/user-event'; @@ -1031,12 +1031,12 @@ describe('DialogTrigger', function () { let {container} = props; return ( - container.current}> + container.current}> Trigger contents - + ); } diff --git a/packages/@react-spectrum/menu/test/MenuTrigger.test.js b/packages/@react-spectrum/menu/test/MenuTrigger.test.js index 2e6752da3b0..827aa598dd1 100644 --- a/packages/@react-spectrum/menu/test/MenuTrigger.test.js +++ b/packages/@react-spectrum/menu/test/MenuTrigger.test.js @@ -29,7 +29,7 @@ import {Link} from '@react-spectrum/link'; import {Provider} from '@react-spectrum/provider'; import React from 'react'; import {theme} from '@react-spectrum/theme-default'; -import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; @@ -735,7 +735,7 @@ describe('MenuTrigger', function () { function InfoMenu(props) { return ( - props.container.current}> + props.container.current}> @@ -744,7 +744,7 @@ describe('MenuTrigger', function () { Three - + ); } diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index c80cb5b26c0..defc24868ea 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -1,4 +1,4 @@ - + import {classNames} from '@react-spectrum/utils'; import {ColumnSize} from '@react-types/table'; import eCursor from 'bundle-text:./cursors/Cur_MoveToRight_9_9.svg'; @@ -16,7 +16,7 @@ import {TableColumnResizeState} from '@react-stately/table'; import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; import {useTableColumnResize} from '@react-aria/table'; import {useTableContext, useVirtualizerContext} from './TableViewBase'; -import {useUNSTABLE_PortalContext} from '@react-aria/overlays'; +import {useUNSAFE_PortalContext} from '@react-aria/overlays'; // @ts-ignore import wCursor from 'bundle-text:./cursors/Cur_MoveToLeft_9_9.svg'; @@ -132,6 +132,6 @@ export const Resizer = React.forwardRef(function Resizer(props: ResizerProps< function CursorOverlay(props) { let {show, children} = props; - let {getContainer} = useUNSTABLE_PortalContext(); + let {getContainer} = useUNSAFE_PortalContext(); return show ? ReactDOM.createPortal(children, getContainer?.() ?? document.body) : null; } diff --git a/packages/@react-spectrum/table/test/TableSizing.test.tsx b/packages/@react-spectrum/table/test/TableSizing.test.tsx index 76fb9c07b67..53260a5ae15 100644 --- a/packages/@react-spectrum/table/test/TableSizing.test.tsx +++ b/packages/@react-spectrum/table/test/TableSizing.test.tsx @@ -26,7 +26,7 @@ import {resizingTests} from '@react-aria/table/test/tableResizingTests'; import {Scale} from '@react-types/provider'; import {setInteractionModality} from '@react-aria/interactions'; import {theme} from '@react-spectrum/theme-default'; -import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import userEvent from '@testing-library/user-event'; let columns = [ @@ -1047,7 +1047,7 @@ describe('TableViewSizing', function () { let Example = (props) => { let container = useRef(null); return ( - container.current}> + container.current}> Foo @@ -1063,7 +1063,7 @@ describe('TableViewSizing', function () {
- + ); }; let customPortalRender = (props) => render(); diff --git a/packages/@react-spectrum/toast/src/Toaster.tsx b/packages/@react-spectrum/toast/src/Toaster.tsx index 356fc97fc4c..0fb2ae7ad2e 100644 --- a/packages/@react-spectrum/toast/src/Toaster.tsx +++ b/packages/@react-spectrum/toast/src/Toaster.tsx @@ -20,7 +20,7 @@ import ReactDOM from 'react-dom'; import toastContainerStyles from './toastContainer.css'; import type {ToastPlacement} from './ToastContainer'; import {ToastState} from '@react-stately/toast'; -import {useUNSTABLE_PortalContext} from '@react-aria/overlays'; +import {useUNSAFE_PortalContext} from '@react-aria/overlays'; interface ToastContainerProps extends AriaToastRegionProps { children: ReactNode, @@ -39,7 +39,7 @@ export function Toaster(props: ToastContainerProps): ReactElement { let ref = useRef(null); let {regionProps} = useToastRegion(props, state, ref); let {focusProps, isFocusVisible} = useFocusRing(); - let {getContainer} = useUNSTABLE_PortalContext(); + let {getContainer} = useUNSAFE_PortalContext(); let [position, placement] = useMemo(() => { let [pos = 'bottom', place = 'center'] = props.placement?.split(' ') || []; diff --git a/packages/@react-spectrum/toast/stories/Toast.stories.tsx b/packages/@react-spectrum/toast/stories/Toast.stories.tsx index 9e7d0cb26e1..a4e1e697151 100644 --- a/packages/@react-spectrum/toast/stories/Toast.stories.tsx +++ b/packages/@react-spectrum/toast/stories/Toast.stories.tsx @@ -21,8 +21,8 @@ import {Heading} from '@react-spectrum/text'; import React, {SyntheticEvent, useEffect, useMemo, useRef, useState} from 'react'; import {SpectrumToastOptions, ToastPlacement} from '../src/ToastContainer'; import {ToastContainer, ToastQueue} from '../'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import {UNSTABLE_createLandmarkController, useLandmark} from '@react-aria/landmark'; -import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; export default { title: 'Toast', @@ -384,11 +384,11 @@ function FullscreenApp(props) { }, []); return (
- ref.current}> + ref.current}> Enter fullscreen {isFullscreen && } - + {!isFullscreen && }
); diff --git a/packages/@react-spectrum/tooltip/test/TooltipTrigger.test.js b/packages/@react-spectrum/tooltip/test/TooltipTrigger.test.js index c6be0220a08..86eb44719c7 100644 --- a/packages/@react-spectrum/tooltip/test/TooltipTrigger.test.js +++ b/packages/@react-spectrum/tooltip/test/TooltipTrigger.test.js @@ -16,7 +16,7 @@ import {Provider} from '@react-spectrum/provider'; import React from 'react'; import {theme} from '@react-spectrum/theme-default'; import {Tooltip, TooltipTrigger} from '../'; -import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import userEvent from '@testing-library/user-event'; // Sync with useTooltipTriggerState.ts @@ -1003,14 +1003,14 @@ describe('TooltipTrigger', function () { describe('portalContainer', () => { function InfoTooltip(props) { return ( - props.container.current}> + props.container.current}>
hello
-
+ ); } @@ -1049,24 +1049,24 @@ describe('TooltipTrigger', function () { describe('portalContainer overwrite', () => { function InfoTooltip(props) { return ( - +
hello
-
+ ); } function App() { let container = React.useRef(null); return ( <> - container.current}> + container.current}>
- + ); } diff --git a/packages/react-aria-components/src/Modal.tsx b/packages/react-aria-components/src/Modal.tsx index 3c8624ac29c..e76520fc10b 100644 --- a/packages/react-aria-components/src/Modal.tsx +++ b/packages/react-aria-components/src/Modal.tsx @@ -30,7 +30,7 @@ export interface ModalOverlayProps extends AriaModalOverlayProps, OverlayTrigger /** * The container element in which the overlay portal will be placed. This may have unknown behavior depending on where it is portalled to. * @default document.body - * @deprecated - Use a parent UNSTABLE_PortalProvider to set your portal container instead. + * @deprecated - Use a parent UNSAFE_PortalProvider to set your portal container instead. */ UNSTABLE_portalContainer?: Element } diff --git a/packages/react-aria-components/src/Popover.tsx b/packages/react-aria-components/src/Popover.tsx index 417b87a934d..33946892d9c 100644 --- a/packages/react-aria-components/src/Popover.tsx +++ b/packages/react-aria-components/src/Popover.tsx @@ -46,7 +46,7 @@ export interface PopoverProps extends Omit, Omit | null>(null); @@ -68,7 +68,7 @@ export const ToastRegion = /*#__PURE__*/ (forwardRef as forwardRefType)(function }); let portalContainer; - let {getContainer} = useUNSTABLE_PortalContext(); + let {getContainer} = useUNSAFE_PortalContext(); if (!isSSR) { portalContainer = document.body; if (getContainer) { diff --git a/packages/react-aria-components/src/Tooltip.tsx b/packages/react-aria-components/src/Tooltip.tsx index 6ec1bacc05f..0e4302da8b0 100644 --- a/packages/react-aria-components/src/Tooltip.tsx +++ b/packages/react-aria-components/src/Tooltip.tsx @@ -41,7 +41,7 @@ export interface TooltipProps extends PositionProps, Pick { @@ -325,9 +325,9 @@ describe('Dialog', () => { let container = useRef(null); return ( <> - container.current}> + container.current}> - +
); diff --git a/packages/react-aria-components/test/Menu.test.tsx b/packages/react-aria-components/test/Menu.test.tsx index 79b84f8f525..fc30b7796e8 100644 --- a/packages/react-aria-components/test/Menu.test.tsx +++ b/packages/react-aria-components/test/Menu.test.tsx @@ -15,7 +15,7 @@ import {AriaMenuTests} from './AriaMenu.test-util'; import {Button, Collection, Header, Heading, Input, Keyboard, Label, Menu, MenuContext, MenuItem, MenuSection, MenuTrigger, Popover, Pressable, Separator, SubmenuTrigger, Text, TextField} from '..'; import React, {useState} from 'react'; import {Selection, SelectionMode} from '@react-types/shared'; -import {UNSTABLE_PortalProvider} from '@react-aria/overlays'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; @@ -1336,7 +1336,7 @@ describe('Menu', () => { describe('portalContainer', () => { function InfoMenu(props) { return ( - props.container.current}> + props.container.current}> -
+
Toasts are portalled here!
@@ -110,6 +110,16 @@ function App() { ``` +```css hidden +@import '../../../react-aria-components/docs/Button.mdx' layer(button); +@import '../../../react-aria-components/docs/Toast.mdx' layer(toast); +@import "@react-aria/example-theme"; + +.react-aria-ToastRegion { + position: unset; +} +``` + ## Contexts The `getContainer` set by the nearest PortalProvider can be accessed by calling `useUNSAFE_PortalContext`. This can be