Skip to content

Commit ed373e6

Browse files
committed
Full rewrite.
1 parent 5af7a3b commit ed373e6

File tree

2 files changed

+109
-157
lines changed

2 files changed

+109
-157
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
*/
125125
"indent": [1, 2, {"SwitchCase": 1}], // http://eslint.org/docs/rules/indent
126126
"brace-style": [2, // http://eslint.org/docs/rules/brace-style
127-
"stroustrup", {
127+
"1tbs", {
128128
"allowSingleLine": true
129129
}],
130130
"quotes": [

src/ReactCSSTransitionReplace.jsx

Lines changed: 108 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ function createTransitionTimeoutPropValidator(transitionType) {
3030
+ 'https://fb.me/react-animation-transition-group-timeout for more ' + 'information.')
3131

3232
// If the duration isn't a number
33-
}
34-
else if (typeof props[timeoutPropName] != 'number') {
33+
} else if (typeof props[timeoutPropName] != 'number') {
3534
return new Error(timeoutPropName + ' must be a number (in milliseconds)')
3635
}
3736
}
@@ -65,7 +64,6 @@ export default class ReactCSSTransitionReplace extends React.Component {
6564
transitionEnterTimeout: createTransitionTimeoutPropValidator('Enter'),
6665
transitionLeaveTimeout: createTransitionTimeoutPropValidator('Leave'),
6766
overflowHidden: PropTypes.bool,
68-
changeWidth: PropTypes.bool,
6967
}
7068

