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

[core] refactor(Popover): improve popover captureDismiss mechanism #4328

Merged
merged 2 commits into from
Sep 21, 2020
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
1 change: 1 addition & 0 deletions packages/core/src/common/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export const PANEL_STACK_VIEW = `${PANEL_STACK}-view`;
export const POPOVER = `${NS}-popover`;
export const POPOVER_ARROW = `${POPOVER}-arrow`;
export const POPOVER_BACKDROP = `${POPOVER}-backdrop`;
export const POPOVER_CAPTURING_DISMISS = `${POPOVER}-capturing-dismiss`;
export const POPOVER_CONTENT = `${POPOVER}-content`;
export const POPOVER_CONTENT_SIZING = `${POPOVER_CONTENT}-sizing`;
export const POPOVER_DISMISS = `${POPOVER}-dismiss`;
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/common/utils/reactUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,10 @@ export function isElementOfType<P = {}>(
element.type.displayName === ComponentType.displayName
);
}

/**
* Returns React.createRef if it's available, or a ref-like object if not.
*/
export function createReactRef<T>() {
return typeof React.createRef !== "undefined" ? React.createRef<T>() : { current: null };
}
17 changes: 12 additions & 5 deletions packages/core/src/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export interface IPopoverState {
@polyfill
export class Popover extends AbstractPureComponent2<IPopoverProps, IPopoverState> {
public static displayName = `${DISPLAYNAME_PREFIX}.Popover`;
private popoverRef = Utils.createReactRef<HTMLDivElement>();

public static defaultProps: IPopoverProps = {
boundary: "scrollParent",
Expand Down Expand Up @@ -312,14 +313,20 @@ export class Popover extends AbstractPureComponent2<IPopoverProps, IPopoverState
{
[Classes.DARK]: this.props.inheritDarkTheme && this.state.hasDarkParent,
[Classes.MINIMAL]: this.props.minimal,
[Classes.POPOVER_CAPTURING_DISMISS]: this.props.captureDismiss,
},
this.props.popoverClassName,
);

return (
<div className={Classes.TRANSITION_CONTAINER} ref={popperProps.ref} style={popperProps.style}>
<ResizeSensor onResize={this.reposition}>
<div className={popoverClasses} style={{ transformOrigin }} {...popoverHandlers}>
<div
className={popoverClasses}
style={{ transformOrigin }}
ref={this.popoverRef}
{...popoverHandlers}
>
{this.isArrowEnabled() && (
<PopoverArrow arrowProps={popperProps.arrowProps} placement={popperProps.placement} />
)}
Expand Down Expand Up @@ -495,15 +502,15 @@ export class Popover extends AbstractPureComponent2<IPopoverProps, IPopoverState

private handlePopoverClick = (e: React.MouseEvent<HTMLElement>) => {
const eventTarget = e.target as HTMLElement;
const eventPopover = eventTarget.closest(`.${Classes.POPOVER}`);
const isEventFromSelf = eventPopover === this.popoverRef.current;
const isEventPopoverCapturing = eventPopover.classList.contains(Classes.POPOVER_CAPTURING_DISMISS);
// an OVERRIDE inside a DISMISS does not dismiss, and a DISMISS inside an OVERRIDE will dismiss.
const dismissElement = eventTarget.closest(`.${Classes.POPOVER_DISMISS}, .${Classes.POPOVER_DISMISS_OVERRIDE}`);
const shouldDismiss = dismissElement != null && dismissElement.classList.contains(Classes.POPOVER_DISMISS);
const isDisabled = eventTarget.closest(`:disabled, .${Classes.DISABLED}`) != null;
if (shouldDismiss && !isDisabled && !e.isDefaultPrevented()) {
if (shouldDismiss && !isDisabled && (!isEventPopoverCapturing || isEventFromSelf)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

is isDisabled still relevant here? Not sure if the check was to avoid preventDefault on an event that should not have been touched, or if we genuinely didn't want to close the popover if its target is disabled

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm pretty sure it's redundant, since a disabled element can't emit a click event. However I don't know if any blueprint component only uses disabled class but doesn't respect that rule (I checked anchor button and they do respect not triggering onClick) if there's no element which applies disabled class and allow clicking I can remove that too.

this.setOpenState(false, e);
if (this.props.captureDismiss) {
e.preventDefault();
}
}
};

Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/components/popover/popoverSharedProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,9 @@ export interface IPopoverSharedProps extends IOverlayableProps, IProps {
boundary?: PopperBoundary;

/**
* When enabled, `preventDefault()` is invoked on `click` events that close
* this popover, which will prevent those clicks from closing outer
* popovers. When disabled, clicking inside a `Classes.POPOVER_DISMISS`
* element will close the parent popover.
* When enabled, clicking inside a `Classes.POPOVER_DISMISS`
* will only close the parent popover and not outer popovers.
* When disabled, both parent outer popovers will be closed.
*
* See http://blueprintjs.com/docs/#core/components/popover.closing-on-click
* @default false
Expand Down