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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/core/src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export interface IAlertProps extends IOverlayLifecycleProps, IProps {
*/
transitionDuration?: number;

/**
* The container element into which the overlay renders its contents, when `usePortal` is `true`.
* This prop is ignored if `usePortal` is `false`.
* @default document.body
*/
portalContainer?: HTMLElement;

/**
* Handler invoked when the alert is canceled. Alerts can be **canceled** in the following ways:
* - clicking the cancel button (if `cancelButtonText` is defined)
Expand Down Expand Up @@ -130,6 +137,7 @@ export class Alert extends AbstractPureComponent<IAlertProps, {}> {
canEscapeKeyClose={canEscapeKeyCancel}
canOutsideClickClose={canOutsideClickCancel}
onClose={this.handleCancel}
portalContainer={this.props.portalContainer}
>
<div className={Classes.ALERT_BODY}>
<Icon icon={icon} iconSize={40} intent={intent} />
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/components/overlay/overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export interface IOverlayableProps extends IOverlayLifecycleProps {

/**
* Whether the overlay should be wrapped in a `Portal`, which renders its contents in a new
* element attached to `document.body`.
* element attached to `portalContainer` prop.
*
* This prop essentially determines which element is covered by the backdrop: if `false`,
* then only its parent is covered; otherwise, the entire page is covered (because the parent
Expand All @@ -69,6 +69,13 @@ export interface IOverlayableProps extends IOverlayLifecycleProps {
*/
usePortal?: boolean;

/**
* The container element into which the overlay renders its contents, when `usePortal` is `true`.
* This prop is ignored if `usePortal` is `false`.
* @default document.body
*/
portalContainer?: HTMLElement;

/**
* A callback that is invoked when user interaction causes the overlay to close, such as
* clicking on the overlay or pressing the `esc` key (if enabled).
Expand Down Expand Up @@ -185,7 +192,7 @@ export class Overlay extends React.PureComponent<IOverlayProps, IOverlayState> {
return null;
}

const { children, className, usePortal, isOpen } = this.props;
const { children, className, usePortal, portalContainer, isOpen } = this.props;

// TransitionGroup types require single array of children; does not support nested arrays.
// So we must collapse backdrop and children into one array, and every item must be wrapped in a
Expand Down Expand Up @@ -214,7 +221,7 @@ export class Overlay extends React.PureComponent<IOverlayProps, IOverlayState> {
</TransitionGroup>
);
if (usePortal) {
return <Portal>{transitionGroup}</Portal>;
return <Portal container={portalContainer}>{transitionGroup}</Portal>;
} else {
return transitionGroup;
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export class Popover extends AbstractPureComponent<IPopoverProps, IPopoverState>
transitionDuration={this.props.transitionDuration}
transitionName={Classes.POPOVER}
usePortal={this.props.usePortal}
portalContainer={this.props.portalContainer}
>
<Popper
innerRef={this.refHandlers.popover}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/popover/popoverSharedProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export interface IPopoverSharedProps extends IOverlayableProps, IProps {

/**
* Whether the popover should be rendered inside a `Portal` attached to
* `document.body`.
* `portalContainer` prop.
*
* Rendering content inside a `Portal` allows the popover content to escape
* the physical bounds of its parent while still being positioned correctly
Expand Down
14 changes: 13 additions & 1 deletion packages/core/src/components/portal/portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export interface IPortalProps extends IProps {
* Callback invoked when the children of this `Portal` have been added to the DOM.
*/
onChildrenMount?: () => void;

/**
* The HTML element that children will be mounted to.
* @default document.body
*/
container?: HTMLElement;
}

