Skip to content

Commit c5e379d

Browse files
dimensijquense
authored andcommitted
feat: add SwitchTransition component (#470)
* add switch transition component * change prop type * add some documentations and tests * add export in entry point * update switchtransition for new api
1 parent f1383c5 commit c5e379d

2 files changed

Lines changed: 320 additions & 0 deletions

File tree

‎src/SwitchTransition.js‎

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
importReactfrom'react';
2+
importPropTypesfrom'prop-types';
3+
import{ENTERED,ENTERING,EXITING}from'./Transition'
4+
importTransitionGroupContextfrom'./TransitionGroupContext';
5+
6+
functionareChildrenDifferent(oldChildren,newChildren){
7+
if(oldChildren===newChildren)returnfalse;
8+
if(
9+
React.isValidElement(oldChildren)&&
10+
React.isValidElement(newChildren)&&
11+
oldChildren.key!=null&&
12+
oldChildren.key===newChildren.key
13+
){
14+
returnfalse;
15+
}
16+
returntrue;
17+
}
18+
19+
/**
20+
* Enum of modes for SwitchTransition component
21+
* @enum { string }
22+
*/
23+
exportconstmodes={
24+
out: 'out-in',
25+
in: 'in-out'
26+
};
27+
28+
constcallHook=(element,name,cb)=>(...args)=>{
29+
element.props[name]&&element.props[name](...args)
30+
cb()
31+
}
32+
33+
constleaveRenders={
34+
[modes.out]: ({ current, changeState })=>
35+
React.cloneElement(current,{
36+
in: false,
37+
onExited: callHook(current,'onExited',()=>{
38+
changeState(ENTERING,null);
39+
})
40+
}),
41+
[modes.in]: ({ current, changeState, children })=>[
42+
current,
43+
React.cloneElement(children,{
44+
in: true,
45+
onEntered: callHook(children,'onEntered',()=>{
46+
changeState(ENTERING);
47+
})
48+
})
49+
]
50+
};
51+
52+
constenterRenders={
53+
[modes.out]: ({ children, changeState })=>
54+
React.cloneElement(children,{
55+
in: true,
56+
onEntered: callHook(children,'onEntered',()=>{
57+
changeState(ENTERED,React.cloneElement(children,{in: true}));
58+
})
59+
}),
60+
[modes.in]: ({ current, children, changeState })=>[
61+
React.cloneElement(current,{
62+
in: false,
63+
onExited: callHook(current,'onExited',()=>{
64+
changeState(ENTERED,React.cloneElement(children,{in: true}));
65+
})
66+
}),
67+
React.cloneElement(children,{
68+
in: true
69+
})
70+
]
71+
};
72+
73+
/**
74+
* A transition component inspired by the [vue transition modes](https://vuejs.org/v2/guide/transitions.html#Transition-Modes).
75+
* You can use it when you want to control the render between state transitions.
76+
* Based on the selected mode and the child's key which is the `Transition` or `CSSTransition` component, the `SwitchTransition` makes a consistent transition between them.
77+
*
78+
* If the `out-in` mode is selected, the `SwitchTransition` waits until the old child leaves and then inserts a new child.
79+
* If the `in-out` mode is selected, the `SwitchTransition` inserts a new child first, waits for the new child to enter and then removes the old child
80+
*
81+
* ```jsx
82+
*
83+
* function App() {
84+
* const [state, setState] = useState(false);
85+
* return (
86+
* <SwitchTransition>
87+
* <FadeTransition key={state ? "Goodbye, world!" : "Hello, world!"}
88+
* addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
89+
* classNames='fade' >
90+
* <button onClick={() => setState(state => !state)}>
91+
* {state ? "Goodbye, world!" : "Hello, world!"}
92+
* </button>
93+
* </FadeTransition>
94+
* </SwitchTransition>
95+
* )
96+
* }
97+
* ```
98+
*/
99+
exportclassSwitchTransitionextendsReact.Component{
100+
state={
101+
status: ENTERED,
102+
current: null
103+
};
104+
105+
appeared=false;
106+
107+
componentDidMount(){
108+
this.appeared=true;
109+
}
110+
111+
staticgetDerivedStateFromProps(props,state){
112+
if(props.children==null){
113+
return{
114+
current: null
115+
}
116+
}
117+
118+
if(state.status===ENTERING&&props.mode===modes.in){
119+
return{
120+
status: ENTERING
121+
};
122+
}
123+
124+
if(state.current&&areChildrenDifferent(state.current,props.children)){
125+
return{
126+
status: EXITING
127+
};
128+
}
129+
130+
return{
131+
current: React.cloneElement(props.children,{
132+
in: true
133+
})
134+
};
135+
}
136+
137+
changeState=(status,current=this.state.current)=>{
138+
this.setState({
139+
status,
140+
current
141+
});
142+
};
143+
144+
render(){
145+
const{
146+
props: { children, mode },
147+
state: { status, current }
148+
}=this;
149+
150+
constdata={ children, current,changeState: this.changeState, status };
151+
letcomponent
152+
switch(status){
153+
caseENTERING:
154+
component=enterRenders[mode](data);
155+
break;
156+
caseEXITING:
157+
component=leaveRenders[mode](data)
158+
break;
159+
caseENTERED:
160+
component=current;
161+
}
162+
163+
return(
164+
<TransitionGroupContext.Providervalue={{isMounting: !this.appeared}}>
165+
{component}
166+
</TransitionGroupContext.Provider>
167+
)
168+
}
169+
}
170+
171+
172+
SwitchTransition.propTypes={
173+
/**
174+
* Transition modes.
175+
* `out-in`: Current element transitions out first, then when complete, the new element transitions in.
176+
* `in-out: New element transitions in first, then when complete, the current element transitions out.`
177+
*
178+
* @type {'out-in'|'in-out'}
179+
*/
180+
mode: PropTypes.oneOf([modes.in,modes.out]),
181+
/**
182+
* Any `Transition` or `CSSTransition` component
183+
*/
184+
children: PropTypes.oneOfType([
185+
PropTypes.element.isRequired,
186+
]),
187+
}
188+
189+
SwitchTransition.defaultProps={
190+
mode: modes.out
191+
}

‎test/SwitchTransition-test.js‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
importReactfrom'react';
2+
3+
import{mount}from'enzyme';
4+
5+
importTransition,{ENTERED}from'../src/Transition';
6+
import{SwitchTransition,modes}from'../src/SwitchTransition';
7+
8+
describe('SwitchTransition',()=>{
9+
letlog,Parent;
10+
beforeEach(()=>{
11+
log=[];
12+
letevents={
13+
onEnter: (_,m)=>log.push(m ? 'appear' : 'enter'),
14+
onEntering: (_,m)=>log.push(m ? 'appearing' : 'entering'),
15+
onEntered: (_,m)=>log.push(m ? 'appeared' : 'entered'),
16+
onExit: ()=>log.push('exit'),
17+
onExiting: ()=>log.push('exiting'),
18+
onExited: ()=>log.push('exited')
19+
};
20+
21+
Parent=functionParent({ on, rendered =true}){
22+
return(
23+
<SwitchTransition>
24+
{rendered ? (
25+
<Transitiontimeout={0}key={on ? 'first' : 'second'}{...events}>
26+
<span/>
27+
</Transition>
28+
) : null}
29+
</SwitchTransition>
30+
);
31+
};
32+
});
33+
34+
it('should have default status ENTERED',()=>{
35+
constwrapper=mount(
36+
<SwitchTransition>
37+
<Transitiontimeout={0}key="first">
38+
<span/>
39+
</Transition>
40+
</SwitchTransition>
41+
);
42+
43+
expect(wrapper.state('status')).toBe(ENTERED);
44+
});
45+
46+
it('should have default mode: out-in',()=>{
47+
constwrapper=mount(
48+
<SwitchTransition>
49+
<Transitiontimeout={0}key="first">
50+
<span/>
51+
</Transition>
52+
</SwitchTransition>
53+
);
54+
55+
expect(wrapper.prop('mode')).toBe(modes.out);
56+
});
57+
58+
it('should work without childs',()=>{
59+
expect(()=>{
60+
mount(
61+
<SwitchTransition>
62+
<Transitiontimeout={0}key="first">
63+
<span/>
64+
</Transition>
65+
</SwitchTransition>
66+
);
67+
}).not.toThrow();
68+
});
69+
70+
it('should switch between components on change state',()=>{
71+
constwrapper=mount(<Parenton={true}/>);
72+
73+
jest.useFakeTimers();
74+
expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe(
75+
'first'
76+
);
77+
wrapper.setProps({on: false});
78+
expect(log).toEqual(['exit','exiting']);
79+
jest.runAllTimers();
80+
expect(log).toEqual([
81+
'exit',
82+
'exiting',
83+
'exited',
84+
'enter',
85+
'entering',
86+
'entered'
87+
]);
88+
expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe(
89+
'second'
90+
);
91+
});
92+
93+
it('should switch between null and component',()=>{
94+
constwrapper=mount(<Parenton={true}rendered={false}/>);
95+
96+
expect(
97+
wrapper.find(SwitchTransition).getElement().props.children
98+
).toBeFalsy();
99+
100+
jest.useFakeTimers();
101+
102+
wrapper.setProps({rendered: true});
103+
jest.runAllTimers();
104+
expect(log).toEqual(['enter','entering','entered']);
105+
expect(
106+
wrapper.find(SwitchTransition).getElement().props.children
107+
).toBeTruthy();
108+
expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe(
109+
'first'
110+
);
111+
112+
wrapper.setProps({on: false,rendered: true});
113+
jest.runAllTimers();
114+
expect(log).toEqual([
115+
'enter',
116+
'entering',
117+
'entered',
118+
'exit',
119+
'exiting',
120+
'exited',
121+
'enter',
122+
'entering',
123+
'entered'
124+
]);
125+
expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe(
126+
'second'
127+
);
128+
});
129+
});

0 commit comments

Comments
 (0)