1
- import { type TransitionEvent , type TransitionEventHandler , useEffect , useRef } from 'react' ;
1
+ import { type TransitionEvent , type TransitionEventHandler , useRef , useState } from 'react' ;
2
2
import { noop } from '@vkontakte/vkjs' ;
3
3
import { useStableCallback } from '../../hooks/useStableCallback' ;
4
- import { useStateWithPrev } from '../../hooks/useStateWithPrev' ;
4
+ import { millisecondsInSecond } from '../date' ;
5
+ import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect' ;
5
6
6
7
/* istanbul ignore next: особенность рендера в браузере когда меняется className, в Jest не воспроизвести */
7
8
const forceReflowForFixNewMountedElement = ( node : Element | null ) => void node ?. scrollTop ;
@@ -37,6 +38,8 @@ export type UseCSSTransition<Ref extends Element = Element> = [
37
38
} ,
38
39
] ;
39
40
41
+ const TRANSITION_FALLBACK_DELAY = 100 ;
42
+
40
43
/**
41
44
* Хук основан на компоненте `CSSTransition` из библиотеки `react-transition-group`.
42
45
*
@@ -64,9 +67,10 @@ export const useCSSTransition = <Ref extends Element = Element>(
64
67
const onExit = useStableCallback ( onExitProp || noop ) ;
65
68
const onExiting = useStableCallback ( onExitingProp || noop ) ;
66
69
const onExited = useStableCallback ( onExitedProp || noop ) ;
70
+ const timerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
67
71
68
72
const ref = useRef < Ref | null > ( null ) ;
69
- const [ [ state , prevState ] , setState ] = useStateWithPrev < UseCSSTransitionState > ( ( ) => {
73
+ const [ state , setState ] = useState < UseCSSTransitionState > ( ( ) => {
70
74
if ( ! inProp ) {
71
75
return 'exited' ;
72
76
}
@@ -79,7 +83,14 @@ export const useCSSTransition = <Ref extends Element = Element>(
79
83
return 'entered' ;
80
84
} ) ;
81
85
82
- useEffect (
86
+ const clearTimer = ( ) => {
87
+ if ( timerRef . current ) {
88
+ clearTimeout ( timerRef . current ) ;
89
+ timerRef . current = null ;
90
+ }
91
+ } ;
92
+
93
+ useIsomorphicLayoutEffect (
83
94
function updateState ( ) {
84
95
if ( inProp ) {
85
96
switch ( state ) {
@@ -150,9 +161,6 @@ export const useCSSTransition = <Ref extends Element = Element>(
150
161
inProp ,
151
162
152
163
state ,
153
- prevState ,
154
- setState ,
155
-
156
164
enableAppear ,
157
165
enableEnter ,
158
166
onEnter ,
@@ -166,35 +174,66 @@ export const useCSSTransition = <Ref extends Element = Element>(
166
174
] ,
167
175
) ;
168
176
169
- const onTransitionEnd = ( event : TransitionEvent ) => {
170
- /* istanbul ignore if: на всякий случай предупреждаем всплытие, нет смысла проверять условие */
171
- if ( event . target !== ref . current ) {
172
- return ;
173
- }
177
+ const completeTransition = useStableCallback ( ( event ?: TransitionEvent ) => {
178
+ clearTimer ( ) ;
174
179
175
180
switch ( state ) {
176
181
case 'appearing' :
177
182
setState ( 'appeared' ) ;
178
- onEntered ( event . propertyName , true ) ;
183
+ onEntered ( event ? .propertyName , true ) ;
179
184
break ;
180
185
case 'entering' :
181
186
setState ( 'entered' ) ;
182
- onEntered ( event . propertyName ) ;
187
+ onEntered ( event ? .propertyName ) ;
183
188
break ;
184
189
case 'exiting' :
185
190
setState ( 'exited' ) ;
186
- onExited ( event . propertyName ) ;
191
+ onExited ( event ? .propertyName ) ;
187
192
break ;
188
193
}
189
- } ;
194
+ } ) ;
195
+
196
+ useIsomorphicLayoutEffect (
197
+ function scheduleTransitionCompletionFallback ( ) {
198
+ const el = ref . current ;
199
+ if ( ! el ) {
200
+ return ;
201
+ }
202
+
203
+ if ( state === 'appearing' || state === 'entering' || state === 'exiting' ) {
204
+ const style = getComputedStyle ( el ) ;
205
+
206
+ const parseTime = ( s : string ) =>
207
+ s . includes ( 'ms' ) ? parseFloat ( s ) : parseFloat ( s ) * millisecondsInSecond ;
208
+
209
+ const duration =
210
+ Math . max ( ...style . transitionDuration . split ( ',' ) . map ( parseTime ) ) +
211
+ Math . max ( ...style . transitionDelay . split ( ',' ) . map ( parseTime ) ) ;
212
+
213
+ if ( duration <= 0 ) {
214
+ completeTransition ( ) ;
215
+ return ;
216
+ }
217
+
218
+ // fallback если onTransitionEnd не пришёл
219
+ // TRANSITION_FALLBACK_DELAY, чтобы минимизировать вероятность,
220
+ // что setTimeout сработает раньше onTransitionEnd
221
+ timerRef . current = setTimeout ( completeTransition , duration + TRANSITION_FALLBACK_DELAY ) ;
222
+
223
+ return clearTimer ;
224
+ }
225
+ return ;
226
+ } ,
227
+ [ completeTransition , state ] ,
228
+ ) ;
190
229
191
230
return [
192
231
state ,
193
232
{
194
233
ref,
195
234
onTransitionEnd :
196
235
state !== 'appeared' && state !== 'entered' && state !== 'exited'
197
- ? onTransitionEnd
236
+ ? completeTransition
198
237
: undefined ,
199
238
} ,
200
239
] ;
0 commit comments