diff --git a/packages/core/src/components/tree/_tree.scss b/packages/core/src/components/tree/_tree.scss index 9cedf40996..bad5dbb196 100644 --- a/packages/core/src/components/tree/_tree.scss +++ b/packages/core/src/components/tree/_tree.scss @@ -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; @@ -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 { @@ -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 { @@ -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; + } } .#{$ns}-tree-node.#{$ns}-tree-node-selected > .#{$ns}-tree-node-content { @@ -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 { diff --git a/packages/core/src/components/tree/treeNode.tsx b/packages/core/src/components/tree/treeNode.tsx index 9a1fffae63..0cb5d4dd4b 100644 --- a/packages/core/src/components/tree/treeNode.tsx +++ b/packages/core/src/components/tree/treeNode.tsx @@ -84,14 +84,7 @@ export class TreeNode extends React.Component, {}> { } 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, { @@ -115,9 +108,7 @@ export class TreeNode extends React.Component, {}> { onDoubleClick={this.handleDoubleClick} ref={this.handleContentRef} > - - {showCaret && } - + {this.maybeRenderCaret()} {label} {this.maybeRenderSecondaryLabel()} @@ -127,6 +118,18 @@ export class TreeNode extends React.Component, {}> { ); } + 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 ; + } + return ; + } + private maybeRenderSecondaryLabel() { if (this.props.secondaryLabel != null) { return {this.props.secondaryLabel}; diff --git a/packages/core/test/tree/treeTests.tsx b/packages/core/test/tree/treeTests.tsx index 3527f5f061..2b0c2219f2 100644 --- a/packages/core/test/tree/treeTests.tsx +++ b/packages/core/test/tree/treeTests.tsx @@ -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("", () => { @@ -46,18 +46,18 @@ describe("", () => { 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", () => { @@ -84,10 +84,10 @@ describe("", () => { ]; 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", () => { @@ -113,7 +113,7 @@ describe("", () => { 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 @@ -123,7 +123,7 @@ describe("", () => { 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]); @@ -138,10 +138,9 @@ describe("", () => { 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", () => { @@ -175,11 +174,9 @@ describe("", () => { contents[2].secondaryLabel =

Paragraph

; 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 => { @@ -212,6 +209,18 @@ describe("", () => { 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) { return mount(); } diff --git a/packages/docs-app/src/examples/core-examples/treeExample.tsx b/packages/docs-app/src/examples/core-examples/treeExample.tsx index cc495d746f..ed680a1454 100644 --- a/packages/docs-app/src/examples/core-examples/treeExample.tsx +++ b/packages/docs-app/src/examples/core-examples/treeExample.tsx @@ -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 { @@ -75,7 +75,11 @@ const INITIAL_STATE: ITreeNode[] = [ id: 1, icon: "folder-close", isExpanded: true, - label: Folder 1, + label: ( + + Folder 1 + + ), childNodes: [ { id: 2, @@ -96,7 +100,11 @@ const INITIAL_STATE: ITreeNode[] = [ id: 4, hasCaret: true, icon: "folder-close", - label: Folder 2, + label: ( + + Folder 2 + + ), childNodes: [ { id: 5, label: "No-Icon Item" }, { id: 6, icon: "tag", label: "Item 1" },