Skip to content

Commit

Permalink
[popover2] Popover2 and ContextMenu2 fixes & enhancements (#4601)
Browse files Browse the repository at this point in the history
  • Loading branch information
adidahiya authored Mar 23, 2021
1 parent ae8e237 commit 0064ea4
Show file tree
Hide file tree
Showing 14 changed files with 261 additions and 98 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/common/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export const LIST = `${NS}-list`;
export const LIST_UNSTYLED = `${NS}-list-unstyled`;
export const RTL = `${NS}-rtl`;

// layout utilities
/** @see https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block */
export const FIXED_POSITIONING_CONTAINING_BLOCK = `${NS}-fixed-positioning-containing-block`;

// components
export const ALERT = `${NS}-alert`;
export const ALERT_BODY = `${ALERT}-body`;
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/components/collapse/collapse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ export class Collapse extends AbstractPureComponent2<ICollapseProps, ICollapseSt
transition: isAutoHeight ? "none" : undefined,
};

// in order to give hints to child elements which rely on CSS fixed positioning, we need to apply a class
// to the element which creates a new containing block with a non-empty `transform` property
// see https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
const contentsStyle = {
// only use heightWhenOpen while closing
transform: displayWithTransform ? "translateY(0)" : `translateY(-${this.state.heightWhenOpen}px)`,
Expand All @@ -196,7 +199,7 @@ export class Collapse extends AbstractPureComponent2<ICollapseProps, ICollapseSt
style: containerStyle,
},
<div
className={Classes.COLLAPSE_BODY}
className={classNames(Classes.COLLAPSE_BODY, Classes.FIXED_POSITIONING_CONTAINING_BLOCK)}
ref={this.contentsRefHandler}
style={contentsStyle}
aria-hidden={!isContentVisible && this.props.keepChildrenMounted}
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/components/tree/_tree.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ $tree-icon-spacing: ($tree-row-height - $pt-icon-size-standard) / 2 !default;
}
}
}

&.#{$ns}-fixed-positioning-containing-block {
// use the same transform as the Collapse component, to mimic the behavior of creating a new
// containing block for children which are position: fixed (like ContextMenu2 targets)
transform: translateY(0);
}
}

.#{$ns}-tree-node-list {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/tree/tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class Tree<T = {}> extends React.Component<ITreeProps<T>> {

