From c9de8f764e30151105ec3f2591fddb1e73e8dea6 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 9 Oct 2019 22:01:14 -0400 Subject: [PATCH 1/6] useSingleton --- demo/index.js | 26 +++++++++++++++++++++++++- src/Tippy.js | 11 +++++++++++ src/hooks.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 3 ++- 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/demo/index.js b/demo/index.js index a4374f4..0cd1ce8 100644 --- a/demo/index.js +++ b/demo/index.js @@ -1,6 +1,6 @@ import React, {useState, useEffect} from 'react'; import ReactDOM from 'react-dom'; -import Tippy, {TippySingleton} from '../src'; +import Tippy, {TippySingleton, useSingleton} from '../src'; import {followCursor} from 'tippy.js'; import 'tippy.js/dist/tippy.css'; @@ -90,6 +90,28 @@ function Singleton() { return {children}; } +function SingletonHook() { + const singleton = useSingleton({delay: 500}); + const [count, setCount] = useState(3); + + let children = []; + for (let i = 0; i < count; i++) { + children.push( + + + , + ); + } + + 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..74fa858 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,46 @@ 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(() => { + component.instance = createSingleton(component.instances, props); + + return () => { + component.instance.destroy(); + component.instances = component.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}; From 6147b596fb3f7f4371ebe0771ac0c60642504e62 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 11 Oct 2019 10:23:29 -0400 Subject: [PATCH 2/6] Added tests --- src/hooks.js | 5 +- test/useSingleton.test.js | 189 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 test/useSingleton.test.js diff --git a/src/hooks.js b/src/hooks.js index 74fa858..9a885e5 100644 --- a/src/hooks.js +++ b/src/hooks.js @@ -34,8 +34,9 @@ export function useInstance(initialValue) { export function useSingleton({ className, ignoreAttributes = true, + onComponent = noop => noop, ...restOfNativeProps -}) { +} = {}) { const component = useInstance({ instance: null, instances: [], @@ -50,6 +51,8 @@ export function useSingleton({ useIsomorphicLayoutEffect(() => { component.instance = createSingleton(component.instances, props); + onComponent(component); + return () => { component.instance.destroy(); component.instances = component.instances.filter( diff --git a/test/useSingleton.test.js b/test/useSingleton.test.js new file mode 100644 index 0000000..0e85293 --- /dev/null +++ b/test/useSingleton.test.js @@ -0,0 +1,189 @@ +import React from 'react'; +import Tippy, {useSingleton as useSingletonBase} from '../src'; +import {render, cleanup} from '@testing-library/react'; + +jest.useFakeTimers(); + +afterEach(cleanup); + +describe('The useSingleton hook', () => { + let component; + + const useSingleton = config => { + return useSingletonBase({ + onComponent: c => (component = c), + ...config, + }); + }; + + it('renders without crashing', () => { + function TestComponent() { + const singleton = useSingleton(); + return ( + <> + +