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

[Tree] Fix a few regressions #2466

Merged
merged 12 commits into from
Oct 4, 2018
31 changes: 18 additions & 13 deletions packages/core/src/components/tree/_tree.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ Styleguide tree
$tree-row-height: $pt-grid-size * 3 !default;
$tree-icon-spacing: ($tree-row-height - $pt-icon-size-standard) / 2 !default;

.#{$ns}-tree {
#{$icon-classes} {
color: $pt-icon-color;
}
}

.#{$ns}-tree-node-list {
margin: 0;
padding-left: 0;
Expand Down Expand Up @@ -78,17 +84,14 @@ $tree-icon-spacing: ($tree-row-height - $pt-icon-size-standard) / 2 !default;

.#{$ns}-tree-node-caret,
.#{$ns}-tree-node-caret-none {
position: relative;
min-width: $pt-grid-size * 3;
// CSS API: override default icon styles, which appear first for some reason
line-height: $tree-row-height !important; // stylelint-disable-line declaration-no-important
min-width: $tree-row-height;
}

.#{$ns}-tree-node-caret {
@include pt-icon-colors();
transform: rotate(0deg);
cursor: pointer;
text-align: center;
padding: $tree-icon-spacing;
transition: transform ($pt-transition-duration * 2) $pt-transition-ease;

&.#{$ns}-tree-node-caret-open {
Expand All @@ -99,16 +102,11 @@ $tree-icon-spacing: ($tree-row-height - $pt-icon-size-standard) / 2 !default;
&.#{$ns}-icon-standard::before {
content: $pt-icon-chevron-right;
}

.#{$ns}-icon {
margin: $tree-icon-spacing;
}
}

.#{$ns}-tree-node-icon {
position: relative;
margin-right: $tree-icon-spacing;
color: $pt-icon-color;
}

.#{$ns}-tree-node-label {
Expand All @@ -125,8 +123,13 @@ $tree-icon-spacing: ($tree-row-height - $pt-icon-size-standard) / 2 !default;

.#{$ns}-tree-node-secondary-label {
padding: 0 ($pt-grid-size / 2);
line-height: $tree-row-height;
user-select: none;

.#{$ns}-popover-wrapper,
.#{$ns}-popover-target {
display: flex;
align-items: center;
giladgray marked this conversation as resolved.
Show resolved Hide resolved
}
}

.#{$ns}-tree-node.#{$ns}-tree-node-selected > .#{$ns}-tree-node-content {
Expand All @@ -153,8 +156,10 @@ $tree-icon-spacing: ($tree-row-height - $pt-icon-size-standard) / 2 !default;
background-color: rgba($gray1, 0.3);
}

.#{$ns}-tree-node-icon {
color: $pt-dark-icon-color;
.#{$ns}-tree {
#{$icon-classes} {
color: $pt-dark-icon-color;
}
}

.#{$ns}-tree-node.#{$ns}-tree-node-selected > .#{$ns}-tree-node-content {
Expand Down
25 changes: 14 additions & 11 deletions packages/core/src/components/tree/treeNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,7 @@ export class TreeNode<T = {}> extends React.Component<ITreeNodeProps<T>, {}> {
}

