Skip to content

Commit

Permalink
[Labs/Popover2] Ease Popover2 migration with prop shims (#1581)
Browse files Browse the repository at this point in the history
  • Loading branch information
brieb authored and adidahiya committed Sep 27, 2017
1 parent 5626890 commit d90adf5
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 15 deletions.
13 changes: 13 additions & 0 deletions packages/labs/src/common/errors.ts
@@ -0,0 +1,13 @@
/*
* Copyright 2015 Palantir Technologies, Inc. All rights reserved.
* Licensed under the BSD-3 License as modified (the “License”); you may obtain a copy
* of the license at https://github.com/palantir/blueprint/blob/master/LICENSE
* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/

const ns = "[Blueprint]";
const deprec = `${ns} DEPRECATION:`;

export const POPOVER2_WARN_DEPRECATED_IS_DISABLED = `${deprec} <Popover2> isDisabled is deprecated. Use disabled.`;
export const POPOVER2_WARN_DEPRECATED_IS_MODAL = `${deprec} <Popover2> isModal is deprecated. Use hasBackdrop.`;
export const POPOVER2_WARN_DEPRECATED_POSITION = `${deprec} <Popover2> position is deprecated. Use placement.`;
106 changes: 95 additions & 11 deletions packages/labs/src/components/popover/popover2.tsx
Expand Up @@ -20,11 +20,14 @@ import {
IProps,
Overlay,
PopoverInteractionKind,
Position,
Utils,
} from "@blueprintjs/core";

import * as Errors from "../../common/errors";
import { Tooltip2 } from "../tooltip/tooltip2";
import { getArrowAngle, PopoverArrow } from "./arrow";
import { positionToPlacement } from "./popoverMigrationUtils";
import { arrowOffsetModifier, getTransformOrigin } from "./popperUtils";

export interface IPopover2Props extends IOverlayableProps, IProps {
Expand Down Expand Up @@ -79,6 +82,12 @@ export interface IPopover2Props extends IOverlayableProps, IProps {
*/
disabled?: boolean;

/**
* Prevents the popover from appearing when `true`.
* @deprecated use `disabled`
*/
isDisabled?: boolean;

/**
* Enables an invisible overlay beneath the popover that captures clicks and prevents
* interaction with the rest of the document until the popover is closed.
Expand All @@ -88,6 +97,13 @@ export interface IPopover2Props extends IOverlayableProps, IProps {
*/
hasBackdrop?: boolean;

/**
* Enables an invisible overlay beneath the popover that captures clicks and prevents
* interaction with the rest of the document until the popover is closed.
* @deprecated use `hasBackdrop`
*/
isModal?: boolean;

/**
* Whether the popover is visible. Passing this prop puts the popover in
* controlled mode, where the only way to change visibility is by updating this property.
Expand Down Expand Up @@ -122,6 +138,12 @@ export interface IPopover2Props extends IOverlayableProps, IProps {
*/
openOnTargetFocus?: boolean;

/**
* The position (relative to the target) at which the popover should appear.
* @deprecated use `placement`
*/
position?: Position;

/**
* A space-delimited string of class names that are applied to the popover (but not the target).
*/
Expand Down Expand Up @@ -174,6 +196,15 @@ export interface IPopover2State {
transformOrigin?: string;
isOpen?: boolean;
hasDarkParent?: boolean;

/** Migrated `disabled` value that considers the `disabled` and `isDisabled` props. */
disabled?: boolean;

/** Migrated `hasBackdrop` value that considers the `hasBackdrop` and `isModal` props. */
hasBackdrop?: boolean;

/** Migrated `placement` value that considers the `placement` and `position` props. */
placement?: Placement;
}

@PureRender
Expand All @@ -182,8 +213,6 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>

public static defaultProps: IPopover2Props = {
defaultIsOpen: false,
disabled: false,
hasBackdrop: false,
hoverCloseDelay: 300,
hoverOpenDelay: 150,
inheritDarkTheme: true,
Expand All @@ -192,7 +221,6 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>
minimal: false,
modifiers: {},
openOnTargetFocus: true,
placement: "auto",
rootElementTag: "span",
transitionDuration: 300,
};
Expand All @@ -218,20 +246,24 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>
public constructor(props?: IPopover2Props, context?: any) {
super(props, context);

let isOpen = props.defaultIsOpen && !props.disabled;
const disabled = getDisabled(props);
let isOpen = props.defaultIsOpen && !disabled;
if (props.isOpen != null) {
isOpen = props.isOpen;
}

this.state = {
disabled,
hasBackdrop: getHasBackdrop(props),
hasDarkParent: false,
isOpen,
placement: getPlacement(props),
};
}

public render() {
const { className } = this.props;
const { isOpen } = this.state;
const { isOpen, disabled, hasBackdrop } = this.state;

let targetProps: React.HTMLAttributes<HTMLElement>;
if (this.isHoverInteractionKind()) {
Expand Down Expand Up @@ -264,7 +296,7 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>
});

const isContentEmpty = children.content == null;
if (isContentEmpty && !this.props.disabled && isOpen !== false && !Utils.isNodeEnv("production")) {
if (isContentEmpty && !disabled && isOpen !== false && !Utils.isNodeEnv("production")) {
console.warn("[Blueprint] Disabling <Popover2> with empty/whitespace content...");
}

Expand All @@ -282,7 +314,7 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>
className={this.props.portalClassName}
didOpen={this.handleContentMount}
enforceFocus={this.props.enforceFocus}
hasBackdrop={this.props.hasBackdrop}
hasBackdrop={hasBackdrop}
inline={this.props.inline}
isOpen={isOpen && !isContentEmpty}
onClose={this.handleOverlayClose}
Expand All @@ -302,14 +334,22 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>
public componentWillReceiveProps(nextProps: IPopover2Props) {
super.componentWillReceiveProps(nextProps);

if (nextProps.isOpen == null && nextProps.disabled && !this.props.disabled) {
const nextDisabled = getDisabled(nextProps);

if (nextProps.isOpen == null && nextDisabled && !this.state.disabled) {
// ok to use setOpenState here because disabled and isOpen are mutex.
this.setOpenState(false);
} else if (nextProps.isOpen !== this.props.isOpen) {
// propagate isOpen prop directly to state, circumventing onInteraction callback
// (which would be invoked if this went through setOpenState)
this.setState({ isOpen: nextProps.isOpen });
}

this.setState({
disabled: nextDisabled,
hasBackdrop: getHasBackdrop(nextProps),
placement: getPlacement(nextProps),
});
}

public componentWillUpdate(_: IPopover2Props, nextState: IPopover2State) {
Expand All @@ -329,6 +369,18 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>
super.componentWillUnmount();
}

protected validateProps(props: IPopover2Props & { children?: React.ReactNode }) {
if (props.isDisabled !== undefined) {
console.warn(Errors.POPOVER2_WARN_DEPRECATED_IS_DISABLED);
}
if (props.isModal !== undefined) {
console.warn(Errors.POPOVER2_WARN_DEPRECATED_IS_MODAL);
}
if (props.position !== undefined) {
console.warn(Errors.POPOVER2_WARN_DEPRECATED_POSITION);
}
}

private updateDarkParent() {
if (!this.props.inline) {
const hasDarkParent = this.targetElement.closest(`.${Classes.DARK}`) != null;
Expand All @@ -338,6 +390,8 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>

private renderPopper(content: JSX.Element) {
const { inline, interactionKind, modifiers } = this.props;
const { placement } = this.state;

const popoverHandlers: React.HTMLAttributes<HTMLDivElement> = {
// always check popover clicks for dismiss class
onClick: this.handlePopoverClick,
Expand Down Expand Up @@ -378,7 +432,7 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>
};

return (
<Popper className={Classes.TRANSITION_CONTAINER} placement={this.props.placement} modifiers={allModifiers}>
<Popper className={Classes.TRANSITION_CONTAINER} placement={placement} modifiers={allModifiers}>
<div
className={popoverClasses}
ref={this.refHandlers.popover}
Expand Down Expand Up @@ -440,7 +494,7 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>
!this.props.openOnTargetFocus
) {
this.handleMouseLeave(e);
} else if (!this.props.disabled) {
} else if (!this.state.disabled) {
// only begin opening popover when it is enabled
this.setOpenState(true, e, this.props.hoverOpenDelay);
}
Expand Down Expand Up @@ -470,7 +524,7 @@ export class Popover2 extends AbstractComponent<IPopover2Props, IPopover2State>

private handleTargetClick = (e: React.MouseEvent<HTMLElement>) => {
// ensure click did not originate from within inline popover before closing
if (!this.props.disabled && !this.isElementInPopover(e.target as HTMLElement)) {
if (!this.state.disabled && !this.isElementInPopover(e.target as HTMLElement)) {
if (this.props.isOpen == null) {
this.setState(prevState => ({ isOpen: !prevState.isOpen }));
} else {
Expand Down Expand Up @@ -535,3 +589,33 @@ function ensureElement(child: React.ReactChild | undefined) {
return child;
}
}

function getDisabled(props: IPopover2Props): boolean {
if (props.disabled !== undefined) {
return props.disabled;
} else if (props.isDisabled !== undefined) {
return props.isDisabled;
} else {
return false;
}
}

function getHasBackdrop(props: IPopover2Props): boolean {
if (props.hasBackdrop !== undefined) {
return props.hasBackdrop;
} else if (props.isModal !== undefined) {
return props.isModal;
} else {
return false;
}
}

function getPlacement(props: IPopover2Props): Placement {
if (props.placement !== undefined) {
return props.placement;
} else if (props.position !== undefined) {
return positionToPlacement(props.position);
} else {
return "auto";
}
}
48 changes: 48 additions & 0 deletions packages/labs/src/components/popover/popoverMigrationUtils.ts
@@ -0,0 +1,48 @@
/*
* Copyright 2017 Palantir Technologies, Inc. All rights reserved.
* Licensed under the BSD-3 License as modified (the “License”); you may obtain a copy
* of the license at https://github.com/palantir/blueprint/blob/master/LICENSE
* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/

import { Position } from "@blueprintjs/core";
import { Placement } from "./popover2";

/**
* Convert a position to a placement.
* @param position the position to convert
*/
export function positionToPlacement(position: Position): Placement {
switch (position) {
case Position.TOP_LEFT:
return "top-start";
case Position.TOP:
return "top";
case Position.TOP_RIGHT:
return "top-end";
case Position.RIGHT_TOP:
return "right-start";
case Position.RIGHT:
return "right";
case Position.RIGHT_BOTTOM:
return "right-end";
case Position.BOTTOM_RIGHT:
return "bottom-end";
case Position.BOTTOM:
return "bottom";
case Position.BOTTOM_LEFT:
return "bottom-start";
case Position.LEFT_BOTTOM:
return "left-end";
case Position.LEFT:
return "left";
case Position.LEFT_TOP:
return "left-start";
default:
return assertNever(position);
}
}

function assertNever(x: never): never {
throw new Error("Unexpected position: " + x);
}

1 comment on commit d90adf5

@blueprint-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Labs/Popover2] Ease Popover2 migration with prop shims (#1581)

Preview: documentation
Coverage: core | datetime

Please sign in to comment.