1
- import { useEffect , useRef } from 'react' ;
1
+ import { useLayoutEffect , useRef } from 'react' ;
2
2
import { css } from '@linaria/core' ;
3
3
4
4
import { useLatestFunc } from './hooks' ;
@@ -12,6 +12,21 @@ import type {
12
12
RenderEditCellProps
13
13
} from './types' ;
14
14
15
+ declare global {
16
+ const scheduler : Scheduler | undefined ;
17
+ }
18
+
19
+ interface Scheduler {
20
+ readonly postTask ?: (
21
+ callback : ( ) => void ,
22
+ options ?: {
23
+ priority ?: 'user-blocking' | 'user-visible' | 'background' ;
24
+ signal ?: AbortSignal ;
25
+ delay ?: number ;
26
+ }
27
+ ) => Promise < unknown > ;
28
+ }
29
+
15
30
/*
16
31
* To check for outside `mousedown` events, we listen to all `mousedown` events at their birth,
17
32
* i.e. on the window during the capture phase, and at their death, i.e. on the window during the bubble phase.
@@ -21,13 +36,15 @@ import type {
21
36
*
22
37
* The event can be `stopPropagation()`ed halfway through, so they may not always bubble back up to the window,
23
38
* so an alternative check must be used. The check must happen after the event can reach the "inside" container,
24
- * and not before it run to completion. `requestAnimationFrame` is the best way we know how to achieve this.
39
+ * and not before it run to completion. `postTask`/` requestAnimationFrame` are the best way we know to achieve this.
25
40
* Usually we want click event handlers from parent components to access the latest commited values,
26
41
* so `mousedown` is used instead of `click`.
27
42
*
28
43
* We must also rely on React's event capturing/bubbling to handle elements rendered in a portal.
29
44
*/
30
45
46
+ const canUsePostTask = typeof scheduler === 'object' && typeof scheduler . postTask === 'function' ;
47
+
31
48
const cellEditing = css `
32
49
@layer rdg.EditCell {
33
50
padding : 0 ;
@@ -56,33 +73,68 @@ export default function EditCell<R, SR>({
56
73
onKeyDown,
57
74
navigate
58
75
} : EditCellProps < R , SR > ) {
76
+ const captureEventRef = useRef < MouseEvent | undefined > ( undefined ) ;
77
+ const abortControllerRef = useRef < AbortController > ( undefined ) ;
59
78
const frameRequestRef = useRef < number > ( undefined ) ;
60
79
const commitOnOutsideClick = column . editorOptions ?. commitOnOutsideClick ?? true ;
61
80
62
- // We need to prevent the `useEffect ` from cleaning up between re-renders,
81
+ // We need to prevent the `useLayoutEffect ` from cleaning up between re-renders,
63
82
// as `onWindowCaptureMouseDown` might otherwise miss valid mousedown events.
64
83
// To that end we instead access the latest props via useLatestFunc.
65
84
const commitOnOutsideMouseDown = useLatestFunc ( ( ) => {
66
85
onClose ( true , false ) ;
67
86
} ) ;
68
87
69
- useEffect ( ( ) => {
88
+ useLayoutEffect ( ( ) => {
70
89
if ( ! commitOnOutsideClick ) return ;
71
90
72
- function onWindowCaptureMouseDown ( ) {
73
- frameRequestRef . current = requestAnimationFrame ( commitOnOutsideMouseDown ) ;
91
+ function onWindowCaptureMouseDown ( event : MouseEvent ) {
92
+ captureEventRef . current = event ;
93
+
94
+ if ( canUsePostTask ) {
95
+ const abortController = new AbortController ( ) ;
96
+ const { signal } = abortController ;
97
+ abortControllerRef . current = abortController ;
98
+ // Use postTask to ensure that the event is not called in the middle of a React render
99
+ // and that it is called before the next paint.
100
+ scheduler
101
+ . postTask ( commitOnOutsideMouseDown , {
102
+ priority : 'user-blocking' ,
103
+ signal
104
+ } )
105
+ // ignore abort errors
106
+ . catch ( ( ) => { } ) ;
107
+ } else {
108
+ frameRequestRef . current = requestAnimationFrame ( commitOnOutsideMouseDown ) ;
109
+ }
110
+ }
111
+
112
+ function onWindowMouseDown ( event : MouseEvent ) {
113
+ if ( captureEventRef . current === event ) {
114
+ commitOnOutsideMouseDown ( ) ;
115
+ }
74
116
}
75
117
76
118
addEventListener ( 'mousedown' , onWindowCaptureMouseDown , { capture : true } ) ;
119
+ addEventListener ( 'mousedown' , onWindowMouseDown ) ;
77
120
78
121
return ( ) => {
79
122
removeEventListener ( 'mousedown' , onWindowCaptureMouseDown , { capture : true } ) ;
80
- cancelFrameRequest ( ) ;
123
+ removeEventListener ( 'mousedown' , onWindowMouseDown ) ;
124
+ cancelTask ( ) ;
81
125
} ;
82
126
} , [ commitOnOutsideClick , commitOnOutsideMouseDown ] ) ;
83
127
84
- function cancelFrameRequest ( ) {
85
- cancelAnimationFrame ( frameRequestRef . current ! ) ;
128
+ function cancelTask ( ) {
129
+ captureEventRef . current = undefined ;
130
+ if ( abortControllerRef . current !== undefined ) {
131
+ abortControllerRef . current . abort ( ) ;
132
+ abortControllerRef . current = undefined ;
133
+ }
134
+ if ( frameRequestRef . current !== undefined ) {
135
+ cancelAnimationFrame ( frameRequestRef . current ) ;
136
+ frameRequestRef . current = undefined ;
137
+ }
86
138
}
87
139
88
140
function handleKeyDown ( event : React . KeyboardEvent < HTMLDivElement > ) {
@@ -143,7 +195,7 @@ export default function EditCell<R, SR>({
143
195
className = { className }
144
196
style = { getCellStyle ( column , colSpan ) }
145
197
onKeyDown = { handleKeyDown }
146
- onMouseDownCapture = { cancelFrameRequest }
198
+ onMouseDownCapture = { cancelTask }
147
199
>
148
200
{ column . renderEditCell != null && (
149
201
< >
0 commit comments