@@ -24,6 +24,7 @@ import ReactFlow, {
2424 ReactFlowProvider ,
2525 Edge ,
2626 useViewport ,
27+ XYPosition ,
2728} from "reactflow" ;
2829import "reactflow/dist/style.css" ;
2930
@@ -47,6 +48,7 @@ import { YMap } from "yjs/dist/src/types/YMap";
4748import FloatingEdge from "./nodes/FloatingEdge" ;
4849import CustomConnectionLine from "./nodes/CustomConnectionLine" ;
4950import HelperLines from "./HelperLines" ;
51+ import { getAbsPos } from "../lib/store/canvasSlice" ;
5052
5153const nodeTypes = { SCOPE : ScopeNode , CODE : CodeNode , RICH : RichNode } ;
5254const edgeTypes = {
@@ -273,6 +275,152 @@ function useInitNodes() {
273275 return { loading } ;
274276}
275277
278+ function getBestNode (
279+ nodes : Node [ ] ,
280+ from ,
281+ direction : "up" | "down" | "left" | "right"
282+ ) {
283+ // find the best node to jump to from (x,y) in the given direction
284+ let bestNode : Node | null = null ;
285+ let bestDistance = Infinity ;
286+ nodes = nodes . filter ( ( node ) => {
287+ switch ( direction ) {
288+ case "up" :
289+ return (
290+ node . position . y + node . height ! / 2 <
291+ from . position . y + from . height ! / 2
292+ ) ;
293+ case "down" :
294+ return (
295+ node . position . y + node . height ! / 2 >
296+ from . position . y + from . height ! / 2
297+ ) ;
298+ case "left" :
299+ return (
300+ node . position . x + node . width ! / 2 < from . position . x + from . width ! / 2
301+ ) ;
302+ case "right" :
303+ return (
304+ node . position . x + node . width ! / 2 > from . position . x + from . width ! / 2
305+ ) ;
306+ }
307+ } ) ;
308+ for ( let node of nodes ) {
309+ // I should start from the edge, instead of the center
310+ const startPoint : XYPosition = ( ( ) => {
311+ // the center
312+ // return {
313+ // x: from.position.x + from.width! / 2,
314+ // y: from.position.y + from.height! / 2,
315+ // };
316+ // the edge depending on direction.
317+ switch ( direction ) {
318+ case "up" :
319+ return {
320+ x : from . position . x + from . width ! / 2 ,
321+ y : from . position . y ,
322+ } ;
323+ case "down" :
324+ return {
325+ x : from . position . x + from . width ! / 2 ,
326+ y : from . position . y + from . height ! ,
327+ } ;
328+ case "left" :
329+ return {
330+ x : from . position . x ,
331+ y : from . position . y + from . height ! / 2 ,
332+ } ;
333+ case "right" :
334+ return {
335+ x : from . position . x + from . width ! ,
336+ y : from . position . y + from . height ! / 2 ,
337+ } ;
338+ }
339+ } ) ( ) ;
340+ let distance =
341+ Math . pow ( node . position . x + node . width ! / 2 - startPoint . x , 2 ) *
342+ ( [ "left" , "right" ] . includes ( direction ) ? 1 : 2 ) +
343+ Math . pow ( node . position . y + node . height ! / 2 - startPoint . y , 2 ) *
344+ ( [ "up" , "down" ] . includes ( direction ) ? 1 : 2 ) ;
345+ if ( distance < bestDistance ) {
346+ bestDistance = distance ;
347+ bestNode = node ;
348+ }
349+ }
350+ return bestNode ;
351+ }
352+
353+ function useJump ( ) {
354+ const store = useContext ( RepoContext ) ! ;
355+
356+ const selectPod = useStore ( store , ( state ) => state . selectPod ) ;
357+ const resetSelection = useStore ( store , ( state ) => state . resetSelection ) ;
358+ const nodesMap = useStore ( store , ( state ) => state . ydoc . getMap < Node > ( "pods" ) ) ;
359+
360+ const reactflow = useReactFlow ( ) ;
361+
362+ const selectedPods = useStore ( store , ( state ) => state . selectedPods ) ;
363+ const handleKeyDown = ( event ) => {
364+ const id = selectedPods . values ( ) . next ( ) . value ; // Assuming only one node can be selected at a time
365+ if ( ! id ) {
366+ console . log ( "No node selected" ) ;
367+ return ; // Ignore arrow key presses if there's no selected node or if the user is typing in an input field
368+ }
369+ const pod = nodesMap . get ( id ) ;
370+ if ( ! pod ) {
371+ console . log ( "pod is undefined" ) ;
372+ return ;
373+ }
374+
375+ // get the sibling nodes
376+ const nodes = Array . from ( nodesMap . values ( ) ) . filter (
377+ ( node ) => node . parentNode === pod . parentNode
378+ ) ;
379+
380+ let to : null | Node = null ;
381+
382+ switch ( event . key ) {
383+ case "ArrowUp" :
384+ to = getBestNode ( nodes , pod , "up" ) ;
385+ break ;
386+ case "ArrowDown" :
387+ to = getBestNode ( nodes , pod , "down" ) ;
388+ break ;
389+ case "ArrowLeft" :
390+ to = getBestNode ( nodes , pod , "left" ) ;
391+ break ;
392+ case "ArrowRight" :
393+ to = getBestNode ( nodes , pod , "right" ) ;
394+ break ;
395+ default :
396+ return ;
397+ }
398+
399+ if ( to ) {
400+ // set the to node as selected
401+ resetSelection ( ) ;
402+ selectPod ( to . id , true ) ;
403+ // move the viewport to the to node
404+ // get the absolute position of the to node
405+ const pos = getAbsPos ( to , nodesMap ) ;
406+
407+ reactflow . setCenter ( pos . x + to . width ! / 2 , pos . y + to . height ! / 2 , {
408+ zoom : reactflow . getZoom ( ) ,
409+ duration : 800 ,
410+ } ) ;
411+ }
412+
413+ event . preventDefault ( ) ; // Prevent default browser behavior for arrow keys
414+ } ;
415+
416+ useEffect ( ( ) => {
417+ window . addEventListener ( "keydown" , handleKeyDown ) ;
418+ return ( ) => {
419+ window . removeEventListener ( "keydown" , handleKeyDown ) ;
420+ } ;
421+ } , [ selectedPods ] ) ;
422+ }
423+
276424function usePaste ( reactFlowWrapper ) {
277425 const store = useContext ( RepoContext ) ;
278426 if ( ! store ) throw new Error ( "Missing BearContext.Provider in the tree" ) ;
@@ -461,6 +609,7 @@ function CanvasImplWrap() {
461609 useEdgesYjsObserver ( ) ;
462610 usePaste ( reactFlowWrapper ) ;
463611 useCut ( reactFlowWrapper ) ;
612+ useJump ( ) ;
464613
465614 const { loading } = useInitNodes ( ) ;
466615 if ( loading ) return < div > Loading...</ div > ;
@@ -678,6 +827,7 @@ function CanvasImpl() {
678827 // TODO restore previous viewport
679828 defaultViewport = { { zoom : 1 , x : 0 , y : 0 } }
680829 proOptions = { { hideAttribution : true } }
830+ disableKeyboardA11y = { true }
681831 >
682832 < Box >
683833 < MiniMap
0 commit comments