Repository: atomiks/tippyjs (language: javascript)

Issue:
triggerTarget override doesn't work with a singleton
## Bug description

I want to set the `triggerTarget` prop for each tippy in a singleton individually. however the tippys don't trigger despite passing `overrides: ["triggerTarget"]` to `createSingleton()`.

## Reproduction

<!-- Please create a CodePen to reproduce the bug. It can be difficult to understand the problem otherwise. It should be reproduced exclusively using Tippy.js on CodePen. -->

CodePen link: https://codepen.io/thoughtsofcode/pen/poyddRM



Discussion / hints:

Fix patch:
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<ReferenceElement> = [];
+  let triggerTargets: Array<Element> = [];
   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 08c857ec..850cfa89 100644
--- a/src/createTippy.ts
+++ b/src/createTippy.ts
@@ -313,7 +313,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;
       }


Test patch:
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');
+  });
 });