Skip to content

Commit 7c1da9f

Browse files
Expose UNSTABLE container for RSP overlays (#5723)
* Expose UNSTABLE container for RSP overlays * Add DialogContainer as well --------- Co-authored-by: Daniel Lu <dl1644@gmail.com>
1 parent 026e65f commit 7c1da9f

File tree

12 files changed

+253
-13
lines changed

12 files changed

+253
-13
lines changed

packages/@react-spectrum/dialog/src/DialogContainer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export function DialogContainer(props: SpectrumDialogContainerProps) {
2727
type = 'modal',
2828
onDismiss,
2929
isDismissable,
30-
isKeyboardDismissDisabled
30+
isKeyboardDismissDisabled,
31+
UNSTABLE_portalContainer
3132
} = props;
3233

3334
let childArray = React.Children.toArray(children);
@@ -67,6 +68,7 @@ export function DialogContainer(props: SpectrumDialogContainerProps) {
6768

6869
return (
6970
<Modal
71+
container={UNSTABLE_portalContainer}
7072
state={state}
7173
type={type}
7274
isDismissable={isDismissable}

packages/@react-spectrum/dialog/src/DialogTrigger.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ function DialogTrigger(props: SpectrumDialogTriggerProps) {
2828
targetRef,
2929
isDismissable,
3030
isKeyboardDismissDisabled,
31+
UNSTABLE_portalContainer,
3132
...positionProps
3233
} = props;
3334
if (!Array.isArray(children) || children.length > 2) {
@@ -71,6 +72,7 @@ function DialogTrigger(props: SpectrumDialogTriggerProps) {
7172
return (
7273
<PopoverTrigger
7374
{...positionProps}
75+
UNSTABLE_portalContainer={UNSTABLE_portalContainer}
7476
state={state}
7577
targetRef={targetRef}
7678
trigger={trigger}
@@ -89,6 +91,7 @@ function DialogTrigger(props: SpectrumDialogTriggerProps) {
8991
<Modal
9092
state={state}
9193
isDismissable={type === 'modal' ? isDismissable : false}
94+
container={UNSTABLE_portalContainer}
9295
type={type}
9396
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
9497
onExiting={onExiting}
@@ -100,6 +103,7 @@ function DialogTrigger(props: SpectrumDialogTriggerProps) {
100103
return (
101104
<Tray
102105
state={state}
106+
container={UNSTABLE_portalContainer}
103107
isKeyboardDismissDisabled={isKeyboardDismissDisabled}>
104108
{typeof content === 'function' ? content(state.close) : content}
105109
</Tray>
@@ -143,7 +147,7 @@ DialogTrigger.getCollectionNode = function* (props: SpectrumDialogTriggerProps)
143147
let _DialogTrigger = DialogTrigger as (props: SpectrumDialogTriggerProps) => JSX.Element;
144148
export {_DialogTrigger as DialogTrigger};
145149

146-
function PopoverTrigger({state, targetRef, trigger, content, hideArrow, ...props}) {
150+
function PopoverTrigger({state, targetRef, trigger, content, hideArrow, UNSTABLE_portalContainer, ...props}) {
147151
let triggerRef = useRef<HTMLElement>(null);
148152
let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state, triggerRef);
149153

@@ -155,6 +159,7 @@ function PopoverTrigger({state, targetRef, trigger, content, hideArrow, ...props
155159
let overlay = (
156160
<Popover
157161
{...props}
162+
container={UNSTABLE_portalContainer}
158163
hideArrow={hideArrow}
159164
triggerRef={targetRef || triggerRef}
160165
state={state}>

packages/@react-spectrum/dialog/test/DialogContainer.test.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,18 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {act, fireEvent, render, triggerPress, within} from '@react-spectrum/test-utils';
13+
import {act, fireEvent, pointerMap, render, triggerPress, within} from '@react-spectrum/test-utils';
14+
import {ActionButton, Button} from '@react-spectrum/button';
15+
import {ButtonGroup} from '@react-spectrum/buttongroup';
16+
import {Content, Header} from '@react-spectrum/view';
17+
import {Dialog, DialogContainer, useDialogContainer} from '../src';
1418
import {DialogContainerExample, MenuExample, NestedDialogContainerExample} from '../stories/DialogContainerExamples';
19+
import {Divider} from '@react-spectrum/divider';
20+
import {Heading, Text} from '@react-spectrum/text';
1521
import {Provider} from '@react-spectrum/provider';
16-
import React from 'react';
22+
import React, {useState} from 'react';
1723
import {theme} from '@react-spectrum/theme-default';
24+
import userEvent from '@testing-library/user-event';
1825

1926
describe('DialogContainer', function () {
2027
beforeAll(() => {
@@ -207,4 +214,57 @@ describe('DialogContainer', function () {
207214

208215
expect(document.activeElement).toBe(button);
209216
});
217+
218+
describe('portalContainer', () => {
219+
let user;
220+
beforeAll(() => {
221+
user = userEvent.setup({delay: null, pointerMap});
222+
jest.useFakeTimers();
223+
});
224+
function ExampleDialog(props) {
225+
let container = useDialogContainer();
226+
227+
return (
228+
<Dialog>
229+
<Heading>The Heading</Heading>
230+
<Header>The Header</Header>
231+
<Divider />
232+
<Content><Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin sit amet tristique risus. In sit amet suscipit lorem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In condimentum imperdiet metus non condimentum. Duis eu velit et quam accumsan tempus at id velit. Duis elementum elementum purus, id tempus mauris posuere a. Nunc vestibulum sapien pellentesque lectus commodo ornare.</Text></Content>
233+
{!props.isDismissable &&
234+
<ButtonGroup>
235+
<Button variant="secondary" onPress={container.dismiss}>Cancel</Button>
236+
<Button variant="cta" onPress={container.dismiss}>Confirm</Button>
237+
</ButtonGroup>
238+
}
239+
</Dialog>
240+
);
241+
}
242+
function App(props) {
243+
let [container, setContainer] = useState();
244+
let [isOpen, setOpen] = useState(false);
245+
246+
return (
247+
<Provider theme={theme}>
248+
<ActionButton onPress={() => setOpen(true)}>Open dialog</ActionButton>
249+
<DialogContainer onDismiss={() => setOpen(false)} UNSTABLE_portalContainer={container} {...props}>
250+
{isOpen &&
251+
<ExampleDialog {...props} />
252+
}
253+
</DialogContainer>
254+
<div ref={setContainer} data-testid="custom-container" />
255+
</Provider>
256+
);
257+
}
258+
259+
it('should render the dialog in the portal container', async () => {
260+
let {getByRole, getByTestId} = render(
261+
<App />
262+
);
263+
264+
let button = getByRole('button');
265+
await user.click(button);
266+
267+
expect(getByRole('dialog').closest('[data-testid="custom-container"]')).toBe(getByTestId('custom-container'));
268+
});
269+
});
210270
});

packages/@react-spectrum/dialog/test/DialogTrigger.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,4 +1026,55 @@ describe('DialogTrigger', function () {
10261026
expect(document.activeElement).toBe(innerButton);
10271027
});
10281028

1029+
describe('portalContainer', () => {
1030+
function InfoDialog(props) {
1031+
return (
1032+
<Provider theme={theme}>
1033+
<DialogTrigger type={props.type} UNSTABLE_portalContainer={props.container}>
1034+
<ActionButton>Trigger</ActionButton>
1035+
<Dialog>contents</Dialog>
1036+
</DialogTrigger>
1037+
</Provider>
1038+
);
1039+
}
1040+
function App(props) {
1041+
let [container, setContainer] = React.useState();
1042+
return (
1043+
<>
1044+
<InfoDialog type={props.type} container={container} />
1045+
<div ref={setContainer} data-testid="custom-container" />
1046+
</>
1047+
);
1048+
}
1049+
it('should render the dialog in the portal container', async () => {
1050+
let {getByRole, getByTestId} = render(
1051+
<App />
1052+
);
1053+
1054+
let button = getByRole('button');
1055+
await user.click(button);
1056+
1057+
expect(getByRole('dialog').closest('[data-testid="custom-container"]')).toBe(getByTestId('custom-container'));
1058+
});
1059+
it('should render the tray in the portal container', async () => {
1060+
let {getByRole, getByTestId} = render(
1061+
<App type="tray" />
1062+
);
1063+
1064+
let button = getByRole('button');
1065+
await user.click(button);
1066+
1067+
expect(getByRole('dialog').closest('[data-testid="custom-container"]')).toBe(getByTestId('custom-container'));
1068+
});
1069+
it('should render the popover in the portal container', async () => {
1070+
let {getByRole, getByTestId} = render(
1071+
<App type="popover" />
1072+
);
1073+
1074+
let button = getByRole('button');
1075+
await user.click(button);
1076+
1077+
expect(getByRole('dialog').closest('[data-testid="custom-container"]')).toBe(getByTestId('custom-container'));
1078+
});
1079+
});
10291080
});

packages/@react-spectrum/menu/src/MenuTrigger.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ function MenuTrigger(props: SpectrumMenuTriggerProps, ref: DOMRef<HTMLElement>)
3333
shouldFlip = true,
3434
direction = 'bottom',
3535
closeOnSelect,
36-
trigger = 'press'
36+
trigger = 'press',
37+
UNSTABLE_portalContainer
3738
} = props;
3839

3940
let [menuTrigger, menu] = React.Children.toArray(children);
@@ -74,7 +75,7 @@ function MenuTrigger(props: SpectrumMenuTriggerProps, ref: DOMRef<HTMLElement>)
7475
let overlay;
7576
if (isMobile) {
7677
overlay = (
77-
<Tray state={state} isFixedHeight>
78+
<Tray state={state} isFixedHeight container={UNSTABLE_portalContainer}>
7879
{menu}
7980
</Tray>
8081
);
@@ -83,6 +84,7 @@ function MenuTrigger(props: SpectrumMenuTriggerProps, ref: DOMRef<HTMLElement>)
8384
<Popover
8485
UNSAFE_style={{clipPath: 'unset', overflow: 'visible', filter: 'unset', borderWidth: '0px'}}
8586
state={state}
87+
container={UNSTABLE_portalContainer}
8688
triggerRef={menuTriggerRef}
8789
scrollRef={menuRef}
8890
placement={initialPlacement}

packages/@react-spectrum/menu/test/MenuTrigger.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ describe('MenuTrigger', function () {
7070
let onSelect = jest.fn();
7171
let onSelectionChange = jest.fn();
7272
let user;
73+
let windowSpy;
7374

7475
beforeAll(function () {
7576
user = userEvent.setup({delay: null, pointerMap});
@@ -80,6 +81,10 @@ describe('MenuTrigger', function () {
8081
jest.useFakeTimers();
8182
});
8283

84+
beforeEach(() => {
85+
windowSpy = jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 1024);
86+
});
87+
8388
afterEach(() => {
8489
onOpenChange.mockClear();
8590
onOpen.mockClear();
@@ -1225,4 +1230,51 @@ describe('MenuTrigger', function () {
12251230
});
12261231
});
12271232
});
1233+
1234+
describe('portalContainer', () => {
1235+
function InfoMenu(props) {
1236+
return (
1237+
<Provider theme={theme}>
1238+
<MenuTrigger UNSTABLE_portalContainer={props.container}>
1239+
<ActionButton aria-label="trigger" />
1240+
<Menu>
1241+
<Item key="1">One</Item>
1242+
<Item key="">Two</Item>
1243+
<Item key="3">Three</Item>
1244+
</Menu>
1245+
</MenuTrigger>
1246+
</Provider>
1247+
);
1248+
}
1249+
function App() {
1250+
let [container, setContainer] = React.useState();
1251+
return (
1252+
<>
1253+
<InfoMenu container={container} />
1254+
<div ref={setContainer} data-testid="custom-container" />
1255+
</>
1256+
);
1257+
}
1258+
it('should render the menu in the portal container', async () => {
1259+
let {getByRole, getByTestId} = render(
1260+
<App />
1261+
);
1262+
1263+
let button = getByRole('button');
1264+
await user.click(button);
1265+
1266+
expect(getByRole('menu').closest('[data-testid="custom-container"]')).toBe(getByTestId('custom-container'));
1267+
});
1268+
it('should render the menu tray in the portal container', async () => {
1269+
windowSpy.mockImplementation(() => 700);
1270+
let {getByRole, getByTestId} = render(
1271+
<App />
1272+
);
1273+
1274+
let button = getByRole('button');
1275+
await user.click(button);
1276+
1277+
expect(getByRole('menu').closest('[data-testid="custom-container"]')).toBe(getByTestId('custom-container'));
1278+
});
1279+
});
12281280
});

packages/@react-spectrum/overlays/src/Tray.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {useViewportSize} from '@react-aria/utils';
2525
interface TrayProps extends AriaModalOverlayProps, StyleProps, Omit<OverlayProps, 'nodeRef' | 'shouldContainFocus'> {
2626
children: ReactNode,
2727
state: OverlayTriggerState,
28-
isFixedHeight?: boolean
28+
isFixedHeight?: boolean,
29+
container?: Element
2930
}
3031

3132
interface TrayWrapperProps extends TrayProps {

packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ function TooltipTrigger(props: SpectrumTooltipTriggerProps) {
2929
crossOffset = DEFAULT_CROSS_OFFSET,
3030
isDisabled,
3131
offset = DEFAULT_OFFSET,
32-
trigger: triggerAction
32+
trigger: triggerAction,
33+
UNSTABLE_portalContainer
3334
} = props;
3435

3536
let [trigger, tooltip] = React.Children.toArray(children) as [ReactElement, ReactElement];
@@ -88,7 +89,7 @@ function TooltipTrigger(props: SpectrumTooltipTriggerProps) {
8889
arrowRef: arrowRef,
8990
...tooltipProps
9091
}}>
91-
<Overlay isOpen={state.isOpen} nodeRef={overlayRef}>
92+
<Overlay isOpen={state.isOpen} nodeRef={overlayRef} container={UNSTABLE_portalContainer}>
9293
{tooltip}
9394
</Overlay>
9495
</TooltipContext.Provider>

packages/@react-spectrum/tooltip/test/TooltipTrigger.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,4 +961,46 @@ describe('TooltipTrigger', function () {
961961
expect(queryByRole('tooltip')).toBeNull();
962962
});
963963
});
964+
965+
describe('portalContainer', () => {
966+
function InfoTooltip(props) {
967+
return (
968+
<TooltipTrigger UNSTABLE_portalContainer={props.container}>
969+
<ActionButton aria-label="trigger" />
970+
<Tooltip>
971+
<div data-testid="content">hello</div>
972+
</Tooltip>
973+
</TooltipTrigger>
974+
);
975+
}
976+
function App() {
977+
let [container, setContainer] = React.useState();
978+
return (
979+
<>
980+
<InfoTooltip container={container} />
981+
<div ref={setContainer} data-testid="custom-container" />
982+
</>
983+
);
984+
}
985+
it('should render the tooltip in the portal container', async () => {
986+
let {getByRole, getByTestId} = render(
987+
<Provider theme={theme}>
988+
<App />
989+
</Provider>
990+
);
991+
992+
let button = getByRole('button');
993+
act(() => {
994+
button.focus();
995+
});
996+
997+
expect(getByTestId('content').closest('[data-testid="custom-container"]')).toBe(getByTestId('custom-container'));
998+
act(() => {
999+
button.blur();
1000+
});
1001+
act(() => {
1002+
jest.advanceTimersByTime(CLOSE_TIME);
1003+
});
1004+
});
1005+
});
9641006
});

0 commit comments

Comments
 (0)