diff --git a/src/PopperContent.js b/src/PopperContent.js index 47cbd2885..1744e8335 100644 --- a/src/PopperContent.js +++ b/src/PopperContent.js @@ -73,7 +73,7 @@ class PopperContent extends React.Component { } setTargetNode(node) { - this.targetNode = node; + this.targetNode = typeof node === 'string' ? getTarget(node) : node; } getTargetNode() { @@ -177,7 +177,7 @@ class PopperContent extends React.Component { } render() { - this.setTargetNode(getTarget(this.props.target)); + this.setTargetNode(this.props.target); if (this.state.isOpen) { return this.props.container === 'inline' ? diff --git a/src/TooltipPopoverWrapper.js b/src/TooltipPopoverWrapper.js index 29f8feeed..2105645fe 100644 --- a/src/TooltipPopoverWrapper.js +++ b/src/TooltipPopoverWrapper.js @@ -61,11 +61,16 @@ function isInDOMSubtree(element, subtreeRoot) { return subtreeRoot && (element === subtreeRoot || subtreeRoot.contains(element)); } +function isInDOMSubtrees(element, subtreeRoots = []) { + return subtreeRoots && subtreeRoots.length && subtreeRoots.find(subTreeRoot=> isInDOMSubtree(element, subTreeRoot)); +} + class TooltipPopoverWrapper extends React.Component { constructor(props) { super(props); - this._target = null; + this._targets = []; + this.currentTargetElement = null; this.addTargetEvents = this.addTargetEvents.bind(this); this.handleDocumentClick = this.handleDocumentClick.bind(this); this.removeTargetEvents = this.removeTargetEvents.bind(this); @@ -80,7 +85,6 @@ class TooltipPopoverWrapper extends React.Component { this.hide = this.hide.bind(this); this.onEscKeyDown = this.onEscKeyDown.bind(this); this.getRef = this.getRef.bind(this); - this.onClosed = this.onClosed.bind(this); this.state = { isOpen: props.isOpen }; this._isMounted = false; } @@ -93,6 +97,7 @@ class TooltipPopoverWrapper extends React.Component { componentWillUnmount() { this._isMounted = false; this.removeTargetEvents(); + this._targets = null; this.clearShowTimeout(); this.clearHideTimeout(); } @@ -157,6 +162,7 @@ class TooltipPopoverWrapper extends React.Component { show(e) { if (!this.props.isOpen) { this.clearShowTimeout(); + this.currentTargetElement = e && e.target; this.toggle(e); } } @@ -173,6 +179,7 @@ class TooltipPopoverWrapper extends React.Component { hide(e) { if (this.props.isOpen) { this.clearHideTimeout(); + this.currentTargetElement = null; this.toggle(e); } } @@ -201,7 +208,7 @@ class TooltipPopoverWrapper extends React.Component { handleDocumentClick(e) { const triggers = this.props.trigger.split(' '); - if (triggers.indexOf('legacy') > -1 && (this.props.isOpen || isInDOMSubtree(e.target, this._target))) { + if (triggers.indexOf('legacy') > -1 && (this.props.isOpen || isInDOMSubtrees(e.target, this._targets))) { if (this._hideTimeout) { this.clearHideTimeout(); } @@ -210,7 +217,7 @@ class TooltipPopoverWrapper extends React.Component { } else if (!this.props.isOpen) { this.showWithDelay(e); } - } else if (triggers.indexOf('click') > -1 && isInDOMSubtree(e.target, this._target)) { + } else if (triggers.indexOf('click') > -1 && isInDOMSubtrees(e.target, this._targets)) { if (this._hideTimeout) { this.clearHideTimeout(); } @@ -223,6 +230,18 @@ class TooltipPopoverWrapper extends React.Component { } } + addEventOnTargets(type, handler, isBubble) { + this._targets.forEach(target=> { + target.addEventListener(type, handler, isBubble); + }); + } + + removeEventOnTargets(type, handler, isBubble) { + this._targets.forEach(target=> { + target.removeEventListener(type, handler, isBubble); + }); + } + addTargetEvents() { if (this.props.trigger) { let triggers = this.props.trigger.split(' '); @@ -231,54 +250,55 @@ class TooltipPopoverWrapper extends React.Component { document.addEventListener('click', this.handleDocumentClick, true); } - if (this._target) { + if (this._targets && this._targets.length) { if (triggers.indexOf('hover') > -1) { - this._target.addEventListener( + this.addEventOnTargets( 'mouseover', this.showWithDelay, true ); - this._target.addEventListener( + this.addEventOnTargets( 'mouseout', this.hideWithDelay, true ); } if (triggers.indexOf('focus') > -1) { - this._target.addEventListener('focusin', this.show, true); - this._target.addEventListener('focusout', this.hide, true); + this.addEventOnTargets('focusin', this.show, true); + this.addEventOnTargets('focusout', this.hide, true); } - this._target.addEventListener('keydown', this.onEscKeyDown, true); + this.addEventOnTargets('keydown', this.onEscKeyDown, true); } } } } removeTargetEvents() { - if (this._target) { - this._target.removeEventListener( + if (this._targets) { + this.removeEventOnTargets( 'mouseover', this.showWithDelay, true ); - this._target.removeEventListener( + this.removeEventOnTargets( 'mouseout', this.hideWithDelay, true ); - this._target.removeEventListener('keydown', this.onEscKeyDown, true); - this._target.removeEventListener('focusin', this.show, true); - this._target.removeEventListener('focusout', this.hide, true); + this.removeEventOnTargets('keydown', this.onEscKeyDown, true); + this.removeEventOnTargets('focusin', this.show, true); + this.removeEventOnTargets('focusout', this.hide, true); } document.removeEventListener('click', this.handleDocumentClick, true) } updateTarget() { - const newTarget = getTarget(this.props.target); - if (newTarget !== this._target) { + const newTarget = getTarget(this.props.target, true); + if (newTarget !== this._targets) { this.removeTargetEvents(); - this._target = newTarget; + this._targets = newTarget ? Array.from(newTarget) : []; + this.currentTargetElement = this.currentTargetElement || this._targets[0]; this.addTargetEvents(); } } @@ -287,16 +307,12 @@ class TooltipPopoverWrapper extends React.Component { if (this.props.disabled || !this._isMounted) { return e && e.preventDefault(); } - + return this.props.toggle(e); } - onClosed() { - this.setState({ isOpen: false }); - } - render() { - if (!this.state.isOpen) { + if (!this.props.isOpen) { return null; } @@ -330,7 +346,7 @@ class TooltipPopoverWrapper extends React.Component { return ( diff --git a/src/__tests__/PopperContent.spec.js b/src/__tests__/PopperContent.spec.js index f817439ad..a0607cb1c 100644 --- a/src/__tests__/PopperContent.spec.js +++ b/src/__tests__/PopperContent.spec.js @@ -42,6 +42,14 @@ describe('PopperContent', () => { expect(wrapper.text()).toBe('Yo!'); }); + it('should render children when isOpen is true and container is inline and DOM node passed directly for target', () => { + const targetElement = element.querySelector('#target'); + + const wrapper = mount(Yo!); + expect(targetElement).toBeDefined(); + expect(wrapper.text()).toBe('Yo!'); + }); + it('should render an Arrow in the Popper when isOpen is true and container is inline', () => { const wrapper = mount(Yo!); diff --git a/src/__tests__/TooltipPopoverWrapper.spec.js b/src/__tests__/TooltipPopoverWrapper.spec.js index 540b6ad6d..590df7581 100644 --- a/src/__tests__/TooltipPopoverWrapper.spec.js +++ b/src/__tests__/TooltipPopoverWrapper.spec.js @@ -375,6 +375,44 @@ describe('Tooltip', () => { wrapper.detach(); }); + describe('multi target', () => { + let targets, targetContainer; + beforeEach(() => { + targetContainer = document.createElement('div'); + targetContainer.innerHTML = `Target 1Target 2` + element.appendChild(targetContainer); + targets = targetContainer.querySelectorAll('.example'); + }); + + afterEach(() => { + element.removeChild(targetContainer); + targets = null; + }); + + it("should attach tooltip on multiple target when a target selector matches multiple elements", () => { + const wrapper = mount( + Yo!, + { attachTo: container }); + + targets[0].dispatchEvent(new Event('click')); + jest.runTimersToTime(0) + expect(isOpen).toBe(true); + + targets[0].dispatchEvent(new Event('click')); + jest.runTimersToTime(0) + expect(isOpen).toBe(false); + + targets[1].dispatchEvent(new Event('click')); + jest.runTimersToTime(0) + expect(isOpen).toBe(true); + + targets[1].dispatchEvent(new Event('click')); + jest.runTimersToTime(0) + expect(isOpen).toBe(false); + wrapper.detach(); + }); + }); + describe('delay', () => { it('should accept a number', () => { isOpen = true; diff --git a/src/__tests__/utils.spec.js b/src/__tests__/utils.spec.js index 58197a473..3b90b8b17 100644 --- a/src/__tests__/utils.spec.js +++ b/src/__tests__/utils.spec.js @@ -116,6 +116,20 @@ describe('Utils', () => { expect(spy).toHaveBeenCalled(); }); + it('should return all matching elements if allElement param is true', () => { + const element = document.createElement('div'); + element.innerHTML = `span 1 + span 2`; + document.body.appendChild(element); + + jest.spyOn(document, 'querySelectorAll'); + const elements = Utils.getTarget('.example', true); + expect(elements.length).toEqual(2); + expect(elements[1].textContent).toEqual('span 2'); + expect(document.querySelectorAll).toHaveBeenCalledWith('.example'); + document.querySelectorAll.mockRestore(); + }); + it('should query the document for the target if the target is a string', () => { const element = document.createElement('div'); element.className = 'thing'; diff --git a/src/utils.js b/src/utils.js index 5853fade0..52447d35e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -299,9 +299,9 @@ export function isArrayOrNodeList(els) { return Array.isArray(els) || (canUseDOM && typeof els.length === 'number'); } -export function getTarget(target) { +export function getTarget(target, allElements) { const els = findDOMElements(target); - if (isArrayOrNodeList(els)) { + if (isArrayOrNodeList(els) && !allElements) { return els[0]; } return els;