public render() {
return (
<div className={classNames(Classes.TREE, this.props.className)}>
<div className={classNames(Classes.TREE, Classes.FIXED_POSITIONING_CONTAINING_BLOCK, this.props.className)}>
{this.renderNodes(this.props.contents, [], Classes.TREE_ROOT)}
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions packages/core/test/tree/treeTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@ describe("<Tree>", () => {

it("renders its contents", () => {
const tree = renderTree({ contents: [{ id: 0, label: "Node" }] });
assert.lengthOf(tree.find({ className: Classes.TREE }), 1);
assert.lengthOf(tree.find(`.${Classes.TREE}`), 1);
});

it("handles undefined input well", () => {
const tree = renderTree({ contents: undefined });
assert.lengthOf(tree.find({ className: Classes.TREE }), 1);
assert.lengthOf(tree.find(`.${Classes.TREE}`), 1);
});

it("handles empty input well", () => {
const tree = renderTree({ contents: [] });
assert.lengthOf(tree.find({ className: Classes.TREE }), 1);
assert.lengthOf(tree.find(`.${Classes.TREE}`), 1);
});

it("hasCaret forces a caret to be/not be displayed", () => {
Expand Down
1 change: 1 addition & 0 deletions packages/docs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"classnames": "^2.2",
"dom4": "^2.1.5",
"downloadjs": "^1.4.7",
"lodash-es": "^4.17.15",
"moment": "^2.29.1",
"normalize.css": "^8.0.1",
"popper.js": "^1.16.1",
Expand Down
153 changes: 100 additions & 53 deletions packages/docs-app/src/examples/core-examples/treeExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,82 +14,117 @@
* limitations under the License.
*/

import * as React from "react";
import { cloneDeep } from "lodash-es";
import React, { useCallback, useReducer } from "react";

import { Classes, Icon, Intent, ITreeNode, Tree } from "@blueprintjs/core";
import { Example, IExampleProps } from "@blueprintjs/docs-theme";
import { Tooltip2 } from "@blueprintjs/popover2";
import { Classes as Popover2Classes, ContextMenu2, Tooltip2 } from "@blueprintjs/popover2";

export interface ITreeExampleState {
nodes: ITreeNode[];
type NodePath = number[];

type TreeAction =
| { type: "SET_IS_EXPANDED"; payload: { path: NodePath; isExpanded: boolean } }
| { type: "DESELECT_ALL" }
| { type: "SET_IS_SELECTED"; payload: { path: NodePath; isSelected: boolean } };

function forEachNode(nodes: ITreeNode[] | undefined, callback: (node: ITreeNode) => void) {
if (nodes === undefined) {
return;
}

for (const node of nodes) {
callback(node);
forEachNode(node.childNodes, callback);
}
}

// use Component so it re-renders everytime: `nodes` are not a primitive type
// and therefore aren't included in shallow prop comparison
export class TreeExample extends React.Component<IExampleProps, ITreeExampleState> {
public state: ITreeExampleState = { nodes: INITIAL_STATE };
function forNodeAtPath(nodes: ITreeNode[], path: NodePath, callback: (node: ITreeNode) => void) {
callback(Tree.nodeFromPath(path, nodes));
}

public render() {
return (
<Example options={false} {...this.props}>
<Tree
contents={this.state.nodes}
onNodeClick={this.handleNodeClick}
onNodeCollapse={this.handleNodeCollapse}
onNodeExpand={this.handleNodeExpand}
className={Classes.ELEVATION_0}
/>
</Example>
);
function treeExampleReducer(state: ITreeNode[], action: TreeAction) {
switch (action.type) {
case "DESELECT_ALL":
const newState1 = cloneDeep(state);
forEachNode(newState1, node => (node.isSelected = false));
return newState1;
case "SET_IS_EXPANDED":
const newState2 = cloneDeep(state);
forNodeAtPath(newState2, action.payload.path, node => (node.isExpanded = action.payload.isExpanded));
return newState2;
case "SET_IS_SELECTED":
const newState3 = cloneDeep(state);
forNodeAtPath(newState3, action.payload.path, node => (node.isSelected = action.payload.isSelected));
return newState3;
default:
return state;
}
}

export const TreeExample: React.FC<IExampleProps> = props => {
const [nodes, dispatch] = useReducer(treeExampleReducer, INITIAL_STATE);

private handleNodeClick = (nodeData: ITreeNode, _nodePath: number[], e: React.MouseEvent<HTMLElement>) => {
const originallySelected = nodeData.isSelected;
const handleNodeClick = useCallback((node: ITreeNode, nodePath: NodePath, e: React.MouseEvent<HTMLElement>) => {
const originallySelected = node.isSelected;
if (!e.shiftKey) {
this.forEachNode(this.state.nodes, n => (n.isSelected = false));
dispatch({ type: "DESELECT_ALL" });
}
nodeData.isSelected = originallySelected == null ? true : !originallySelected;
this.setState(this.state);
};
dispatch({
payload: { path: nodePath, isSelected: originallySelected == null ? true : !originallySelected },
type: "SET_IS_SELECTED",
});
}, []);

private handleNodeCollapse = (nodeData: ITreeNode) => {
nodeData.isExpanded = false;
this.setState(this.state);
};
const handleNodeCollapse = useCallback((_node: ITreeNode, nodePath: NodePath) => {
dispatch({
payload: { path: nodePath, isExpanded: false },
type: "SET_IS_EXPANDED",
});
}, []);

private handleNodeExpand = (nodeData: ITreeNode) => {
nodeData.isExpanded = true;
this.setState(this.state);
};
const handleNodeExpand = useCallback((_node: ITreeNode, nodePath: NodePath) => {
dispatch({
payload: { path: nodePath, isExpanded: true },
type: "SET_IS_EXPANDED",
});
}, []);

private forEachNode(nodes: ITreeNode[], callback: (node: ITreeNode) => void) {
if (nodes == null) {
return;
}

for (const node of nodes) {
callback(node);
this.forEachNode(node.childNodes, callback);
}
}
}
return (
<Example options={false} {...props}>
<Tree
contents={nodes}
onNodeClick={handleNodeClick}
onNodeCollapse={handleNodeCollapse}
onNodeExpand={handleNodeExpand}
className={Classes.ELEVATION_0}
/>
</Example>
);
};

/* tslint:disable:object-literal-sort-keys so childNodes can come last */
const INITIAL_STATE: ITreeNode[] = [
{
id: 0,
hasCaret: true,
icon: "folder-close",
label: "Folder 0",
label: (
<ContextMenu2 popoverClassName={Popover2Classes.POPOVER2_CONTENT_SIZING} content={<div>Hello there!</div>}>
Folder 0
</ContextMenu2>
),
},
{
id: 1,
icon: "folder-close",
isExpanded: true,
label: (
<Tooltip2 content="I'm a folder <3" placement="right">
Folder 1
</Tooltip2>
<ContextMenu2 popoverClassName={Popover2Classes.POPOVER2_CONTENT_SIZING} content={<div>Hello there!</div>}>
<Tooltip2 content="I'm a folder <3" placement="right">
Folder 1
</Tooltip2>
</ContextMenu2>
),
childNodes: [
{
Expand All @@ -112,9 +147,14 @@ const INITIAL_STATE: ITreeNode[] = [
hasCaret: true,
icon: "folder-close",
label: (
<Tooltip2 content="foo" placement="right">
Folder 2
</Tooltip2>
<ContextMenu2
popoverClassName={Popover2Classes.POPOVER2_CONTENT_SIZING}
content={<div>Hello there!</div>}
>
<Tooltip2 content="foo" placement="right">
Folder 2
</Tooltip2>
</ContextMenu2>
),
childNodes: [
{ id: 5, label: "No-Icon Item" },
Expand All @@ -123,7 +163,14 @@ const INITIAL_STATE: ITreeNode[] = [
id: 7,
hasCaret: true,
icon: "folder-close",
label: "Folder 3",
label: (
<ContextMenu2
popoverClassName={Popover2Classes.POPOVER2_CONTENT_SIZING}
content={<div>Hello there!</div>}
>
Folder 3
</ContextMenu2>
),
childNodes: [
{ id: 8, icon: "document", label: "Item 0" },
{ id: 9, icon: "tag", label: "Item 1" },
Expand Down
1 change: 1 addition & 0 deletions packages/popover2/src/blueprint-popover2.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Licensed under the Apache License, Version 2.0.
*/

@import "context-menu2";
@import "popover2";
@import "popover2-in-button-group";
@import "popover2-in-control-group";
Expand Down
4 changes: 2 additions & 2 deletions packages/popover2/src/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { Classes } from "@blueprintjs/core";

const NS = Classes.getClassNamespace();

export const CONTEXT_MENU2 = `${NS}-context-menu`;
export const CONTEXT_MENU2_POPOVER_TARGET = `${CONTEXT_MENU2}-popover-target`;
export const CONTEXT_MENU2 = `${NS}-context-menu2`;
export const CONTEXT_MENU2_POPOVER2_TARGET = `${CONTEXT_MENU2}-popover2-target`;

export const POPOVER2 = `${NS}-popover2`;
export const POPOVER2_ARROW = `${POPOVER2}-arrow`;
Expand Down
4 changes: 4 additions & 0 deletions packages/popover2/src/context-menu2.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ pattern, so you may use information about the context menu's state to in your re

@## Props

To enable/disable the context menu popover, use the `disabled` prop. Note that it is inadvisable to change
the value of this prop inside the `onContextMenu` callback for this component; doing so can lead to unpredictable
behavior.

@interface ContextMenu2Props
12 changes: 12 additions & 0 deletions packages/popover2/src/context-menu2.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2021 Palantir Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0.

@import "./common";

.#{$ns}-context-menu2 .#{$ns}-popover2-target {
display: block;
}

.#{$ns}-context-menu2-popover2-target {
position: fixed;
}
Loading

1 comment on commit 0064ea4

@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] Popover2 and ContextMenu2 fixes & enhancements (#4601)

Previews: documentation | landing | table

Please sign in to comment.