Skip to content

Conversation

@BenHenning
Copy link
Collaborator

@BenHenning BenHenning commented May 6, 2025

The basics

The details

Resolves

Fixes #8820
Fixes #9000
Fixes #9001
Fixes #9003
Fixes #8998
Fixes part of #8771

Proposed Changes

At a high level, this PR makes everything that's selectable (that is, implements ISelectable) now focusable by making ISelectable extend IFocusableNode. The brings with it a bunch of new possibilities, also introduced in this PR:

  • A change to how selection is managed in common to be completely derived from focus (since we now guarantee that selection auto-updates for ISelectables when they are focused).
  • An update to ensure that icons and bubbles are now focusable themselves. They are not yet navigable via the keyboard, but that should be a straightforward next step now that they've been made focusable.
    • Note that this means a specific change to make both IIcon and IBubble extensions of IFocusableNode. Both imply having visual elements which is why these root interfaces have been updated (unlike in other cases like Block and Connection).
  • Replacements for all common.setSelected() calls to now use getFocusManager().focusNode(). Since selection is represented by focus, focusing with auto-selection is logically equivalent to the old common.setSelected() call (except for clearing a selection state).
  • LineCursor no longer needs to fall back to selection since it's completely covered by focus cases.
  • Identifying comments, bubbles, and icons in WorkspaceSvg has been done somewhat generically to provide a reasonable pathway for custom implementations to properly hook into workspace focus.

Reason for Changes

Fundamentally, these changes are needed to simplify selection logic as well as add missing components to the focus system (which will then allow them to become navigable and fully functional for keyboard navigation).

Some key design decisions that were made:

  • ISelectable and common.setSelected()/common.getSelected() have been kept even though they are trivially removable currently. This is because we still want to maintain selectable state and an API to represent it for when multi selection can be designed into core Blockly. common.setSelected() is still marked as @internal.
  • A new canBeFocused method has been added to IFocusableNode.
    • This provides a means for components to participate in the focus system without being obligated to provide a focusable element. Since we can't easily prevent this from being a dynamic property, it should more or less work so long as the contract documented is followed.
    • This may be able to replace INavigable.isNavigable().
  • The SELECTED event firing logic has been pulled into a new common method. It's now called directly by ISelectable implementations and, being that it's @internal, shouldn't be used outside of core Blockly. There may well be a better location for this to live in the long-term, but that's a design decision better settled as part of multi-select.
  • BlockSvg selection is now directly attached to shadow blocks rather than being forced to delegate to the block's parent. Instead, this is being checked at the places where it could matter (namely for gestures).
    • BlockSvg's dispose logic no longer needs to care about selections since they're tied to focus, and focus should automatically adjust when a block is removed from the workspace.
  • Bubble has a bit of tricky changes to its constructor as a result of needs from TextInputBubble. Specifically, for reasons I am not 100% sure of it seems that TextInputBubble's textarea must be focused in order to receive input (this is not the case for RenderedWorkspaceComment despite very similar implementations). Thus, Bubble provides a way to customize the focusable element that it uses (but otherwise defaults to its root group).
    • Note that some of the bringToFront() logic was simplified since it can be tied to focus being received, now.
    • Some of the extra focus logic in TextInputBubble is no longer necessary. FocusManager provides fairly strong guarantees for restoring focus for certain DOM operations (though this can get complicated as is seen for BlockSvg.bringToFront()).
  • IHasBubble has been updated to require being able to return the current bubble (which is used by WorkspaceSvg to find a focusable bubble in response to DOM changes).
  • Clicking the workspace does not remove focus or selection from workspace comments #8998 is fixed due to RenderedWorkspaceComment now being focusable (and thus unfocusable to return focus to the workspace itself for gesture management). Note that there was one change in LayerManager to avoid unnecessary appends (which can cause a cycle in FocusManager for focused elements when the operation is called in onFocusNode). Some effort could be made to break the cycle in FocusManager itself (which is tricky), but this approach has a net benefit to avoid an operation that simply isn't needed.
  • WorkspaceDragger has had its selection/focus logic on drag start removed due to it being unnecessary. See: feat!: Make everything ISelectable focusable #9004 (comment).

