@@ -9,7 +9,9 @@ import * as common from '../common.js';
99import { BubbleDragStrategy } from '../dragging/bubble_drag_strategy.js' ;
1010import { getFocusManager } from '../focus_manager.js' ;
1111import { IBubble } from '../interfaces/i_bubble.js' ;
12+ import type { IFocusableNode } from '../interfaces/i_focusable_node.js' ;
1213import type { IFocusableTree } from '../interfaces/i_focusable_tree.js' ;
14+ import type { IHasBubble } from '../interfaces/i_has_bubble.js' ;
1315import { ISelectable } from '../interfaces/i_selectable.js' ;
1416import { ContainerRegion } from '../metrics_manager.js' ;
1517import { Scrollbar } from '../scrollbar.js' ;
@@ -27,7 +29,7 @@ import {WorkspaceSvg} from '../workspace_svg.js';
2729 * bubble, where it has a "tail" that points to the block, and a "head" that
2830 * displays arbitrary svg elements.
2931 */
30- export abstract class Bubble implements IBubble , ISelectable {
32+ export abstract class Bubble implements IBubble , ISelectable , IFocusableNode {
3133 /** The width of the border around the bubble. */
3234 static readonly BORDER_WIDTH = 6 ;
3335
@@ -100,12 +102,14 @@ export abstract class Bubble implements IBubble, ISelectable {
100102 * element that's represented by this bubble (as a focusable node). This
101103 * element will have its ID overwritten. If not provided, the focusable
102104 * element of this node will default to the bubble's SVG root.
105+ * @param owner The object responsible for hosting/spawning this bubble.
103106 */
104107 constructor (
105108 public readonly workspace : WorkspaceSvg ,
106109 protected anchor : Coordinate ,
107110 protected ownerRect ?: Rect ,
108111 overriddenFocusableElement ?: SVGElement | HTMLElement ,
112+ protected owner ?: IHasBubble & IFocusableNode ,
109113 ) {
110114 this . id = idGenerator . getNextUniqueId ( ) ;
111115 this . svgRoot = dom . createSvgElement (
@@ -145,6 +149,13 @@ export abstract class Bubble implements IBubble, ISelectable {
145149 this ,
146150 this . onMouseDown ,
147151 ) ;
152+
153+ browserEvents . conditionalBind (
154+ this . focusableElement ,
155+ 'keydown' ,
156+ this ,
157+ this . onKeyDown ,
158+ ) ;
148159 }
149160
150161 /** Dispose of this bubble. */
@@ -229,6 +240,19 @@ export abstract class Bubble implements IBubble, ISelectable {
229240 getFocusManager ( ) . focusNode ( this ) ;
230241 }
231242
243+ /**
244+ * Handles key events when this bubble is focused. By default, closes the
245+ * bubble on Escape.
246+ *
247+ * @param e The keyboard event to handle.
248+ */
249+ protected onKeyDown ( e : KeyboardEvent ) {
250+ if ( e . key === 'Escape' && this . owner ) {
251+ this . owner . setBubbleVisible ( false ) ;
252+ getFocusManager ( ) . focusNode ( this . owner ) ;
253+ }
254+ }
255+
232256 /** Positions the bubble relative to its anchor. Does not render its tail. */
233257 protected positionRelativeToAnchor ( ) {
234258 let left = this . anchor . x ;
@@ -683,6 +707,10 @@ export abstract class Bubble implements IBubble, ISelectable {
683707 onNodeFocus ( ) : void {
684708 this . select ( ) ;
685709 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 ) ;
686714 }
687715
688716 /** See IFocusableNode.onNodeBlur. */
@@ -694,4 +722,11 @@ export abstract class Bubble implements IBubble, ISelectable {
694722 canBeFocused ( ) : boolean {
695723 return true ;
696724 }
725+
726+ /**
727+ * Returns the object that owns/hosts this bubble, if any.
728+ */
729+ getOwner ( ) : ( IHasBubble & IFocusableNode ) | undefined {
730+ return this . owner ;
731+ }
697732}
0 commit comments