Skip to content

Commit 47307a9

Browse files
authored
refactor: Make focusable elements responsible for scrolling themselves into bounds. (#9288)
* refactor: Make focusable elements responsible for scrolling themselves into bounds. * chore: Add tests for scrolling focused elements into view. * fix: Removed inadvertent `.only`. * fix: Scroll parent block of connections into bounds on focus.
1 parent fd0aaed commit 47307a9

File tree

11 files changed

+276
-36
lines changed

11 files changed

+276
-36
lines changed

core/block_svg.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,6 +1844,9 @@ export class BlockSvg
18441844
/** See IFocusableNode.onNodeFocus. */
18451845
onNodeFocus(): void {
18461846
this.select();
1847+
this.workspace.scrollBoundsIntoView(
1848+
this.getBoundingRectangleWithoutChildren(),
1849+
);
18471850
}
18481851

18491852
/** See IFocusableNode.onNodeBlur. */

core/bubbles/bubble.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,10 @@ export abstract class Bubble implements IBubble, ISelectable, IFocusableNode {
707707
onNodeFocus(): void {
708708
this.select();
709709
this.bringToFront();
710+
const xy = this.getRelativeToSurfaceXY();
711+
const size = this.getSize();
712+
const bounds = new Rect(xy.y, xy.y + size.height, xy.x, xy.x + size.width);
713+
this.workspace.scrollBoundsIntoView(bounds);
710714
}
711715

712716
/** See IFocusableNode.onNodeBlur. */

core/comments/comment_bar_button.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ export abstract class CommentBarButton implements IFocusableNode {
8787
}
8888

8989
/** Called when this button's focusable DOM element gains focus. */
90-
onNodeFocus() {}
90+
onNodeFocus() {
91+
const commentView = this.getCommentView();
92+
const xy = commentView.getRelativeToSurfaceXY();
93+
const size = commentView.getSize();
94+
const bounds = new Rect(xy.y, xy.y + size.height, xy.x, xy.x + size.width);
95+
commentView.workspace.scrollBoundsIntoView(bounds);
96+
}
9197

9298
/** Called when this button's focusable DOM element loses focus. */
9399
onNodeBlur() {}

core/comments/comment_editor.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {IFocusableNode} from '../interfaces/i_focusable_node.js';
1010
import {IFocusableTree} from '../interfaces/i_focusable_tree.js';
1111
import * as touch from '../touch.js';
1212
import * as dom from '../utils/dom.js';
13+
import {Rect} from '../utils/rect.js';
1314
import {Size} from '../utils/size.js';
1415
import {Svg} from '../utils/svg.js';
16+
import * as svgMath from '../utils/svg_math.js';
1517
import {WorkspaceSvg} from '../workspace_svg.js';
1618

1719
/**
@@ -188,7 +190,16 @@ export class CommentEditor implements IFocusableNode {
188190
getFocusableTree(): IFocusableTree {
189191
return this.workspace;
190192
}
191-
onNodeFocus(): void {}
193+
onNodeFocus(): void {
194+
const bbox = Rect.from(this.foreignObject.getBoundingClientRect());
195+
this.workspace.scrollBoundsIntoView(
196+
Rect.createFromPoint(
197+
svgMath.screenToWsCoordinates(this.workspace, bbox.getOrigin()),
198+
bbox.getWidth(),
199+
bbox.getHeight(),
200+
),
201+
);
202+
}
192203
onNodeBlur(): void {}
193204
canBeFocused(): boolean {
194205
if (this.id) return true;

core/comments/rendered_workspace_comment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ export class RenderedWorkspaceComment
347347
this.select();
348348
// Ensure that the comment is always at the top when focused.
349349
this.workspace.getLayerManager()?.append(this, layers.BLOCK);
350+
this.workspace.scrollBoundsIntoView(this.getBoundingRectangle());
350351
}
351352

352353
/** See IFocusableNode.onNodeBlur. */

core/field.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1380,7 +1380,12 @@ export abstract class Field<T = any>
13801380
}
13811381

13821382
/** See IFocusableNode.onNodeFocus. */
1383-
onNodeFocus(): void {}
1383+
onNodeFocus(): void {
1384+
const block = this.getSourceBlock() as BlockSvg;
1385+
block.workspace.scrollBoundsIntoView(
1386+
block.getBoundingRectangleWithoutChildren(),
1387+
);
1388+
}
13841389

13851390
/** See IFocusableNode.onNodeBlur. */
13861391
onNodeBlur(): void {}

core/flyout_button.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,11 @@ export class FlyoutButton
398398
}
399399

