8000 fix: Synchronize gestures and focus by BenHenning · Pull Request #8981 · google/blockly · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

fix: Synchronize gestures and focus #8981

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

Conversation

BenHenning
Copy link
Contributor
@BenHenning BenHenning commented May 2, 2025

The basics

The details

Resolves

Fixes #8952
Fixes #8950
Fixes #8971

Fixes a bunch of other keyboard + mouse synchronization issues found during the testing discussed in google/blockly-keyboard-experimentation#482 (comment).

Proposed Changes

This introduces a number of changes to:

  • Ensure that gestures which change selection state also change focus.
  • Ensure that ephemeral focus is more robust against certain classes of failures.

Reason for Changes

There are some ephemeral focus issues that can come up with certain actions (like clicking or dragging) don't properly change focus. Beyond that, some users will likely mix clicking and keyboard navigation, so it's essential that focus and selection state stay in sync when switching between these two types of navigation modalities.

Other changes:

  • Drop-down div was actually incorrectly releasing ephemeral focus before animated closes finish which could reset focus afterwards, breaking focus state.
  • Both drop-down and widget divs have been updated to only return focus after marking the workspace as focused since the last focused node should always be the thing returned.
  • In a number of gesture cases selection has been removed. This is due to BlockSvg self-managing its selection state based on focus, so focusing is sufficient. I've also centralized some of the focusing calls (such as putting one in bringBlockToFront to ensure focusing happens after potential DOM changes).
  • Similarly, BlockSvg's own bringToFront has been updated to automatically restore focus after the operation completes. Since bringToFront can always result in focus loss, this provides robustness to ensure focus is restored.
  • Block pasting now ensures that focus is properly set, however a delay is needed due to additional rendering changes that need to complete (I didn't dig deeply into the why of this).
  • Dragging has been updated to always focus the moved block if it's not in the process of being deleted.
  • There was some selection resetting logic removed from gesture's doWorkspaceClick function. As far as I can tell, this won't be needed anymore since blocks self-regulate their selection state now.
  • FocusManager has a new extra check for ephemeral focus to catch a specific class of failure where the browser takes away focus immediately after it's returned. This can happen if there are delay timing situations (like animations) which result in a focused node being deleted (which then results in the document body receiving focus). The robustness check is possibly not needed, but it help discover the animation issue with drop-down div and shows some promise for helping to fix the variables-closing-flyout problem.

Some caveats:

  • Some undo/redo steps can still result in focus being lost. This may introduce some regressions for selection state, and definitely introduces some annoyances with keyboard navigation. More work will be needed to understand how to better redirect focus (and to what) in cases when blocks disappear.
  • There are many other places where focus is being forced or selection state being overwritten that could, in theory, cause issues with focus state. These may need to be fixed in the future.
  • There's a lot of redundancy with focusNode being called more than it needs to be. FocusManager does avoid calling focus() more than once for the same node, but it's possible for focus state to switch between multiple nodes or elements even for a single gesture (for example, due to bringing the block to the front causing a DOM refresh). While the eventual consistency nature of the manager means this isn't a real problem, it may cause problems with screen reader output 8000 in the future and warrant another pass at reducing focusNode calls (particularly for gestures and the click event pipeline).

Test Coverage

This PR is largely relying on existing tests for regression verification, though no new tests have been added for the specific regression cases.

#8910 is tracking improving FocusManager tests which could include some cases for the new ephemeral focus improvements.

#8915 is tracking general accessibility testing which could include adding tests for these specific regression cases.

Documentation

No new documentation is expected to be needed here.

Additional Information

These changes originate from both #8875 and from a branch @rachel-fenichel created to investigate some of the failures this PR addresses. These changes have also been verified against both Core's playground and the keyboard navigation plugin's test environment.

rachel-fenichel and others added 4 commits May 1, 2025 11:19
These fixes cover a number of specific cases, plus some additional
safety improvements were added for widget and drop-down divs, plus
ephemeral focus management.
Copy link
Contributor Author
@BenHenning BenHenning left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-reviewed PR.

Addresses self-review comments.
Copy link
Contributor Author
@BenHenning BenHenning left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-reviewed changes.

@BenHenning BenHenning marked this pull request as ready for review May 2, 2025 23:16
@BenHenning BenHenning requested a review from a team as a code owner May 2, 2025 23:16
@BenHenning BenHenning requested a review from rachel-fenichel May 2, 2025 23:16
@BenHenning BenHenning changed the title feat: synchronize gestures and focus feat: Synchronize gestures and focus May 2, 2025
@BenHenning BenHenning added the PR: feature Adds a feature label May 2, 2025
@BenHenning BenHenning changed the title feat: Synchronize gestures and focus fix: Synchronize gestures and focus May 2, 2025
@BenHenning BenHenning added PR: fix Fixes a bug and removed PR: feature Adds a feature labels May 2, 2025
Copy link
Collaborator
@rachel-fenichel rachel-fenichel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: "changes have been verified", what did you do to manually verify?

// Sometimes there's a delay before the block is fully created and ready for
// focusing, so wait slightly before focusing the newly pasted block.
const nodeToFocus: IFocusableNode = block;
setTimeout(() => getFocusManager().focusNode(nodeToFocus), 0);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This delay may be related to the bumpNeighbours behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm that seems really plausible, though I can't find a call to bump in this call path but I'm sure I'm missing large portions of what's happening here.

This does make me think that it might be nice to have a way to attach logic to run with the next rendering pass so that we can predictably run things that depend on rendering (vs. using a timeout and hoping it's correct).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
 * @returns A promise that resolves after the currently queued renders have
 *     been completed.
 */
export function finishQueuedRenders(): Promise<void> {
  // If there are no queued renders, return a resolved promise so `then`
  // callbacks trigger immediately.
  return afterRendersPromise ? afterRendersPromise : Promise.resolve();
}

You can find examples of this pattern in the codebase. I think it will do what you want.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking at this again: please switch to using finishQueuedRenders before merging. It is a stronger guarantee on timing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I evidently didn't look hard enough for this exact mechanism. Nice suggestion--I updated the code and tested pasting again. Selection seems to correctly select the most recently pasted block as intended.

@@ -129,6 +131,12 @@ export class Dragger implements IDragger {
root.dispose();
}
eventUtils.setGroup(false);

if (!wouldDelete && isFocusableNode(this.draggable)) {
// Ensure focusable nodes that have finished dragging (but aren't being)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"(but aren't being) deleted" -> "(but aren't being deleted)"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch--fixed!

Addresses reviewer comment.
@BenHenning
Copy link
Contributor Author

Re: "changes have been verified", what did you do to manually verify?

I can re-run these steps with a video recording if that's helpful, but here's what I did:

Core playground

  • Dragged to insert a block. While it was selected, attempted to copy and paste it several times. Paste worked. This demonstrates a fix to Cannot paste when a block is selected #8952.
    • Did the same thing with click insertion.
    • Did the same thing with an existing block that I then dragging to put it into the "dragged" state which broke pasting earlier in my repros.
  • Added a binary operation block and edited one of the operators text fields. Saw the flyout did not reopen, demonstrating a fix for Returning ephemeral focus moves focus to the toolbox #8950.
    • Did the same for selecting an operator.
    • Repeated both of these (editing the field and changing the operator) for both dragging the 8000 block onto the workspace and clicking to add it.

Keyboard nav environment

  • Many iterations on clicking different blocks and fields, then using keyboard navigation to ensure that state is correctly synchronized. Confirmed the same flows with just keyboard navigation to ensure that there weren't regressions (which happened a few times when trying to fix field input). This verified fixes for most of the selection issues brought up in the focus issues findings document.
  • Verified both dragging and clicking to insert blocks onto the workspace to ensure the block maintains selection.
  • Multiple iterations of dragging blocks between stacks, between blocks, and on/off stacks to ensure that they remain highlighted. Repeated some of these with keyboard movement to verify some of the cases in the focus issues findings document.
  • Multiple iterations of copying and pasting blocks to ensure that pasting maintained the correct selection (new block only). Also tried cutting to verify that Error caused when you keyboard navigate to a block, cut (CMD+X), paste (CMD+V), then undo (CMD+Z) via keyboard shortcuts. #8971 has also been fixed.

There may be other things I've tried, but I think this covers the majority. If there's anything else that you think is worth testing or re-testing, please let me know.

@BenHenning
Copy link
Contributor Author

PTAL @rachel-fenichel to the latest changes & my comment above.

@rachel-fenichel
Copy link
Collaborator

Thanks for the list--that gives us things to turn into automated tests down the line, and means that you can ask other folks to reverify for you if issues pop up.

@BenHenning
Copy link
Contributor Author

Enabling auto-merge. Please only approve if you're happy with this being merged.

@BenHenning BenHenning enabled auto-merge (squash) May 3, 2025 00:42
@BenHenning BenHenning removed their assignment May 5, 2025
Copy link
Contributor Author
@BenHenning BenHenning left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-reviewed latest.

@BenHenning
Copy link
Contributor Author

PTAL again @rachel-fenichel. I think everything's been addressed so far.

@BenHenning BenHenning merged commit bbd97ea into google:rc/v12.0.0 May 5, 2025
7 checks passed
@BenHenning BenHenning deleted the synchronize-gestures-and-focus branch May 5, 2025 17:30
BenHenning added a commit that referenced this pull request May 6, 2025
## The basics

- [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change)

## The details
### Resolves

Fixes #8994

### Proposed Changes

This removes an error that was previously thrown by `FocusManager` when attempting to focus an invalid node (such as one that's been removed from its parent).

### Reason for Changes

#8994 (comment) goes into more detail. While this error did cover legitimately wrong cases to try and focus things (and helped to catch some real problems), fixing this 'properly' may become a leaky boat problem where we have to track down every possible asynchronous scenario that could produce such a case. One class of this is ephemeral focus which had robustness improvements itself in #8981 that, by effect, caused this issue in the first place. Holistically fixing this with enforced API contracts alone isn't simple due to the nature of how these components interact.

This change ensures that there's a sane default to fall back on if an invalid node is passed in. Note that `FocusManager` was designed specifically to disallow defocusing a node (since fallbacks can get messy and introduce unpredictable user experiences), and this sort of allows that now. However, this seems like a reasonable approach as it defaults to the behavior when focusing a tree explicitly which allows the tree to fallback to a more suitable default (such as the first item to select in the toolbox for that particular tree). In many cases this will default back to the tree's root node (such as the workspace root group) since sometimes the removed node is still the "last focused node" of the tree (and is considered valid for the purpose of determining a fallback; tree implementations could further specialize by checking whether that node is still valid).

### Test Coverage

Some new tests were added to cover this case, but more may be useful to add as part of #8910.

### Documentation

No documentation needs to be added or updated as part of this (beyond code documentation changes).

### Additional Information

This original issue was found by @RoboErikG when testing #8995. I also verified this against the keyboard navigation plugin repository.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: fix Fixes a bug
Projects
None yet
2 participants
0