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 @@ -17,6 +17,7 @@ export const tippy: typeof tippyCore;

export interface TippySingletonProps extends Partial<Props> {
children: Array<React.ReactElement<any>>;
className?: string;
}

export const TippySingleton: React.FunctionComponent<TippySingletonProps>;
19 changes: 7 additions & 12 deletions src/Tippy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import React, {forwardRef, cloneElement, useState} from 'react';
import PropTypes from 'prop-types';
import {createPortal} from 'react-dom';
import tippy from 'tippy.js';
import {preserveRef, ssrSafeCreateDiv, updateClassName} from './utils';
import {useInstance, useIsomorphicLayoutEffect} from './hooks';
import {preserveRef, ssrSafeCreateDiv} from './utils';
import {
useInstance,
useIsomorphicLayoutEffect,
useUpdateClassName,
} from './hooks';

export function Tippy({
children,
Expand Down Expand Up @@ -83,16 +87,7 @@ export function Tippy({
}
});

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

return (
<>
Expand Down
29 changes: 22 additions & 7 deletions src/TippySingleton.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
import {Children, cloneElement} from 'react';
import PropTypes from 'prop-types';
import {createSingleton} from 'tippy.js';
import {useIsomorphicLayoutEffect, useInstance} from './hooks';
import {
useIsomorphicLayoutEffect,
useInstance,
useUpdateClassName,
} from './hooks';

export default function TippySingleton({children, ...props}) {
export default function TippySingleton({
children,
className,
ignoreAttributes = true,
...restOfNativeProps
}) {
const component = useInstance({
instances: [],
renders: 1,
});

const props = {
ignoreAttributes,
...restOfNativeProps,
};

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

component.singleton = singleton;
component.instance = instance;

return () => {
singleton.destroy();

instance.destroy();
component.instances = instances.filter(i => !i.state.isDestroyed);
};
}, [children.length]);
Expand All @@ -28,9 +41,11 @@ export default function TippySingleton({children, ...props}) {
return;
}

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

useUpdateClassName(component, className);

return Children.map(children, child => {
return cloneElement(child, {
enabled: false,
Expand Down
22 changes: 17 additions & 5 deletions src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import {isBrowser} from './utils';
import {isBrowser, updateClassName} from './utils';
import {useLayoutEffect, useEffect, useRef} from 'react';

export const useIsomorphicLayoutEffect = isBrowser
? useLayoutEffect
: useEffect;

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

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(?)
Expand All @@ -13,7 +29,3 @@ export function useInstance(initialValue) {

return ref.current;
}

export const useIsomorphicLayoutEffect = isBrowser
? useLayoutEffect
: useEffect;
85 changes: 79 additions & 6 deletions test/TippySingleton.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import React from 'react';
import Tippy, {TippySingleton} from '../src';
import Tippy, {TippySingleton as TippySingletonBase} from '../src';
import {render, cleanup} from '@testing-library/react';

jest.useFakeTimers();

afterEach(cleanup);

describe('<TippySingleton />', () => {
let instance;

function TippySingleton(props) {
return <TippySingletonBase {...props} onCreate={i => (instance = i)} />;
}

it('renders without crashing', () => {
render(
<TippySingleton delay={100}>
Expand Down Expand Up @@ -106,11 +112,11 @@ describe('<TippySingleton />', () => {
);
});

it('uses `onCreate` prop', () => {
const spy = jest.fn();
test('props.className: single name is added to tooltip', () => {
const className = 'hello';

render(
<TippySingleton onCreate={spy}>
<TippySingleton className={className}>
<Tippy content="tooltip">
<button />
</Tippy>
Expand All @@ -120,8 +126,75 @@ describe('<TippySingleton />', () => {
</TippySingleton>,
);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].popper).toBeDefined();
expect(instance.popper.querySelector(`.${className}`)).not.toBeNull();
});

test('props.className: multiple names are added to tooltip', () => {
const classNames = 'hello world';

render(
<TippySingleton className={classNames}>
<Tippy content="tooltip">
<button />
</Tippy>
<Tippy content="tooltip">
<button />
</Tippy>
</TippySingleton>,
);

expect(instance.popper.querySelector('.hello')).not.toBeNull();
expect(instance.popper.querySelector('.world')).not.toBeNull();
});

test('props.className: extra whitespace is ignored', () => {
const className = ' hello world ';

render(
<TippySingleton className={className}>
<Tippy content="tooltip">
<button />
</Tippy>
<Tippy content="tooltip">
<button />
</Tippy>
</TippySingleton>,
);

const {tooltip} = instance.popperChildren;

expect(tooltip.className).toBe('tippy-tooltip hello world');
});

test('props.className: updating does not leave stale className behind', () => {
const {rerender} = render(
<TippySingleton className="one">
<Tippy content="tooltip">
<button />
</Tippy>
<Tippy content="tooltip">
<button />
</Tippy>
</TippySingleton>,
);

const {tooltip} = instance.popperChildren;

expect(tooltip.classList.contains('one')).toBe(true);

rerender(
<TippySingleton className="two">
<Tippy content="tooltip">
<button />
</Tippy>
<Tippy content="tooltip">
<button />
</Tippy>
</TippySingleton>,
);

expect(tooltip.classList.contains('one')).toBe(false);
expect(tooltip.classList.contains('two')).toBe(true);
});
});

Expand Down