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 anIFocusableNode
.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:
- 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.
- 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 aBlockSvg
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.
- Asks the focus manager what component has Blockly focus. The focus
manager returns the block as an
- 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:
- Receives a DOM
focusout
event on the first field's DOM element and afocusin
event on the second field's DOM element. - Determines that the DOM element that received focus corresponds to the second field.
- 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.)
- 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
andIFocusableNode
.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
andIIcon
extendsIFocusableNode
.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 implementIFocusableNode
because it is stored in aFlyoutItem
and theFlyoutItem
constructor requires anIFocusableNode
.Classes that extend a class that implements
IFocusableNode
.For example,
ToolboxSeparator
extendsToolboxItem
, which implementsIFocusableNode
. 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
orDropDownDiv
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
orIVariableMap
.
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 IFocusableNode
is 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.