From 0e40e6a69d6e3b94154787bb048d53b8d2112ea5 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 4 Dec 2025 12:35:22 -0800 Subject: [PATCH 1/2] feat: Make navigation looping configurable --- core/keyboard_nav/block_navigation_policy.ts | 10 +++- core/keyboard_nav/line_cursor.ts | 61 ++++++++++++++++---- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/core/keyboard_nav/block_navigation_policy.ts b/core/keyboard_nav/block_navigation_policy.ts index f79efcf2529..3449c73f534 100644 --- a/core/keyboard_nav/block_navigation_policy.ts +++ b/core/keyboard_nav/block_navigation_policy.ts @@ -181,7 +181,8 @@ function getBlockNavigationCandidates( * `delta` relative to the current element's stack when navigating backwards. */ export function navigateStacks(current: ISelectable, delta: number) { - const stacks: IFocusableNode[] = (current.workspace as WorkspaceSvg) + const workspace = current.workspace as WorkspaceSvg; + const stacks: IFocusableNode[] = workspace .getTopBoundedElements(true) .filter((element: IBoundedElement) => isFocusableNode(element)); const currentIndex = stacks.indexOf( @@ -189,12 +190,15 @@ export function navigateStacks(current: ISelectable, delta: number) { ); const targetIndex = currentIndex + delta; let result: IFocusableNode | null = null; + const loop = workspace.getCursor().getNavigationLoops(); if (targetIndex >= 0 && targetIndex < stacks.length) { result = stacks[targetIndex]; - } else if (targetIndex < 0) { + } else if (loop && targetIndex < 0) { result = stacks[stacks.length - 1]; - } else if (targetIndex >= stacks.length) { + } else if (loop && targetIndex >= stacks.length) { result = stacks[0]; + } else { + return null; } // When navigating to a previous block stack, our previous sibling is the last diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 549e51a9352..3cdb6f677a5 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -42,6 +42,9 @@ export class LineCursor extends Marker { /** Locations to try moving the cursor to after a deletion. */ private potentialNodes: IFocusableNode[] | null = null; + /** Whether or not navigation loops around when reaching the end. */ + private navigationLoops = true; + /** * @param workspace The workspace this cursor belongs to. */ @@ -64,7 +67,7 @@ export class LineCursor extends Marker { const newNode = this.getNextNode( curNode, this.getValidationFunction(NavigationDirection.NEXT), - true, + this.getNavigationLoops(), ); if (newNode) { @@ -89,7 +92,7 @@ export class LineCursor extends Marker { const newNode = this.getNextNode( curNode, this.getValidationFunction(NavigationDirection.IN), - true, + this.getNavigationLoops(), ); if (newNode) { @@ -112,7 +115,7 @@ export class LineCursor extends Marker { const newNode = this.getPreviousNode( curNode, this.getValidationFunction(NavigationDirection.PREVIOUS), - true, + this.getNavigationLoops(), ); if (newNode) { @@ -137,7 +140,7 @@ export class LineCursor extends Marker { const newNode = this.getPreviousNode( curNode, this.getValidationFunction(NavigationDirection.OUT), - true, + this.getNavigationLoops(), ); if (newNode) { @@ -158,12 +161,12 @@ export class LineCursor extends Marker { const inNode = this.getNextNode( curNode, this.getValidationFunction(NavigationDirection.IN), - true, + this.getNavigationLoops(), ); const nextNode = this.getNextNode( curNode, this.getValidationFunction(NavigationDirection.NEXT), - true, + this.getNavigationLoops(), ); return inNode === nextNode; @@ -221,9 +224,19 @@ export class LineCursor extends Marker { isValid: (p1: IFocusableNode | null) => boolean, loop: boolean, ): IFocusableNode | null { - if (!node || (!loop && this.getLastNode() === node)) return null; + const originalLoop = this.getNavigationLoops(); + this.setNavigationLoops(loop); + + let result: IFocusableNode | null; + if (!node || (!loop && this.getLastNode() === node)) { + result = null; + } else { + result = this.getNextNodeImpl(node, isValid); + } - return this.getNextNodeImpl(node, isValid); + this.setNavigationLoops(originalLoop); + + return result; } /** @@ -275,9 +288,19 @@ export class LineCursor extends Marker { isValid: (p1: IFocusableNode | null) => boolean, loop: boolean, ): IFocusableNode | null { - if (!node || (!loop && this.getFirstNode() === node)) return null; + const originalLoop = this.getNavigationLoops(); + this.setNavigationLoops(loop); + + let result: IFocusableNode | null; + if (!node || (!loop && this.getFirstNode() === node)) { + result = null; + } else { + result = this.getPreviousNodeImpl(node, isValid); + } - return this.getPreviousNodeImpl(node, isValid); + this.setNavigationLoops(originalLoop); + + return result; } /** @@ -538,6 +561,24 @@ export class LineCursor extends Marker { const first = this.getFirstNode(); return this.getPreviousNode(first, () => true, true); } + + /** + * Sets whether or not navigation should loop around when reaching the end + * of the workspace. + * + * @param loops True if navigation should loop around, otherwise false. + */ + setNavigationLoops(loops: boolean) { + this.navigationLoops = loops; + } + + /** + * Returns whether or not navigation loops around when reaching the end of + * the workspace. + */ + getNavigationLoops(): boolean { + return this.navigationLoops; + } } registry.register(registry.Type.CURSOR, registry.DEFAULT, LineCursor); From 9d39e49fd24ce5ecbfad4920ff43098fdcdf2edc Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 5 Dec 2025 14:18:47 -0800 Subject: [PATCH 2/2] chore: Add TODO to clean up API --- core/keyboard_nav/line_cursor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 3cdb6f677a5..fe2779e48a9 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -222,6 +222,7 @@ export class LineCursor extends Marker { getNextNode( node: IFocusableNode | null, isValid: (p1: IFocusableNode | null) => boolean, + // TODO: Consider deprecating and removing this argument. loop: boolean, ): IFocusableNode | null { const originalLoop = this.getNavigationLoops(); @@ -286,6 +287,7 @@ export class LineCursor extends Marker { getPreviousNode( node: IFocusableNode | null, isValid: (p1: IFocusableNode | null) => boolean, + // TODO: Consider deprecating and removing this argument. loop: boolean, ): IFocusableNode | null { const originalLoop = this.getNavigationLoops();