From 6db3c499475178650c001f8365099e5d7e3b505c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Tue, 2 Jun 2026 15:34:23 +0200 Subject: [PATCH 1/2] Toggle debug mode at runtime --- .changeset/debugger-at-runtime.md | 5 ++ docs/api-reference/SpatialNavigation.md | 40 ++++++++++++++ docs/guides/debugging.md | 28 ++++++++++ packages/core/src/SpatialNavigation.ts | 69 ++++++++++++++++++------- packages/core/src/VisualDebugger.ts | 25 ++++++++- 5 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 .changeset/debugger-at-runtime.md diff --git a/.changeset/debugger-at-runtime.md b/.changeset/debugger-at-runtime.md new file mode 100644 index 0000000..8fc8e8e --- /dev/null +++ b/.changeset/debugger-at-runtime.md @@ -0,0 +1,5 @@ +--- +'@noriginmedia/norigin-spatial-navigation-core': minor +--- + +Added `setDebug` and `setVisualDebug` methods to the `SpatialNavigation` to enable/disable at runtime the navigation decision logs and the canvas overlay showing bounding boxes and navigation paths. diff --git a/docs/api-reference/SpatialNavigation.md b/docs/api-reference/SpatialNavigation.md index 5b0fbee..ba417d3 100644 --- a/docs/api-reference/SpatialNavigation.md +++ b/docs/api-reference/SpatialNavigation.md @@ -24,6 +24,8 @@ import { setKeyMap, setThrottle, updateRtl, + setDebug, + setVisualDebug, ROOT_FOCUS_KEY } from '@noriginmedia/norigin-spatial-navigation-core'; ``` @@ -310,6 +312,44 @@ See [RTL Support](../guides/rtl-support.md) for more. --- +## `setDebug(debug)` + +Toggle debug mode at runtime. It logs navigation decisions to the browser console. + +```typescript +setDebug(debug: boolean): void +``` + +```typescript +import { setDebug } from '@noriginmedia/norigin-spatial-navigation-react'; + +setDebug(true); // enable logging of navigation decisions +setDebug(false); // disable logging of navigation decisions +``` + +See [Debugging](../guides/debugging.md) for more. + +--- + +## `setVisualDebug(visualDebug)` + +Toggle visual debug mode at runtime. It draws a canvas overlay showing component bounding boxes and navigation paths. + +```typescript +setVisualDebug(visualDebug: boolean): void +``` + +```typescript +import { setVisualDebug } from '@noriginmedia/norigin-spatial-navigation-react'; + +setVisualDebug(true); // enable visual debugger +setVisualDebug(false); // disable visual debugger +``` + +See [Debugging](../guides/debugging.md) for more. + +--- + ## `ROOT_FOCUS_KEY` A constant string (`'SN:ROOT'`) representing the root of the focus tree. Pass it to `setFocus` to boot navigation. diff --git a/docs/guides/debugging.md b/docs/guides/debugging.md index aa63b9f..bb255bb 100644 --- a/docs/guides/debugging.md +++ b/docs/guides/debugging.md @@ -20,6 +20,20 @@ init({ debug: true, visualDebug: false }); Disable this in production, it generates a significant volume of log output. +### Enabling Debug Logger at Runtime + +Use `setDebug` to enable or disable logging navigation decisions to the browser console. This is useful when users can toggle debug mode while the application is running. + +```typescript +import { setDebug } from '@noriginmedia/norigin-spatial-navigation-core'; + +// Turn on logging navigation decisions to the browser console +setDebug(true); + +// Turn off logging navigation decisions to the browser console +setDebug(false); +``` + --- ## Visual Debugger @@ -36,6 +50,20 @@ init({ debug: false, visualDebug: true }); The visual debugger is the fastest way to diagnose unexpected navigation behavior because you can see exactly what the library "sees" in terms of component positions. +### Enabling Visual Debugger at Runtime + +Use `setVisualDebug` to enable or disable drawing a canvas overlay showing component bounding boxes and navigation paths. This is useful when users can toggle visual debugger while the application is running. + +```typescript +import { setVisualDebug } from '@noriginmedia/norigin-spatial-navigation-core'; + +// Turn on drawing a canvas overlay +setVisualDebug(true); + +// Turn off drawing a canvas overlay +setVisualDebug(false); +``` + --- ## Common Problems and Solutions diff --git a/packages/core/src/SpatialNavigation.ts b/packages/core/src/SpatialNavigation.ts index 07d11ab..de9b11a 100644 --- a/packages/core/src/SpatialNavigation.ts +++ b/packages/core/src/SpatialNavigation.ts @@ -722,31 +722,14 @@ class SpatialNavigationService { customDistanceCalculationFunction; this.onUtterText = onUtterText ?? undefined; - this.debug = debug; + this.setDebug(debug); if (!this.nativeMode) { if (Number.isInteger(throttleParam) && throttleParam > 0) { this.throttle = throttleParam; } this.bindEventHandlers(); - if (visualDebug) { - this.visualDebugger = new VisualDebugger(this.writingDirection); - const draw = () => { - requestAnimationFrame(() => { - this.visualDebugger.clearLayouts(); - forOwn(this.focusableComponents, (component, focusKey) => { - this.visualDebugger.drawLayout( - component.layout, - focusKey, - component.parentFocusKey - ); - }); - draw(); - }); - }; - - draw(); - } + this.setVisualDebug(visualDebug); } else { console.warn( 'nativeMode option is deprecated and will be removed in the next version.' @@ -783,6 +766,7 @@ class SpatialNavigationService { this.keyMap = DEFAULT_KEY_MAP; this.onUtterText = undefined; + this.setVisualDebug(false); this.unbindEventHandlers(); } } @@ -1898,6 +1882,49 @@ class SpatialNavigationService { updateRtl(rtl: boolean) { this.writingDirection = rtl ? WritingDirection.RTL : WritingDirection.LTR; } + + setDebug(debug: boolean) { + if (!this.enabled) { + return; + } + + this.debug = debug; + } + + setVisualDebug(visualDebug: boolean) { + if (!this.enabled) { + return; + } + if (visualDebug === !!this.visualDebugger) { + return; + } + + if (visualDebug) { + this.visualDebugger = new VisualDebugger(this.writingDirection); + const draw = () => { + requestAnimationFrame(() => { + if (!this.visualDebugger) { + return; + } + + this.visualDebugger.clearLayouts(); + forOwn(this.focusableComponents, (component, focusKey) => { + this.visualDebugger.drawLayout( + component.layout, + focusKey, + component.parentFocusKey + ); + }); + draw(); + }); + }; + + draw(); + } else { + this.visualDebugger.destroy(); + this.visualDebugger = null; + } + } } /** @@ -1917,5 +1944,7 @@ export const { updateAllLayouts, getCurrentFocusKey, doesFocusableExist, - updateRtl + updateRtl, + setDebug, + setVisualDebug } = SpatialNavigation; diff --git a/packages/core/src/VisualDebugger.ts b/packages/core/src/VisualDebugger.ts index 52d8ef9..c1bf158 100644 --- a/packages/core/src/VisualDebugger.ts +++ b/packages/core/src/VisualDebugger.ts @@ -15,6 +15,9 @@ interface NodeLayout { height: number; } +const DEBUG_CANVAS_ID = 'sn-debug'; +const LAYOUTS_CANVAS_ID = 'sn-layouts'; + class VisualDebugger { private debugCtx: CanvasRenderingContext2D; @@ -25,12 +28,12 @@ class VisualDebugger { constructor(writingDirection: WritingDirection) { if (hasDOM) { this.debugCtx = VisualDebugger.createCanvas( - 'sn-debug', + DEBUG_CANVAS_ID, '1010', writingDirection ); this.layoutsCtx = VisualDebugger.createCanvas( - 'sn-layouts', + LAYOUTS_CANVAS_ID, '1000', writingDirection ); @@ -83,6 +86,24 @@ class VisualDebugger { this.layoutsCtx.clearRect(0, 0, WIDTH, HEIGHT); } + destroy() { + this.clear(); + this.clearLayouts(); + + this.debugCtx = null; + this.layoutsCtx = null; + + const debugCanvas = document.getElementById(DEBUG_CANVAS_ID); + if (debugCanvas) { + debugCanvas.parentNode.removeChild(debugCanvas); + } + + const layoutsCanvas = document.getElementById(LAYOUTS_CANVAS_ID); + if (layoutsCanvas) { + layoutsCanvas.parentNode.removeChild(layoutsCanvas); + } + } + drawLayout(layout: NodeLayout, focusKey: string, parentFocusKey: string) { if (!hasDOM) { return; From ed6675e635e51ea26b81064f7b60582662aae1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Wed, 3 Jun 2026 15:08:44 +0200 Subject: [PATCH 2/2] Cancelling animation frame requset on destroy --- packages/core/src/SpatialNavigation.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/src/SpatialNavigation.ts b/packages/core/src/SpatialNavigation.ts index de9b11a..da8798f 100644 --- a/packages/core/src/SpatialNavigation.ts +++ b/packages/core/src/SpatialNavigation.ts @@ -208,6 +208,8 @@ class SpatialNavigationService { private visualDebugger: VisualDebugger; + private animationFrameRequestId: number; + /** * Focus key of the currently focused element */ @@ -683,6 +685,7 @@ class SpatialNavigationService { this.debug = false; this.visualDebugger = null; + this.animationFrameRequestId = -1; this.logIndex = 0; @@ -1902,7 +1905,7 @@ class SpatialNavigationService { if (visualDebug) { this.visualDebugger = new VisualDebugger(this.writingDirection); const draw = () => { - requestAnimationFrame(() => { + this.animationFrameRequestId = requestAnimationFrame(() => { if (!this.visualDebugger) { return; } @@ -1921,6 +1924,10 @@ class SpatialNavigationService { draw(); } else { + if (this.animationFrameRequestId > -1) { + cancelAnimationFrame(this.animationFrameRequestId); + this.animationFrameRequestId = -1; + } this.visualDebugger.destroy(); this.visualDebugger = null; }