8000 feat: Update line cursor to use focus manager by BenHenning · Pull Request #8941 · google/blockly · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: Update line cursor to use focus manager #8941

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
41c8215
feat: Make WorkspaceSvg focusable.
BenHenning Apr 21, 2025
14b486e
chore: remove accidental 'test.only'.
BenHenning Apr 21, 2025
26cf8db
feat: Make Toolbox & Flyout focusable.
BenHenning Apr 22, 2025
5ef2d7e
Merge branch 'add-focus-manager-callbacks-and-improvements' into make…
BenHenning Apr 22, 2025
996208d
Merge branch 'make-workspace-focusable' into make-toolbox-and-flyout-…
BenHenning Apr 22, 2025
d3acbff
feat!: Force lifecycle methods for fields.
BenHenning Apr 22, 2025
ed0f140
feat: Make fields ephemerally focusable.
BenHenning Apr 22, 2025
94672d9
chore: Lint fixes.
BenHenning Apr 22, 2025
2430646
chore: Remove incorrect aria-label.
BenHenning Apr 22, 2025
41bc01a
feat: Make RenderedConnection focusable.
BenHenning Apr 23, 2025
4479b82
Merge branch 'add-focus-manager-callbacks-and-improvements' into make…
BenHenning Apr 23, 2025
2637736
fix: Ensure Block paths are focusable.
BenHenning Apr 23, 2025
49192ba
chore: Fix line comment.
BenHenning Apr 23, 2025
917c4b6
Merge branch 'make-workspace-focusable' into make-toolbox-and-flyout-…
BenHenning Apr 23, 2025
d276dbc
chore: reduce branching.
BenHenning Apr 24, 2025
c819130
Merge branch 'make-toolbox-and-flyout-focusable' into make-fields-foc…
BenHenning Apr 24, 2025
90fdde2
feat: make drop down & widget divs focusable.
BenHenning Apr 24, 2025
7c2f705
chore: undo breaking field changes.
BenHenning Apr 24, 2025
9726389
chore: some more clean-ups after removals.
BenHenning Apr 24, 2025
1094787
feat: fix field node retrieval.
BenHenning Apr 24, 2025
082a6ef
chore: lint fixes.
BenHenning Apr 24, 2025
0006a45
Merge branch 'make-fields-focusable' into make-connections-focusable
BenHenning Apr 24, 2025
7f14372
feat: add remaining connection support
BenHenning Apr 24, 2025
59198db
chore: lint fixes.
BenHenning Apr 24, 2025
a346a92
fix: remove unnecessary shadow check.
BenHenning Apr 24, 2025
4ed61bf
Merge branch 'make-workspace-focusable' into make-toolbox-and-flyout-…
BenHenning Apr 24, 2025
898c5a4
Merge branch 'make-toolbox-and-flyout-focusable' into make-fields-foc…
BenHenning Apr 24, 2025
fdf4b4f
Merge branch 'make-fields-focusable' into make-connections-focusable
BenHenning Apr 24, 2025
80c8859
chore: add braces.
BenHenning Apr 24, 2025
b3bd5e7
Merge branch 'rc/v12.0.0' into make-workspace-focusable
BenHenning Apr 24, 2025
1f0cefc
Merge branch 'make-workspace-focusable' into make-toolbox-and-flyout-…
BenHenning Apr 24, 2025
c2384c6
chore: empty commit to make CI pass.
BenHenning Apr 24, 2025
57391a7
Merge branch 'rc/v12.0.0' into make-toolbox-and-flyout-focusable
BenHenning Apr 24, 2025
8057051
Merge branch 'make-toolbox-and-flyout-focusable' into make-fields-foc…
BenHenning Apr 24, 2025
ad96cad
Merge branch 'make-fields-focusable' into make-connections-focusable
BenHenning Apr 24, 2025
c75aea7
feat: Make WorkspaceSvg and BlockSvg focusable (#8916)
BenHenning Apr 24, 2025
d4883f5
feat: Make toolbox and flyout focusable (#8920)
BenHenning Apr 24, 2025
2a1a8b3
Merge branch 'make-toolbox-and-flyout-focusable-roll-forward' into ma…
BenHenning Apr 29, 2025
6abdd90
Merge branch 'make-fields-focusable' into make-connections-focusable
BenHenning Apr 29, 2025
8fc5c05
chore: remove connections rendering changes.
BenHenning Apr 29, 2025
cb2148b
feat: Update LineCursor to use FocusManager.
BenHenning Apr 29, 2025
2cae22e
fix: A few things for the plugin.
BenHenning Apr 30, 2025
8f65a3b
fix: Undo break for Block focusability.
BenHenning Apr 30, 2025
e5abf72
fix: Remove CSS for active/passive focus.
BenHenning Apr 30, 2025
e849e0c
Merge branch 'make-workspace-focusable-roll-forward' into make-toolbo…
BenHenning Apr 30, 2025
585c950
feat: Make FlyoutButton focusable.
BenHenning Apr 30, 2025
a520554
Merge branch 'make-toolbox-and-flyout-focusable-roll-forward' into ma…
BenHenning Apr 30, 2025
3168ff8
Merge branch 'make-fields-focusable' into make-connections-focusable
BenHenning Apr 30, 2025
60ab80a
Merge branch 'make-connections-focusable' into update-line-cursor-to-…
BenHenning Apr 30, 2025
151d8f4
fix: Make RenderedConnection display correctly.
BenHenning Apr 30, 2025
d6dcc4b
fix: Actually make FlyoutButton focusable.
BenHenning Apr 30, 2025
a9cf3d7
chore: lint fixes.
BenHenning Apr 30, 2025
f18670a
chore: Use strict equals.
BenHenning Apr 30, 2025
34970cc
chore: Empty commit to re-trigger CI.
BenHenning Apr 30, 2025
b11aa43
Merge branch 'make-toolbox-and-flyout-focusable-roll-forward' into ma…
BenHenning Apr 30, 2025
3cf8fb8
chore: Add field doc.
BenHenning Apr 30, 2025
896dbd7
Merge branch 'make-fields-focusable' into make-connections-focusable
BenHenning Apr 30, 2025
d146f61
chore: Add field docs.
BenHenning Apr 30, 2025
3735771
Merge branch 'make-connections-focusable' into update-line-cursor-to-…
BenHenning Apr 30, 2025
acf4b9e
chore: lint fixes.
BenHenning Apr 30, 2025
e3a6f98
Merge branch 'rc/v12.0.0' into make-workspace-focusable-roll-forward
BenHenning Apr 30, 2025
b738b4a
Merge branch 'make-workspace-focusable-roll-forward' into make-toolbo…
BenHenning Apr 30, 2025
ef6f661
Merge branch 'rc/v12.0.0' into make-toolbox-and-flyout-focusable-roll…
BenHenning Apr 30, 2025
b493147
Merge branch 'make-toolbox-and-flyout-focusable-roll-forward' into ma…
BenHenning Apr 30, 2025
dba2346
Merge branch 'rc/v12.0.0' into make-fields-focusable
BenHenning Apr 30, 2025
8146d5d
Merge branch 'make-fields-focusable' into make-connections-focusable
BenHenning Apr 30, 2025
7eaddb8
Merge branch 'make-connections-focusable' into update-line-cursor-to-…
BenHenning Apr 30, 2025
524b197
Merge branch 'rc/v12.0.0' into make-connections-focusable
BenHenning Apr 30, 2025
8818f36
Merge branch 'make-connections-focusable' into update-line-cursor-to-…
BenHenning Apr 30, 2025
ae62af3
fix: FocusManager tracking issue + flyout refocus.
BenHenning May 1, 2025
766dd23
fix: disable outlines for most things.
BenHenning May 1, 2025
c78fc68
fix: Fix broken Mocha tests.
BenHenning May 1, 2025
f6c80b5
Merge branch 'rc/v12.0.0' into update-line-cursor-to-use-focus-manager
BenHenning May 1, 2025
1f3b903
fix: Fix deiniting order for tests.
BenHenning May 1, 2025
cc2a879
fix: remove variables flyout focusing.
BenHenning May 2, 2025
d1ffabe
chore: empty commit to re-trigger CI
BenHenning May 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/blockly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ import * as inputs from './inputs.js';
import {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
import {LabelFlyoutInflater} from './label_flyout_inflater.js';
import {SeparatorFlyoutInflater} from './separator_flyout_inflater.js';
import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js';

import {Input} from './inputs/input.js';
import {InsertionMarkerPreviewer} from './insertion_marker_previewer.js';
Expand Down Expand Up @@ -527,6 +528,7 @@ export {
FlyoutMetricsManager,
FlyoutSeparator,
FocusManager,
FocusableTreeTraverser,
CodeGenerator as Generator,
Gesture,
Grid,
Expand Down
88 changes: 54 additions & 34 deletions core/focus_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,15 @@ export class FocusManager {
constructor(
addGlobalEventListener: (type: string, listener: EventListener) => void,
) {
// Register root document focus listeners for tracking when focus leaves all
// tracked focusable trees.
addGlobalEventListener('focusin', (event) => {
if (!(event instanceof FocusEvent)) return;

// The target that now has focus.
const activeElement = document.activeElement;
// Note that 'element' here is the element *gaining* focus.
const maybeFocus = (element: Element | EventTarget | null) => {
let newNode: IFocusableNode | null | undefined = null;
if (
activeElement instanceof HTMLElement ||
activeElement instanceof SVGElement
) {
// If the target losing focus maps to any tree, then it should be
// updated. Per the contract of findFocusableNodeFor only one tree
// should claim the element.
if (element instanceof HTMLElement || element instanceof SVGElement) {
// If the target losing or gaining focus maps to any tree, then it
// should be updated. Per the contract of findFocusableNodeFor only one
// tree should claim the element, so the search can be exited early.
for (const tree of this.registeredTrees) {
newNode = FocusableTreeTraverser.findFocusableNodeFor(
activeElement,
tree,
);
newNode = FocusableTreeTraverser.findFocusableNodeFor(element, tree);
if (newNode) break;
}
}
Expand All @@ -103,6 +92,26 @@ export class FocusManager {
} else {
this.defocusCurrentFocusedNode();
}
};

// Register root document focus listeners for tracking when focus leaves all
// tracked focusable trees. Note that focusin and focusout can be somewhat
// overlapping in the information that they provide. This is fine because
// they both aim to check for focus changes on the element gaining or having
// received focus, and maybeFocus should behave relatively deterministic.
addGlobalEventListener('focusin', (event) => {
if (!(event instanceof FocusEvent)) return;

// When something receives focus, always use the current active element as
// the source of truth.
maybeFocus(document.activeElement);
});
addGlobalEventListener('focusout', (event) => {
if (!(event instanceof FocusEvent)) return;

// When something loses focus, it seems that document.activeElement may
// not necessarily be correct. Instead, use relatedTarget.
maybeFocus(event.relatedTarget);
});
}

Expand Down Expand Up @@ -247,7 +256,7 @@ export class FocusManager {

const prevNode = this.focusedNode;
const prevTree = prevNode?.getFocusableTree();
if (prevNode && prevTree !== nextTree) {
if (prevNode) {
this.passivelyFocusNode(prevNode, nextTree);
}

Expand Down Expand Up @@ -374,7 +383,9 @@ export class FocusManager {
// node's focusable element (which *is* allowed to be invisible until the
// node needs to be focused).
this.lockFocusStateChanges = true;
node.getFocusableTree().onTreeFocus(node, prevTree);
if (node.getFocusableTree() !== prevTree) {
node.getFocusableTree().onTreeFocus(node, prevTree);
}
node.onNodeFocus();
this.lockFocusStateChanges = false;

Expand All @@ -399,11 +410,15 @@ export class FocusManager {
nextTree: IFocusableTree | null,
): void {
this.lockFocusStateChanges = true;
node.getFocusableTree().onTreeBlur(nextTree);
if (node.getFocusableTree() !== nextTree) {
node.getFocusableTree().onTreeBlur(nextTree);
}
node.onNodeBlur();
this.lockFocusStateChanges = false;

this.setNodeToVisualPassiveFocus(node);
if (node.getFocusableTree() !== nextTree) {
this.setNodeToVisualPassiveFocus(node);
}
}

/**
Expand Down Expand Up @@ -441,19 +456,24 @@ export class FocusManager {
dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME);
dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME);
}
}

let focusManager: FocusManager | null = null;
private static focusManager: FocusManager | null = null;

/**
* Returns the page-global FocusManager.
*
* The returned instance is guaranteed to not change across function calls, but
* may change across page loads.
*/
export function getFocusManager(): FocusManager {
if (!focusManager) {
focusManager = new FocusManager(document.addEventListener);
/**
* Returns the page-global FocusManager.
*
* The returned instance is guaranteed to not change across function calls, but
* may change across page loads.
*/
static getFocusManager(): FocusManager {
if (!FocusManager.focusManager) {
FocusManager.focusManager = new FocusManager(document.addEventListener);
}
return FocusManager.focusManager;
}
return focusManager;
}

/** Convenience function for FocusManager.getFocusManager. */
export function getFocusManager(): FocusManager {
return FocusManager.getFocusManager();
}
114 changes: 39 additions & 75 deletions core/keyboard_nav/line_cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ import {BlockSvg} from '../block_svg.js';
import * as common from '../common.js';
import type {Connection} from '../connection.js';
import {ConnectionType} from '../connection_type.js';
import type {Abstract} from '../events/events_abstract.js';
import {Click, ClickTarget} from '../events/events_click.js';
import {EventType} from '../events/type.js';
import * as eventUtils from '../events/utils.js';
import type {Field} from '../field.js';
import {getFocusManager} from '../focus_manager.js';
import {isFocusableNode} from '../interfaces/i_focusable_node.js';
import * as registry from '../registry.js';
import type {MarkerSvg} from '../renderers/common/marker_svg.js';
import type {PathObject} from '../renderers/zelos/path_object.js';
Expand Down Expand Up @@ -70,23 +68,12 @@ export class LineCursor extends Marker {
options?: Partial<CursorOptions>,
) {
super();
// Bind changeListener to facilitate future disposal.
this.changeListener = this.changeListener.bind(this);
this.workspace.addChangeListener(this.changeListener);
// Regularise options and apply defaults.
this.options = {...defaultOptions, ...options};

this.isZelos = workspace.getRenderer() instanceof Renderer;
}

/**
* Clean up this cursor.
*/
dispose() {
this.workspace.removeChangeListener(this.changeListener);
super.dispose();
}

/**
* Moves the cursor to the next previous connection, next connection or block
* in the pre order traversal. Finds the next node in the pre order traversal.
Expand Down Expand Up @@ -331,8 +318,8 @@ export class LineCursor extends Marker {
* @param node The current position in the AST.
* @param isValid A function true/false depending on whether the given node
* should be traversed.
* @param loop Whether to loop around to the beginning of the workspace if
* novalid node was found.
* @param loop Whether to loop around to the beginning of the workspace if no
* valid node was found.
* @returns The next node in the traversal.
*/
getNextNode(
Expand Down Expand Up @@ -385,8 +372,8 @@ export class LineCursor extends Marker {
* @param node The current position in the AST.
* @param isValid A function true/false depending on whether the given node
* should be traversed.
* @param loop Whether to loop around to the end of the workspace if no
* valid node was found.
* @param loop Whether to loop around to the end of the workspace if no valid
* node was found.
* @returns The previous node in the traversal or null if no previous node
* exists.
*/
Expand Down Expand Up @@ -527,7 +514,12 @@ export class LineCursor extends Marker {
* @returns The current field, connection, or block the cursor is on.
*/
override getCurNode(): ASTNode | null {
this.updateCurNodeFromSelection();
if (!this.updateCurNodeFromFocus()) {
// Fall back to selection if focus fails to sync. This can happen for
// non-focusable nodes or for cases when focus may not properly propagate
// (such as for mouse clicks).
this.updateCurNodeFromSelection();
}
return super.getCurNode();
}

Expand Down Expand Up @@ -572,16 +564,15 @@ export class LineCursor extends Marker {
* this.drawMarker() instead of this.drawer.draw() directly.
*
* @param newNode The new location of the cursor.
* @param updateSelection If true (the default) we'll update the selection
* too.
*/
override setCurNode(newNode: ASTNode | null, updateSelection = true) {
if (updateSelection) {
this.updateSelectionFromNode(newNode);
}

override setCurNode(newNode: ASTNode | null) {
super.setCurNode(newNode);

const newNodeLocation = newNode?.getLocation();
if (isFocusableNode(newNodeLocation)) {
getFocusManager().focusNode(newNodeLocation);
}

// Try to scroll cursor into view.
if (newNode?.getType() === ASTNode.types.BLOCK) {
const block = newNode.getLocation() as BlockSvg;
Expand Down Expand Up @@ -709,32 +700,6 @@ export class LineCursor extends Marker {
}
}

/**
* Event listener that syncs the cursor location to the selected block on
* SELECTED events.
*
* This does not run early enough in all cases so `getCurNode()` also updates
* the node from the selection.
*
* @param event The `Selected` event.
*/
private changeListener(event: Abstract) {
switch (event.type) {
case EventType.SELECTED:
this.updateCurNodeFromSelection();
break;
case EventType.CLICK: {
const click = event as Click;
if (
click.workspaceId === this.workspace.id &&
click.targetType === ClickTarget.WORKSPACE
) {
this.setCurNode(null);
}
}
}
}

/**
* Updates the current node to match the selection.
*
Expand All @@ -749,7 +714,7 @@ export class LineCursor extends Marker {
const selected = common.getSelected();

if (selected === null && curNode?.getType() === ASTNode.types.BLOCK) {
this.setCurNode(null, false);
this.setCurNode(null);
return;
}
if (selected?.workspace !== this.workspace) {
Expand All @@ -770,36 +735,35 @@ export class LineCursor extends Marker {
block = block.getParent();
}
if (block) {
this.setCurNode(ASTNode.createBlockNode(block), false);
this.setCurNode(ASTNode.createBlockNode(block));
}
}
}

/**
* Updates the selection from the node.
* Updates the current node to match what's currently focused.
*
* Clears the selection for non-block nodes.
* Clears the selection for shadow blocks as the selection is drawn on
* the parent but the cursor will be drawn on the shadow block itself.
* We need to take care not to later clear the current node due to that null
* selection, so we track the latest selection we're in sync with.
*
* @param newNode The new node.
* @returns Whether the current node has been set successfully from the
* current focused node.
*/
private updateSelectionFromNode(newNode: ASTNode | null) {
if (newNode?.getType() === ASTNode.types.BLOCK) {
if (common.getSelected() !== newNode.getLocation()) {
eventUtils.disable();
common.setSelected(newNode.getLocation() as BlockSvg);
eventUtils.enable();
}
} else {
if (common.getSelected()) {
eventUtils.disable();
common.setSelected(null);
eventUtils.enable();
private updateCurNodeFromFocus(): boolean {
const focused = getFocusManager().getFocusedNode();

if (focused instanceof BlockSvg) {
const block: BlockSvg | null = focused;
if (block && block.workspace === this.workspace) {
if (block.getRootBlock() === block && this.workspace.isFlyout) {
// This block actually represents a stack. Note that this is needed
// because ASTNode special cases stack for cross-block navigation.
this.setCurNode(ASTNode.createStackNode(block));
} else {
this.setCurNode(ASTNode.createBlockNode(block));
}
return true;
}
}

return false;
}

/**
Expand Down
19 changes: 10 additions & 9 deletions tests/mocha/cursor_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import {ASTNode} from '../../build/src/core/keyboard_nav/ast_node.js';
import {assert} from '../../node_modules/chai/chai.js';
import {createRenderedBlock} from './test_helpers/block_definitions.js';
import {
sharedTestSetup,
sharedTestTeardown,
Expand Down Expand Up @@ -63,11 +64,11 @@ suite('Cursor', function () {
]);
this.workspace = Blockly.inject('blocklyDiv', {});
this.cursor = this.workspace.getCursor();
const blockA = this.workspace.newBlock('input_statement');
const blockB = this.workspace.newBlock('input_statement');
const blockC = this.workspace.newBlock('input_statement');
const blockD = this.workspace.newBlock('input_statement');
const blockE = this.workspace.newBlock('field_input');
const blockA = createRenderedBlock(this.workspace, 'input_statement');
const blockB = createRenderedBlock(this.workspace, 'input_statement');
const blockC = createRenderedBlock(this.workspace, 'input_statement');
const blockD = createRenderedBlock(this.workspace, 'input_statement');
const blockE = createRenderedBlock(this.workspace, 'field_input');

blockA.nextConnection.connect(blockB.previousConnection);
blockA.inputList[0].connection.connect(blockE.outputConnection);
Expand Down Expand Up @@ -105,12 +106,12 @@ suite('Cursor', function () {
);
});

test('In - From output connection', function () {
test('In - From attached input connection', function () {
const fieldBlock = this.blocks.E;
const outputNode = ASTNode.createConnectionNode(
fieldBlock.outputConnection,
const inputConnectionNode = ASTNode.createConnectionNode(
this.blocks.A.inputList[0].connection,
);
this.cursor.setCurNode(outputNode);
this.cursor.setCurNode(inputConnectionNode);
this.cursor.in();
const curNode = this.cursor.getCurNode();
assert.equal(curNode.getLocation(), fieldBlock);
Expand Down
Loading
Loading
0