Skip to content
This repository was archived by the owner on Apr 14, 2020. It is now read-only.

Commit 3dc3413

Browse files
authored
Merge pull request #116 from madou/animator-triggering-self
feat: adds triggerSelfKey to animator component
2 parents f6f55b7 + 6ad648e commit 3dc3413

File tree

25 files changed

+411
-73
lines changed

25 files changed

+411
-73
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# yubaba 🧙✨
22

3-
/juːba:ba/ out of the box animated experiences for [React.js](https://reactjs.org/) 🧙✨
3+
/juːbaːba/ out of the box animated experiences for [React.js](https://reactjs.org/) 🧙✨
44

55
[![npm](https://img.shields.io/npm/v/yubaba.svg)](https://www.npmjs.com/package/yubaba) [![npm bundle size (minified + gzip)](https://badgen.net/bundlephobia/minzip/yubaba)](https://bundlephobia.com/result?p=yubaba)
66

@@ -44,7 +44,7 @@ yarn add yubaba react@^16.4.x react-dom@^16.4.x emotion@^10.x.x
4444
import Animator, { Move } from 'yubaba';
4545

4646
({ isLarge }) => (
47-
<Animator name="my-first-baba" key={isLarge}>
47+
<Animator name="my-first-baba" triggerSelfKey={isLarge}>
4848
<Move>{anim => <div {...anim} className={isLarge ? 'large' : 'small'} />}</Move>
4949
</Animator>
5050
);

doczrc.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const primaryText = 'rgba(255, 255, 255, 0.95)';
77
const background = `linear-gradient(135deg, ${altPrimary} 25%, ${primary} 100%)`;
88

99
module.exports = {
10-
title: 'yubaba 🧙✨',
10+
title: `yubaba ${pkg.description}`,
1111
description: `yubaba ${pkg.description}`,
1212
typescript: true,
1313
dest: '/docs',
@@ -44,6 +44,7 @@ module.exports = {
4444
display: none;
4545
}
4646
47+
a[class^='MenuLink__LinkAnchor-'],
4748
a[class^='MenuLink__createLink-'] {
4849
font-weight: 400;
4950
@@ -55,6 +56,11 @@ module.exports = {
5556
5657
a[class^='SmallLink__Link'] {
5758
opacity: 0.65;
59+
60+
:hover,
61+
:focus {
62+
opacity: 0.9;
63+
}
5864
}
5965
`,
6066
h1: css`

packages/yubaba/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"main": "dist/cjs/packages/yubaba/src/index.js",
88
"module": "dist/esm/packages/yubaba/src/index.js",
99
"sideEffects": false,
10-
"description": "/juːba:ba/ out of the box animated experiences for React.js 🧙✨",
10+
"description": "/juːbaːba/ out of the box animated experiences for React.js 🧙✨",
1111
"keywords": [
1212
"react",
1313
"flip",

packages/yubaba/src/Animator/__docs__/docs.mdx

+68-9
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,83 @@ import Animator from '../index';
88

99
# Animator
1010

11-
This is the brains component.
12-
When unmounting or flipping the `in` prop from `true` to `false`,
13-
it will execute all the animations **top to bottom** if a matching Animator pair (either itself or another Animator element) is found within `50ms`.
11+
You will use this component with most animations.
12+
It does a few things:
1413

15-
> **Tip -** See [Getting started](/getting-started) for more information on how to use this component.
14+
- take snapshots of the DOM
15+
- pass data to the animations
16+
- execute and orchestrate the animations
1617

17-
## Usage
18+
You'll really only need to be concerned with _execute and orchestrate the animations_.
19+
There are three ways you can execute animations,
20+
listed below.
21+
22+
> **Tip -** Missing some context? Have a look at [Getting started](/getting-started) first.
23+
24+
## Animate unmounted to mounted
25+
26+
You'll find this is the goto way for triggering animations.
27+
It will animate between an element that unmounts and an element that mounts over a state change.
28+
[Moving to another element](/getting-started#moving-to-another-element) is a good example of this.
29+
30+
```js
31+
import Animator, { Move } from 'yubaba';
32+
33+
({ isShown }) => [
34+
!isShown && (
35+
<Animator name="default">
36+
<Move>{children}</Move>
37+
</Animator>
38+
),
39+
isShown && (
40+
<Animator name="default">
41+
<Move>{children}</Move>
42+
</Animator>
43+
),
44+
];
45+
```
46+
47+
## Animate self
48+
49+
Using the `triggerSelfKey` prop to force an animation on itself over a state change.
50+
[Moving to the same element](/getting-started#moving-to-the-same-element) is a good example of this.
1851

1952
```js
20-
import Animator from 'yubaba';
53+
import Animator, { Move } from 'yubaba';
2154

22-
const ListItem = ({ index }) => (
23-
<Animator name={`item-${index}`}>
24-
{({ ref, style, className }) => <div ref={ref} style={style} className={className} />}
55+
({ children, itemId }) => (
56+
<Animator triggerSelfKey={itemId} name="self-target">
57+
<Move>{children}</Move>
2558
</Animator>
2659
);
2760
```
2861

62+
> **Tip -** You can't use this with the `in` prop,
63+
> if you try you'll get a dev error.
64+
65+
## Animate persisted to mounted
66+
67+
Animate between a react element that never unmounts and one that mounts/unmounts over a state change.
68+
Using the `in` prop to mark a persisted component if it is considered in or not.
69+
[Moving from a persisted element](/getting-started#moving-from-a-persisted-element) is a good example of this.
70+
71+
```js
72+
import Animator, { Move } from 'yubaba';
73+
74+
({ isShown }) => [
75+
<Animator name="default" in={!isShown}>
76+
<Move>{children}</Move>
77+
</Animator>,
78+
isShown && (
79+
<Animator name="default">
80+
<Move>{children}</Move>
81+
</Animator>
82+
),
83+
];
84+
```
85+
86+
> **Tip -** You can also use the same method to animate over unmounted to persisted.
87+
2988
## Props
3089

3190
<Props of={Animator} />

packages/yubaba/src/Animator/__snapshots__/test.tsx.snap

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`<Animator /> persisted animations should throw when changing into "in" after initial mount 1`] = `
4+
"yubaba v0.0.0
5+
6+
You're switching between persisted and unpersisted, don't do this. Either always set the \\"in\\" prop as true or false, or keep as undefined."
7+
`;
8+
9+
exports[`<Animator /> self targetted animations should throw when changing into "triggerSelfKey" after initial mount 1`] = `
10+
"yubaba v0.0.0
11+
12+
You're switching between self triggering modes, don't do this. Either always set the \\"triggerSelfKey\\" prop, or keep as undefined."
13+
`;
14+
15+
exports[`<Animator /> self targetted animations should throw when using both "in" and "triggerSelfKey" props after initial mount 1`] = `
16+
"yubaba v0.0.0
17+
18+
Don't use \\"in\\" and \\"triggerSelfKey\\" together. If your element is persisted use \\"in\\". If your element is targeting itself for animations use \\"triggerSelfKey\\"."
19+
`;
20+
321
exports[`<Animator /> should pass dom data to child animation 1`] = `
422
Array [
523
Object {

packages/yubaba/src/Animator/index.tsx

+84-26
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface ChildProps {
3636
className?: string;
3737
}
3838

39-
export interface State {
39+
export interface AnimatorState {
4040
childProps: ChildProps;
4141
animationsMarkup: React.ReactPortal[];
4242
}
@@ -48,11 +48,18 @@ export interface AnimatorProps extends CollectorChildrenProps, InjectedProps {
4848
name: string;
4949

5050
/**
51-
* Used alternatively to the implicit animation triggering via unmounting or mounting of Animator components.
52-
* Only use `in` if your component is expected to persist through the entire lifecyle of the app.
53-
* When you transition to the "next page" make sure to set your "in" to false. When you transition
54-
* back to the original page set the "in" prop back to true. This lets the Animator components know when to
55-
* execute the animations.
51+
* Will trigger animations over itself when this prop changes.
52+
*
53+
* You can't use the with the "in" prop.
54+
*/
55+
triggerSelfKey?: string;
56+
57+
/**
58+
* Use if your element is expected to persist through an animation.
59+
* When you transition to the next state set your "in" to false and vice versa.
60+
* This lets the Animator components know when to execute the animations.
61+
*
62+
* You can't use this with the "triggerSelfKey".
5663
*/
5764
in?: boolean;
5865

@@ -75,7 +82,7 @@ export interface AnimatorProps extends CollectorChildrenProps, InjectedProps {
7582
container: HTMLElement | (() => HTMLElement);
7683
}
7784

78-
export default class Animator extends React.PureComponent<AnimatorProps, State> {
85+
export default class Animator extends React.PureComponent<AnimatorProps, AnimatorState> {
7986
static displayName = 'Animator';
8087

8188
static defaultProps = {
@@ -84,7 +91,7 @@ export default class Animator extends React.PureComponent<AnimatorProps, State>
8491
container: document.body,
8592
};
8693

87-
state: State = {
94+
state: AnimatorState = {
8895
animationsMarkup: [],
8996
childProps: {},
9097
};
@@ -115,55 +122,95 @@ export default class Animator extends React.PureComponent<AnimatorProps, State>
115122
if (componentIn === undefined || componentIn) {
116123
// Ok nothing is there yet, show ourself and store DOM data for later.
117124
// We'll be waiting for another Animator to mount.
118-
this.showSelfAndNotifyManager();
125+
this.notifyVisibilityManagerAnimationsAreFinished();
119126
}
120127
}
121128

122-
componentWillUpdate(prevProps: AnimatorProps) {
123-
const { in: isIn } = this.props;
124-
if (prevProps.in === false && isIn === true) {
125-
// We're being removed from "in". Let's recalculate our DOM position.
129+
getSnapshotBeforeUpdate(prevProps: AnimatorProps) {
130+
if (prevProps.in === true && this.props.in === false) {
126131
this.storeDOMData();
127132
this.delayedClearStore();
128133
this.abortAnimations();
129134
}
135+
136+
if (prevProps.triggerSelfKey !== this.props.triggerSelfKey) {
137+
this.storeDOMData();
138+
this.delayedClearStore();
139+
}
140+
141+
// we can return snapshot here to circumvent the entire storing of dom data.
142+
// would remove the need for setting a name!
143+
return null;
130144
}
131145

132-
componentDidUpdate(prevProps: AnimatorProps) {
133-
const { in: isIn, name } = this.props;
146+
componentDidUpdate(prevProps: AnimatorProps, _: AnimatorState) {
147+
const inPropSame = this.props.in === prevProps.in;
148+
const triggerSelfKeyPropSame = this.props.triggerSelfKey === prevProps.triggerSelfKey;
134149

135-
if (isIn === prevProps.in) {
150+
if (inPropSame && triggerSelfKeyPropSame) {
136151
// Nothing has changed, return early.
137152
return;
138153
}
139154

140-
if (
141-
process.env.NODE_ENV === 'development' &&
142-
(isIn === undefined || prevProps.in === undefined)
143-
) {
144-
warn(
145-
`You're switching between controlled and uncontrolled, don't do this. Either always set the "in" prop as true or false, or keep as undefined.`
155+
if (process.env.NODE_ENV === 'development') {
156+
precondition(
157+
!(this.props.in !== undefined && this.props.triggerSelfKey !== undefined),
158+
`Don't use "in" and "triggerSelfKey" together. If your element is persisted use "in". If your element is targeting itself for animations use "triggerSelfKey".`
159+
);
160+
}
161+
162+
if (process.env.NODE_ENV === 'development') {
163+
precondition(
164+
!((this.props.in === undefined || prevProps.in === undefined) && !inPropSame),
165+
`You're switching between persisted and unpersisted, don't do this. Either always set the "in" prop as true or false, or keep as undefined.`
166+
);
167+
}
168+
169+
if (process.env.NODE_ENV === 'development') {
170+
precondition(
171+
!(
172+
(this.props.triggerSelfKey === undefined || prevProps.triggerSelfKey === undefined) &&
173+
!triggerSelfKeyPropSame
174+
),
175+
`You're switching between self triggering modes, don't do this. Either always set the "triggerSelfKey" prop, or keep as undefined.`
146176
);
147177
}
148178

149-
if (isIn) {
150-
if (store.has(name)) {
179+
if (this.props.in) {
180+
if (store.has(this.props.name)) {
151181
this.executeAnimations();
182+
// return early dont tell manager yet dawg
152183
return;
153184
}
185+
// No animation to trigger, tell manager we're all good regardless.
186+
this.notifyVisibilityManagerAnimationsAreFinished();
187+
return;
188+
}
154189

155-
this.showSelfAndNotifyManager();
190+
if (!triggerSelfKeyPropSame) {
191+
// Defer execution to the next frame to capture correctly.
192+
// Make sure to keep react state the same for any inflight animations to be captured correctly.
193+
requestAnimationFrame(() => {
194+
this.abortAnimations();
195+
this.executeAnimations();
196+
});
156197
}
157198
}
158199

159200
componentWillUnmount() {
201+
if (this.props.triggerSelfKey) {
202+
this.abortAnimations();
203+
this.unmounting = true;
204+
return;
205+
}
206+
160207
this.storeDOMData();
161208
this.delayedClearStore();
162209
this.abortAnimations();
163210
this.unmounting = true;
164211
}
165212

166-
showSelfAndNotifyManager() {
213+
notifyVisibilityManagerAnimationsAreFinished() {
167214
const { context, name } = this.props;
168215

169216
// If a VisibilityManager is a parent up the tree context will be available.
@@ -233,6 +280,7 @@ If it's an image, try and have the image loaded before mounting, or set a static
233280
const { name, container: getContainer, context } = this.props;
234281
const container = typeof getContainer === 'function' ? getContainer() : getContainer;
235282
const fromTarget = store.get(name);
283+
let aborted = false;
236284

237285
if (fromTarget) {
238286
const { collectorData, elementData } = fromTarget;
@@ -332,6 +380,10 @@ If it's an image, try and have the image loaded before mounting, or set a static
332380
container.removeChild(elementToMountChildren);
333381
}
334382

383+
if (targetData.payload.abort) {
384+
targetData.payload.abort();
385+
}
386+
335387
if (this.unmounting) {
336388
return;
337389
}
@@ -370,6 +422,8 @@ If it's an image, try and have the image loaded before mounting, or set a static
370422
);
371423

372424
this.abortAnimations = () => {
425+
aborted = true;
426+
373427
if (this.animating) {
374428
this.animating = false;
375429
blocks.forEach(block => block.forEach(anim => anim.cleanup()));
@@ -423,6 +477,10 @@ If it's an image, try and have the image loaded before mounting, or set a static
423477
);
424478
})
425479
.then(() => {
480+
if (aborted) {
481+
return;
482+
}
483+
426484
blocks.forEach(block => block.forEach(anim => anim.cleanup()));
427485
})
428486
.then(() => {

0 commit comments

Comments
 (0)