Simple Tree Navigation Using Arrow Keys

Hi @walter, I have a few questions regarding the accessibility navigation in the structured diagram. The overall navigation seems to be working fine, and I see that we’re relying on _findNearestTowards based on the arrow key pressed.

However, there are a few cases where the behavior seems inconsistent:

  1. When nodes are close to each other, navigation works as expected.
  2. When two nodes are near each other, the link is skipped, and focus moves directly to the other node.
  3. In a scenario with three sibling nodes, if one is positioned near the parent, pressing the left arrow sometimes shifts focus to the parent instead of a sibling.

To ensure more consistent and predictable navigation, we may need a more standardized approach, such as breadth-first (BFS) traversal.

Is that possible here?

Yes, that’s possible. You can find an implementation of that in the DrawCommandHandler extension, which has a mode that implements the standard tree view control navigation and expand/collapse functionality using arrow keys.

Hi @walter, I explored the DrawCommandHandler and looked into the sample provided, but I wasn’t able to fully understand or relate it to our use case.

We’re currently using the sample code you shared earlier, and overall it’s working well—except for the _findNearestTowards method. We’re facing issues specifically with its navigation logic.

Would it be possible for you to provide a sample implementation of _findNearestTowards that uses BFS (Breadth-First Search) for node navigation?

For example, imagine a parent node with three children, and each child contains interactive elements. When pressing the Down Arrow, the focus should move to the leftmost child. Then, pressing the Right Arrow should move the focus to the sibling nodes (middle or right child), and pressing Left Arrow should navigate in the opposite direction. Essentially, it should traverse children on the same level before moving deeper into the hierarchy.

A sample in that direction would be really helpful!

Did you try the “tree” arrowKeyBehavior? That’s meant to implement the typical tree-view-like behavior for arrow keys, which is basically DFS, but it’s what many people are used to when trees are linearized. But a separate BFS behavior sounds like a reasonable alternative.

Yes, @walter, I tried arrowKeyBehavior, and it looks like it uses the same navigation method — _findNearestPartTowards. We need a more consistent navigation flow.

I’m asking if you tried navigation after setting DrawCommandHandler.arrowKeyBehavior to “tree”.
That does not use _findNearestPartTowards.

Yes, I tried it. First, it goes to Beta, then Gamma, followed by Delta, Epsilon, and finally Epsilon1, and then moving to the right-side nodes from top to bottom. However, I still didn’t observe any BFS-like consistency in the navigation flow.

As I said, the standard arrow key behavior in regular tree-view components supports a depth-first traversal as if the whole tree had been flattened into a list. What you propose sounds much simpler and should be easy to implement.

Hi @walter,
Any references or guidance would be greatly appreciated.

We’re implementing a use case involving breadth-first traversal combined with keyboard navigation. Specifically, we want to enable navigation using the arrow keys::

  • Left/Right arrows should move the selection horizontally between sibling nodes (e.g., between nodes B and C at the same level).
  • Up/Down arrows should navigate vertically to parent and child nodes (e.g., from B up to A, or down to D).

The diagram below illustrates the structure we’re working with. Any suggestions on how best to achieve this in GoJS?

I have one more question, @walter. I’ve noticed that the canvas (diagram) itself always appears selected. When we navigate between nodes using the arrow keys, the node selection updates correctly, but the diagram background still seems to remain selected.

Is there a way to clear or prevent the diagram canvas from staying selected during node navigation?

Please read: Removing the blue focus border of the diagram - GoJS - Northwoods Software

Here’s a simple tree-navigation mechanism using arrow keys:

<!DOCTYPE html>
<html>
<head>
  <title>Simple Tree Navigation with Arrow Keys</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:800px"></div>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script id="code">
class SimpleTreeCommandHandler extends go.CommandHandler {
  constructor(init) {
    super();
    if (init) Object.assign(this, init);
  }