export interface IPortalState {
Expand Down Expand Up @@ -49,6 +55,9 @@ const REACT_CONTEXT_TYPES: ValidationMap<IPortalContext> = {
export class Portal extends React.Component<IPortalProps, IPortalState> {
public static displayName = `${DISPLAYNAME_PREFIX}.Portal`;
public static contextTypes = REACT_CONTEXT_TYPES;
public static defaultProps: IPortalProps = {
container: typeof document !== "undefined" ? document.body : null,
};

public context: IPortalContext;
public state: IPortalState = { hasMounted: false };
Expand All @@ -67,8 +76,11 @@ export class Portal extends React.Component<IPortalProps, IPortalState> {
}

public componentDidMount() {
if (!this.props.container) {
return;
}
this.portalElement = this.createContainerElement();
document.body.appendChild(this.portalElement);
this.props.container.appendChild(this.portalElement);
this.setState({ hasMounted: true }, this.props.onChildrenMount);
if (cannotCreatePortal) {
this.unstableRenderNoPortal();
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export class Tooltip extends React.PureComponent<ITooltipProps, {}> {
interactionKind={PopoverInteractionKind.HOVER_TARGET_ONLY}
lazy={true}
popoverClassName={classes}
portalContainer={this.props.portalContainer}
>
{children}
</Popover>
Expand Down
13 changes: 13 additions & 0 deletions packages/core/test/alert/alertTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ describe("<Alert>", () => {
assert.lengthOf(wrapper.find(`.${Classes.ALERT_FOOTER}`), 1);
});

it("renders contents to specified container correctly", () => {
const container = document.createElement("div");
document.body.appendChild(container);
mount(
<Alert isOpen={true} portalContainer={container}>
<p>Are you sure you want to delete this file?</p>
<p>There is no going back.</p>
</Alert>,
);
assert.lengthOf(container.getElementsByClassName(Classes.ALERT), 1);
document.body.removeChild(container);
});

it("renders the icon correctly", () => {
const wrapper = shallow(
<Alert icon="warning-sign" isOpen={true} confirmButtonText="Delete">
Expand Down
12 changes: 12 additions & 0 deletions packages/core/test/dialog/dialogTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ describe("<Dialog>", () => {
});
});

it("renders contents to specified container correctly", () => {
const container = document.createElement("div");
document.body.appendChild(container);
mount(
<Dialog isOpen={true} portalContainer={container}>
{createDialogContents()}
</Dialog>,
);
assert.lengthOf(container.getElementsByClassName(Classes.DIALOG), 1, `missing ${Classes.DIALOG}`);
document.body.removeChild(container);
});

it("attempts to close when overlay backdrop element is moused down", () => {
const onClose = spy();
const dialog = mount(
Expand Down
13 changes: 13 additions & 0 deletions packages/core/test/overlay/overlayTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ describe("<Overlay>", () => {
overlay.unmount();
});

it("renders contents to specified container correctly", () => {
const CLASS_TO_TEST = "bp-test-content";
const container = document.createElement("div");
document.body.appendChild(container);
mountWrapper(
<Overlay isOpen={true} portalContainer={container}>
<p className={CLASS_TO_TEST}>test</p>
</Overlay>,
);
assert.lengthOf(container.getElementsByClassName(CLASS_TO_TEST), 1);
document.body.removeChild(container);
});

it("renders Portal after first opened", () => {
mountWrapper(<Overlay isOpen={false}>{createOverlayContents()}</Overlay>);
assert.lengthOf(wrapper.find(Portal), 0, "unexpected Portal");
Expand Down
8 changes: 8 additions & 0 deletions packages/core/test/popover/popoverTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ describe("<Popover>", () => {
assert.lengthOf(wrapper.find(Portal), 1);
});

it("renders to specified container correctly", () => {
const container = document.createElement("div");
document.body.appendChild(container);
wrapper = renderPopover({ isOpen: true, usePortal: true, portalContainer: container });
assert.lengthOf(container.getElementsByClassName(Classes.POPOVER_CONTENT), 1);
document.body.removeChild(container);
});

it("does not render Portal when usePortal=false", () => {
wrapper = renderPopover({ isOpen: true, usePortal: false });
assert.lengthOf(wrapper.find(Portal), 0);
Expand Down
13 changes: 13 additions & 0 deletions packages/core/test/portal/portalTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ describe("<Portal>", () => {
assert.lengthOf(document.getElementsByClassName(CLASS_TO_TEST), 1);
});

it("attaches contents to specified container", () => {
const CLASS_TO_TEST = "bp-test-content";
const container = document.createElement("div");
document.body.appendChild(container);
portal = mount(
<Portal container={container}>
<p className={CLASS_TO_TEST}>test</p>
</Portal>,
);
assert.lengthOf(container.getElementsByClassName(CLASS_TO_TEST), 1);
document.body.removeChild(container);
});

it("propagates className to portal element", () => {
const CLASS_TO_TEST = "bp-test-klass";
portal = mount(
Expand Down