Caveats:

  • This PR doesn't change where BlockSvg's addSelect/removeSelect are used directly. It's assumed these are still needed, but they may be incidentally obsolete if there's any part of the dataflow that forces focus (since that will then force selection state, anyway).
  • This will cause a failure in the keyboard navigation plugin until fix: Support now-focusable bubbles and icons. blockly-keyboard-experimentation#502 is submitted (due to the breaking API change being introduced here).
  • Focus events are a bit spammy now as they'll send a deselect between selection events (i.e. selecting something new will involve a deselect event being fired first). This is the existing behavior for blocks (but is relatively new since that behavior was introduced when BlockSvg was updated to auto-select on focus) and is new for other selectables.
  • The changes to selection here and in feat: Make WorkspaceSvg and BlockSvg focusable (roll forward) #8938 mean that common.getSelected() will return null for cases when there was previously a selection but not active focus (such as right clicking on a block).
  • RenderedConnection technically depends on the shadow block selection logic that was removed, however it's actually quite challenging to implement that there. RenderedConnection cannot import BlockSvg directly, and fixing this import ordering could potentially be quite complex. That being said, it seems that the selection management there is actually no longer needed due to focus automatically driving selection states. See feat!: Make everything ISelectable focusable #9004 (comment) for much more details on why.
  • One test has been removed from the block tests since it was directly verifying unhighlighting a shadow's parent when disposing the shadow child and there's no strong analogue to change this (as the behavior no longer exists).
  • This doesn't solve Make mutator bubbles focusable and navigable #9002 despite the mutator icon now being focusable and it containing a workspace that supports focus. Some nested focus logic needs to be implemented in WorkspaceSvg in order to ensure cross-workspace hand-off works correctly, though this might partly come down to a usability and navigation question that should be solved outside this PR.

Test Coverage

Tests have not been added here, but there are legitimate cases for future testing:

Manually testing has been detailed in this comment: #9004 (comment).

Documentation

As part of the broader focus management documentation, we will likely want to describe how to account for focus support when implementing custom icons and bubbles (though the base classes are meant to mostly solve these as-is) when wanting to maintain keyboard navigation support.

Additional Information

None currently.

@github-actions github-actions bot added breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature labels May 6, 2025
@BenHenning
Copy link
Collaborator Author

BenHenning commented May 6, 2025