  doKeyDown() {
    const e = this.diagram.lastInput;
    let k = e.code;
    if (k === "ArrowUp" || k === "ArrowDown" || k === "ArrowLeft" || k === "ArrowRight") {
      let sel = this.diagram.selection.first();
      if (sel === null) sel = this.diagram.findTreeRoots().first();
      if (!(sel instanceof go.Node)) return;
      const a = this.computeTreeDirection();
      const vert = (a === 0 || a === 180);  // how siblings are arranged relative to each other
      if (vert) {
        if (a === 180) {
          if (k === "ArrowRight") k = "ArrowLeft";
          else if (k === "ArrowLeft") k = "ArrowRight";
        }
      } else {  // a === 90 || a === 270
        if (a === 90) {
          switch (k) {
            case "ArrowUp": k = "ArrowLeft"; break;
            case "ArrowDown": k = "ArrowRight"; break;
            case "ArrowLeft": k = "ArrowUp"; break;
            case "ArrowRight": k = "ArrowDown"; break;
          }
        } else {
          switch (k) {
            case "ArrowUp": k = "ArrowRight"; break;
            case "ArrowDown": k = "ArrowLeft"; break;
            case "ArrowLeft": k = "ArrowUp"; break;
            case "ArrowRight": k = "ArrowDown"; break;
          }
        }
      }
      switch (k) {
        case "ArrowUp":
          sel = this.findChild(sel.findTreeParentNode(), sel, true, vert)
          break;
        case "ArrowDown":
          sel = this.findChild(sel.findTreeParentNode(), sel, false, vert)
          break;
        case "ArrowLeft":
          sel = sel.findTreeParentNode();
          break;
        case "ArrowRight":
          sel = this.findChild(sel, null, true, vert)
          break;
      }
      if (sel) this.selectNext(sel);
      return;
    }
    super.doKeyDown();
  }

  findChild(parent, child, before, vertical) {
    if (!parent) return null;
    const children = new go.List(parent.findTreeChildrenNodes());
    children.sort(vertical ? (a, b) => a.location.y - b.location.y : (a, b) => a.location.x - b.location.x);
    if (child) {
      const idx = children.indexOf(child);
      if (before && idx >= 1) {
        return children.elt(idx-1);
      } else if (!before && idx < children.length-1) {
        return children.elt(idx+1);
      }
    } else {  // assume find first child
      const first = children.first();
      if (first) return first;
    }
    return null;
  }

  computeTreeDirection() {
    return (this.diagram.layout instanceof go.TreeLayout) ? this.diagram.layout.angle : 0;
  }

  selectNext(node) {
    this.diagram.select(node);
    if (!this.diagram.viewportBounds.containsRect(node.actualBounds)) {
      this.scrollToPart(node);
    }
  }
}

const myDiagram =
  new go.Diagram("myDiagramDiv", {
      commandHandler: new SimpleTreeCommandHandler(),
      layout: new go.TreeLayout(),
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  new go.Node("Auto")
    .add(
      new go.Shape({ fill: "white", stroke: "green" })
        .bind("fill", "color"),
      new go.TextBlock({ margin: 8 })
        .bind("text")
    );

function makeTree(level, count, max, nodeDataArray, parentdata) {
  var numchildren = Math.floor(Math.random() * 10);
  for (var i = 0; i < numchildren; i++) {
    if (count >= max) return count;
    count++;
    var childdata = { key: count, category: "Simple", text: `Node ${count}`, parent: parentdata.key };
    nodeDataArray.push(childdata);
    if (level > 0 && Math.random() > 0.5) {
      count = makeTree(level - 1, count, max, nodeDataArray, childdata);
    }
  }
  return count;
}

{ // create a random tree
  const nodeDataArray = [{ key: 0, text: "Root" }];
  const max = 99;
  let count = 0;
  while (count < max) {
    count = makeTree(3, count, max, nodeDataArray, nodeDataArray[0]);
  }
  myDiagram.model = new go.TreeModel(nodeDataArray);
}
  </script>
</body>
</html>

This issue only occurs when we’re navigating using the arrow keys for accessibility, @walter.
Do you think it’s still related to the article you mentioned above?

We are working on the canvas and attempting to add adornments to the nodes within it. Could this be causing the issue?

As that forum topic discusses, that border is implemented by the browser when the diagram has keyboard focus and has nothing to do with GoJS.

1 Like

If I understand you, try adding:

  <style>
    canvas {
      outline: none;
      -webkit-tap-highlight-color: rgba(255, 255, 255, 0); /* mobile webkit */
    }
  </style>
1 Like

Hi @walter,
Would it be possible to integrate the BFS changes into the earlier code you shared for navigating within Nodes and Links—specifically into their GraphObjects—using Enter, Escape, and Shift+Enter, as described in this post?

Right now, it’s a bit confusing for us to integrate both. What we’re looking for is a unified approach that combines the BFS logic you provided with the navigation mechanism into GraphObjects and handling of loops, as you outlined in the same thread - Need more insight on a11y / accessibility - #21 by vinesh

If possible, could you provide a single version of the code with both changes integrated? Otherwise, we’re happy to try a shot at merging them ourselves.

There’s a limit to how much custom coding we’ll do for free as part of your support. We do have many other customers who also want help with their apps. I believe you can do it, but it will take some thought, study, and experimentation.

1 Like