7169
static defaultProps = {
@@ -74,23 +72,25 @@ export default class ReactCSSTransitionReplace extends React.Component {
7472
transitionLeave: true,
7573
overflowHidden: true,
7674
component: 'span',
77-
changeWidth: false,
75+
childComponent: 'span',
7876
}
7977

8078
state = {
79+
currentKey: '1',
8180
currentChild: this.props.children ? React.Children.only(this.props.children) : undefined,
82-
currentChildKey: this.props.children ? '1' : '',
83-
nextChild: undefined,
84-
activeHeightTransition: false,
85-
nextChildKey: '',
81+
prevChildren: {},
8682
height: null,
87-
width: null,
88-
isLeaving: false,
83+
}
84+
85+
componentWillMount() {
86+
this.shouldEnterCurrent = false
87+
this.keysToLeave = []
88+
this.transitioningKeys = {}
8989
}
9090

9191
componentDidMount() {
9292
if (this.props.transitionAppear && this.state.currentChild) {
93-
this.appearCurrent()
93+
this.performAppear(this.state.currentKey)
9494
}
9595
}
9696

@@ -99,152 +99,115 @@ export default class ReactCSSTransitionReplace extends React.Component {
9999
}
100100

101101
componentWillReceiveProps(nextProps) {
102-
// Setting false indicates that the child has changed, but it is a removal so there is no next child.
103-
const nextChild = nextProps.children ? React.Children.only(nextProps.children) : false
104-
const currentChild = this.state.currentChild
105-
106-
// Avoid silencing the transition when this.state.nextChild exists because it means that there’s
107-
// already a transition ongoing that has to be replaced.
108-
if (currentChild && nextChild && nextChild.key === currentChild.key && !this.state.nextChild) {
109-
// Nothing changed, but we are re-rendering so update the currentChild.
110-
return this.setState({
111-
currentChild: nextChild,
112-
})
113-
}
102+
const nextChild = nextProps.children ? React.Children.only(nextProps.children) : null
103+
const {currentChild} = this.state
114104

115-
if (!currentChild && !nextChild && this.state.nextChild) {
116-
// The container was empty before and the entering element is being removed again while
117-
// transitioning in. Since a CSS transition can't be reversed cleanly midway the height
118-
// is just forced back to zero immediately and the child removed.
119-
return this.cancelTransition()
105+
if ((!currentChild && !nextChild) || (currentChild && nextChild && currentChild.key === nextChild.key)) {
106+
return
120107
}
121108

122109
const {state} = this
110+
const {currentKey} = state
123111

124-
// When transitionLeave is set to false, refs.curr does not exist when refs.next is being
125-
// transitioned into existence. When another child is set for this component at the point
126-
// where only refs.next exists, we want to use the width/height of refs.next instead of
127-
// refs.curr.
128-
const ref = this.refs.curr || this.refs.next
129-
130-
// Set the next child to start the transition, and set the current height.
131-
this.setState({
132-
nextChild,
133-
activeHeightTransition: false,
134-
nextChildKey: state.currentChildKey ? String(Number(state.currentChildKey) + 1) : '1',
135-
height: state.currentChild ? ReactDOM.findDOMNode(ref).offsetHeight : 0,
136-
width: state.currentChild && this.props.changeWidth ? ReactDOM.findDOMNode(ref).offsetWidth : null,
137-
})
138-
139-
// Enqueue setting the next height to trigger the height transition.
140-
this.enqueueHeightTransition(nextChild)
141-
}
112+
const nextState = {
113+
currentKey: String(Number(currentKey) + 1),
114+
currentChild: nextChild,
115+
height: 0,
116+
}
142117

143-
componentDidUpdate() {
144-
if (!this.isTransitioning && !this.state.isLeaving) {
145-
const {currentChild, nextChild} = this.state
118+
if (nextChild) {
119+
this.shouldEnterCurrent = true
120+
}
146121

147-
if (currentChild && (nextChild || nextChild === false || nextChild === null) && this.props.transitionLeave) {
148-
this.leaveCurrent()
122+
if (currentChild) {
123+
nextState.height = ReactDOM.findDOMNode(this.refs[currentKey]).offsetHeight
124+
nextState.prevChildren = {
125+
...state.prevChildren,
126+
[currentKey]: currentChild,
149127
}
150-
if (nextChild) {
151-
this.enterNext()
128+
if (!this.transitioningKeys[currentKey]) {
129+
this.keysToLeave.push(currentKey)
152130
}
153131
}
132+
133+
this.setState(nextState)
154134
}
155135

156-
enqueueHeightTransition(nextChild, tickCount = 0) {
157-
this.timeout = setTimeout(() => {
158-
if (!nextChild) {
159-
return this.setState({
160-
activeHeightTransition: true,
161-
height: 0,
162-
width: this.props.changeWidth ? 0 : null,
163-
})
164-
}
136+
componentDidUpdate() {
137+
if (this.shouldEnterCurrent) {
138+
this.shouldEnterCurrent = false
139+
this.performEnter(this.state.currentKey)
140+
}
165141

166-
const nextNode = ReactDOM.findDOMNode(this.refs.next)
167-
if (nextNode) {
168-
this.setState({
169-
activeHeightTransition: true,
170-
height: nextNode.offsetHeight,
171-
width: this.props.changeWidth ? nextNode.offsetWidth : null,
172-
})
173-
}
174-
else {
175-
// The DOM hasn't rendered the entering element yet, so wait another tick.
176-
// Getting stuck in a loop shouldn't happen, but it's better to be safe.
177-
if (tickCount < 10) {
178-
this.enqueueHeightTransition(nextChild, tickCount + 1)
179-
}
180-
}
181-
}, TICK)
142+
const keysToLeave = this.keysToLeave
143+
this.keysToLeave = []
144+
keysToLeave.forEach(this.performLeave)
182145
}
183146

184-
appearCurrent() {
185-
this.refs.curr.componentWillAppear(this._handleDoneAppearing)
186-
this.isTransitioning = true
147+
performAppear(key) {
148+
this.transitioningKeys[key] = true
149+
this.refs[key].componentWillAppear(this.handleDoneAppearing.bind(this, key))
187150
}
188151

189-
_handleDoneAppearing = () => {
190-
this.isTransitioning = false
152+
handleDoneAppearing = (key) => {
153+
delete this.transitioningKeys[key]
154+
if (key !== this.state.currentKey) {
155+
// This child was removed before it had fully appeared. Remove it.
156+
this.performLeave(key)
157+
}
191158
}
192159

193-
enterNext() {
194-
this.refs.next.componentWillEnter(this._handleDoneEntering)
195-
this.isTransitioning = true
160+
performEnter(key) {
161+
this.transitioningKeys[key] = true
162+
this.refs[key].componentWillEnter(this.handleDoneEntering.bind(this, key))
163+
this.enqueueHeightTransition()
196164
}
197165

198-
_handleDoneEntering = () => {
199-
const {state} = this
200-
201-
this.isTransitioning = false
202-
this.setState({
203-
currentChild: state.nextChild,
204-
currentChildKey: state.nextChildKey,
205-
activeHeightTransition: false,
206-
nextChild: undefined,
207-
nextChildKey: '',
208-
height: null,
209-
width: null,
210-
})
166+
handleDoneEntering(key) {
167+
delete this.transitioningKeys[key]
168+
if (key === this.state.currentKey) {
169+
// The current child has finished entering so the height transition is also cleared.
170+
this.setState({height: null})
171+
} else {
172+
// This child was removed before it had fully appeared. Remove it.
173+
this.performLeave(key)
174+
}
211175
}
212176

213-
leaveCurrent() {
214-
this.refs.curr.componentWillLeave(this._handleDoneLeaving)
215-
this.isTransitioning = true
216-
this.setState({isLeaving: true})
177+
performLeave = (key) => {
178+
this.transitioningKeys[key] = true
179+
this.refs[key].componentWillLeave(this.handleDoneLeaving.bind(this, key))
180+
if (!this.state.currentChild) {
181+
// The enter transition dominates, but if there is no
182+
// entering component the height is set to zero.
183+
this.enqueueHeightTransition()
184+
}
217185
}
218186

219-
// When the leave transition time-out expires the animation classes are removed, so the
220-
// element must be removed from the DOM if the enter transition is still in progress.
221-
_handleDoneLeaving = () => {
222-
if (this.isTransitioning) {
223-
const state = {currentChild: undefined, isLeaving: false}
187+
handleDoneLeaving(key) {
188+
delete this.transitioningKeys[key]
224189

225-
if (!this.state.nextChild) {
226-
this.isTransitioning = false
227-
state.height = null
228-
state.width = null
229-
}
190+
const nextState = {prevChildren: {...this.state.prevChildren}}
191+
delete nextState.prevChildren[key]
230192

231-
this.setState(state)
193+
if (!this.state.currentChild) {
194+
nextState.height = null
232195
}
196+
197+
this.setState(nextState)
233198
}
234199

235-
cancelTransition() {
236-
this.isTransitioning = false
237-
clearTimeout(this.timeout)
238-
return this.setState({
239-
nextChild: undefined,
240-
activeHeightTransition: false,
241-
nextChildKey: '',
242-
height: null,
243-
width: null,
244-
})
200+
enqueueHeightTransition() {
201+
const {state} = this
202+
this.timeout = setTimeout(() => {
203+
if (!state.currentChild) {
204+
return this.setState({height: 0})
205+
}
206+
this.setState({height: ReactDOM.findDOMNode(this.refs[state.currentKey]).offsetHeight})
207+
}, TICK)
245208
}
246209

247-
_wrapChild(child, moreProps) {
210+
wrapChild(child, moreProps) {
248211
let transitionName = this.props.transitionName
249212

250213
if (typeof transitionName == 'object' && transitionName !== null) {
@@ -268,42 +231,22 @@ export default class ReactCSSTransitionReplace extends React.Component {
268231
}
269232

270233
render() {
271-
const {currentChild, currentChildKey, nextChild, nextChildKey, height, width, isLeaving, activeHeightTransition} = this.state
234+
const {currentKey, currentChild, prevChildren, height} = this.state
272235
const childrenToRender = []
273236

274237
const {
275-
overflowHidden, transitionName, changeWidth, component,
238+
overflowHidden, transitionName, component, childComponent,
276239
transitionAppear, transitionEnter, transitionLeave,
277240
transitionAppearTimeout, transitionEnterTimeout, transitionLeaveTimeout,
278241
...containerProps
279242
} = this.props
280243

281-
if (currentChild && !nextChild && !transitionLeave || currentChild && transitionLeave) {
282-
childrenToRender.push(
283-
React.createElement(
284-
'span',
285-
{key: currentChildKey},
286-
this._wrapChild(
287-
typeof currentChild.type == 'string' ? currentChild : React.cloneElement(currentChild, {isLeaving}),
288-
{ref: 'curr'})
289-
)
290-
)
291-
}
292-
293-
294244
if (height !== null) {
295245
const heightClassName = (typeof transitionName == 'object' && transitionName !== null)
296246
? transitionName.height || ''
297247
: `${transitionName}-height`
298248

299-
// Similarly to ReactCSSTransitionGroup, adding `-height-active` suffix to the
300-
// container when we are transitioning height.
301-
const activeHeightClassName = (nextChild && activeHeightTransition && heightClassName)
302-
? `${heightClassName}-active`
303-
: ''
304-
305-
containerProps.className = `${containerProps.className || ''} ${heightClassName} ${activeHeightClassName}`
306-
249+
containerProps.className = `${containerProps.className || ''} ${heightClassName}`
307250
containerProps.style = {
308251
...containerProps.style,
309252
position: 'relative',
@@ -314,26 +257,35 @@ export default class ReactCSSTransitionReplace extends React.Component {
314257
if (overflowHidden) {
315258
containerProps.style.overflow = 'hidden'
316259
}
317-
318-
if (changeWidth) {
319-
containerProps.style.width = width
320-
}
321260
}
322261

323-
if (nextChild) {
262+
Object.keys(prevChildren).forEach(key => {
324263
childrenToRender.push(
325-
React.createElement('span',
264+
React.createElement(childComponent,
326265
{
266+
key,
327267
style: {
328268
position: 'absolute',
329269
top: 0,
330270
left: 0,
331271
right: 0,
332272
bottom: 0,
333273
},
334-
key: nextChildKey,
335274
},
336-
this._wrapChild(nextChild, {ref: 'next'})
275+
this.wrapChild(
276+
typeof prevChildren[key].type == 'string'
277+
? prevChildren[key]
278+
: React.cloneElement(prevChildren[key], {isLeaving: true}),
279+
{ref: key})
280+
)
281+
)
282+
})
283+
284+
if (currentChild) {
285+
childrenToRender.push(
286+
React.createElement(childComponent,
287+
{key: currentKey},
288+
this.wrapChild(currentChild, {ref: currentKey})
337289
)
338290
)
339291
}

0 commit comments

Comments
 (0)