|
4 | 4 | * SPDX-License-Identifier: Apache-2.0 |
5 | 5 | */ |
6 | 6 |
|
7 | | -import {ISelectable} from '../blockly.js'; |
8 | 7 | import * as browserEvents from '../browser_events.js'; |
9 | 8 | import * as common from '../common.js'; |
10 | 9 | import {BubbleDragStrategy} from '../dragging/bubble_drag_strategy.js'; |
| 10 | +import {getFocusManager} from '../focus_manager.js'; |
11 | 11 | import {IBubble} from '../interfaces/i_bubble.js'; |
| 12 | +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; |
| 13 | +import {ISelectable} from '../interfaces/i_selectable.js'; |
12 | 14 | import {ContainerRegion} from '../metrics_manager.js'; |
13 | 15 | import {Scrollbar} from '../scrollbar.js'; |
14 | 16 | import {Coordinate} from '../utils/coordinate.js'; |
@@ -86,17 +88,24 @@ export abstract class Bubble implements IBubble, ISelectable { |
86 | 88 |
|
87 | 89 | private dragStrategy = new BubbleDragStrategy(this, this.workspace); |
88 | 90 |
|
| 91 | + private focusableElement: SVGElement | HTMLElement; |
| 92 | + |
89 | 93 | /** |
90 | 94 | * @param workspace The workspace this bubble belongs to. |
91 | 95 | * @param anchor The anchor location of the thing this bubble is attached to. |
92 | 96 | * The tail of the bubble will point to this location. |
93 | 97 | * @param ownerRect An optional rect we don't want the bubble to overlap with |
94 | 98 | * when automatically positioning. |
| 99 | + * @param overriddenFocusableElement An optional replacement to the focusable |
| 100 | + * element that's represented by this bubble (as a focusable node). This |
| 101 | + * element will have its ID and tabindex overwritten. If not provided, the |
| 102 | + * focusable element of this node will default to the bubble's SVG root. |
95 | 103 | */ |
96 | 104 | constructor( |
97 | 105 | public readonly workspace: WorkspaceSvg, |
98 | 106 | protected anchor: Coordinate, |
99 | 107 | protected ownerRect?: Rect, |
| 108 | + overriddenFocusableElement?: SVGElement | HTMLElement, |
100 | 109 | ) { |
101 | 110 | this.id = idGenerator.getNextUniqueId(); |
102 | 111 | this.svgRoot = dom.createSvgElement( |
@@ -127,6 +136,10 @@ export abstract class Bubble implements IBubble, ISelectable { |
127 | 136 | ); |
128 | 137 | this.contentContainer = dom.createSvgElement(Svg.G, {}, this.svgRoot); |
129 | 138 |
|
| 139 | + this.focusableElement = overriddenFocusableElement ?? this.svgRoot; |
| 140 | + this.focusableElement.setAttribute('id', this.id); |
| 141 | + this.focusableElement.setAttribute('tabindex', '-1'); |
| 142 | + |
130 | 143 | browserEvents.conditionalBind( |
131 | 144 | this.background, |
132 | 145 | 'pointerdown', |
@@ -208,11 +221,13 @@ export abstract class Bubble implements IBubble, ISelectable { |
208 | 221 | this.background.setAttribute('fill', colour); |
209 | 222 | } |
210 | 223 |
|
211 | | - /** Brings the bubble to the front and passes the pointer event off to the gesture system. */ |
| 224 | + /** |
| 225 | + * Passes the pointer event off to the gesture system and ensures the bubble |
| 226 | + * is focused. |
| 227 | + */ |
212 | 228 | private onMouseDown(e: PointerEvent) { |
213 | 229 | this.workspace.getGesture(e)?.handleBubbleStart(e, this); |
214 | | - this.bringToFront(); |
215 | | - common.setSelected(this); |
| 230 | + getFocusManager().focusNode(this); |
216 | 231 | } |
217 | 232 |
|
218 | 233 | /** Positions the bubble relative to its anchor. Does not render its tail. */ |
@@ -647,9 +662,37 @@ export abstract class Bubble implements IBubble, ISelectable { |
647 | 662 |
|
648 | 663 | select(): void { |
649 | 664 | // Bubbles don't have any visual for being selected. |
| 665 | + common.fireSelectedEvent(this); |
650 | 666 | } |
651 | 667 |
|
652 | 668 | unselect(): void { |
653 | 669 | // Bubbles don't have any visual for being selected. |
| 670 | + common.fireSelectedEvent(null); |
| 671 | + } |
| 672 | + |
| 673 | + /** See IFocusableNode.getFocusableElement. */ |
| 674 | + getFocusableElement(): HTMLElement | SVGElement { |
| 675 | + return this.focusableElement; |
| 676 | + } |
| 677 | + |
| 678 | + /** See IFocusableNode.getFocusableTree. */ |
| 679 | + getFocusableTree(): IFocusableTree { |
| 680 | + return this.workspace; |
| 681 | + } |
| 682 | + |
| 683 | + /** See IFocusableNode.onNodeFocus. */ |
| 684 | + onNodeFocus(): void { |
| 685 | + this.select(); |
| 686 | + this.bringToFront(); |
| 687 | + } |
| 688 | + |
| 689 | + /** See IFocusableNode.onNodeBlur. */ |
| 690 | + onNodeBlur(): void { |
| 691 | + this.unselect(); |
| 692 | + } |
| 693 | + |
| 694 | + /** See IFocusableNode.canBeFocused. */ |
| 695 | + canBeFocused(): boolean { |
| 696 | + return true; |
654 | 697 | } |
655 | 698 | } |
0 commit comments