Skip to content

Commit 3a2a8a0

Browse files
author
Jan Fischer
committed
Merge branch 'feature/timeline-target-refs'
2 parents 094ec24 + 8071add commit 3a2a8a0

File tree

11 files changed

+205
-39
lines changed

11 files changed

+205
-39
lines changed

packages/docz/src/components/Timeline.mdx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ menu: Components
66
import { Fragment } from 'react';
77
import { Playground, Props } from 'docz'
88
import { Controls, PlayState } from './../../../react-gsap/src/'
9-
import { Timeline, TimelinePropsDummy } from './Timeline'
9+
import { Timeline, TimelinePropsDummy, TargetWithNames } from './Timeline'
1010
import { Tween } from './Tween'
1111

1212
# Timeline
@@ -78,6 +78,39 @@ If you don't add a target you transform all target components.
7878
</Controls>
7979
</Playground>
8080

81+
## Advanced multiple targets
82+
83+
If you need to target individual elements you can use a special forwardRef function.
84+
The `targets` parameter provide the `set` function, which you can use to set a ref to a certain key.
85+
86+
If you use an array as value, as seen in the example, you can save multiple elements as array under one key and use e.g. the `stagger` prop.
87+
88+
```javascript
89+
const TargetWithNames = forwardRef((props, targets) => (
90+
<div style={{ textAlign: 'center' }}>
91+
<h3 ref={div => targets.set('div1', div)}>THIS</h3>
92+
<SplitChars
93+
ref={(div: ReactElement) => targets.set('div2', [div])}
94+
wrapper={<h3 style={{ display: 'inline-block' }} />}
95+
>
96+
TEST
97+
</SplitChars>
98+
<h3 ref={div => targets.set('div3', div)}>IS A</h3>
99+
</div>
100+
));
101+
102+
```
103+
104+
<Playground>
105+
<Controls playState={PlayState.stop}>
106+
<Timeline target={<TargetWithNames />}>
107+
<Tween from={{ x: '-100vw' }} target="div1" position="0" />
108+
<Tween from={{ x: '-100vw' }} target="div2" position="2" stagger={0.1} />
109+
<Tween from={{ x: '-100vw' }} target="div3" position="1" />
110+
</Timeline>
111+
</Controls>
112+
</Playground>
113+
81114
## Nested Timelines
82115

83116
You can nest other Timelines or HTML structures.
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1-
import React from 'react';
1+
import React, { forwardRef, ReactElement } from 'react';
22
import Timeline, { TimelineProps } from './../../../react-gsap/src/Timeline';
3+
import { SplitChars } from './../../../react-gsap/src';
34

45
export const TimelinePropsDummy: React.FunctionComponent<TimelineProps> = props => (
56
<div {...props} />
67
);
7-
export { Timeline };
8+
9+
const TargetWithNames = forwardRef((props, targets: any) => (
10+
<div style={{ textAlign: 'center' }}>
11+
<h3 ref={div => targets.set('div1', div)}>THIS</h3>
12+
<SplitChars
13+
ref={(div: ReactElement) => targets.set('div2', [div])}
14+
wrapper={<h3 style={{ display: 'inline-block' }} />}
15+
>
16+
TEST
17+
</SplitChars>
18+
<h3 ref={div => targets.set('div3', div)}>IS A</h3>
19+
</div>
20+
));
21+
22+
export { Timeline, TargetWithNames };

packages/playground/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"gsap": "^3.2.6",
1818
"react": "^16.13.1",
1919
"react-dom": "^16.13.1",
20-
"react-gsap": "2.0.6",
20+
"react-gsap": "2.2.0",
2121
"react-router-dom": "^5.1.2",
2222
"react-scripts": "3.4.1",
2323
"react-transition-group": "^4.3.0",

