@@ -18,7 +18,7 @@ import { createPortal } from "react-dom";
1818import { CSS as CSSUtil } from "@dnd-kit/utilities" ;
1919import clsx from "clsx" ;
2020
21- import { getLogicalBoundingClientRect } from "@cloudscape-design/component-toolkit/internal" ;
21+ import { createSingletonHandler , getLogicalBoundingClientRect } from "@cloudscape-design/component-toolkit/internal" ;
2222import {
2323 useInternalDragHandleInteractionState ,
2424 UseInternalDragHandleInteractionStateProps ,
@@ -174,6 +174,18 @@ export const ItemContainer = forwardRef(ItemContainerComponent);
174174// isn't set in stone.
175175export const CLICK_DRAG_THRESHOLD = 3 ;
176176
177+ // We use singleton helper to avoid creating event listeners per each board item.
178+ const usePointerEventListeners = createSingletonHandler <
179+ { type : "move" ; event : PointerEvent } | { type : "up" ; event : PointerEvent }
180+ > ( ( setEvent ) => {
181+ const handleGlobalPointerMove = ( event : PointerEvent ) => setEvent ( { type : "move" , event } ) ;
182+ const handleGlobalPointerUp = ( event : PointerEvent ) => setEvent ( { type : "up" , event } ) ;
183+ const controller = new AbortController ( ) ;
184+ window . addEventListener ( "pointermove" , handleGlobalPointerMove , { signal : controller . signal } ) ;
185+ window . addEventListener ( "pointerup" , handleGlobalPointerUp , { signal : controller . signal } ) ;
186+ return ( ) => controller . abort ( ) ;
187+ } ) ;
188+
177189function ItemContainerComponent (
178190 { item, placed, acquired, inTransition, transform, getItemSize, onKeyMove, children, isRtl } : ItemContainerProps ,
179191 ref : Ref < ItemContainerRef > ,
@@ -185,6 +197,8 @@ function ItemContainerComponent(
185197 const [ isHidden , setIsHidden ] = useState ( false ) ;
186198 const muteEventsRef = useRef ( false ) ;
187199 const itemRef = useRef < HTMLDivElement > ( null ) ;
200+ // Keeps the starting position of active pointer-based d&d transition.
201+ // If undefined - it means the pointer-based d&d is not engaged.
188202 const initialPointerDownPosition = useRef < { x : number ; y : number } | undefined > ( ) ;
189203 const draggableApi = useDraggable ( {
190204 draggableItem : item ,
@@ -306,12 +320,19 @@ function ItemContainerComponent(
306320 }
307321
308322 function onHandleKeyDown ( operation : HandleOperation , event : KeyboardEvent ) {
323+ // We do not expect keyboard input when pointer-based d&d is performed.
324+ // Upon receiving any, we discard the current transition immediately.
325+ if ( transition && initialPointerDownPosition . current ) {
326+ initialPointerDownPosition . current = undefined ;
327+ draggableApi . discardTransition ( ) ;
328+ return ;
329+ }
330+
309331 const discard = ( ) => {
310332 if ( transition || acquired ) {
311333 draggableApi . discardTransition ( ) ;
312334 }
313335 } ;
314-
315336 switch ( event . key ) {
316337 case "ArrowUp" :
317338 return handleDirectionalMovement ( "up" , operation ) ;
@@ -334,27 +355,15 @@ function ItemContainerComponent(
334355 // 1. If the last interaction is not "keyboard" (the user clicked on another handle issuing a new transition);
335356 // 2. If the item is acquired by the board (in that case the focus moves to the board item which is expected, palette item is hidden and all events handlers must be muted).
336357 selectedHook . current . processBlur ( ) ;
337- initialPointerDownPosition . current = undefined ;
358+
338359 if ( acquired || ( transition && transition . interactionType === "keyboard" && ! muteEventsRef . current ) ) {
360+ initialPointerDownPosition . current = undefined ;
339361 draggableApi . submitTransition ( ) ;
340362 }
341363 }
342364
343- function handleGlobalPointerMove ( event : PointerEvent ) {
344- if ( hasPointerMovedBeyondThreshold ( event , initialPointerDownPosition . current ) ) {
345- selectedHook . current . processPointerMove ( event ) ;
346- }
347- }
348-
349- function handleGlobalPointerUp ( event : PointerEvent ) {
350- selectedHook . current . processPointerUp ( event ) ;
351- initialPointerDownPosition . current = undefined ;
352- // Clean up global listeners after interaction ends
353- window . removeEventListener ( "pointermove" , handleGlobalPointerMove ) ;
354- window . removeEventListener ( "pointerup" , handleGlobalPointerUp ) ;
355- }
356-
357- function onDragHandlePointerDown ( event : ReactPointerEvent , operation : HandleOperation ) {
365+ // Pointer-down handler, added to each drag- and resize handle.
366+ function onHandlePointerDown ( event : ReactPointerEvent , operation : HandleOperation ) {
358367 // Prevent UI issues when right-clicking: in such a case the OS context menu will be shown and
359368 // the board while the board-item is active. Because of the context menu under the pointer,
360369 // onPointerUp is not called anymore. In such a case the board item would stuck in onPointerUp.
@@ -369,11 +378,25 @@ function ItemContainerComponent(
369378 selectedHook . current = resizeInteractionHook ;
370379 }
371380 selectedHook . current . processPointerDown ( event . nativeEvent ) ;
372-
373- // If pointerdown is on our button, start listening for global move and up
374- window . addEventListener ( "pointermove" , handleGlobalPointerMove ) ;
375- window . addEventListener ( "pointerup" , handleGlobalPointerUp ) ;
376381 }
382+ // Global pointer-move and pointer-up handlers.
383+ usePointerEventListeners ( ( { type, event } ) => {
384+ switch ( type ) {
385+ case "move" : {
386+ if ( hasPointerMovedBeyondThreshold ( event , initialPointerDownPosition . current ) ) {
387+ selectedHook . current . processPointerMove ( event ) ;
388+ }
389+ break ;
390+ }
391+ case "up" : {
392+ if ( initialPointerDownPosition . current ) {
393+ selectedHook . current . processPointerUp ( event ) ;
394+ }
395+ initialPointerDownPosition . current = undefined ;
396+ break ;
397+ }
398+ }
399+ } ) ;
377400
378401 function handlePointerInteractionStart ( event : PointerEvent , operation : "drag" | "resize" ) {
379402 const currentItemElement = itemRef . current ;
@@ -470,9 +493,11 @@ function ItemContainerComponent(
470493 onDndEndAction : ( ) => transition && draggableApi . submitTransition ( ) ,
471494 onUapActionStartAction : ( ) => handleIncrementalTransition ( "resize" ) ,
472495 } ;
496+
497+ // When d&d is active, either drag- or resize hook should be used.
498+ // We determine the correct one in the onHandlePointerDown handler, and store it to the selectedHook ref.
473499 const dragInteractionHook = useInternalDragHandleInteractionState ( dragHookProps ) ;
474500 const resizeInteractionHook = useInternalDragHandleInteractionState ( resizeHookProps ) ;
475- // We use a ref to the hook for the handle which is currently active. Distinguishment is managed in the handle button's onPointerDown callback.
476501 const selectedHook = useRef ( dragInteractionHook ) ;
477502
478503 const isActive = ( ! ! transition && ! isHidden ) || ! ! acquired ;
@@ -499,7 +524,7 @@ function ItemContainerComponent(
499524 isHidden,
500525 dragHandle : {
501526 ref : dragHandleRef ,
502- onPointerDown : ( e ) => onDragHandlePointerDown ( e , "drag" ) ,
527+ onPointerDown : ( e ) => onHandlePointerDown ( e , "drag" ) ,
503528 onKeyDown : ( event : KeyboardEvent ) => onHandleKeyDown ( "drag" , event ) ,
504529 activeState : determineHandleActiveState ( {
505530 isHandleActive : isActive ,
@@ -513,7 +538,7 @@ function ItemContainerComponent(
513538 } ,
514539 resizeHandle : placed
515540 ? {
516- onPointerDown : ( e ) => onDragHandlePointerDown ( e , "resize" ) ,
541+ onPointerDown : ( e ) => onHandlePointerDown ( e , "resize" ) ,
517542 onKeyDown : ( event : KeyboardEvent ) => onHandleKeyDown ( "resize" , event ) ,
518543 activeState : determineHandleActiveState ( {
519544 isHandleActive : isActive ,
0 commit comments