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

Submenu flipping with Popper.js! #2053

Merged
merged 18 commits into from
Jan 31, 2018
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion packages/core/src/common/abstractPureComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { isNodeEnv } from "./utils";
* An abstract component that Blueprint components can extend
* in order to add some common functionality like runtime props validation.
*/
export abstract class AbstractPureComponent<P, S> extends React.PureComponent<P, S> {
export abstract class AbstractPureComponent<P, S = {}> extends React.PureComponent<P, S> {
/** Component displayName should be `public static`. This property exists to prevent incorrect usage. */
protected displayName: never;

Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/components/menu/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2018 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the terms of the LICENSE file distributed with this project.
*/

import { isFunction } from "../../common/utils";

export interface IMenuItemContext {
getSubmenuPopperModifiers?(): Popper.Modifiers;
}

export const MenuItemContextTypes: React.ValidationMap<IMenuItemContext> = {
getSubmenuPopperModifiers: assertFunctionProp,
};

// simple alternative to prop-types dependency
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see the point to this -- we're already depending on prop-types implicitly through react. You should use the package directly and also remove this code:

// simple alternative to prop-types dependency

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well dang, i didn't realize react depended on it!

blueprint git:(gg/submenus) yarn why prop-types
yarn why v1.3.2
[1/4] 🤔  Why do we have the module "prop-types"...?
[2/4] 🚚  Initialising dependency graph...
[3/4] 🔍  Finding dependency...
[4/4] 🚡  Calculating file sizes...
info Reasons this module exists
   - "@blueprintjs/core#react-popper" depends on it
   - "@blueprintjs/core#react" depends on it
   - "@blueprintjs/core#react-transition-group" depends on it

Copy link
Contributor Author

@giladgray giladgray Jan 30, 2018

Choose a reason for hiding this comment

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

no-implicit-dependency-wise, is prop-types a new peerDep, alongside react?

Copy link
Contributor

Choose a reason for hiding this comment

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

no, it's a regular dependency, not a peerDep.

function assertFunctionProp<T>(obj: T, key: keyof T) {
// context method is optional, so allow nulls
if (obj[key] == null || isFunction(obj[key])) {
return undefined;
}
return new Error(`[Blueprint] context ${key} must be function. received ${typeof obj[key]}.`);
}
35 changes: 22 additions & 13 deletions packages/core/src/components/menu/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,34 @@ To add a submenu to a `Menu`, simply nest `MenuItem`s within another `MenuItem`.
The submenu opens to the right of its parent by default, but will adjust and flip to the left if
there is not enough room to the right.

`Menu` provides two `submenu` props to adjust this flipping behavior: you can customize the boundary element
and the padding within that boundary element. Note that a `MenuItem` _must be inside_ a `Menu` element
Copy link
Contributor

Choose a reason for hiding this comment

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

When would I need to customize these things? Genuinely curious.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

you might want to keep your Menu inside a container other than the viewport, maybe on a canvas. padding i think is less useful and less likely to be used so i could be convinced to remove it, but it's easy enough to support.

for these props to affect the submenus. On standalone `MenuItem`s (without a parent `Menu`, an anti-pattern) you can
use `popoverProps.modifiers` directly to achieve the same result.
Copy link
Contributor

Choose a reason for hiding this comment

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

If it's an anti-pattern, why even mention it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah i had this exact thought. i figure it's going to happen (is already happening) so why not pass judgment on it.


```jsx
<MenuItem text="Submenu">
<MenuItem text="Child one" />
<MenuItem text="Child two" />
<MenuItem text="Child three" />
</MenuItem>
<Menu>
<MenuItem text="Submenu">
<MenuItem text="Child one" />
<MenuItem text="Child two" />
<MenuItem text="Child three" />
</MenuItem>
</Menu>
```

Alternatively, you can pass an array of `IMenuItemProps` to the `submenu` prop:

```jsx
React.createElement(MenuItem, {
submenu: [
{ text: "Child one" },
{ text: "Child two" },
{ text: "Child three" },
],
text: "parent",
});
React.createElement(Menu, {},
React.createElement(MenuItem, {
submenu: [
{ text: "Child one" },
{ text: "Child two" },
{ text: "Child three" },
],
text: "parent",
}),
);
```

<div class="pt-callout pt-intent-warning pt-icon-warning-sign">
Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,38 @@ import * as React from "react";

import * as Classes from "../../common/classes";
import { IProps } from "../../common/props";
import { IMenuItemContext, MenuItemContextTypes } from "./context";

export interface IMenuProps extends IProps {
/** Ref handler that receives the HTML `<ul>` element backing this component. */
ulRef?: (ref: HTMLUListElement) => any;

/**
* Boundary element for position of submenu. If submenu reaches the edge of the boundary,
* it will flip to the other side.
* This prop will apply to all `MenuItem`s inside this `Menu`, via React context.
* @see https://popper.js.org/popper-documentation.html#modifiers..flip.boundariesElement
* @default "viewport"
*/
submenuBoundaryElement?: "scrollParent" | "viewport" | "window" | Element;

/**
* Pixel width of minimum distance between boundary element and submenu content.
* Submenus will flip to the other side if they come within this distance of the boundary.
* This prop will apply to all `MenuItem`s inside this `Menu`, via React context.
* @see https://popper.js.org/popper-documentation.html#modifiers..preventOverflow.padding
* @default 5
*/
submenuBoundaryPadding?: number;
}

export class Menu extends React.Component<IMenuProps, {}> {
public static childContextTypes = MenuItemContextTypes;
public static displayName = "Blueprint2.Menu";
public static defaultProps: IMenuProps = {
submenuBoundaryElement: "viewport",
submenuBoundaryPadding: 5,
};

public render() {
return (
Expand All @@ -25,4 +49,16 @@ export class Menu extends React.Component<IMenuProps, {}> {
</ul>
);
}

public getChildContext(): IMenuItemContext {
return {
getSubmenuPopperModifiers: () => {
const { submenuBoundaryElement: boundariesElement, submenuBoundaryPadding: padding } = this.props;
return {
flip: { boundariesElement, padding },
preventOverflow: { boundariesElement, padding },
};
},
};
}
}
Loading