diff --git a/src/component/Tooltip.tsx b/src/component/Tooltip.tsx index c2cba2e2de..ef0b93f22e 100644 --- a/src/component/Tooltip.tsx +++ b/src/component/Tooltip.tsx @@ -115,6 +115,8 @@ export class Tooltip extends P state = { boxWidth: -1, boxHeight: -1, + dismissed: false, + dismissedAtCoordinate: { x: 0, y: 0 }, }; private wrapperNode: HTMLDivElement; @@ -128,7 +130,18 @@ export class Tooltip extends P } updateBBox() { - const { boxWidth, boxHeight } = this.state; + const { boxWidth, boxHeight, dismissed } = this.state; + if (dismissed) { + this.wrapperNode.blur(); + if ( + this.props.coordinate.x !== this.state.dismissedAtCoordinate.x || + this.props.coordinate.y !== this.state.dismissedAtCoordinate.y + ) { + this.setState({ dismissed: false }); + } + } else { + this.wrapperNode.focus(); + } if (this.wrapperNode && this.wrapperNode.getBoundingClientRect) { const box = this.wrapperNode.getBoundingClientRect(); @@ -186,7 +199,7 @@ export class Tooltip extends P const { content, viewBox, coordinate, position, active, wrapperStyle } = this.props; let outerStyle: CSSProperties = { pointerEvents: 'none', - visibility: active && hasPayload ? 'visible' : 'hidden', + visibility: !this.state.dismissed && active && hasPayload ? 'visible' : 'hidden', position: 'absolute', top: 0, left: 0, @@ -246,6 +259,19 @@ export class Tooltip extends P return (
{ + if (event.key === 'Escape') { + this.setState({ + dismissed: true, + dismissedAtCoordinate: { + ...this.state.dismissedAtCoordinate, + x: this.props.coordinate.x, + y: this.props.coordinate.y, + }, + }); + } + }} className={cls} style={outerStyle} ref={node => { diff --git a/test/specs/chart/LineChartSpec.js b/test/specs/chart/LineChartSpec.js index 0b44c7716a..2cc8a99929 100644 --- a/test/specs/chart/LineChartSpec.js +++ b/test/specs/chart/LineChartSpec.js @@ -7,21 +7,20 @@ import { mount, render } from 'enzyme'; import sinon from 'sinon'; const data = [ - { name: 'Page A', uv: 400, pv: 2400, amt: 2400 }, - { name: 'Page B', uv: 300, pv: 4567, amt: 2400 }, - { name: 'Page C', uv: 300, pv: 1398, amt: 2400 }, - { name: 'Page D', uv: 200, pv: 9800, amt: 2400 }, - { name: 'Page E', uv: 278, pv: 3908, amt: 2400 }, - { name: 'Page F', uv: 189, pv: 4800, amt: 2400 }, + { name: 'Page A', uv: 400, pv: 2400, amt: 2400 }, + { name: 'Page B', uv: 300, pv: 4567, amt: 2400 }, + { name: 'Page C', uv: 300, pv: 1398, amt: 2400 }, + { name: 'Page D', uv: 200, pv: 9800, amt: 2400 }, + { name: 'Page E', uv: 278, pv: 3908, amt: 2400 }, + { name: 'Page F', uv: 189, pv: 4800, amt: 2400 }, ]; describe('', () => { - it('Render 1 line in simple LineChart', () => { const wrapper = render( - + , ); expect(wrapper.find('.recharts-line .recharts-line-curve').length).to.equal(1); }); @@ -32,7 +31,7 @@ describe('', () => { - + , ); expect(wrapper.find('.recharts-line .recharts-line-curve').length).to.equal(1); }); @@ -43,7 +42,7 @@ describe('', () => { - + , ); const curves = wrapper.find('.recharts-line .recharts-line-curve'); expect(curves.length).to.equal(1); @@ -66,7 +65,7 @@ describe('', () => { - + , ); const curves = wrapper.find('.recharts-line .recharts-line-curve'); expect(curves.length).to.equal(1); @@ -74,7 +73,7 @@ describe('', () => { expect(path.length - path.split('M').join('').length).to.equal(2); }); - it('Render one paths when connectNulls is true', () => { + it('Render one paths when connectNulls is true', () => { const breakData = [ { name: 'Page A', uv: 400, pv: 2400, amt: 2400 }, { name: 'Page B', uv: 300, pv: 4567, amt: 2400 }, @@ -89,7 +88,7 @@ describe('', () => { - + , ); const curves = wrapper.find('.recharts-line .recharts-line-curve'); expect(curves.length).to.equal(1); @@ -98,14 +97,13 @@ describe('', () => { }); it('Renders customized active dot when activeDot is set to be a ReactElement', () => { - const ActiveDot = ({ cx, cy }) => - ; + const ActiveDot = ({ cx, cy }) => ; const wrapper = mount( } type="monotone" dataKey="uv" stroke="#ff7300" /> - + , ); wrapper.setState({ @@ -121,14 +119,13 @@ describe('', () => { }); it('Renders customized active dot when activeDot is set to be a function', () => { - const renderActiveDot = ({ cx, cy }) => - ; + const renderActiveDot = ({ cx, cy }) => ; const wrapper = mount( - + , ); wrapper.setState({ @@ -148,7 +145,7 @@ describe('', () => { const wrapper = render( - + , ); expect(wrapper.find('.recharts-line .recharts-line-curve').length).to.equal(1); }); @@ -157,7 +154,7 @@ describe('', () => { const wrapper = render( - + , ); expect(wrapper.find('.recharts-line .recharts-line-curve').length).to.equal(0); expect(wrapper.find('.recharts-line .recharts-line-dot').length).to.equal(1); @@ -167,20 +164,24 @@ describe('', () => { const wrapper = render( - + , ); expect(wrapper.find('.recharts-label').length).to.equal(6); }); it('Renders 6 labels when label is setted to be a function', () => { - const renderLabel = (props) => { + const renderLabel = props => { const { x, y, key } = props; - return test; + return ( + + test + + ); }; const wrapper = render( - + , ); const labels = wrapper.find('.customized-label'); @@ -188,14 +189,18 @@ describe('', () => { }); it('Renders 6 labels when label is setted to be a react element', () => { - const CustomizedLabel = (props) => { + const CustomizedLabel = props => { const { x, y, key } = props; - return test; + return ( + + test + + ); }; const wrapper = render( } dataKey="uv" stroke="#ff7300" /> - + , ); const labels = wrapper.find('.customized-label'); @@ -206,13 +211,13 @@ describe('', () => { const wrapper = render( - + , ); expect(wrapper.find('.recharts-line-dot').length).to.equal(6); }); it('Renders 6 dots when dot is setted to be a function', () => { - const renderDot = (props) => { + const renderDot = props => { const { cx, cy, key } = props; return ; @@ -220,13 +225,13 @@ describe('', () => { const wrapper = render( - + , ); expect(wrapper.find('.customized-dot').length).to.equal(6); }); it('Renders 6 dots when dot is setted to be a react element', () => { - const Dot = (props) => { + const Dot = props => { const { cx, cy, key } = props; return ; @@ -234,36 +239,41 @@ describe('', () => { const wrapper = render( } type="monotone" dataKey="uv" stroke="#ff7300" /> - + , ); expect(wrapper.find('.customized-dot').length).to.equal(6); }); - it("Renders 3 lines before and after a parents state change", () => { + it('Renders 3 lines before and after a parents state change', () => { class LineChartContainer extends React.Component { - constructor(props) { - super(props); - this.state = { - data : [ - {name: 'A', time: 6}, - {name: 'B', time: 2}, - {name: 'C', time: 4} - ], - dataKeys : ["time"], - additionalStateItem : false - }; - } - render() { - return ( - - {this.state.dataKeys.map((key,i) => - - )} - - ); - } - }; + constructor(props) { + super(props); + this.state = { + data: [ + { name: 'A', time: 6 }, + { name: 'B', time: 2 }, + { name: 'C', time: 4 }, + ], + dataKeys: ['time'], + additionalStateItem: false, + }; + } + + render() { + return ( + + {this.state.dataKeys.map((key, i) => ( + + ))} + + ); + } + } const wrapper = mount(); expect(wrapper.find(LineChart).children().props().children).to.have.lengthOf(3); @@ -277,14 +287,13 @@ describe('', () => { const wrapper = mount( - + , ); wrapper.setProps({ data: [] }); }); it('click on Curve should invoke onClick callback', () => { - let propsOfCallback; - let eventOfCallback; + let propsOfCallback, eventOfCallback; const onClick = sinon.spy((props, event) => { propsOfCallback = props; eventOfCallback = event; @@ -293,8 +302,15 @@ describe('', () => { const onMouseUp = sinon.spy(); const wrapper = mount( - - + + , ); const curve = wrapper.find(Curve); curve.simulate('click'); @@ -316,7 +332,7 @@ describe('', () => { - + , ); const chartWidth = width - margin.left - margin.right; @@ -341,13 +357,11 @@ describe('', () => { tooltipCursors = wrapper.find('.recharts-tooltip-cursor').hostNodes(); expect(tooltipCursors.length).to.equal(1); - expect(tooltipCursors.at(0).props().d).to.equal(`M${expectedX},${margin.top}L${expectedX},${chartBottom}`); // simulate leaving the area wrapper.simulate('mouseLeave'); expect(wrapper.find('.recharts-tooltip-cursor').hostNodes.length).to.equal(0); - }); it('Should update the line chart when the brush changes', () => { @@ -368,22 +382,22 @@ describe('', () => { let dataIndex = 2; - // verify one of the dots that we expect to move when the brush happens + // verify one of the dots that we expect to move when the brush happens expect(lineDots.childAt(dataIndex).props().payload).to.equal(data[dataIndex]); expect(lineDots.childAt(dataIndex).props().cx).to.equal(164); expect(lineDots.childAt(dataIndex).props().cy).to.equal(100); - // simulate a brush to only include the data elements at indices 2-4 + // simulate a brush to only include the data elements at indices 2-4 wrapper.instance().handleBrushChange({ startIndex: 2, endIndex: 5 }); - // we should only have three dots now + // we should only have three dots now const newLineDots = wrapper.find('.recharts-line-dots').hostNodes(); expect(newLineDots.length).to.equal(1); expect(newLineDots.children().length).to.equal(data.length); dataIndex = 0; - // make sure the new first dot is the same as the old 2 dot, just in a new place + // make sure the new first dot is the same as the old 2 dot, just in a new place expect(newLineDots.childAt(dataIndex).props().payload).to.equal(data[dataIndex]); expect(newLineDots.childAt(dataIndex).props().cx).to.equal(margin.left); expect(newLineDots.childAt(dataIndex).props().cy).to.equal(20); @@ -397,15 +411,14 @@ describe('', () => { }); }); - describe(' - Pure Rendering', () => { const pureElements = [Line]; const spies = []; - // CartesianAxis is what is actually render for XAxis and YAxis + // CartesianAxis is what is actually render for XAxis and YAxis let axisSpy; - // spy on each pure element before each test, and restore the spy afterwards + // spy on each pure element before each test, and restore the spy afterwards beforeEach(() => { pureElements.forEach((el, i) => (spies[i] = sinon.spy(el.prototype, 'render'))); axisSpy = sinon.spy(CartesianAxis.prototype, 'render'); @@ -423,9 +436,9 @@ describe(' - Pure Rendering', () => { - ); + ); - // protect against the future where someone might mess up our clean rendering + // protect against the future where someone might mess up our clean rendering it('should only render Line once when the mouse enters and moves', () => { const wrapper = mount(chart); @@ -440,8 +453,8 @@ describe(' - Pure Rendering', () => { expect(axisSpy.callCount).to.equal(2); }); - // protect against the future where someone might mess up our clean rendering - it('should only render Line once when the brush moves but doesn\'t change start/end indices', () => { + // protect against the future where someone might mess up our clean rendering + it("should only render Line once when the brush moves but doesn't change start/end indices", () => { const wrapper = mount(chart); spies.forEach(el => expect(el.callCount).to.equal(1)); @@ -450,7 +463,6 @@ describe(' - Pure Rendering', () => { spies.forEach(el => expect(el.callCount).to.equal(1)); expect(axisSpy.callCount).to.equal(2); }); - }); describe(' - Pure Rendering with legend', () => { @@ -497,7 +509,7 @@ describe(' - Pure Rendering with legend', () => { }); // protect against the future where someone might mess up our clean rendering - it('should only render Line once when the brush moves but doesn\'t change start/end indices', () => { + it("should only render Line once when the brush moves but doesn't change start/end indices", () => { const wrapper = mount(chart); spies.forEach(el => expect(el.callCount).to.equal(1)); @@ -506,10 +518,9 @@ describe(' - Pure Rendering with legend', () => { spies.forEach(el => expect(el.callCount).to.equal(1)); expect(axisSpy.callCount).to.equal(2); }); - }); -describe(" - Rendering two line charts with syncId", () => { +describe(' - Rendering two line charts with syncId', () => { const margin = { top: 20, right: 20, bottom: 20, left: 20 }; const height = 400; const width = 400; @@ -526,9 +537,8 @@ describe(" - Rendering two line charts with syncId", () => { const runAllPromises = () => new Promise(resolve => setTimeout(resolve)); - it("should show tooltips for both charts synced by index on MouseEnter and hide on MouseLeave", async() => { - const ActiveDot = ({ cx, cy }) => - ; + it('should show tooltips for both charts synced by index on MouseEnter and hide on MouseLeave/Escape', async () => { + const ActiveDot = ({ cx, cy }) => ; const chart1 = ( @@ -550,17 +560,18 @@ describe(" - Rendering two line charts with syncId", () => {
{chart1} {chart2} -
+
, ); const chartWidth = width - margin.left - margin.right; const dotSpacing = chartWidth / (data.length - 1); // simulate entering just past Page A of Chart1 to test snapping of the cursor line - expect( - wrapper.find(".recharts-tooltip-cursor").hostNodes().length - ).to.equal(0); - wrapper.find(LineChart).at(0).simulate("mouseEnter", { + expect(wrapper.find('.recharts-tooltip-cursor').hostNodes().length).to.equal(0); + wrapper + .find(LineChart) + .at(0) + .simulate('mouseEnter', { pageX: margin.left + 0.1 * dotSpacing, pageY: height / 2, }); @@ -569,31 +580,44 @@ describe(" - Rendering two line charts with syncId", () => { wrapper.update(); // There are two tooltips - one for each LineChart as they have the same syncId - const tooltipCursors = wrapper.find(".recharts-tooltip-cursor").hostNodes(); + const tooltipCursors = wrapper.find('.recharts-tooltip-cursor').hostNodes(); expect(tooltipCursors.length).to.equal(2); - const tooltipsValueWrapper = wrapper.find(".recharts-tooltip-item-value"); + const tooltipsValueWrapper = wrapper.find('.recharts-tooltip-item-value'); // make sure tooltips display the correct values, i.e. the value of the first item in the data - expect(tooltipsValueWrapper.at(0).text()).to.equal("400"); - expect(tooltipsValueWrapper.at(1).text()).to.equal("500"); + expect(tooltipsValueWrapper.at(0).text()).to.equal('400'); + expect(tooltipsValueWrapper.at(1).text()).to.equal('500'); // Check the activeDots are highlighted - const activeDotNodes = wrapper.find(".recharts-active-dot").hostNodes(); + const activeDotNodes = wrapper.find('.recharts-active-dot').hostNodes(); expect(activeDotNodes.length).to.equal(2); const activeDotWrapper = wrapper.find(ActiveDot); expect(activeDotWrapper.at(0).props().value).to.equal(400); - expect(activeDotWrapper.at(1).props().value).to.equal(500); + expect(activeDotWrapper.at(1).props().value).to.equal(500); // simulate leaving the area - wrapper.find(LineChart).at(0).simulate("mouseLeave"); - expect(wrapper.find(".recharts-tooltip-cursor").hostNodes.length).to.equal(0); + wrapper.find(LineChart).at(0).simulate('mouseLeave'); + expect(wrapper.find('.recharts-tooltip-cursor').hostNodes.length).to.equal(0); + + // simulate pressing Escape key + wrapper + .find(LineChart) + .at(0) + .simulate('mouseEnter', { + pageX: margin.left + 0.1 * dotSpacing, + pageY: height / 2, + }); + + await runAllPromises(); + wrapper.update(); + wrapper.find(LineChart).at(0).simulate('keydown', { key: 'Escape' }); + expect(wrapper.find('.recharts-tooltip-cursor').hostNodes.length).to.equal(0); }); - it("should show tooltips using syncMethod: 'value' for both charts on MouseEnter and hide on MouseLeave", async() => { - const ActiveDot = ({ cx, cy }) => - ; + it("should show tooltips using syncMethod: 'value' for both charts on MouseEnter and hide on MouseLeave", async () => { + const ActiveDot = ({ cx, cy }) => ; const chart1 = ( @@ -613,62 +637,76 @@ describe(" - Rendering two line charts with syncId", () => {
{chart1} {chart2} -
+ , ); const chartWidth = width - margin.left - margin.right; const dotSpacing = chartWidth / (data.length - 1); // simulate entering just past Page A of Chart1 to test snapping of the cursor line - expect( - wrapper.find(".recharts-tooltip-cursor").hostNodes().length - ).to.equal(0); - wrapper.find(LineChart).at(0).simulate("mouseEnter", { + expect(wrapper.find('.recharts-tooltip-cursor').hostNodes().length).to.equal(0); + wrapper + .find(LineChart) + .at(0) + .simulate('mouseEnter', { pageX: margin.left + 0.1 * dotSpacing, pageY: height / 2, }); - + await runAllPromises(); wrapper.update(); // There are two tooltips - one for each LineChart as they have the same syncId - const tooltipCursors = wrapper.find(".recharts-tooltip-cursor").hostNodes(); + const tooltipCursors = wrapper.find('.recharts-tooltip-cursor').hostNodes(); expect(tooltipCursors.length).to.equal(2); - const tooltipsValueWrapper = wrapper.find(".recharts-tooltip-item-value"); + const tooltipsValueWrapper = wrapper.find('.recharts-tooltip-item-value'); // make sure tooltips display the correct values, synced by data value - expect(tooltipsValueWrapper.at(0).text()).to.equal("400"); - expect(tooltipsValueWrapper.at(1).text()).to.equal("230"); + expect(tooltipsValueWrapper.at(0).text()).to.equal('400'); + expect(tooltipsValueWrapper.at(1).text()).to.equal('230'); // Check the activeDots are highlighted - const activeDotNodes = wrapper.find(".recharts-active-dot").hostNodes(); + const activeDotNodes = wrapper.find('.recharts-active-dot').hostNodes(); expect(activeDotNodes.length).to.equal(2); const activeDotWrapper = wrapper.find(ActiveDot); expect(activeDotWrapper.at(0).props().value).to.equal(400); - expect(activeDotWrapper.at(1).props().value).to.equal(230); + expect(activeDotWrapper.at(1).props().value).to.equal(230); // simulate leaving the area - wrapper.find(LineChart).at(0).simulate("mouseLeave"); - expect(wrapper.find(".recharts-tooltip-cursor").hostNodes.length).to.equal(0); + wrapper.find(LineChart).at(0).simulate('mouseLeave'); + expect(wrapper.find('.recharts-tooltip-cursor').hostNodes.length).to.equal(0); }); - it("should show tooltips using syncMethod: [function] for both charts on MouseEnter and hide on MouseLeave", async() => { - const ActiveDot = ({ cx, cy }) => - ; + it('should show tooltips using syncMethod: [function] for both charts on MouseEnter and hide on MouseLeave', async () => { + const ActiveDot = ({ cx, cy }) => ; const syncMethodFunction = (tooltipTicks, data) => data.activeTooltipIndex + 1; const chart1 = ( - + } type="monotone" dataKey="uv" stroke="#ff7300" /> ); const chart2 = ( - + } type="monotone" dataKey="uv" stroke="#ff7300" /> @@ -678,44 +716,45 @@ describe(" - Rendering two line charts with syncId", () => {
{chart1} {chart2} -
+ , ); const chartWidth = width - margin.left - margin.right; const dotSpacing = chartWidth / (data.length - 1); // simulate entering just past Page A of Chart1 to test snapping of the cursor line - expect( - wrapper.find(".recharts-tooltip-cursor").hostNodes().length - ).to.equal(0); - wrapper.find(LineChart).at(0).simulate("mouseEnter", { + expect(wrapper.find('.recharts-tooltip-cursor').hostNodes().length).to.equal(0); + wrapper + .find(LineChart) + .at(0) + .simulate('mouseEnter', { pageX: margin.left + 0.1 * dotSpacing, pageY: height / 2, }); - + await runAllPromises(); wrapper.update(); // There are two tooltips - one for each LineChart as they have the same syncId - const tooltipCursors = wrapper.find(".recharts-tooltip-cursor").hostNodes(); + const tooltipCursors = wrapper.find('.recharts-tooltip-cursor').hostNodes(); expect(tooltipCursors.length).to.equal(2); - const tooltipsValueWrapper = wrapper.find(".recharts-tooltip-item-value"); + const tooltipsValueWrapper = wrapper.find('.recharts-tooltip-item-value'); // make sure tooltips display the correct values, synced by data value - expect(tooltipsValueWrapper.at(0).text()).to.equal("400"); - expect(tooltipsValueWrapper.at(1).text()).to.equal("550"); + expect(tooltipsValueWrapper.at(0).text()).to.equal('400'); + expect(tooltipsValueWrapper.at(1).text()).to.equal('550'); // Check the activeDots are highlighted - const activeDotNodes = wrapper.find(".recharts-active-dot").hostNodes(); + const activeDotNodes = wrapper.find('.recharts-active-dot').hostNodes(); expect(activeDotNodes.length).to.equal(2); const activeDotWrapper = wrapper.find(ActiveDot); expect(activeDotWrapper.at(0).props().value).to.equal(400); - expect(activeDotWrapper.at(1).props().value).to.equal(550); + expect(activeDotWrapper.at(1).props().value).to.equal(550); // simulate leaving the area - wrapper.find(LineChart).at(0).simulate("mouseLeave"); - expect(wrapper.find(".recharts-tooltip-cursor").hostNodes.length).to.equal(0); + wrapper.find(LineChart).at(0).simulate('mouseLeave'); + expect(wrapper.find('.recharts-tooltip-cursor').hostNodes.length).to.equal(0); }); });