Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from Sep 27, 2017
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}