Focus system

The focus system keeps track of the user's location (focus) in a Blockly editor. It is used by Blockly and by custom code to determine what component (block, field, toolbox category, etc.) currently has focus and to move that focus to another component.

It is important to understand the focus system so you can ensure that your custom code works correctly with it.

Architecture

The focus system has three parts:

  • The FocusManager is a singleton that coordinates focus across all of Blockly. It is used by Blockly and by custom code to find out what component has Blockly focus, as well as to move Blockly focus to a different component. It also listens for DOM focus events, synchronizes Blockly focus and DOM focus, and manages the CSS classes that indicate which component has focus.

    The focus manager is primarily used by Blockly. It is sometimes used by custom code to interact with the focus system.

  • An IFocusableTree is an independent area of a Blockly editor, such as a workspace or toolbox. It is composed of focusable nodes, such as blocks and fields. Trees can also have subtrees. For example, a mutator workspace on a block in the main workspace is a subtree of the main workspace.

    IFocusableTree is primarily used by the focus manager. Unless you write a custom toolbox, you probably won't need to implement it.

  • An IFocusableNode is a Blockly component that can have focus, such as a block, field, or toolbox category. Focusable nodes have a DOM element that displays the node and which has DOM focus when the node has Blockly focus. Note that trees are also focusable nodes. For example, you can focus on the workspace as a whole.

    The methods in IFocusableNode are primarily called by the focus manager.

    IFocusableNode itself is used to represent the component that has focus. For example, when a user selects an item on a block's context menu, the block is passed to the item's callback function as an IFocusableNode.

    If you write custom components, you may need to implement IFocusableNode.

Types of focus

The focus system defines a number of different types of focus.

Blockly focus and DOM focus

The two main types of focus are Blockly focus and DOM focus.

  • Blockly focus specifies which Blockly component (block, field, toolbox category, etc.) has focus. It is necessary for working at the level of Blockly components. For example, the keyboard navigation plugin allows users to use arrow keys to move from component to component, such as from a block to a field. Similarly, the context menu system builds a menu that is appropriate for the current component -- that is, it builds different menus for workspaces, blocks, and workspace comments.

  • DOM focus specifies which DOM element has focus. It is necessary for working at the level of DOM elements. For example, screen readers present information about the element that currently has DOM focus and tabs move (change focus) from DOM element to DOM element.

The focus manager keeps Blockly focus and DOM focus in sync, so when a node (Blockly component) has Blockly focus, its underlying DOM element has DOM focus and vice versa.

Active and passive focus

Blockly focus is further divided into active focus and passive focus. Active focus means that a node will receive user input, such as a key press. Passive focus means that a node previously had active focus, but lost it when the user moved to a node in another tree (for example, they moved from the workspace to the toolbox) or away from the Blockly editor altogether. If the tree regains focus, the passively focused node will regain active focus.

Each tree has a separate focus context. That is, at most one node in the tree can have focus. Whether that focus is active or passive depends on whether the tree has focus. There can be at most one node with active focus on the entire page.

The focus manager uses different highlights (CSS classes) for actively and passively focused nodes. These allow users to understand where they are and where they will return to.

Ephemeral focus

There is another type of focus called ephemeral focus. Separate workflows, such as dialogs or field editors, request ephemeral focus from the focus manager. When the focus manager grants ephemeral focus, it suspends the focus system. From a practical standpoint, this means that such workflows can capture and act on DOM focus events without having to worry that the focus system might act on them as well.

When the focus manager grants ephemeral focus, it changes the actively focused node to passive focus. It restores active focus when ephemeral focus is returned.

Examples

The following examples illustrate how Blockly uses the focus system. They should help you understand how your code fits into the focus system and how your code might use the focus system.

Move focus with the keyboard

