8
8
createContext ,
9
9
useContext ,
10
10
useState ,
11
+ useEffect ,
12
+ useCallback ,
13
+ type RefObject ,
11
14
} from "react" ;
12
15
import * as Primitive from "@radix-ui/react-dialog" ;
13
16
import { css , theme , type CSS } from "../stitches.config" ;
@@ -120,30 +123,87 @@ type Point = { x: number; y: number };
120
123
type Size = { width : number ; height : number } ;
121
124
type Rect = Point & Size ;
122
125
126
+ const centeredContent = {
127
+ top : "50%" ,
128
+ left : "50%" ,
129
+ transform : "translate(-50%, -50%)" ,
130
+ width : "100vw" ,
131
+ height : "100vh" ,
132
+ } as const ;
133
+
123
134
type UseDraggableProps = {
124
- isMaximized ? : boolean ;
135
+ isMaximized : boolean ;
125
136
minWidth ?: number ;
126
137
minHeight ?: number ;
127
138
} & Partial < Rect > ;
128
139
129
140
const useDraggable = ( {
130
- x,
131
- y,
132
141
width,
133
142
height,
134
143
minHeight,
135
144
minWidth,
145
+ isMaximized,
146
+ ...props
136
147
} : UseDraggableProps ) => {
137
- const { isMaximized } = useContext ( DialogContext ) ;
138
- const initialDataRef = useRef <
148
+ const [ x , setX ] = useState ( props . x ) ;
149
+ const [ y , setY ] = useState ( props . y ) ;
150
+
151
+ const lastDragDataRef = useRef <
139
152
| undefined
140
153
| {
141
154
point : Point ;
142
155
rect : Rect ;
143
156
}
144
157
> ( undefined ) ;
158
+
145
159
const ref = useRef < HTMLDivElement | null > ( null ) ;
146
160
161
+ const calcStyle = useCallback ( ( ) => {
162
+ const style : CSSProperties = isMaximized
163
+ ? centeredContent
164
+ : {
165
+ ...centeredContent ,
166
+ width,
167
+ height,
168
+ } ;
169
+
170
+ if ( minWidth !== undefined ) {
171
+ style . minWidth = minWidth ;
172
+ }
173
+ if ( minHeight !== undefined ) {
174
+ style . minHeight = minHeight ;
175
+ }
176
+
177
+ if ( isMaximized === false ) {
178
+ if ( x !== undefined ) {
179
+ style . left = x ;
180
+ style . transform = "none" ;
181
+ }
182
+ if ( y !== undefined ) {
183
+ style . top = y ;
184
+ style . transform = "none" ;
185
+ }
186
+ }
187
+ return style ;
188
+ } , [ x , y , width , height , isMaximized , minWidth , minHeight ] ) ;
189
+
190
+ const [ style , setStyle ] = useState ( calcStyle ( ) ) ;
191
+
192
+ useEffect ( ( ) => {
193
+ setStyle ( calcStyle ( ) ) ;
194
+ } , [ calcStyle ] ) ;
195
+
196
+ useEffect ( ( ) => {
197
+ if ( lastDragDataRef . current ) {
198
+ // Until user draggs, we need component props to define the position, because floating panel needs to adjust it after rendering.
199
+ // We don't want to use the props x/y value after user has dragged manually. At this point position is defined
200
+ // by drag interaction and props can't override it, otherwise position will jump for unpredictable reasons, e.g. when parent decides to update.
201
+ return ;
202
+ }
203
+ setX ( props . x ) ;
204
+ setY ( props . y ) ;
205
+ } , [ props . x , props . y ] ) ;
206
+
147
207
const handleDragStart : DragEventHandler = ( event ) => {
148
208
const target = ref . current ;
149
209
if ( target === null ) {
@@ -157,7 +217,7 @@ const useDraggable = ({
157
217
target . style . left = `${ rect . x } px` ;
158
218
target . style . top = `${ rect . y } px` ;
159
219
target . style . transform = "none" ;
160
- initialDataRef . current = {
220
+ lastDragDataRef . current = {
161
221
point : { x : event . pageX , y : event . pageY } ,
162
222
rect,
163
223
} ;
@@ -169,12 +229,12 @@ const useDraggable = ({
169
229
if (
170
230
event . pageX <= 0 ||
171
231
event . pageY <= 0 ||
172
- initialDataRef . current === undefined ||
232
+ lastDragDataRef . current === undefined ||
173
233
target === null
174
234
) {
175
235
return ;
176
236
}
177
- const { rect, point } = initialDataRef . current ;
237
+ const { rect, point } = lastDragDataRef . current ;
178
238
const movementX = point . x - event . pageX ;
179
239
const movementY = point . y - event . pageY ;
180
240
let left = Math . max ( rect . x - movementX , 0 ) ;
@@ -186,80 +246,49 @@ const useDraggable = ({
186
246
target . style . top = `${ top } px` ;
187
247
} ;
188
248
189
- const style : CSSProperties = isMaximized
190
- ? {
191
- ...centeredContent ,
192
- width : "100vw" ,
193
- height : "100vh" ,
194
- }
195
- : {
196
- ...centeredContent ,
197
- width,
198
- height,
199
- } ;
200
-
201
- if ( minWidth !== undefined ) {
202
- style . minWidth = minWidth ;
203
- }
204
- if ( minHeight !== undefined ) {
205
- style . minHeight = minHeight ;
206
- }
207
- if ( isMaximized === false ) {
208
- if ( x !== undefined ) {
209
- style . left = x ;
210
- delete style . transform ;
211
- }
212
- if ( y !== undefined ) {
213
- style . top = y ;
214
- delete style . transform ;
249
+ const handleDragEnd : DragEventHandler = ( ) => {
250
+ const target = ref . current ;
251
+ if ( target === null ) {
252
+ return ;
215
253
}
216
- }
254
+ const rect = target . getBoundingClientRect ( ) ;
255
+ setX ( rect . x ) ;
256
+ setY ( rect . y ) ;
257
+ } ;
258
+
217
259
return {
218
260
onDragStart : handleDragStart ,
219
261
onDrag : handleDrag ,
262
+ onDragEnd : handleDragEnd ,
220
263
style,
221
264
ref,
222
265
} ;
223
266
} ;
224
267
225
268
// This is needed to prevent pointer events on the iframe from interfering with dragging and resizing.
226
- const useSetPointerEvents = ( ) => {
269
+ const useSetPointerEvents = ( elementRef : RefObject < HTMLElement | null > ) => {
227
270
const { enableCanvasPointerEvents, disableCanvasPointerEvents } =
228
271
useDisableCanvasPointerEvents ( ) ;
229
272
230
- const setPointerEvents = ( value : string ) => {
231
- return ( ) => {
232
- value === "none"
233
- ? disableCanvasPointerEvents ( )
234
- : enableCanvasPointerEvents ( ) ;
235
- // RAF is needed otherwise dragstart event won't fire because of pointer-events: none
236
- requestAnimationFrame ( ( ) => {
237
- if ( element ) {
238
- element . style . pointerEvents = value ;
239
- }
240
- } ) ;
241
- } ;
242
- } ;
243
-
244
- const [ element , ref ] = useResize ( {
245
- onResizeStart : setPointerEvents ( "none" ) ,
246
- onResizeEnd : setPointerEvents ( "auto" ) ,
247
- } ) ;
248
-
249
- const { resize, draggable } = useContext ( DialogContext ) ;
250
-
251
- if ( resize === "none" && draggable !== true ) {
252
- return { } ;
253
- }
254
-
255
- return {
256
- ref,
257
- onDragStartCapture : setPointerEvents ( "none" ) ,
258
- onDragEndCapture : setPointerEvents ( "auto" ) ,
259
- } ;
273
+ return useCallback (
274
+ ( value : string ) => {
275
+ return ( ) => {
276
+ value === "none"
277
+ ? disableCanvasPointerEvents ( )
278
+ : enableCanvasPointerEvents ( ) ;
279
+ // RAF is needed otherwise dragstart event won't fire because of pointer-events: none
280
+ requestAnimationFrame ( ( ) => {
281
+ if ( elementRef . current ) {
282
+ elementRef . current . style . pointerEvents = value ;
283
+ }
284
+ } ) ;
285
+ } ;
286
+ } ,
287
+ [ elementRef , enableCanvasPointerEvents , disableCanvasPointerEvents ]
288
+ ) ;
260
289
} ;
261
290
262
- export const DialogContent = forwardRef (
291
+ const ContentContainer = forwardRef (
263
292
(
264
293
{
265
294
children,
@@ -273,36 +302,53 @@ export const DialogContent = forwardRef(
273
302
minHeight,
274
303
...props
275
304
} : ComponentProps < typeof Primitive . Content > &
276
- UseDraggableProps & {
305
+ Partial < UseDraggableProps > & {
277
306
css ?: CSS ;
278
307
} ,
279
308
forwardedRef : Ref < HTMLDivElement >
280
309
) => {
281
- const { resize } = useContext ( DialogContext ) ;
282
- const { ref : draggableRef , ...draggableProps } = useDraggable ( {
310
+ const { resize, isMaximized } = useContext ( DialogContext ) ;
311
+ const { ref, ...draggableProps } = useDraggable ( {
283
312
width,
284
313
height,
285
314
x,
286
315
y,
287
316
minWidth,
288
317
minHeight,
318
+ isMaximized,
319
+ } ) ;
320
+ const setPointerEvents = useSetPointerEvents ( ref ) ;
321
+
322
+ const [ _ , setElement ] = useResize ( {
323
+ onResizeStart : setPointerEvents ?.( "none" ) ,
324
+ onResizeEnd : setPointerEvents ?.( "auto" ) ,
289
325
} ) ;
290
326
291
- const { ref : pointerEventsRef , ...pointerEventsProps } =
292
- useSetPointerEvents ( ) ;
327
+ return (
328
+ < Primitive . Content
329
+ className = { contentStyle ( { className, css, resize } ) }
330
+ onDragStartCapture = { setPointerEvents ( "none" ) }
331
+ onDragEndCapture = { setPointerEvents ( "auto" ) }
332
+ { ...draggableProps }
333
+ { ...props }
334
+ ref = { mergeRefs ( forwardedRef , ref , setElement ) }
335
+ >
336
+ { children }
337
+ </ Primitive . Content >
338
+ ) ;
339
+ }
340
+ ) ;
341
+ ContentContainer . displayName = "ContentContainer" ;
293
342
343
+ export const DialogContent = forwardRef (
344
+ (
345
+ props : ComponentProps < typeof ContentContainer > ,
346
+ forwardedRef : Ref < HTMLDivElement >
347
+ ) => {
294
348
return (
295
349
< Primitive . Portal >
296
350
< Primitive . Overlay className = { overlayStyle ( ) } />
297
- < Primitive . Content
298
- className = { contentStyle ( { className, css, resize } ) }
299
- { ...draggableProps }
300
- { ...pointerEventsProps }
301
- { ...props }
302
- ref = { mergeRefs ( forwardedRef , draggableRef , pointerEventsRef ) }
303
- >
304
- { children }
305
- </ Primitive . Content >
351
+ < ContentContainer { ...props } ref = { forwardedRef } />
306
352
</ Primitive . Portal >
307
353
) ;
308
354
}
@@ -378,12 +424,6 @@ const overlayStyle = css({
378
424
inset : 0 ,
379
425
} ) ;
380
426
381
- const centeredContent : CSSProperties = {
382
- top : "50%" ,
383
- left : "50%" ,
384
- transform : "translate(-50%, -50%)" ,
385
- } ;
386
-
387
427
const contentStyle = css ( panelStyle , {
388
428
position : "fixed" ,
389
429
width : "min-content" ,
0 commit comments