packages/playground/src/examples/Animation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const RubberBand = ({
3434
children,
3535
...rest
3636
}: {
37-
children: React.ReactNode;
37+
children: React.ReactElement;
3838
[key: string]: any;
3939
}) => (
4040
<Timeline target={children} {...rest}>

packages/playground/src/examples/Timeline.tsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import React, { Fragment } from 'react';
1+
import React, {
2+
forwardRef,
3+
Fragment,
4+
useRef,
5+
useImperativeHandle,
6+
ReactElement,
7+
ReactHTMLElement,
8+
} from 'react';
29
import styled from 'styled-components';
3-
import { Tween, Timeline, SplitWords, SplitLetters, Controls, PlayState } from 'react-gsap';
10+
import { Tween, Timeline, SplitWords, SplitChars, Controls, PlayState } from 'react-gsap';
411

512
const TimelineStyled = styled.div``;
613

@@ -87,9 +94,9 @@ const TimelineComponent = () => (
8794
wrapper={<div style={{ position: 'relative', left: '90px' }} />}
8895
target={
8996
<Fragment>
90-
<SplitLetters wrapper={<div style={{ fontSize: '80px', display: 'inline-block' }} />}>
97+
<SplitChars wrapper={<div style={{ fontSize: '80px', display: 'inline-block' }} />}>
9198
AIIIIGHHT
92-
</SplitLetters>
99+
</SplitChars>
93100
</Fragment>
94101
}
95102
labels={[
@@ -114,4 +121,27 @@ const TimelineComponent = () => (
114121
</TimelineStyled>
115122
);
116123

117-
export default TimelineComponent;
124+
const TargetWithNames = forwardRef((props, targets: any) => (
125+
<div>
126+
<div ref={div => targets.set('div1', div)}>first</div>
127+
<SplitChars
128+
ref={(div: ReactElement) => targets.set('div2', [div])}
129+
wrapper={<span style={{ display: 'inline-block' }} />}
130+
>
131+
second
132+
</SplitChars>
133+
<div ref={div => targets.set('div3', div)}>third</div>
134+
</div>
135+
));
136+
137+
const TimelineTargets = () => {
138+
return (
139+
<Timeline target={<TargetWithNames />}>
140+
<Tween to={{ x: '200px' }} target="div3" position="0" />
141+
<Tween to={{ x: '200px' }} target="div1" position="0.5" />
142+
<Tween to={{ x: '200px' }} target="div2" position="1" stagger={0.1} />
143+
</Timeline>
144+
);
145+
};
146+
147+
export default TimelineTargets;

packages/react-gsap/package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-gsap",
3-
"version": "2.1.0",
3+
"version": "2.2.0",
44
"description": "React components for GSAP",
55
"author": "bitworking",
66
"license": "MIT",
@@ -25,8 +25,8 @@
2525
},
2626
"peerDependencies": {
2727
"gsap": ">=3",
28-
"react": ">=16",
29-
"react-dom": ">=16"
28+
"react": ">=16.6",
29+
"react-dom": ">=16.6"
3030
},
3131
"husky": {
3232
"hooks": {
@@ -44,12 +44,16 @@
4444
"devDependencies": {
4545
"@types/react": "^16.9.34",
4646
"@types/react-dom": "^16.9.7",
47+
"@types/react-is": "^16.7.1",
4748
"gsap": "^3.2.6",
4849
"husky": "^4.2.5",
4950
"react": "^16.13.1",
5051
"react-dom": "^16.13.1",
5152
"tsdx": "^0.13.2",
5253
"tslib": "^1.11.1",
5354
"typescript": "^3.8.3"
55+
},
56+
"dependencies": {
57+
"react-is": "^16.13.1"
5458
}
5559
}

packages/react-gsap/src/Timeline.tsx

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1-
import React, { Fragment } from 'react';
1+
import React, { Fragment, ReactNode, ReactElement, forwardRef } from 'react';
22
import { gsap } from 'gsap';
3+
import { isForwardRef, isFragment } from 'react-is';
34
import { PlayState } from './types';
45
import { getTweenFunction, setPlayState, refOrInnerRef, nullishCoalescing } from './helper';
56
import Provider, { Context } from './Provider';
7+
import { TweenProps } from './Tween';
68

79
type Label = {
810
label: string;
911
position: string | number;
1012
};
1113

14+
export type Targets = Map<string | number, ReactElement | ReactElement[]>;
15+
export type TargetsRef = {
16+
set: (key: string, target: any) => void;
17+
};
18+
19+
export type Target = ReactElement | null;
20+
1221
export type TimelineProps = {
13-
children: React.ReactNode;
14-
wrapper?: React.ReactElement;
15-
target?: any;
22+
children: ReactNode;
23+
wrapper?: ReactElement;
24+
target?: Target;
1625
position?: string | number;
1726
labels?: Label[];
1827

@@ -29,7 +38,13 @@ class Timeline extends Provider<TimelineProps> {
2938
static contextType = Context;
3039

3140
timeline: any;
32-
targets: any[] = [];
41+
targets: Targets = new Map();
42+
43+
constructor(props: TimelineProps) {
44+
super(props);
45+
46+
this.setTarget = this.setTarget.bind(this);
47+
}
3348

3449
setPlayState(playState: PlayState) {
3550
const { playState: previousPlayState } = this.props;
@@ -45,7 +60,7 @@ class Timeline extends Provider<TimelineProps> {
4560
}
4661

4762
getSnapshotBeforeUpdate() {
48-
this.targets = [];
63+
this.targets = new Map();
4964
return null;
5065
}
5166

@@ -105,12 +120,24 @@ class Timeline extends Provider<TimelineProps> {
105120
// add tweens or nested timelines to timeline
106121
this.consumers.forEach(consumer => {
107122
if (consumer.tween && !consumer.props.children) {
108-
const { position, target, stagger, ...vars } = consumer.props;
109-
const tween = getTweenFunction(nullishCoalescing(this.targets[target], this.targets), {
110-
stagger,
111-
...vars,
112-
});
123+
const { position, target, stagger, ...vars } = consumer.props as TweenProps;
124+
125+
// get target if not nullish
126+
let targets = null;
127+
if (target !== null && typeof target !== 'undefined') {
128+
targets = this.targets.get(target);
129+
}
130+
131+
const tween = getTweenFunction(
132+
// @ts-ignore
133+
nullishCoalescing(targets, Array.from(this.targets.values())),
134+
{
135+
stagger,
136+
...vars,
137+
}
138+
);
113139
this.timeline.add(tween, nullishCoalescing(position, '+=0'));
140+
consumer.setGSAP(tween);
114141
} else {
115142
const { position } = consumer.props;
116143
this.timeline.add(consumer.getGSAP(), nullishCoalescing(position, '+=0'));
@@ -139,12 +166,28 @@ class Timeline extends Provider<TimelineProps> {
139166
}
140167

141168
addTarget(target: any) {
142-
// target is null at unmount
143169
if (target !== null) {
144-
this.targets.push(target);
170+
this.targets.set(this.targets.size, target);
145171
}
146172
}
147173

174+
setTarget(key: string, target: any) {
175+
if (target !== null) {
176+
if (this.targets.has(key)) {
177+
const targets = this.targets.get(key);
178+
if (Array.isArray(targets)) {
179+
this.targets.set(key, [...targets, ...target]);
180+
return;
181+
}
182+
}
183+
this.targets.set(key, target);
184+
}
185+
}
186+
187+
setTargets(targets: Targets) {
188+
this.targets = targets;
189+
}
190+
148191
getTargets() {
149192
return this.targets;
150193
}
@@ -156,20 +199,40 @@ class Timeline extends Provider<TimelineProps> {
156199
});
157200
}
158201

159-
render() {
160-
let { target, children, wrapper } = this.props;
202+
renderTarget(target?: Target): ReactNode {
203+
if (!target) {
204+
return null;
205+
}
161206

162-
let output = (
207+
// if is forwardRef clone and pass targets as ref
208+
if (isForwardRef(target)) {
209+
return <target.type ref={{ set: this.setTarget }} />;
210+
}
211+
212+
// else iterate the first level of children and set targets
213+
return (
163214
<Fragment>
164215
{/* First render the target */}
165-
{React.Children.map(target, child => {
166-
if (child.type.toString() === 'Symbol(react.fragment)') {
216+
{React.Children.map<ReactElement, ReactElement>(target, child => {
217+
if (isFragment(child)) {
167218
return React.Children.map(child.props.children, fragmentChild => {
168219
return this.cloneElement(fragmentChild);
169220
});
170221
}
171222
return this.cloneElement(child);
172223
})}
224+
</Fragment>
225+
);
226+
}
227+
228+
render() {
229+
let { target, children, wrapper } = this.props;
230+
231+
const renderedTarget = this.renderTarget(target);
232+
233+
let output = (
234+
<Fragment>
235+
{renderedTarget}
173236
{children}
174237
</Fragment>
175238
);

0 commit comments

Comments
 (0)