Skip to content
This repository was archived by the owner on Nov 9, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const tippy: typeof tippyCore;

export interface TippySingletonProps extends Partial<KnownProps> {
children: Array<React.ReactElement<any>>;
enabled?: boolean;
className?: string;
plugins?: Plugin[];
[key: string]: any;
Expand Down
2 changes: 1 addition & 1 deletion src/Tippy.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
useInstance,
useIsomorphicLayoutEffect,
useUpdateClassName,
} from './hooks';
} from './util-hooks';

export function Tippy({
children,
Expand Down
32 changes: 8 additions & 24 deletions src/TippySingleton.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import {Children, cloneElement} from 'react';
import PropTypes from 'prop-types';
import {createSingleton} from 'tippy.js';
import {
useIsomorphicLayoutEffect,
useInstance,
useUpdateClassName,
} from './hooks';
useSingletonUpdate,
useSingletonCreate,
} from './util-hooks';

export default function TippySingleton({
children,
className,
plugins,
enabled = true,
ignoreAttributes = true,
...restOfNativeProps
}) {
Expand All @@ -24,28 +25,11 @@ export default function TippySingleton({
...restOfNativeProps,
};

useIsomorphicLayoutEffect(() => {
const {instances} = component;
const instance = createSingleton(instances, props, plugins);
const deps = [children.length];

component.instance = instance;

return () => {
instance.destroy();
component.instances = instances.filter(i => !i.state.isDestroyed);
};
}, [children.length]);

useIsomorphicLayoutEffect(() => {
if (component.renders === 1) {
component.renders++;
return;
}

component.instance.setProps(props);
});

useUpdateClassName(component, className, children.length);
useSingletonCreate(component, props, plugins, enabled, deps);
useSingletonUpdate(component, props, enabled);
useUpdateClassName(component, className, deps);

return Children.map(children, child => {
return cloneElement(child, {
Expand Down
64 changes: 11 additions & 53 deletions src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,14 @@
import {isBrowser, updateClassName} from './utils';
import {useLayoutEffect, useEffect, useRef} from 'react';
import {createSingleton} from 'tippy.js';

export const useIsomorphicLayoutEffect = isBrowser
? useLayoutEffect
: useEffect;

export function useUpdateClassName(component, className, childrenDep) {
useIsomorphicLayoutEffect(() => {
const {tooltip} = component.instance.popperChildren;
if (className) {
updateClassName(tooltip, 'add', className);
return () => {
updateClassName(tooltip, 'remove', className);
};
}
}, [className, childrenDep]);
}

export function useInstance(initialValue) {
// Using refs instead of state as it's recommended to not store imperative
// values in state due to memory problems in React(?)
const ref = useRef();

if (!ref.current) {
ref.current =
typeof initialValue === 'function' ? initialValue() : initialValue;
}

return ref.current;
}
import {
useInstance,
useSingletonCreate,
useSingletonUpdate,
useUpdateClassName,
} from './util-hooks';

export function useSingleton({
className,
plugins,
enabled = true,
ignoreAttributes = true,
...restOfNativeProps
} = {}) {
Expand All @@ -48,28 +23,11 @@ export function useSingleton({
...restOfNativeProps,
};

useIsomorphicLayoutEffect(() => {
const {instances} = component;
const instance = createSingleton(instances, props, plugins);

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);
});
const deps = [component.instances.length];

useUpdateClassName(component, className, component.instances.length);
useSingletonCreate(component, props, plugins, enabled, deps);
useSingletonUpdate(component, props, enabled);
useUpdateClassName(component, className, deps);

return instance => {
component.instances.push(instance);
Expand Down
69 changes: 69 additions & 0 deletions src/util-hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {isBrowser, updateClassName} from './utils';
import {useLayoutEffect, useEffect, useRef} from 'react';
import {createSingleton} from 'tippy.js';

export const useIsomorphicLayoutEffect = isBrowser
? useLayoutEffect
: useEffect;

export function useUpdateClassName(component, className, deps) {
useIsomorphicLayoutEffect(() => {
const {tooltip} = component.instance.popperChildren;
if (className) {
updateClassName(tooltip, 'add', className);
return () => {
updateClassName(tooltip, 'remove', className);
};
}
}, [className, ...deps]);
}

export function useInstance(initialValue) {
// Using refs instead of state as it's recommended to not store imperative
// values in state due to memory problems in React(?)
const ref = useRef();

if (!ref.current) {
ref.current =
typeof initialValue === 'function' ? initialValue() : initialValue;
}

return ref.current;
}

export function useSingletonCreate(component, props, plugins, enabled, deps) {
useIsomorphicLayoutEffect(() => {
const {instances} = component;
const instance = createSingleton(instances, props, plugins);

component.instance = instance;

if (!enabled) {
instance.disable();
}

return () => {
instance.destroy();
component.instances = instances.filter(i => !i.state.isDestroyed);
};
}, deps);
}

export function useSingletonUpdate(component, props, enabled) {
useIsomorphicLayoutEffect(() => {
if (component.renders === 1) {
component.renders++;
return;
}

const {instance} = component;

instance.setProps(props);

if (enabled) {
instance.enable();
} else {
instance.disable();
}
});
}
56 changes: 56 additions & 0 deletions test/TippySingleton.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,62 @@ describe('<TippySingleton />', () => {
expect(tooltip.classList.contains('one')).toBe(true);
});

test('props.enabled initially `true`', () => {
const {rerender} = render(
<TippySingleton enabled={true}>
<Tippy content="tooltip">
<button />
</Tippy>
<Tippy content="tooltip">
<button />
</Tippy>
</TippySingleton>,
);

expect(instance.state.isEnabled).toBe(true);

rerender(
<TippySingleton enabled={false}>
<Tippy content="tooltip">
<button />
</Tippy>
<Tippy content="tooltip">
<button />
</Tippy>
</TippySingleton>,
);

expect(instance.state.isEnabled).toBe(false);
});

test('props.enabled initially `false`', () => {
const {rerender} = render(
<TippySingleton enabled={false}>
<Tippy content="tooltip">
<button />
</Tippy>
<Tippy content="tooltip">
<button />
</Tippy>
</TippySingleton>,
);

expect(instance.state.isEnabled).toBe(false);

rerender(
<TippySingleton enabled={true}>
<Tippy content="tooltip">
<button />
</Tippy>
<Tippy content="tooltip">
<button />
</Tippy>
</TippySingleton>,
);

expect(instance.state.isEnabled).toBe(true);
});

test('props.plugins', () => {
const plugins = [{fn: () => ({})}];

Expand Down
54 changes: 54 additions & 0 deletions test/useSingleton.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,60 @@ describe('The useSingleton hook', () => {
).toBe(true);
});

test('props.enabled initially `true`', () => {
let instance;

function App({enabled}) {
const singleton = useSingleton({
enabled,
onCreate(i) {
instance = i;
},
});

return (
<Tippy singleton={singleton}>
<button />
</Tippy>
);
}

const {rerender} = render(<App enabled={true} />);

expect(instance.state.isEnabled).toBe(true);

rerender(<App enabled={false} />);

expect(instance.state.isEnabled).toBe(false);
});

test('props.enabled initially `false`', () => {
let instance;

function App({enabled}) {
const singleton = useSingleton({
enabled,
onCreate(i) {
instance = i;
},
});

return (
<Tippy singleton={singleton}>
<button />
</Tippy>
);
}

const {rerender} = render(<App enabled={false} />);

expect(instance.state.isEnabled).toBe(false);

rerender(<App enabled={true} />);

expect(instance.state.isEnabled).toBe(true);
});

test('props.plugins', () => {
const plugins = [{fn: () => ({})}];

Expand Down