Skip to content

Commit

Permalink
[popover2] feat: new props matchTargetWidth, modifiersCustom (#5307)
Browse files Browse the repository at this point in the history
  • Loading branch information
adidahiya authored May 16, 2022
1 parent 2ec174a commit a6453da
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ const INTERACTION_KINDS = [

export interface IPopover2ExampleState {
boundary?: "scrollParent" | "body" | "clippingParents";
buttonText: string;
canEscapeKeyClose?: boolean;
exampleIndex?: number;
hasBackdrop?: boolean;
inheritDarkTheme?: boolean;
interactionKind?: Popover2InteractionKind;
isControlled: boolean;
isOpen?: boolean;
matchTargetWidth: boolean;
minimal?: boolean;
modifiers?: Popover2SharedProps<HTMLElement>["modifiers"];
placement?: Placement;
Expand All @@ -82,13 +84,15 @@ export class Popover2Example extends React.PureComponent<IExampleProps, IPopover

public state: IPopover2ExampleState = {
boundary: "scrollParent",
buttonText: "Popover target",
canEscapeKeyClose: true,
exampleIndex: 0,
hasBackdrop: false,
inheritDarkTheme: true,
interactionKind: "click",
isControlled: false,
isOpen: false,
matchTargetWidth: false,
minimal: false,
modifiers: {
arrow: { enabled: true },
Expand Down Expand Up @@ -125,6 +129,13 @@ export class Popover2Example extends React.PureComponent<IExampleProps, IPopover

private toggleIsOpen = handleBooleanChange(isOpen => this.setState({ isOpen }));

private toggleMatchTargetWidth = handleBooleanChange(matchTargetWidth => {
this.setState({
buttonText: matchTargetWidth ? "(Slightly wider) popover target" : "Popover target",
matchTargetWidth,
});
});

private toggleMinimal = handleBooleanChange(minimal => this.setState({ minimal }));

private toggleUsePortal = handleBooleanChange(usePortal => {
Expand All @@ -150,8 +161,7 @@ export class Popover2Example extends React.PureComponent<IExampleProps, IPopover
}

public render() {
const { boundary, exampleIndex, sliderValue, ...popoverProps } = this.state;
console.info(popoverProps);
const { boundary, buttonText, exampleIndex, sliderValue, ...popoverProps } = this.state;
return (
<Example options={this.renderOptions()} {...this.props}>
<div className="docs-popover2-example-scroll" ref={this.centerScroll}>
Expand All @@ -170,7 +180,7 @@ export class Popover2Example extends React.PureComponent<IExampleProps, IPopover
isOpen={this.state.isControlled ? this.state.isOpen : undefined}
content={this.getContents(exampleIndex)}
>
<Button intent={Intent.PRIMARY} text="Popover target" tabIndex={0} />
<Button intent={Intent.PRIMARY} text={buttonText} tabIndex={0} />
</Popover2>
<p>
Scroll around this container to experiment
Expand All @@ -183,7 +193,7 @@ export class Popover2Example extends React.PureComponent<IExampleProps, IPopover
}

private renderOptions() {
const { modifiers, placement } = this.state;
const { matchTargetWidth, modifiers, placement } = this.state;
const { arrow, flip, preventOverflow } = modifiers;

// popper.js requires this modiifer for "auto" placement
Expand Down Expand Up @@ -265,6 +275,8 @@ export class Popover2Example extends React.PureComponent<IExampleProps, IPopover
<option value="window">window</option>
</HTMLSelect>
</Switch>
<Switch checked={matchTargetWidth} label="Match target width" onChange={this.toggleMatchTargetWidth} />

<Label>
<AnchorButton
href={POPPER_DOCS_URL}
Expand Down
6 changes: 6 additions & 0 deletions packages/popover2/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ module.exports = function (config) {
const baseConfig = createKarmaConfig({
dirname: __dirname,
coverageExcludes: ["src/popover2Arrow.tsx"],
coverageOverrides: {
"src/customModifiers.ts": {
lines: 66,
statements: 66,
},
},
});
config.set(baseConfig);
config.set({
Expand Down
6 changes: 6 additions & 0 deletions packages/popover2/src/_popover2.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ $dark-popover2-arrow-box-shadow:
}
}

&.#{$ns}-popover2-match-target-width {
// parent element will have width styles set by our custom popper.js modifier,
// so we should fill that container's width
width: 100%;
}

&.#{$ns}-dark,
.#{$ns}-dark & {
@include popover2-appearance(
Expand Down
1 change: 1 addition & 0 deletions packages/popover2/src/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const POPOVER2_CONTENT_PLACEMENT = `${POPOVER2}-placement`;
export const POPOVER2_CONTENT_SIZING = `${POPOVER2_CONTENT}-sizing`;
export const POPOVER2_DISMISS = `${POPOVER2}-dismiss`;
export const POPOVER2_DISMISS_OVERRIDE = `${POPOVER2_DISMISS}-override`;
export const POPOVER2_MATCH_TARGET_WIDTH = `${POPOVER2}-match-target-width`;
export const POPOVER2_OPEN = `${POPOVER2}-open`;
export const POPOVER2_POPPER_ESCAPED = `${POPOVER2}-popper-escaped`;
export const POPOVER2_REFERENCE_HIDDEN = `${POPOVER2}-reference-hidden`;
Expand Down
39 changes: 39 additions & 0 deletions packages/popover2/src/customModifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2022 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @fileoverview custom Popper.js modifiers
* @see https://popper.js.org/docs/v2/modifiers/#custom-modifiers
*/

import { Modifier } from "@popperjs/core";

// tslint:disable object-literal-sort-keys

// adapted from https://popper.js.org/docs/v2/modifiers/community-modifiers/
export const matchReferenceWidthModifier: Modifier<"matchReferenceWidth", any> = {
enabled: true,
name: "matchReferenceWidth",
phase: "beforeWrite",
requires: ["computeStyles"],
fn: ({ state }) => {
state.styles.popper.width = `${state.rects.reference.width}px`;
},
effect: ({ state }) => {
const referenceWidth = state.elements.reference.getBoundingClientRect().width;
state.elements.popper.style.width = `${referenceWidth}px`;
},
};
9 changes: 6 additions & 3 deletions packages/popover2/src/popover2.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,15 @@ automatically by enabling the modifiers `flip` and `preventOverflow`.

@### Modifiers

Modifiers allow you customize Popper.js's positioning behavior. `Popover2` configures several of Popper.js's built-in modifiers
Modifiers allow us to customize Popper.js's positioning behavior. `Popover2` configures several of Popper.js's built-in modifiers
to handle things such as flipping, preventing overflow from a boundary element, and positioning the arrow.

You may override these default modifiers with the `modifiers` prop, which is an object with key-value pairs representing the
You may override the default modifiers with the `modifiers` prop, which is an object with key-value pairs representing the
modifier name and its options object, respectively. See the [Popper.js modifiers docs page](https://popper.js.org/docs/v2/modifiers/)
for more info. It is not currently possible to add your own custom modifiers through `Popover2`.
for more info.

You may also add custom modifiers using the `modifiersCustom` prop. See the
[Popper.js custom modifiers documentation](https://popper.js.org/docs/v2/modifiers/#custom-modifiers) for more info.

@### Controlled mode

Expand Down
21 changes: 17 additions & 4 deletions packages/popover2/src/popover2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { State as PopperState, PositioningStrategy } from "@popperjs/core";
import classNames from "classnames";
import * as React from "react";
import { Manager, Popper, PopperChildrenProps, Reference, ReferenceChildrenProps, StrictModifier } from "react-popper";
import { Manager, Modifier, Popper, PopperChildrenProps, Reference, ReferenceChildrenProps } from "react-popper";

import {
AbstractPureComponent2,
Expand All @@ -32,6 +32,7 @@ import {
} from "@blueprintjs/core";

import * as Classes from "./classes";
import { matchReferenceWidthModifier } from "./customModifiers";
import * as Errors from "./errors";
import { Popover2Arrow, POPOVER_ARROW_SVG_SIZE } from "./popover2Arrow";
import { positionToPlacement } from "./popover2PlacementUtils";
Expand Down Expand Up @@ -132,6 +133,7 @@ export class Popover2<T> extends AbstractPureComponent2<Popover2Props<T>, IPopov
hoverOpenDelay: 150,
inheritDarkTheme: true,
interactionKind: Popover2InteractionKind.CLICK,
matchTargetWidth: false,
minimal: false,
openOnTargetFocus: true,
// N.B. we don't set a default for `placement` or `position` here because that would trigger
Expand Down Expand Up @@ -416,6 +418,7 @@ export class Popover2<T> extends AbstractPureComponent2<Popover2Props<T>, IPopov
[CoreClasses.DARK]: this.props.inheritDarkTheme && this.state.hasDarkParent,
[CoreClasses.MINIMAL]: this.props.minimal,
[Classes.POPOVER2_CAPTURING_DISMISS]: this.props.captureDismiss,
[Classes.POPOVER2_MATCH_TARGET_WIDTH]: this.props.matchTargetWidth,
[Classes.POPOVER2_REFERENCE_HIDDEN]: popperProps.isReferenceHidden === true,
[Classes.POPOVER2_POPPER_ESCAPED]: popperProps.hasPopperEscaped === true,
},
Expand Down Expand Up @@ -467,9 +470,9 @@ export class Popover2<T> extends AbstractPureComponent2<Popover2Props<T>, IPopov
);
};

private getPopperModifiers(): StrictModifier[] {
const { modifiers } = this.props;
return [
private getPopperModifiers(): ReadonlyArray<Modifier<any>> {
const { matchTargetWidth, modifiers, modifiersCustom } = this.props;
const popperModifiers: Array<Modifier<any>> = [
{
enabled: this.isArrowEnabled(),
name: "arrow",
Expand Down Expand Up @@ -517,6 +520,16 @@ export class Popover2<T> extends AbstractPureComponent2<Popover2Props<T>, IPopov
},
},
];

if (matchTargetWidth) {
popperModifiers.push(matchReferenceWidthModifier);
}

if (modifiersCustom !== undefined) {
popperModifiers.push(...modifiersCustom);
}

return popperModifiers;
}

private handleTargetFocus = (e: React.FocusEvent<HTMLElement>) => {
Expand Down
18 changes: 17 additions & 1 deletion packages/popover2/src/popover2SharedProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { Boundary, Placement, placements, RootBoundary, StrictModifiers } from "@popperjs/core";
import { Boundary, Modifier, Placement, placements, RootBoundary, StrictModifiers } from "@popperjs/core";
import * as React from "react";
import { StrictModifier } from "react-popper";

Expand Down Expand Up @@ -123,6 +123,15 @@ export interface IPopover2SharedProps<TProps> extends OverlayableProps, Props {
*/
isOpen?: boolean;

/**
* Whether the popover content should be sized to match the width of the target.
* This is sometimes useful for dropdown menus. This prop is implemented using
* a Popper.js custom modifier.
*
* @default false
*/
matchTargetWidth?: boolean;

/**
* Whether to apply minimal styling to this popover or tooltip. Minimal popovers
* do not have an arrow pointing to their target and use a subtler animation.
Expand All @@ -147,6 +156,13 @@ export interface IPopover2SharedProps<TProps> extends OverlayableProps, Props {
[M in StrictModifierNames]: Partial<Omit<StrictModifier<M>, "name">>;
}>;

/**
* Custom modifiers to add to the popper instance.
*
* @see https://popper.js.org/docs/v2/modifiers/#custom-modifiers
*/
modifiersCustom?: ReadonlyArray<Partial<Modifier<any, object>>>;

/**
* Callback invoked in controlled mode when the popover open state *would*
* change due to user interaction.
Expand Down
12 changes: 12 additions & 0 deletions packages/popover2/test/popover2Tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,18 @@ describe("<Popover2>", () => {
wrapper = renderPopover({ minimal: true, isOpen: true });
assert.lengthOf(wrapper.find(Popover2Arrow), 0);
});

it("matches target width via custom modifier", () => {
wrapper = renderPopover({ matchTargetWidth: true, isOpen: true, placement: "bottom" });
const targetElement = wrapper.find("[data-testid='target-button']").getDOMNode();
const popoverElement = wrapper.find(`.${Classes.POPOVER2}`).getDOMNode();
assert.closeTo(
popoverElement.clientWidth,
targetElement.clientWidth,
5,
"content width should equal target width +/- 5px",
);
});
});

describe("closing on click", () => {
Expand Down

1 comment on commit a6453da

@blueprint-bot
Copy link

Choose a reason for hiding this comment

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

[popover2] feat: new props matchTargetWidth, modifiersCustom (#5307)

Previews: documentation | landing | table | demo

Please sign in to comment.