Skip to content

Commit

Permalink
Add show, showNext, showPrevious methods to singletons (#892)
Browse files Browse the repository at this point in the history
* feat: Add `show`, `showNext`, `showPrevious` methods to singletons

- Singletons now support calling `show` method with 3 overloads
  - Specific tippy instance
  - Specific reference element
  - Index number (relative to references array)
  - Loads first tippy instance if no argument is passed (this was broken)
- Introduced `showNext` and `showPrevious` methods
  - Both methods will cycle once they reach edges
- `showOnCreate` now works correctly for singletons
- Updated addons.mdx docs

* test: added ITs for singleton show, showNext, showPrevious methods and showOnCreate prop

Co-authored-by: Theodoros Antoniou <dogoku@gmail.com>
  • Loading branch information
theo-staizen and dogoku committed Feb 13, 2021
1 parent 38caf47 commit f98615b
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 25 deletions.
128 changes: 103 additions & 25 deletions src/addons/createSingleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ const createSingleton: CreateSingleton = (

let individualInstances = tippyInstances;
let references: Array<ReferenceElement> = [];
let currentTarget: Element;
let currentTarget: Element | null;
let overrides = optionalProps.overrides;
let interceptSetPropsCleanups: Array<() => void> = [];
let shownOnCreate = false;

function setReferences(): void {
references = individualInstances.map((instance) => instance.reference);
Expand Down Expand Up @@ -66,6 +67,36 @@ const createSingleton: CreateSingleton = (
});
}

// have to pass singleton, as it maybe undefined on first call
function prepareInstance(
singleton: Instance,
target: ReferenceElement
): void {
const index = references.indexOf(target);

// bail-out
if (target === currentTarget) {
return;
}

currentTarget = target;

const overrideProps: Partial<Props> = (overrides || [])
.concat('content')
.reduce((acc, prop) => {
(acc as any)[prop] = individualInstances[index].props[prop];
return acc;
}, {});

singleton.setProps({
...overrideProps,
getReferenceClientRect:
typeof overrideProps.getReferenceClientRect === 'function'
? overrideProps.getReferenceClientRect
: (): ClientRect => target.getBoundingClientRect(),
});
}

enableInstances(false);
setReferences();

Expand All @@ -75,31 +106,23 @@ const createSingleton: CreateSingleton = (
onDestroy(): void {
enableInstances(true);
},
onTrigger(instance, event): void {
const target = event.currentTarget as Element;
const index = references.indexOf(target);

// bail-out
if (target === currentTarget) {
return;
onHidden(): void {
currentTarget = null;
},
onClickOutside(instance): void {
if (instance.props.showOnCreate && !shownOnCreate) {
shownOnCreate = true;
currentTarget = null;
}
},
onShow(instance): void {
if (instance.props.showOnCreate && !shownOnCreate) {
shownOnCreate = true;
prepareInstance(instance, references[0]);
}

currentTarget = target;

const overrideProps: Partial<Props> = (overrides || [])
.concat('content')
.reduce((acc, prop) => {
(acc as any)[prop] = individualInstances[index].props[prop];
return acc;
}, {});

instance.setProps({
...overrideProps,
getReferenceClientRect:
typeof overrideProps.getReferenceClientRect === 'function'
? overrideProps.getReferenceClientRect
: (): ClientRect => target.getBoundingClientRect(),
});
},
onTrigger(instance, event): void {
prepareInstance(instance, event.currentTarget as Element);
},
};
},
Expand All @@ -111,6 +134,61 @@ const createSingleton: CreateSingleton = (
triggerTarget: references,
}) as CreateSingletonInstance<CreateSingletonProps>;

const originalShow = singleton.show;

singleton.show = (target?: ReferenceElement | Instance | number): void => {
originalShow();

// first time, showOnCreate or programmatic call with no params
// default to showing first instance
if (!currentTarget && target == null) {
return prepareInstance(singleton, references[0]);
}

// triggered from event (do nothing as prepareInstance already called by onTrigger)
// programmatic call with no params when already visible (do nothing again)
if (currentTarget && target == null) {
return;
}

// target is index of instance
if (typeof target === 'number') {
return (
references[target] && prepareInstance(singleton, references[target])
);
}

// target is a child tippy instance
if (individualInstances.includes(target as Instance)) {
const ref = (target as Instance).reference;
return prepareInstance(singleton, ref);
}

// target is a ReferenceElement
if (references.includes(target as ReferenceElement)) {
return prepareInstance(singleton, target as ReferenceElement);
}
};

singleton.showNext = (): void => {
const first = references[0];
if (!currentTarget) {
return singleton.show(0);
}
const index = references.indexOf(currentTarget);
singleton.show(references[index + 1] || first);
};

singleton.showPrevious = (): void => {
const last = references[references.length - 1];
if (!currentTarget) {
return singleton.show(last);
}
const index = references.indexOf(currentTarget);
const target = references[index - 1] || last;
singleton.show(target);
};

const originalSetProps = singleton.setProps;

singleton.setProps = (props): void => {
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ export type CreateSingletonInstance<TProps = CreateSingletonProps> = Instance<
TProps
> & {
setInstances(instances: Instance<any>[]): void;
show(target?: ReferenceElement | Instance | number): void;
showNext(): void;
showPrevious(): void;
};

export type CreateSingleton<TProps = Props> = (
Expand Down
156 changes: 156 additions & 0 deletions test/integration/addons/createSingleton.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,159 @@ describe('.setInstances() method', () => {
expect(singleton.state.isVisible).toBe(true);
});
});

describe('.show() method', () => {
const getInstances = () =>
[{content: 'first'}, {content: 'second'}, {content: 'last'}].map((props) =>
tippy(h(), props)
);

it('shows the first tippy instance when no parameters are passed', () => {
const singletonInstance = createSingleton(getInstances());

singletonInstance.show();

expect(singletonInstance.state.isVisible).toBe(true);
expect(singletonInstance.props.content).toBe('first');
});

it('shows the tippy instance passed as an argument', () => {
const tippyInstances = getInstances();
const singletonInstance = createSingleton(tippyInstances);

singletonInstance.show(tippyInstances[1]);

expect(singletonInstance.state.isVisible).toBe(true);
expect(singletonInstance.props.content).toBe('second');
});

it('shows the tippy instance related to the reference element passed as an argument', () => {
const tippyInstances = getInstances();
const singletonInstance = createSingleton(tippyInstances);

singletonInstance.show(tippyInstances[1].reference);

expect(singletonInstance.state.isVisible).toBe(true);
expect(singletonInstance.props.content).toBe('second');
});

it('shows the tippy instance at the given index number', () => {
const singletonInstance = createSingleton(getInstances());

singletonInstance.show(1);

expect(singletonInstance.state.isVisible).toBe(true);
expect(singletonInstance.props.content).toBe('second');
});
});

describe('.showNext() method', () => {
const getInstances = () =>
[{content: 'first'}, {content: 'second'}, {content: 'last'}].map((props) =>
tippy(h(), props)
);

it('shows the first tippy instance if none is visible', () => {
const singletonInstance = createSingleton(getInstances());

singletonInstance.showNext();

expect(singletonInstance.state.isVisible).toBe(true);
expect(singletonInstance.props.content).toBe('first');
});
it('shows the tippy instance after the currently visible one', () => {
const singletonInstance = createSingleton(getInstances());

singletonInstance.show();

expect(singletonInstance.props.content).toBe('first');
singletonInstance.showNext();
expect(singletonInstance.props.content).toBe('second');
singletonInstance.showNext();
expect(singletonInstance.props.content).toBe('last');
});
it('loops to the beginning if the last instance is visible', () => {
const singletonInstance = createSingleton(getInstances());

singletonInstance.show(2);

expect(singletonInstance.props.content).toBe('last');
singletonInstance.showNext();
expect(singletonInstance.props.content).toBe('first');
});
});

describe('.showPrevious() method', () => {
const getInstances = () =>
[{content: 'first'}, {content: 'second'}, {content: 'last'}].map((props) =>
tippy(h(), props)
);

it('shows the last tippy instance if none is visible', () => {
const singletonInstance = createSingleton(getInstances());

singletonInstance.showPrevious();

expect(singletonInstance.state.isVisible).toBe(true);
expect(singletonInstance.props.content).toBe('last');
});
it('shows the tippy instance before the currently visible one', () => {
const singletonInstance = createSingleton(getInstances(), {
showOnCreate: true,
});

singletonInstance.hide();
singletonInstance.show(2);

expect(singletonInstance.props.content).toBe('last');
singletonInstance.showPrevious();
expect(singletonInstance.props.content).toBe('second');
singletonInstance.showPrevious();
expect(singletonInstance.props.content).toBe('first');
});
it('loops to the end if the first instance is visible', () => {
const singletonInstance = createSingleton(getInstances());

singletonInstance.show();

expect(singletonInstance.props.content).toBe('first');
singletonInstance.showPrevious();
expect(singletonInstance.props.content).toBe('last');
});
});

describe('showOnCreate prop', () => {
it('shows the first tippy instance on creation', () => {
const tippyInstances = [
{content: 'first'},
{content: 'second'},
].map((props) => tippy(h(), props));

const singletonInstance = createSingleton(tippyInstances, {
showOnCreate: true,
});

expect(singletonInstance.state.isVisible).toBe(true);
expect(singletonInstance.props.content).toBe('first');
});

it('resets correctly if showOnCreate is cancelled by a click outside', () => {
const tippyInstances = [
{content: 'first'},
{content: 'second'},
].map((props) => tippy(h(), props));

const singletonInstance = createSingleton(tippyInstances, {
showOnCreate: true,
delay: 500,
});

fireEvent.mouseDown(document.body);
jest.runAllTimers();
fireEvent.mouseEnter(tippyInstances[1].reference);
jest.runAllTimers();

expect(singletonInstance.state.isVisible).toBe(true);
expect(singletonInstance.props.content).toBe('second');
});
});
1 change: 1 addition & 0 deletions test/visual/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ tests.createSingleton = () => {
const singleton = createSingleton(instances, {
delay: 500,
overrides: ['placement', 'duration'],
showOnCreate: true,
});

instances = instances.concat(
Expand Down
40 changes: 40 additions & 0 deletions website/src/pages/v6/addons.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,46 @@ const singleton = createSingleton(tippyInstances, {
});
```

### Showing specific tippy instance

The `.show()` method of singleton accepts an additional parameter:

```js
// Show first child tippy instance if no parameter given
singleton.show();

// Show given child tippy instance
singleton.show(tippyInstances[1]);

// Show child tippy instance related to given reference element
singleton.show(document.querySelector('button'));

// Show child tippy instance at given index
singleton.show(2); // i.e equivalent to passing tippyInstances[2]
```

### Show instances in order

The `.showNext()` and `showPrevious()` methods allow you to loop through and
show the child tippy instances in forward or reverse order respectively,
relative to `tippyInstances` array given in `createSingleton`

```js
// if no child tippy is shown, show first one, otherwise show the next one
singleton.showNext();

// if no child tippy is shown, show last one, otherwise show the previous one
singleton.showPrevious();
```

Both methods will loop to the other end, like pac-man

```js
singleton.show(0); // show first
singleton.showPrevious(); // loops back and shows last item
singleton.showNext(); // loops to the front and shows first item
```

#### Update

You can update the singleton's instances with the `.setInstances()` method:
Expand Down

0 comments on commit f98615b

Please sign in to comment.