public render() {
const { children, className, hasCaret, icon, isExpanded, isSelected, label } = this.props;

const showCaret = hasCaret == null ? React.Children.count(children) > 0 : hasCaret;
const caretStateClass = isExpanded ? Classes.TREE_NODE_CARET_OPEN : Classes.TREE_NODE_CARET_CLOSED;
const caretClasses = showCaret
? classNames(Classes.TREE_NODE_CARET, caretStateClass)
: Classes.TREE_NODE_CARET_NONE;

const { children, className, icon, isExpanded, isSelected, label } = this.props;
const classes = classNames(
Classes.TREE_NODE,
{
Expand All @@ -115,9 +108,7 @@ export class TreeNode<T = {}> extends React.Component<ITreeNodeProps<T>, {}> {
onDoubleClick={this.handleDoubleClick}
ref={this.handleContentRef}
>
<span className={caretClasses} onClick={showCaret ? this.handleCaretClick : undefined}>
{showCaret && <Icon icon="chevron-right" />}
</span>
{this.maybeRenderCaret()}
<Icon className={Classes.TREE_NODE_ICON} icon={icon} />
<span className={Classes.TREE_NODE_LABEL}>{label}</span>
{this.maybeRenderSecondaryLabel()}
Expand All @@ -127,6 +118,18 @@ export class TreeNode<T = {}> extends React.Component<ITreeNodeProps<T>, {}> {
);
}

private maybeRenderCaret() {
const { hasCaret = React.Children.count(this.props.children) > 0 } = this.props;
if (hasCaret) {
const caretClasses = classNames(
Classes.TREE_NODE_CARET,
this.props.isExpanded ? Classes.TREE_NODE_CARET_OPEN : Classes.TREE_NODE_CARET_CLOSED,
);
return <Icon className={caretClasses} onClick={this.handleCaretClick} icon={"chevron-right"} />;
}
return <span className={Classes.TREE_NODE_CARET_NONE} />;
}

private maybeRenderSecondaryLabel() {
if (this.props.secondaryLabel != null) {
return <span className={Classes.TREE_NODE_SECONDARY_LABEL}>{this.props.secondaryLabel}</span>;
Expand Down
57 changes: 33 additions & 24 deletions packages/core/test/tree/treeTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as React from "react";
import * as ReactDOM from "react-dom";
import { spy } from "sinon";

import { mount } from "enzyme";
import { mount, ReactWrapper } from "enzyme";
import { Classes, ITreeNode, ITreeProps, Tree } from "../../src/index";

describe("<Tree>", () => {
Expand Down Expand Up @@ -46,18 +46,18 @@ describe("<Tree>", () => {
contents[2].hasCaret = contents[3].hasCaret = false;

const tree = renderTree({ contents });
assert.lengthOf(tree.find(`.c0 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET}`), 1);
assert.lengthOf(tree.find(`.c1 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET}`), 1);
assert.lengthOf(tree.find(`.c2 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET_NONE}`), 1);
assert.lengthOf(tree.find(`.c3 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET_NONE}`), 1);
assertNodeHasCaret(tree, "c0", true);
assertNodeHasCaret(tree, "c1", true);
assertNodeHasCaret(tree, "c2", false);
assertNodeHasCaret(tree, "c3", false);
});

it("if not specified, caret visibility is determined by the presence of children", () => {
const tree = renderTree();
assert.lengthOf(tree.find(`.c0 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET_NONE}`), 1);
assert.lengthOf(tree.find(`.c1 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET}`), 1);
assert.lengthOf(tree.find(`.c2 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET_NONE}`), 1);
assert.lengthOf(tree.find(`.c3 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET}`), 1);
assertNodeHasCaret(tree, "c0", false);
assertNodeHasCaret(tree, "c1", true);
assertNodeHasCaret(tree, "c2", false);
assertNodeHasCaret(tree, "c3", true);
});

it("caret direction is determined by node expansion", () => {
Expand All @@ -84,10 +84,10 @@ describe("<Tree>", () => {
];

const tree = renderTree({ contents });
assert.lengthOf(tree.find(`.c0 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET_CLOSED}`), 1);
assert.lengthOf(tree.find(`.c1 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET_OPEN}`), 1);
assert.lengthOf(tree.find(`.c2 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET_CLOSED}`), 1);
assert.lengthOf(tree.find(`.c3 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET_OPEN}`), 1);
assertNodeHasClass(tree, "c0", Classes.TREE_NODE_CARET_CLOSED);
assertNodeHasClass(tree, "c1", Classes.TREE_NODE_CARET_OPEN);
assertNodeHasClass(tree, "c2", Classes.TREE_NODE_CARET_CLOSED);
assertNodeHasClass(tree, "c3", Classes.TREE_NODE_CARET_OPEN);
});

it("event callbacks are fired correctly", () => {
Expand All @@ -113,7 +113,7 @@ describe("<Tree>", () => {
assert.isTrue(onNodeClick.calledOnce);
assert.deepEqual(onNodeClick.args[0][1], [0]);

tree.find(`.c1 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET}`).simulate("click");
findNodeClass(tree, "c1", Classes.TREE_NODE_CARET).simulate("click");
assert.isTrue(onNodeExpand.calledOnce);
assert.deepEqual(onNodeExpand.args[0][1], [1]);
// make sure that onNodeClick isn't fired again, only onNodeExpand should be
Expand All @@ -123,7 +123,7 @@ describe("<Tree>", () => {
assert.isTrue(onNodeDoubleClick.calledOnce);
assert.deepEqual(onNodeDoubleClick.args[0][1], [3, 0]);

tree.find(`.c3 > .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_CARET}`).simulate("click");
findNodeClass(tree, "c3", Classes.TREE_NODE_CARET).simulate("click");
assert.isTrue(onNodeCollapse.calledOnce);
assert.deepEqual(onNodeCollapse.args[0][1], [3]);

Expand All @@ -138,10 +138,9 @@ describe("<Tree>", () => {
contents[2].icon = "document";

const tree = renderTree({ contents });
const iconSelector = `.${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_ICON}`;
assert.lengthOf(tree.find(`.c0 > ${iconSelector}`).hostNodes(), 0, "c0");
assert.lengthOf(tree.find(`.c1 > ${iconSelector}`).hostNodes(), 1, "c1");
assert.lengthOf(tree.find(`.c2 > ${iconSelector}`).hostNodes(), 1, "c2");
assertNodeHasClass(tree, "c0", Classes.TREE_NODE_ICON, false);
assertNodeHasClass(tree, "c1", Classes.TREE_NODE_ICON);
assertNodeHasClass(tree, "c2", Classes.TREE_NODE_ICON);
});

it("isExpanded controls node expansion", () => {
Expand Down Expand Up @@ -175,11 +174,9 @@ describe("<Tree>", () => {
contents[2].secondaryLabel = <p>Paragraph</p>;

const tree = renderTree({ contents }).find("li");

const secondaryLabelSelector = `> .${Classes.TREE_NODE_CONTENT} .${Classes.TREE_NODE_SECONDARY_LABEL}`;
assert.lengthOf(tree.find(`.c0 ${secondaryLabelSelector}`), 0);
assert.strictEqual(tree.find(`.c1 ${secondaryLabelSelector}`).text(), "Secondary");
assert.strictEqual(tree.find(`.c2 ${secondaryLabelSelector}`).text(), "Paragraph");
assertNodeHasClass(tree, "c0", Classes.TREE_NODE_SECONDARY_LABEL, false);
assert.strictEqual(findNodeClass(tree, "c1", Classes.TREE_NODE_SECONDARY_LABEL).text(), "Secondary");
assert.strictEqual(findNodeClass(tree, "c2", Classes.TREE_NODE_SECONDARY_LABEL).text(), "Paragraph");
});

it("getNodeContentElement returns references to underlying node elements", done => {
Expand Down Expand Up @@ -212,6 +209,18 @@ describe("<Tree>", () => {
assert.doesNotThrow(() => renderTree({ contents: smallerContents }));
});

function findNodeClass(tree: ReactWrapper, nodeClass: string, childClass: string) {
return tree.find(`.${nodeClass} > .${Classes.TREE_NODE_CONTENT} .${childClass}`).hostNodes();
}

function assertNodeHasClass(tree: ReactWrapper, nodeClass: string, childClass: string, expected = true) {
assert.equal(findNodeClass(tree, nodeClass, childClass).exists(), expected);
}

function assertNodeHasCaret(tree: ReactWrapper, nodeClass: string, hasCaret: boolean) {
return assertNodeHasClass(tree, nodeClass, hasCaret ? Classes.TREE_NODE_CARET : Classes.TREE_NODE_CARET_NONE);
}

function renderTree(props?: Partial<ITreeProps>) {
return mount(<Tree contents={createDefaultContents()} {...props} />);
}
Expand Down
14 changes: 11 additions & 3 deletions packages/docs-app/src/examples/core-examples/treeExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import * as React from "react";

import { Classes, Icon, ITreeNode, Tooltip, Tree } from "@blueprintjs/core";
import { Classes, Icon, ITreeNode, Position, Tooltip, Tree } from "@blueprintjs/core";
import { Example, IExampleProps } from "@blueprintjs/docs-theme";

export interface ITreeExampleState {
Expand Down Expand Up @@ -75,7 +75,11 @@ const INITIAL_STATE: ITreeNode[] = [
id: 1,
icon: "folder-close",
isExpanded: true,
label: <Tooltip content="I'm a folder <3">Folder 1</Tooltip>,
label: (
<Tooltip content="I'm a folder <3" position={Position.RIGHT}>
Folder 1
</Tooltip>
),
childNodes: [
{
id: 2,
Expand All @@ -96,7 +100,11 @@ const INITIAL_STATE: ITreeNode[] = [
id: 4,
hasCaret: true,
icon: "folder-close",
label: <Tooltip content="foo">Folder 2</Tooltip>,
label: (
<Tooltip content="foo" position={Position.RIGHT}>
Folder 2
</Tooltip>
),
childNodes: [
{ id: 5, label: "No-Icon Item" },
{ id: 6, icon: "tag", label: "Item 1" },
Expand Down