Suppose a block with two fields has Blockly focus, as indicated by a highlight (CSS class) on the block's DOM element. Now suppose the user presses the right arrow:

  1. The keyboard navigation plugin:
    • Receives a key press event.
    • Asks the navigation system (a part of core Blockly) to move focus to the "next" component.
  2. The navigation system:
    • Asks the focus manager what component has Blockly focus. The focus manager returns the block as an IFocusableNode.
    • Determines that the IFocusableNode is a BlockSvg and looks at its rules for navigating blocks, which state that it should move Blockly focus from the block as a whole to the first field on the block.
    • Tells the focus manager to move Blockly focus to the first field.
  3. The focus manager:
    • Updates its state to set Blockly focus on the first field.
    • Sets DOM focus on the field's DOM element.
    • Moves the highlight class from the block's element to the field's element.

Move focus with the mouse

Now suppose that the user clicks on the second field on the block. The focus manager:

  1. Receives a DOM focusout event on the first field's DOM element and a focusin event on the second field's DOM element.
  2. Determines that the DOM element that received focus corresponds to the second field.
  3. Updates its state to set Blockly focus on the second field. (The focus manager doesn't need to set DOM focus because the browser already did this.)
  4. Moves the highlight class from the first field's element to the second field's element.

Other examples

Here are some other examples:

  • When a user drags a block from the toolbox to the workspace, the mouse event handler creates a new block and calls the focus manager to set Blockly focus on that block.

  • When a block is deleted, its dispose method calls the focus manager to move focus to the block's parent.

  • Keyboard shortcuts use IFocusableNode to identify the Blockly component to which the shortcut applies.

  • Context menus use IFocusableNode to identify the Blockly component on which the menu was invoked.

Customizations and the focus system

When you customize Blockly, you need to make sure that your code works correctly with the focus system. You may also use the focus system to identify and set the currently focused node.

Custom blocks and toolbox contents

The most common way to customize Blockly is to define custom blocks and customize the contents of the toolbox. Neither of these actions has any affect on the focus system.

Custom classes

Custom classes may need to implement one or both focus interfaces (IFocusableTree and IFocusableNode). It is not always obvious when this is the case.

Some classes clearly need to implement focus interfaces. These include:

  • A class that implements a custom toolbox. This class needs to implement IFocusableTree and IFocusableNode.

  • Classes that create a visible component (such as a field or icon) that users can navigate to. These classes need to implement IFocusableNode.

Some classes need to implement IFocusableNode even though they do not create a visible component or they create a visible component that users cannot navigate to. These include:

  • Classes that implement an interface that extends IFocusableNode.

    For example, the move icon in the keyboard navigation plugin displays a four-way arrow that indicates that the block can be moved with the arrow keys. The icon itself is not visible (the four-way arrow is a bubble) and users cannot navigate to it. However, the icon must implement IFocusableNode because icons implementIIcon and IIcon extends IFocusableNode.

  • Classes used in an API that requires an IFocusableNode.

    For example, the FlyoutSeparator class creates a gap between two items in a flyout. It does not create any DOM elements, so it doesn't have a visible component and users can't navigate to it. However, it must implement IFocusableNode because it is stored in a FlyoutItem and the FlyoutItem constructor requires an IFocusableNode.

  • Classes that extend a class that implements IFocusableNode.

    For example, ToolboxSeparator extends ToolboxItem, which implements IFocusableNode. Although toolbox separators have a visible component, users cannot navigate to them because they cannot be acted on and have no useful content.

Other classes create visible components that the user can navigate to, but do not need to implement IFocusableNode. These include:

  • Classes that create a visible component that manages its own focus, such as a field editor or a dialog. (Note that such classes do need to take ephemeral focus when they start and return it when they end. Using WidgetDiv or DropDownDiv will handle this for you.)

Finally, some classes do not interact with the focus system and do not need to implement IFocusableTree or IFocusableNode. These include:

  • Classes that create a visible component that users cannot navigate to or act on and which contains no information that a screen reader might use. For example, a purely decorative background in a game.

  • Classes entirely unrelated to the focus system, such as classes that implement IMetricsManager or IVariableMap.

If you are uncertain whether your class will interact with the focus system, test it with the keyboard navigation plugin. If this fails, you might need to implement IFocusableTree or IFocusableNode. If it succeeds but you are still unsure, read the code that uses your class to see if either interface is required or if there are any other interactions.

Implement focus interfaces

The easiest way to implement IFocusableTree or IFocusableNodeis to extend a class that implements these interfaces. For example, if you're creating a custom toolbox, extend Toolbox, which implements IFocusableTree and IFocusableNode. If you're creating a custom field, extend Field, which implements IFocusableNode. Be sure to check that your code doesn't interfere with the focus interface code in the base class.

If you do extend a class that implements a focus interface, you generally won't need to override any methods. The most common exception is IFocusableNode.canBeFocused, which you need to override if you don't want users to navigate to your component.

Less common is the need to override the focus callback methods (onTreeFocus and onTreeBlur in IFocusableTree and onNodeFocus and onNodeBlur in IFocusableNode). Note that attempting to change focus (call FocusManager.focusNode or FocusManager.focusTree) from these methods results in an exception.

If you write a custom component from scratch, you'll need to implement the focus interfaces yourself. See the reference documentation for IFocusableTree and IFocusableNode more information.

After you have implemented your class, test it against the keyboard navigation plugin to verify that you can (or cannot) navigate to your component.

Use the focus manager

Some custom classes use the focus manager. The most common reasons to do this are to get the currently focused node and to focus on a different node. To get the focus manager, call Blockly.getFocusManager:

const focusManager = Blockly.getFocusManager();

To get the currently focused node, call getFocusedNode:

const focusedNode = focusManager.getFocusedNode();
// Do something with the focused node.

To move focus to a different node, call focusNode:

// Move focus to a different block.
focusManager.focusNode(myOtherBlock);

To move focus to a tree, call focusTree. This also sets node focus on the tree's root node.

// Move focus to the main workspace.
focusManager.focusTree(myMainWorkspace);

The other common reason to use the focus manager is to take and return ephemeral focus. The takeEphemeralFocus function returns a lambda that you must call to return ephemeral focus.

const returnEphemeralFocus = focusManager.takeEphemeralFocus();
// Do something.
returnEphemeralFocus();

If you use WidgetDiv or DropDownDiv, they will handle ephemeral focus for you.

Tab stops

The focus system sets a tab stop (tabindex of 0) on the root element of all trees (the main workspace, the toolbox, and flyout workspaces). This allows users to use the tab key to navigate around the main regions of a Blockly editor and then (using the keyboard navigation plugin) use arrow keys to navigate within those regions. Do not change these tab stops, as this will interfere with the focus manager's ability to manage them.

You should generally avoid setting tab stops on other DOM elements used by Blockly, as this interferes with Blockly's model of using the tab key to navigate between areas of the editor and arrow keys within those areas. In addition, such tab stops might not work as intended. This is because each focusable node declares a DOM element as its focusable element. If you set a tab stop on a descendant of the focusable element and the user tabs to that element, the focus manager will move DOM focus to the declared focusable element.

It is safe to set tab stops on elements in your application that are outside the Blockly editor. When the user tabs from the editor to such an element, the focus manager changes the Blockly focus from active to passive. For accessibility, you should set the tabindex property to 0 or -1, as recommended by the warning in MDN's description of the tabindex attribute.

DOM focus

For accessibility reasons, applications should avoid calling the focus method on DOM elements. This is disorienting to users of screen readers, as they are suddenly moved to an unknown location in the application.

An additional problem is that the focus manager reacts to focus events by setting DOM focus on the nearest ancestor-or-self of the focused element that is a declared focusable element. This may be different from the element on which focus was called. (If there is no nearest focusable ancestor-or-self, such as when focus is called on an element outside the Blockly editor, the focus manager just changes the actively focused node to passive focus.)

Positionables

Positionables are components that are positioned on top of the workspace and implement IPositionable. Examples are the trashcan and the backpack in the backpack plugin. Positionables are not yet integrated into the focus system.