This still needs to be tested & expanded into a ready-to-merge PR, but the thoughts here are:

  • Make everything ISelectable as IFocusableNode which requires specifically supporting bubbles, icons, and workspace comments (which gives us a whole bunch of stuff along the way).
  • Make ISelectable specifically implement IFocusableNode. This we may not actually want to do thinking about multi-selection, but maybe...? If the modality is we can select multiple ISelectables simultaneously, then the idea of one of them being focused is actually quite plausible (and, consequently, that any of them can be focused at any given moment).
  • Remove singleton selectable state since, without multi select, we know for sure that a selection must now be focused so we can just check the thing with focus for that.
  • Break out the select fire event bits since that's the actual interesting them for ISelectable implementations. I'm...not keen on the approach here and would really like to get guidance on a better place to put it.
  • Remove all uses of setSelected, but keep force it to use focusNode now (since ISelectable is focusable) for the external cases that use setSelected (even though it's internal 🙃).
  • Move the shadow block checks to existing common.getSelected() locations (using a new common.getSelectedBlock() helper). Not keen on the new helper, but similar to the event one this was the easiest pathway toward moving existing code over.
  • LineCursor now completely ignores selection since it can fully rely on focus state at this point.
  • This may be logically breaking for the plugin, but I don't expect it to be API breaking. Still need to test. Edit: this is definitely breaks the plugin due to the new icon and bubble introduced in the plugin.

I haven't changed cases where addSelect & its removal function are used directly on block. I'm assuming we still want these cases, but they may be incidentally obsolete if there's any part of the dataflow that forces focus (since that will then force selection state, anyway).

Edit: updated the description--this will NOT fix #9002 as it doesn't provide a transition pathway for mutators. It will allow the bubble to be focused, though.

@rachel-fenichel
Copy link
Collaborator

This looks directionally correct to me.

@github-actions github-actions bot added breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature and removed PR: feature Adds a feature breaking change Used to mark a PR or issue that changes our public APIs. labels May 6, 2025
*/
let selected: ISelectable | null = null;
export function getSelected(): ISelectable | null {
const focused = getFocusManager().getFocusedNode();
Copy link
Contributor

Choose a reason for hiding this comment

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

I am curious about how well this will work.

One scenario is that in context menu items sometimes the callback would use getSelected, because the block is still selected when the menu is open. But the block won’t be focused when the menu is open. So we would need to call this out as a breaking change. I am curious if there are other situations where a block would have been selected but not (active) focused and I’m not sure I can think of any, maybe in some widget div scenarios

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think the scenario you describe would be broken as of #8938 not this change, but it's equally relevant. It's definitely the case now that right clicking will not maintain block selection since the selection highlight is cleared on focus loss.

It's something worth discussing, I think. Does something really have selection if it doesn't have input focus? My assumption is no which is why the change moved in this direction. If the answer should be yes, then we need to approach the auto-selection differently.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think logically it's fine, but it is a big behavior change that the block isn't selected anymore while you're interacting with non-Blockly parts of the UI (e.g. context menu or dev console) so we just need to be super clear about that in our communication.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What's the best way for me to indicate that bit (per being super clear)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Update the PR description of this PR (and/or any others you've made that are breaking) to include a ## Breaking Changes section that explains 1) who is broken by the change 2) what they need to do in response to the breaking change.

So for the issue about the block not being selected anymore, provide workarounds like using the (now passively-) focused block instead of selected block, using the scope for context menu items and shortcuts instead of selected, etc.

For this PR, you might need other details like what new methods need to be implemented if any for bubbles, etc.

Then remind me to call this out in the handwritten release notes we'll attach to the github release/forum announcement as well.

BenHenning added 6 commits May 7, 2025 19:31
Conflicts:
	core/keyboard_nav/line_cursor.ts
This fixed then regressed a circular dependency causing the node and
advanced compilation steps to fail. This investigation is ongoing.
This ensures the node and advanced compilation test steps now pass.
@github-actions github-actions bot removed the PR: feature Adds a feature label May 8, 2025
Copy link
Collaborator 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 all changes so far.

Comment on lines -562 to -575
// If either block being connected was selected, visually un- and reselect
// it. This has the effect of moving the selection path to the end of the
// list of child nodes in the DOM. Since SVG z-order is determined by node
// order in the DOM, this works around an issue where the selection outline
// path could be partially obscured by a new block inserted after it in the
// DOM.
const selection = common.getSelected();
const selectedBlock =
(selection === parentBlock && parentBlock) ||
(selection === childBlock && childBlock);
if (selectedBlock) {
selectedBlock.removeSelect();
selectedBlock.addSelect();
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Check whatever cases this can occur to see if it still works as expected.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Note that this testing will include verifying #9004 (comment) as well to keep things in one location for RenderedConnection.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Chatting with Rachel about this a bit to try and figure out repro cases, and why this logic is even needed.

For connections, it's needed specifically in Zelos due to selections being a separate path object (where SVG rendering is always back-to-front based on the DOM element order). An easy repro is to open the advanced playground with Zelos enabled, add and connect 2 print blocks, then drag the bottom block to the top of the stack and observe whether the selection highlight gets cut off on the bottom of the block.

I can confirm that this works correctly in this branch even without these manual changes. I will confirm more specifically against develop, but I suspect why this is no longer needed is due to behavior changes in #8981 in how often a block is brought to front. I'm not 100% certain of that yet as nothing in the PR is obviously the reason why this would be different.

For bumping, my repro was a bit trickier to figure out. Here's what I did:

  • Open advanced playground (also with Zelos since that's the only renderer that should be affected by these selection changes).
  • Add a "do something" with return block.
  • Add an empty text block as the return.
  • Add a "create text with" block.
  • Try to drag the "do something" block over the "create text with" block such that the mutator icon is underneath the empty space for the text block (i.e. the white square). This is a nice reference to use since otherwise getting the block underneath to bump can be a bit finicky position-wise.

This will generally trigger a small bump of the "create text with" block.

However, this doesn't trigger the interesting case because the block underneath being bumped is the 'inferior' one and thus not selected. Still trying to find a repro with selection.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, another suggestion from Rachel: this can be triggered by forcing the 'create text with' block to be immovable by using:

Blockly.getMainWorkspace().getBlockById(id).setMovable(false)

In the developer console with the 'create text' block's ID being used in place of 'id' above (using the advanced playground JSON output to find it).

This...unfortunately still seems to not demonstrate the problem. On develop with the selection code removed and this scenario used, the outer block (being dragged) is retaining its selection order without a problem (which makes sense because it's being clicked on and thus being brought to front for the drag).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've dug back to 02b2c21 as the original introducing of the selection correction logic. I'm not sure what the original bug's repro steps were, but this fix is very old and predates gestures plus bring-to-front logic (at least in its current form). I'm now suspecting that the selection bits for bumping have been made obsolete with other bring-to-front mechanisms such that this hasn't been needed for some time. No amount of bumping seems to get in the way of the selected block or the selection itself (both always seem to stay above the bumped block), so I do think it's safe to remove this logic now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Going back to connect_, I think I know what's happening here and verifying would be tricky, but I think this fine to logic out based on the observation that everything is working fine without the selection bits in.

With the latest changes, the data flow roughly looks like this:

  • User sends pointer event to click on a block.
  • Gestures mark that block as focused.
  • The block, upon receiving focus, auto select themselves (and if a previous block was focused, it removes its selection).
  • The process of removing/adding a selection bumps it back to the top in the same way as the old logic here did.

Thus, this shouldn't be needed since the various efforts to focus the block being moved should generally always result with its selection appearing on top.

@github-actions github-actions bot added breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature and removed breaking change Used to mark a PR or issue that changes our public APIs. labels May 8, 2025
Addresses reviewer comment.
@BenHenning BenHenning marked this pull request as ready for review May 8, 2025 01:30
@BenHenning BenHenning requested a review from a team as a code owner May 8, 2025 01:30
@github-actions github-actions bot added breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature labels May 8, 2025
This addresses a bunch of review comments, and fixes selecting workspace
comments.
@github-actions github-actions bot added breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature and removed PR: feature Adds a feature breaking change Used to mark a PR or issue that changes our public APIs. labels May 8, 2025
Copy link
Collaborator 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 changes and working to close open conversation threads that have had their trivial fixes addressed.

@BenHenning
Copy link
Collaborator Author

This is a comment documenting all of the testing things I did (or, rather, linking to where I describe them):

@github-actions github-actions bot added breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature and removed PR: feature Adds a feature breaking change Used to mark a PR or issue that changes our public APIs. labels May 8, 2025
* later on when defocused (since it was previously considered focusable at
* the time of being focused).
*/
canBeFocused(): boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I would probably remove "disabled, read-only" from the list of examples for why you might want this to return false, as those cases should still be focusable for screenreader users most likely. I think it really is just the purely visual decoration / no visual representation case.

@rachel-fenichel rachel-fenichel merged commit 4074cee into RaspberryPiFoundation:rc/v12.0.0 May 9, 2025
7 checks passed
@BenHenning BenHenning deleted the make-everything-selectable-focusable branch May 9, 2025 19:13
BenHenning added a commit that referenced this pull request May 14, 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 RaspberryPiFoundation/blockly-keyboard-experimentation#499

### Proposed Changes

This ensures that non-blocks which hold active focus correctly update `LineCursor`'s internal state.

### Reason for Changes

This is outright a correction in how `LineCursor` has worked up until now, and is now possible after several recent changes (most notably #9004). #9004 updated selection to be more explicitly generic (and based on `IFocusableNode`) which means `LineCursor` should also properly support more than just blocks when synchronizing with focus (in place of selection), particularly since lots of non-block things can be focusable.

What's interesting is that this change isn't strictly necessary, even if it is a reasonable correction and improvement in the robustness of `LineCursor`. Essentially everywhere navigation is handled results in a call to `setCurNode` which correctly sets the cursor's internal state (with no specific correction from focus since only blocks were checked and we already ensure that selecting a block correctly focuses it).

### Test Coverage

It would be nice to add test coverage specifically for the cursor cases, but it seems largely unnecessary since:
1. The main failure cases are test-specific (as mentioned above).
2. These flows are better left tested as part of broader accessibility testing (per #8915).

This has been tested with a cursory playthrough of some basic scenarios (block movement, insertion, deletion, copy & paste, context menus, and interacting with fields).

### Documentation

No new documentation should be needed here.

### Additional Information

This is expected to only affect keyboard navigation plugin behaviors, particularly plugin tests.

It may be worth updating `LineCursor` to completely reflect current focus state rather than holding an internal variable. This, in turn, may end up simplifying solving issues like #8793 (but not necessarily).
cpcallen added a commit to cpcallen/blockly that referenced this pull request Jun 17, 2025
cpcallen added a commit to cpcallen/blockly that referenced this pull request Jun 17, 2025
cpcallen added a commit that referenced this pull request Jun 25, 2025
* refactor(interfaces): Use typeof ... === 'function' to test for methods

  Testing for

      'name' in object

  or

      obj.name !== undefined

  only checks for the existence of the property (and in the latter
  case that the property is not set to undefined).  That's fine if
  the interface specifies a property of indeterminate type, but in
  the usual case that the interface member is a method we can do
  one better and check to make sure the property's value is
  callable.

* refactor(interfaces): Always check obj is not null/undefined

  Since most type predicates take an argument of type any but then
  check for the existence of certain properties, explicitly check
  that the argument is not null or undefined (or check implicitly
  by calling another type predicate that does so first, which
  necessitates adding a few casts because tsc infers the type of
  the argument too narrowly).

* fix(interfaces): Add missing check to hasBubble type predicate

  This appears to have inadvertently been omitted in PR #9004.

* fix(interfaces): Fix misplaced typeof

* fix: Fix typos in JSDocs

* fix(tests): Make Mocks conform to corresponding interfaces

  Introduce a new MockFocusable, and add methods to MockIcon,
  MockBubbleIcon and MockComment, so that they fulfil the
  IFocusableNode, IIcon, IHasBubble and ICommentIcon interfaces
  respectively.

* chore(tests): Add assertions verifying mocks conform to predicates

  Add (test) runtime assertions that:

  - isFocusableNode(MockFocusable) returns true
  - isIcon(MockIcon) returns true
  - hasBubble(MockBubbleIcon) returns true
  - isCommentIcon(MockCommentIcon) returns true

  (The latter is currently failing because Blockly is undefined when
  isCommentIcon calls the MockCommentIcon's getType method.)

* fix(tests): Don't rely on Blockly being set in Mock methods

  For some reason the global Blockly binding is not visible at the
  time when isCommentIcon calls MockCommentIcon's getType method,
  and presumably this problem would apply to getBubbleSize too,
  so directly import the required items.

* refactor(tests): Make MockCommentIcon a MockBubbleIcon

  This slightly simplifies it and makes it less likely to accidentally
  stop conforming to IHasBubble.

* fix(interfaces): Fix incorrect check in isSelectable

  Fix an error which caused ISelectable instances to fail
  isSelectable() checks, one of the results of which is that
  Blockly.common.getSelected() would generally return null.

  Whoops!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bubbles. Icons. Comments. Clicking the workspace does not remove focus or selection from workspace comments Update cursor state in setSelected

3 participants