400400
/** See IFocusableNode.onNodeFocus. */
401-
onNodeFocus(): void {}
401+
onNodeFocus(): void {
402+
const xy = this.getPosition();
403+
const bounds = new Rect(xy.y, xy.y + this.height, xy.x, xy.x + this.width);
404+
this.workspace.scrollBoundsIntoView(bounds);
405+
}
402406

403407
/** See IFocusableNode.onNodeBlur. */
404408
onNodeBlur(): void {}

core/icons/icon.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as tooltip from '../tooltip.js';
1414
import {Coordinate} from '../utils/coordinate.js';
1515
import * as dom from '../utils/dom.js';
1616
import * as idGenerator from '../utils/idgenerator.js';
17+
import {Rect} from '../utils/rect.js';
1718
import {Size} from '../utils/size.js';
1819
import {Svg} from '../utils/svg.js';
1920
import type {WorkspaceSvg} from '../workspace_svg.js';
@@ -168,7 +169,16 @@ export abstract class Icon implements IIcon {
168169
}
169170

170171
/** See IFocusableNode.onNodeFocus. */
171-
onNodeFocus(): void {}
172+
onNodeFocus(): void {
173+
const blockBounds = (this.sourceBlock as BlockSvg).getBoundingRectangle();
174+
const bounds = new Rect(
175+
blockBounds.top + this.offsetInBlock.y,
176+
blockBounds.top + this.offsetInBlock.y + this.getSize().height,
177+
blockBounds.left + this.offsetInBlock.x,
178+
blockBounds.left + this.offsetInBlock.x + this.getSize().width,
179+
);
180+
(this.sourceBlock as BlockSvg).workspace.scrollBoundsIntoView(bounds);
181+
}
172182

173183
/** See IFocusableNode.onNodeBlur. */
174184
onNodeBlur(): void {}

core/keyboard_nav/line_cursor.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@
1414
*/
1515

1616
import {BlockSvg} from '../block_svg.js';
17-
import {CommentBarButton} from '../comments/comment_bar_button.js';
1817
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
19-
import {Field} from '../field.js';
2018
import {getFocusManager} from '../focus_manager.js';
2119
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
2220
import * as registry from '../registry.js';
23-
import {Rect} from '../utils/rect.js';
24-
import {WorkspaceSvg} from '../workspace_svg.js';
21+
import type {WorkspaceSvg} from '../workspace_svg.js';
2522
import {Marker} from './marker.js';
2623

2724
/**
@@ -392,31 +389,6 @@ export class LineCursor extends Marker {
392389
*/
393390
setCurNode(newNode: IFocusableNode) {
394391
getFocusManager().focusNode(newNode);
395-
396-
// Try to scroll cursor into view.
397-
if (newNode instanceof BlockSvg) {
398-
newNode.workspace.scrollBoundsIntoView(
399-
newNode.getBoundingRectangleWithoutChildren(),
400-
);
401-
} else if (newNode instanceof Field) {
402-
const block = newNode.getSourceBlock() as BlockSvg;
403-
block.workspace.scrollBoundsIntoView(
404-
block.getBoundingRectangleWithoutChildren(),
405-
);
406-
} else if (newNode instanceof RenderedWorkspaceComment) {
407-
newNode.workspace.scrollBoundsIntoView(newNode.getBoundingRectangle());
408-
} else if (newNode instanceof CommentBarButton) {
409-
const commentView = newNode.getCommentView();
410-
const xy = commentView.getRelativeToSurfaceXY();
411-
const size = commentView.getSize();
412-
const bounds = new Rect(
413-
xy.y,
414-
xy.y + size.height,
415-
xy.x,
416-
xy.x + size.width,
417-
);
418-
commentView.workspace.scrollBoundsIntoView(bounds);
419-
}
420392
}
421393

422394
/**

core/rendered_connection.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,9 @@ export class RenderedConnection
644644
/** See IFocusableNode.onNodeFocus. */
645645
onNodeFocus(): void {
646646
this.highlight();
647+
this.getSourceBlock().workspace.scrollBoundsIntoView(
648+
this.getSourceBlock().getBoundingRectangleWithoutChildren(),
649+
);
647650
}
648651

649652
/** See IFocusableNode.onNodeBlur. */
@@ -656,12 +659,12 @@ export class RenderedConnection
656659
return true;
657660
}
658661

659-
private findHighlightSvg(): SVGElement | null {
662+
private findHighlightSvg(): SVGPathElement | null {
660663
// This cast is valid as TypeScript's definition is wrong. See:
661664
// https://github.com/microsoft/TypeScript/issues/60996.
662665
return document.getElementById(this.id) as
663666
| unknown
664-
| null as SVGElement | null;
667+
| null as SVGPathElement | null;
665668
}
666669
}
667670

0 commit comments

Comments
 (0)