feat: Add support for keyboard navigation into mutator workspaces. (#9151)

* feat: Add support for keyboard navigation into mutators.

* fix: Prevent mutator bubbles from jumping wildly during keyboard nav.
This commit is contained in:
Aaron Dodson
2025-06-23 09:09:56 -07:00
committed by GitHub
parent 97ffea73be
commit 1e5b4e9f42
6 changed files with 47 additions and 11 deletions

View File

@@ -153,7 +153,11 @@ export class MiniWorkspaceBubble extends Bubble {
* are dealt with by resizing the workspace to show them.
*/
private bumpBlocksIntoBounds() {
if (this.miniWorkspace.isDragging()) return;
if (
this.miniWorkspace.isDragging() &&
!this.miniWorkspace.keyboardMoveInProgress
)
return;
const MARGIN = 20;
@@ -185,7 +189,15 @@ export class MiniWorkspaceBubble extends Bubble {
* mini workspace.
*/
private updateBubbleSize() {
if (this.miniWorkspace.isDragging()) return;
if (
this.miniWorkspace.isDragging() &&
!this.miniWorkspace.keyboardMoveInProgress
)
return;
// Disable autolayout if a keyboard move is in progress to prevent the
// mutator bubble from jumping around.
this.autoLayout &&= !this.miniWorkspace.keyboardMoveInProgress;
const currSize = this.getSize();
const newSize = this.calculateWorkspaceSize();

View File

@@ -14,7 +14,6 @@ import {BlockChange} from '../events/events_block_change.js';
import {isBlockChange, isBlockCreate} from '../events/predicates.js';
import {EventType} from '../events/type.js';
import * as eventUtils from '../events/utils.js';
import type {IBubble} from '../interfaces/i_bubble.js';
import type {IHasBubble} from '../interfaces/i_has_bubble.js';
import * as renderManagement from '../render_management.js';
import {Coordinate} from '../utils/coordinate.js';
@@ -205,7 +204,7 @@ export class MutatorIcon extends Icon implements IHasBubble {
}
/** See IHasBubble.getBubble. */
getBubble(): IBubble | null {
getBubble(): MiniWorkspaceBubble | null {
return this.miniWorkspaceBubble;
}

View File

@@ -5,7 +5,9 @@
*/
import {BlockSvg} from '../block_svg.js';
import {getFocusManager} from '../focus_manager.js';
import {Icon} from '../icons/icon.js';
import {MutatorIcon} from '../icons/mutator_icon.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
import {navigateBlock} from './block_navigation_policy.js';
@@ -17,10 +19,18 @@ export class IconNavigationPolicy implements INavigationPolicy<Icon> {
/**
* Returns the first child of the given icon.
*
* @param _current The icon to return the first child of.
* @param current The icon to return the first child of.
* @returns Null.
*/
getFirstChild(_current: Icon): IFocusableNode | null {
getFirstChild(current: Icon): IFocusableNode | null {
if (
current instanceof MutatorIcon &&
current.bubbleIsVisible() &&
getFocusManager().getFocusedNode() === current
) {
return current.getBubble()?.getWorkspace() ?? null;
}
return null;
}

View File

@@ -62,7 +62,7 @@ export class WorkspaceNavigationPolicy
* @returns True if the given workspace can be focused.
*/
isNavigable(current: WorkspaceSvg): boolean {
return current.canBeFocused();
return current.canBeFocused() && !current.isMutator;
}
/**

View File

@@ -64,9 +64,8 @@ export class Navigator {
getFirstChild(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getFirstChild(current);
if (!result) return null;
// If the child isn't navigable, don't traverse into it; check its peers.
if (!this.get(result)?.isNavigable(result)) {
return this.getNextSibling(result);
return this.getFirstChild(result) || this.getNextSibling(result);
}
return result;
}

View File

@@ -41,6 +41,7 @@ import type {FlyoutButton} from './flyout_button.js';
import {getFocusManager} from './focus_manager.js';
import {Gesture} from './gesture.js';
import {Grid} from './grid.js';
import {MutatorIcon} from './icons/mutator_icon.js';
import {isAutoHideable} from './interfaces/i_autohideable.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import {IContextMenu} from './interfaces/i_contextmenu.js';
@@ -2680,7 +2681,7 @@ export class WorkspaceSvg
/** See IFocusableNode.getFocusableTree. */
getFocusableTree(): IFocusableTree {
return this;
return (this.isMutator && this.options.parentWorkspace) || this;
}
/** See IFocusableNode.onNodeFocus. */
@@ -2710,7 +2711,22 @@ export class WorkspaceSvg
/** See IFocusableTree.getNestedTrees. */
getNestedTrees(): Array<IFocusableTree> {
return [];
const nestedWorkspaces = this.getAllBlocks()
.map((block) => block.getIcons())
.flat()
.filter(
(icon): icon is MutatorIcon =>
icon instanceof MutatorIcon && icon.bubbleIsVisible(),
)
.map((icon) => icon.getBubble()?.getWorkspace())
.filter((workspace) => !!workspace);
const ownFlyout = this.getFlyout(true);
if (ownFlyout) {
nestedWorkspaces.push(ownFlyout.getWorkspace());
}
return nestedWorkspaces;
}
/** See IFocusableTree.lookUpFocusableNode. */