diff --git a/README.md b/README.md index 79c08a7..c33f13f 100644 --- a/README.md +++ b/README.md @@ -291,18 +291,24 @@ You can nest the components like so: ``` -## 📚 `` +## 📚 Singleton Wraps the [`createSingleton()`](https://atomiks.github.io/tippyjs/addons/#singleton) method. +Depending on your component tree, you can use one of the following: + +### `` + +If each of your reference elements are adjacent to one another, with no nesting in the tree. + ```jsx import Tippy, {TippySingleton} from '@tippy.js/react'; function App() { return ( - + + , + ); + } + + useEffect(() => { + setInterval(() => { + setCount(count => (count === 5 ? 1 : count + 1)); + }, 5000); + }, []); + + return <>{children}; +} + function FollowCursor() { return ( @@ -109,6 +131,8 @@ function App() {

Singleton dynamic children

+

Singleton (via useSingleton hook)

+

Plugins

diff --git a/src/Tippy.js b/src/Tippy.js index e023eeb..9ada843 100644 --- a/src/Tippy.js +++ b/src/Tippy.js @@ -15,12 +15,14 @@ export function Tippy({ className, plugins, visible, + singleton, enabled = true, multiple = true, ignoreAttributes = true, ...restOfNativeProps }) { const isControlledMode = visible !== undefined; + const isSingletonMode = singleton !== undefined; const [mounted, setMounted] = useState(false); const component = useInstance(() => ({ @@ -39,6 +41,10 @@ export function Tippy({ props.trigger = 'manual'; } + if (isSingletonMode) { + enabled = false; + } + // CREATE useIsomorphicLayoutEffect(() => { const instance = tippy(component.ref, props, plugins); @@ -53,6 +59,10 @@ export function Tippy({ instance.show(); } + if (isSingletonMode) { + singleton(instance); + } + setMounted(true); return () => { @@ -116,6 +126,7 @@ if (process.env.NODE_ENV !== 'production') { visible: PropTypes.bool, enabled: PropTypes.bool, className: PropTypes.string, + singleton: PropTypes.func, }; } diff --git a/src/hooks.js b/src/hooks.js index c54b92a..d255e23 100644 --- a/src/hooks.js +++ b/src/hooks.js @@ -1,5 +1,6 @@ import {isBrowser, updateClassName} from './utils'; import {useLayoutEffect, useEffect, useRef} from 'react'; +import {createSingleton} from 'tippy.js'; export const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect @@ -29,3 +30,47 @@ export function useInstance(initialValue) { return ref.current; } + +export function useSingleton({ + className, + ignoreAttributes = true, + ...restOfNativeProps +} = {}) { + const component = useInstance({ + instance: null, + instances: [], + renders: 1, + }); + + const props = { + ignoreAttributes, + ...restOfNativeProps, + }; + + useIsomorphicLayoutEffect(() => { + const {instances} = component; + const instance = createSingleton(instances, props); + + component.instance = instance; + + return () => { + instance.destroy(); + component.instances = instances.filter(i => !i.state.isDestroyed); + }; + }, [component.instances.length]); + + useIsomorphicLayoutEffect(() => { + if (component.renders === 1) { + component.renders++; + return; + } + + component.instance.setProps(props); + }); + + useUpdateClassName(component, className, component.instances.length); + + return instance => { + component.instances.push(instance); + }; +} diff --git a/src/index.js b/src/index.js index d61260a..12a9ebd 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ import tippy from 'tippy.js'; import Tippy from './Tippy'; import TippySingleton from './TippySingleton'; +import {useSingleton} from './hooks'; export default Tippy; -export {TippySingleton, tippy}; +export {TippySingleton, useSingleton, tippy}; diff --git a/test/useSingleton.test.js b/test/useSingleton.test.js new file mode 100644 index 0000000..112d160 --- /dev/null +++ b/test/useSingleton.test.js @@ -0,0 +1,204 @@ +import React from 'react'; +import TippyBase, {useSingleton as useSingletonBase} from '../src'; +import {render, cleanup} from '@testing-library/react'; + +jest.useFakeTimers(); + +afterEach(cleanup); + +describe('The useSingleton hook', () => { + let singletonInstance; + let instances = []; + + const useSingleton = ({onCreate = noop => noop, ...config} = {}) => { + return useSingletonBase({ + onCreate: instance => { + singletonInstance = instance; + onCreate(instance); + }, + ...config, + }); + }; + + const Tippy = ({onCreate = noop => noop, ...props} = {}) => { + return ( + instances.push(i) && onCreate(i)} /> + ); + }; + + beforeEach(() => { + singletonInstance = null; + instances = []; + }); + + it('renders without crashing', () => { + function TestComponent() { + const singleton = useSingleton(); + return ( + <> + +