Skip to content

Commit 817e18d

Browse files
fix: right click menu issues (#331)
* fix: right click menu issues PR #326 introduced an issue with stale current node when showing the context menu by mouse. It's only possible to fix this by reinstating the updating of curNode from the selection. We also attempt to avoid showing passive focus when using the context menu and widgets (via mouse and keyboard) for consistency with the normal Blockly experience. Fixes an issue clicking on shadow blocks which could previously show an outline around the shadow and parent block. * chore: remove reference to fixed bug * chore: typo in comment * fix: null node case for action menu * fix: stale curNode when clicking the workspace Due to an unintentially commented out line, clicking the workspace wouldn't clear the current node so e.g. delete would act on a block that didn't have a visual selection. Re-enabling that required revisiting shadow blocks to prevent getCurNode unexpectedly clearing based on a null selection. We now always set/use the selection for shadow blocks and take care not to move the cursor unexpectedly as it can be in two positions for a shadow block selection. --------- Co-authored-by: Grace <[email protected]>
1 parent f4446c9 commit 817e18d

File tree

5 files changed

+185
-42
lines changed

5 files changed

+185
-42
lines changed

src/actions/action_menu.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class ActionMenu {
111111
const cursor = workspace.getCursor();
112112
if (!cursor) throw new Error('workspace has no cursor');
113113
const node = cursor.getCurNode();
114-
if (!node) throw new Error('No node is currently selected');
114+
if (!node) return false;
115115
const nodeType = node.getType();
116116
switch (nodeType) {
117117
case ASTNode.types.BLOCK:

src/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export class KeyboardNavigation {
3030
/** Event handler run when the workspace loses focus. */
3131
private blurListener: () => void;
3232

33+
/** Event handler run when the widget or dropdown div loses focus. */
34+
private widgetDropDownDivFocusOutListener: (e: Event) => void;
35+
3336
/** Event handler run when the toolbox gains focus. */
3437
private toolboxFocusListener: () => void;
3538

@@ -158,6 +161,22 @@ export class KeyboardNavigation {
158161
workspace.getSvgGroup().addEventListener('focus', this.focusListener);
159162
workspace.getSvgGroup().addEventListener('blur', this.blurListener);
160163

164+
this.widgetDropDownDivFocusOutListener = (e: Event) => {
165+
this.navigationController.handleFocusOutWidgetDropdownDiv(
166+
workspace,
167+
(e as FocusEvent).relatedTarget,
168+
);
169+
};
170+
171+
Blockly.WidgetDiv.getDiv()?.addEventListener(
172+
'focusout',
173+
this.widgetDropDownDivFocusOutListener,
174+
);
175+
Blockly.DropDownDiv.getContentDiv()?.addEventListener(
176+
'focusout',
177+
this.widgetDropDownDivFocusOutListener,
178+
);
179+
161180
const toolboxElement = getToolboxElement(workspace);
162181
this.toolboxFocusListener = () => {
163182
this.navigationController.handleFocusToolbox(workspace);
@@ -208,6 +227,15 @@ export class KeyboardNavigation {
208227
.getSvgGroup()
209228
.removeEventListener('focus', this.focusListener);
210229

230+
Blockly.WidgetDiv.getDiv()?.removeEventListener(
231+
'focusout',
232+
this.widgetDropDownDivFocusOutListener,
233+
);
234+
Blockly.DropDownDiv.getContentDiv()?.removeEventListener(
235+
'focusout',
236+
this.widgetDropDownDivFocusOutListener,
237+
);
238+
211239
const toolboxElement = getToolboxElement(this.workspace);
212240
toolboxElement?.removeEventListener('focus', this.toolboxFocusListener);
213241
toolboxElement?.removeEventListener('blur', this.toolboxBlurListener);

src/line_cursor.ts

Lines changed: 112 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import * as Blockly from 'blockly/core';
1717
import {ASTNode, Marker} from 'blockly/core';
18-
import {getWorkspaceElement, scrollBoundsIntoView} from './workspace_utilities';
18+
import {scrollBoundsIntoView} from './workspace_utilities';
1919

2020
/** Options object for LineCursor instances. */
2121
export type CursorOptions = {
@@ -61,7 +61,7 @@ export class LineCursor extends Marker {
6161
) {
6262
super();
6363
// Bind selectListener to facilitate future install/uninstall.
64-
this.selectListener = this.selectListener.bind(this);
64+
this.changeListener = this.changeListener.bind(this);
6565
// Regularise options and apply defaults.
6666
this.options = {...defaultOptions, ...options};
6767

@@ -81,7 +81,7 @@ export class LineCursor extends Marker {
8181
markerManager.setCursor(this);
8282
const oldCursorNode = this.oldCursor?.getCurNode();
8383
if (oldCursorNode) this.setCurNode(oldCursorNode);
84-
this.workspace.addChangeListener(this.selectListener);
84+
this.workspace.addChangeListener(this.changeListener);
8585
this.installed = true;
8686
}
8787

@@ -92,7 +92,7 @@ export class LineCursor extends Marker {
9292
*/
9393
uninstall() {
9494
if (!this.installed) throw new Error('LineCursor not yet installed');
95-
this.workspace.removeChangeListener(this.selectListener.bind(this));
95+
this.workspace.removeChangeListener(this.changeListener.bind(this));
9696
if (this.oldCursor) {
9797
this.workspace.getMarkerManager().setCursor(this.oldCursor);
9898
}
@@ -475,6 +475,21 @@ export class LineCursor extends Marker {
475475
throw new Error('no valid nodes in this.potentialNodes');
476476
}
477477

478+
/**
479+
* Get the current location of the cursor.
480+
*
481+
* Overrides normal Marker getCurNode to update the current node from the selected
482+
* block. This typically happens via the selection listener but that is not called
483+
* immediately when `Gesture` calls `Blockly.common.setSelected`.
484+
* In particular the listener runs after showing the context menu.
485+
*
486+
* @returns The current field, connection, or block the cursor is on.
487+
*/
488+
override getCurNode(): Blockly.ASTNode | null {
489+
this.updateCurNodeFromSelection();
490+
return super.getCurNode();
491+
}
492+
478493
/**
479494
* Sets the object in charge of drawing the marker.
480495
*
@@ -516,28 +531,11 @@ export class LineCursor extends Marker {
516531
* this.drawMarker() instead of this.drawer.draw() directly.
517532
*
518533
* @param newNode The new location of the cursor.
534+
* @param selectionUpToDate If false (the default) we'll update the selection too.
519535
*/
520-
override setCurNode(newNode: ASTNode | null, selectionInSync = false) {
521-
if (newNode?.getLocation() === this.getCurNode()?.getLocation()) {
522-
return;
523-
}
524-
if (!selectionInSync) {
525-
if (
526-
newNode?.getType() === ASTNode.types.BLOCK &&
527-
!(newNode.getLocation() as Blockly.BlockSvg).isShadow()
528-
) {
529-
if (Blockly.common.getSelected() !== newNode.getLocation()) {
530-
Blockly.Events.disable();
531-
Blockly.common.setSelected(newNode.getLocation() as Blockly.BlockSvg);
532-
Blockly.Events.enable();
533-
}
534-
} else {
535-
if (Blockly.common.getSelected()) {
536-
Blockly.Events.disable();
537-
Blockly.common.setSelected(null);
538-
Blockly.Events.enable();
539-
}
540-
}
536+
override setCurNode(newNode: ASTNode | null, selectionUpToDate = false) {
537+
if (!selectionUpToDate) {
538+
this.updateSelectionFromNode(newNode);
541539
}
542540

543541
super.setCurNode(newNode);
@@ -616,6 +614,7 @@ export class LineCursor extends Marker {
616614
// Selection should already be in sync.
617615
} else {
618616
block.addSelect();
617+
block.getParent()?.removeSelect();
619618
}
620619
}
621620

@@ -671,23 +670,98 @@ export class LineCursor extends Marker {
671670
}
672671

673672
/**
674-
* Event listener that syncs the cursor location to the selected
675-
* block on SELECTED events.
673+
* Event listener that syncs the cursor location to the selected block on
674+
* SELECTED events.
675+
*
676+
* This does not run early enough in all cases so `getCurNode()` also updates
677+
* the node from the selection.
678+
*
679+
* @param event The `Selected` event.
676680
*/
677-
private selectListener(event: Blockly.Events.Abstract) {
678-
if (event.type !== Blockly.Events.SELECTED) return;
679-
const selectedEvent = event as Blockly.Events.Selected;
680-
if (selectedEvent.workspaceId !== this.workspace.id) return;
681-
if (selectedEvent.newElementId) {
682-
const block = this.workspace.getBlockById(selectedEvent.newElementId);
683-
if (block) {
684-
const node = ASTNode.createBlockNode(block);
685-
if (node) {
686-
this.setCurNode(node, true);
681+
private changeListener(event: Blockly.Events.Abstract) {
682+
switch (event.type) {
683+
case Blockly.Events.SELECTED:
684+
this.updateCurNodeFromSelection();
685+
break;
686+
case Blockly.Events.CLICK: {
687+
const click = event as Blockly.Events.Click;
688+
if (
689+
click.workspaceId === this.workspace.id &&
690+
click.targetType === Blockly.Events.ClickTarget.WORKSPACE
691+
) {
692+
this.setCurNode(null);
687693
}
688694
}
695+
}
696+
}
697+
698+
/**
699+
* Updates the current node to match the selection.
700+
*
701+
* Clears the current node if it's on a block but the selection is null.
702+
* Sets the node to a block if selected for our workspace.
703+
* For shadow blocks selections the parent is used by default (unless we're
704+
* already on the shadow block via keyboard) as that's where the visual
705+
* selection is.
706+
*/
707+
private updateCurNodeFromSelection() {
708+
const curNode = super.getCurNode();
709+
const selected = Blockly.common.getSelected();
710+
711+
if (
712+
selected === null &&
713+
curNode?.getType() === Blockly.ASTNode.types.BLOCK
714+
) {
715+
this.setCurNode(null, true);
716+
return;
717+
}
718+
if (selected?.workspace !== this.workspace) {
719+
return;
720+
}
721+
if (selected instanceof Blockly.BlockSvg) {
722+
let block: Blockly.BlockSvg | null = selected;
723+
if (selected.isShadow()) {
724+
// OK if the current node is on the parent OR the shadow block.
725+
// The former happens for clicks, the latter for keyboard nav.
726+
if (
727+
curNode &&
728+
(curNode.getLocation() === block ||
729+
curNode.getLocation() === block.getParent())
730+
) {
731+
return;
732+
}
733+
block = block.getParent();
734+
}
735+
if (block) {
736+
this.setCurNode(Blockly.ASTNode.createBlockNode(block)!, true);
737+
}
738+
}
739+
}
740+
741+
/**
742+
* Updates the selection from the node.
743+
*
744+
* Clears the selection for non-block nodes.
745+
* Clears the selection for shadow blocks as the selection is drawn on
746+
* the parent but the cursor will be drawn on the shadow block itself.
747+
* We need to take care not to later clear the current node due to that null
748+
* selection, so we track the latest selection we're in sync with.
749+
*
750+
* @param newNode The new node.
751+
*/
752+
private updateSelectionFromNode(newNode: Blockly.ASTNode | null) {
753+
if (newNode?.getType() === ASTNode.types.BLOCK) {
754+
if (Blockly.common.getSelected() !== newNode.getLocation()) {
755+
Blockly.Events.disable();
756+
Blockly.common.setSelected(newNode.getLocation() as Blockly.BlockSvg);
757+
Blockly.Events.enable();
758+
}
689759
} else {
690-
this.setCurNode(null as never, true);
760+
if (Blockly.common.getSelected()) {
761+
Blockly.Events.disable();
762+
Blockly.common.setSelected(null);
763+
Blockly.Events.enable();
764+
}
691765
}
692766
}
693767
}

src/navigation.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -415,14 +415,22 @@ export class Navigation {
415415
}
416416

417417
/**
418-
* Clears the navigation state and switches to using the passive focus indicator.
418+
* Clears navigation state and switches to using the passive focus indicator
419+
* if it is not the context menu / field input that is causing blur.
419420
*
420421
* @param workspace The workspace that has lost focus.
422+
* @param ignorePopUpDivs Whether to skip the focus indicator change when
423+
* the widget/dropdown divs are open.
421424
*/
422-
handleBlurWorkspace(workspace: Blockly.WorkspaceSvg) {
425+
handleBlurWorkspace(
426+
workspace: Blockly.WorkspaceSvg,
427+
ignorePopUpDivs = false,
428+
) {
423429
this.setState(workspace, Constants.STATE.NOWHERE);
424430
const cursor = workspace.getCursor();
425-
if (cursor) {
431+
const popUpDivsShowing =
432+
Blockly.WidgetDiv.isVisible() || Blockly.DropDownDiv.isVisible();
433+
if (cursor && (ignorePopUpDivs || !popUpDivsShowing)) {
426434
const curNode = cursor.getCurNode();
427435
if (curNode) {
428436
this.passiveFocusIndicator.show(curNode);
@@ -432,6 +440,32 @@ export class Navigation {
432440
}
433441
}
434442

443+
/**
444+
* Handle the widget or dropdown div losing focus (via focusout).
445+
*
446+
* Because we skip the widget/dropdown div cases in `handleBlurWorkspace` we need
447+
* to catch them here.
448+
*
449+
* @param workspace The workspace.
450+
* @param relatedTarget The related target (newly focused element if any).
451+
*/
452+
handleFocusOutWidgetDropdownDiv(
453+
workspace: Blockly.WorkspaceSvg,
454+
relatedTarget: EventTarget | null,
455+
) {
456+
if (relatedTarget === null) {
457+
// Workaround:
458+
// Skip document.body/null case until this blur bug is fixed to avoid
459+
// flipping to passive focus as the user moves their mouse over the
460+
// colour picker.
461+
// https://github.com/google/blockly-samples/issues/2498
462+
return;
463+
}
464+
if (relatedTarget !== getWorkspaceElement(workspace)) {
465+
this.handleBlurWorkspace(workspace, true);
466+
}
467+
}
468+
435469
/**
436470
* Sets browser focus to the toolbox (if any).
437471
*

src/navigation_controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,13 @@ export class NavigationController {
196196
this.navigation.handleBlurWorkspace(workspace);
197197
}
198198

199+
handleFocusOutWidgetDropdownDiv(
200+
workspace: Blockly.WorkspaceSvg,
201+
relatedTarget: EventTarget | null,
202+
) {
203+
this.navigation.handleFocusOutWidgetDropdownDiv(workspace, relatedTarget);
204+
}
205+
199206
focusToolbox(workspace: Blockly.WorkspaceSvg) {
200207
this.navigation.focusToolbox(workspace);
201208
}

0 commit comments

Comments
 (0)