From 9f269fb1487549064830bcaec5a35c6158d1049a Mon Sep 17 00:00:00 2001 From: James N Date: Fri, 5 Nov 2021 21:30:41 +1100 Subject: [PATCH] fix(createSingleton): touch and triggerTarget (#999) --- src/addons/createSingleton.ts | 21 ++++-- src/createTippy.ts | 6 +- .../addons/createSingleton.test.js | 71 ++++++++++++++++--- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/addons/createSingleton.ts b/src/addons/createSingleton.ts index a4aa301f..d76dad33 100644 --- a/src/addons/createSingleton.ts +++ b/src/addons/createSingleton.ts @@ -9,7 +9,7 @@ import { Instance, Props, } from '../types'; -import {removeProperties} from '../utils'; +import {normalizeToArray, removeProperties} from '../utils'; import {errorWhen} from '../validation'; import {applyStyles, Modifier} from '@popperjs/core'; @@ -63,11 +63,20 @@ const createSingleton: CreateSingleton = ( let individualInstances = tippyInstances; let references: Array = []; + let triggerTargets: Array = []; let currentTarget: Element | null; let overrides = optionalProps.overrides; let interceptSetPropsCleanups: Array<() => void> = []; let shownOnCreate = false; + function setTriggerTargets(): void { + triggerTargets = individualInstances + .map((instance) => + normalizeToArray(instance.props.triggerTarget || instance.reference) + ) + .reduce((acc, item) => acc.concat(item), []); + } + function setReferences(): void { references = individualInstances.map((instance) => instance.reference); } @@ -105,7 +114,7 @@ const createSingleton: CreateSingleton = ( singleton: Instance, target: ReferenceElement ): void { - const index = references.indexOf(target); + const index = triggerTargets.indexOf(target); // bail-out if (target === currentTarget) { @@ -126,12 +135,13 @@ const createSingleton: CreateSingleton = ( getReferenceClientRect: typeof overrideProps.getReferenceClientRect === 'function' ? overrideProps.getReferenceClientRect - : (): ClientRect => target.getBoundingClientRect(), + : (): ClientRect => references[index].getBoundingClientRect(), }); } enableInstances(false); setReferences(); + setTriggerTargets(); const plugin: Plugin = { fn() { @@ -164,7 +174,7 @@ const createSingleton: CreateSingleton = ( const singleton = tippy(div(), { ...removeProperties(optionalProps, ['overrides']), plugins: [plugin, ...(optionalProps.plugins || [])], - triggerTarget: references, + triggerTarget: triggerTargets, popperOptions: { ...optionalProps.popperOptions, modifiers: [ @@ -244,9 +254,10 @@ const createSingleton: CreateSingleton = ( enableInstances(false); setReferences(); + setTriggerTargets(); interceptSetPropsCleanups = interceptSetProps(singleton); - singleton.setProps({triggerTarget: references}); + singleton.setProps({triggerTarget: triggerTargets}); }; interceptSetPropsCleanups = interceptSetProps(singleton); diff --git a/src/createTippy.ts b/src/createTippy.ts index a90a76d9..c9d3d4a5 100644 --- a/src/createTippy.ts +++ b/src/createTippy.ts @@ -312,7 +312,11 @@ export default function createTippy( } // Clicked on the event listeners target - if (actualContains(getCurrentTarget(), actualTarget as Element)) { + if ( + normalizeToArray(instance.props.triggerTarget || reference).indexOf( + actualTarget as Element + ) !== -1 + ) { if (currentInput.isTouch) { return; } diff --git a/test/integration/addons/createSingleton.test.js b/test/integration/addons/createSingleton.test.js index 7ccf1ee6..8940d0e0 100644 --- a/test/integration/addons/createSingleton.test.js +++ b/test/integration/addons/createSingleton.test.js @@ -4,6 +4,7 @@ import {h} from '../../utils'; import createSingleton from '../../../src/addons/createSingleton'; import tippy from '../../../src'; import {getFormattedMessage} from '../../../src/validation'; +import {currentInput} from '../../../src/bindGlobalEventListeners'; describe('createSingleton', () => { it('shows when a tippy instance reference is triggered', () => { @@ -416,10 +417,9 @@ describe('.showPrevious() method', () => { describe('showOnCreate prop', () => { it('shows the first tippy instance on creation', () => { - const tippyInstances = [ - {content: 'first'}, - {content: 'second'}, - ].map((props) => tippy(h(), props)); + const tippyInstances = [{content: 'first'}, {content: 'second'}].map( + (props) => tippy(h(), props) + ); const singletonInstance = createSingleton(tippyInstances, { showOnCreate: true, @@ -430,10 +430,9 @@ describe('showOnCreate prop', () => { }); it('resets correctly if showOnCreate is cancelled by a click outside', () => { - const tippyInstances = [ - {content: 'first'}, - {content: 'second'}, - ].map((props) => tippy(h(), props)); + const tippyInstances = [{content: 'first'}, {content: 'second'}].map( + (props) => tippy(h(), props) + ); const singletonInstance = createSingleton(tippyInstances, { showOnCreate: true, @@ -448,4 +447,60 @@ describe('showOnCreate prop', () => { expect(singletonInstance.state.isVisible).toBe(true); expect(singletonInstance.props.content).toBe('second'); }); + + it('does not hide tippy upon clicking next target', () => { + currentInput.isTouch = true; + + const tippyInstances = [{content: 'first'}, {content: 'second'}].map( + (props) => tippy(h(), props) + ); + + const singletonInstance = createSingleton(tippyInstances); + + fireEvent.mouseEnter(tippyInstances[0].reference); + fireEvent.mouseDown(document.body); + fireEvent.mouseEnter(tippyInstances[1].reference); + + jest.runAllTimers(); + + expect(singletonInstance.state.isVisible).toBe(true); + expect(singletonInstance.props.content).toBe('second'); + + currentInput.isTouch = false; + }); + + it('accepts custom `triggerTarget` in individual instances', () => { + const ref1 = h(); + const ref2 = h(); + const triggerTarget1 = h(); + const triggerTarget2 = h(); + + const tippyInstances = [ + {ref: ref1, content: 'first', triggerTarget: triggerTarget1}, + {ref: ref2, content: 'second', triggerTarget: triggerTarget2}, + ].map(({ref, ...props}) => tippy(ref, props)); + + const singletonInstance = createSingleton(tippyInstances); + + fireEvent.mouseEnter(ref1); + jest.runAllTimers(); + expect(singletonInstance.state.isVisible).toBe(false); + + fireEvent.mouseEnter(triggerTarget1); + jest.runAllTimers(); + + expect(singletonInstance.state.isVisible).toBe(true); + expect(singletonInstance.props.content).toBe('first'); + + fireEvent.mouseEnter(triggerTarget2); + jest.runAllTimers(); + + expect(singletonInstance.state.isVisible).toBe(true); + expect(singletonInstance.props.content).toBe('second'); + + fireEvent.mouseEnter(ref1); + jest.runAllTimers(); + + expect(singletonInstance.props.content).toBe('second'); + }); });