Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0ed9db2
feat!: initial integration of FocusableTree/Node.
BenHenning Apr 9, 2025
45b1b50
feat: bunch more focus fixes.
BenHenning Apr 11, 2025
5f9de82
feat: LOTS of functionality changes.
BenHenning Apr 18, 2025
9acf144
chore: finalize focus infra changes pt1.
BenHenning Apr 21, 2025
d5b9741
chore: finalize focus infra changes pt2.
BenHenning Apr 21, 2025
931b424
Merge branch 'add-focus-manager-callbacks-and-improvements' into test…
BenHenning Apr 21, 2025
a9a4f1b
Merge branch 'add-focus-manager-callbacks-and-improvements' into test…
BenHenning Apr 21, 2025
e9a30c0
chore: another attempt to fix CSS inheritance.
BenHenning Apr 21, 2025
41c8215
feat: Make WorkspaceSvg focusable.
BenHenning Apr 21, 2025
14b486e
chore: remove accidental 'test.only'.
BenHenning Apr 21, 2025
26cf8db
feat: Make Toolbox & Flyout focusable.
BenHenning Apr 22, 2025
5ef2d7e
Merge branch 'add-focus-manager-callbacks-and-improvements' into make…
BenHenning Apr 22, 2025
996208d
Merge branch 'make-workspace-focusable' into make-toolbox-and-flyout-…
BenHenning Apr 22, 2025
f6e0dc7
Merge branch 'add-focus-manager-callbacks-and-improvements' into test…
BenHenning Apr 22, 2025
b424902
Merge branch 'make-workspace-focusable' into test-prototype-focus-man…
BenHenning Apr 22, 2025
dca3a81
Merge branch 'make-toolbox-and-flyout-focusable' into test-prototype-…
BenHenning Apr 22, 2025
82bbab6
Fix things & add ARIA labels for demo.
BenHenning Apr 22, 2025
d3acbff
feat!: Force lifecycle methods for fields.
BenHenning Apr 22, 2025
ed0f140
feat: Make fields ephemerally focusable.
BenHenning Apr 22, 2025
94672d9
chore: Lint fixes.
BenHenning Apr 22, 2025
2430646
chore: Remove incorrect aria-label.
BenHenning Apr 22, 2025
41bc01a
feat: Make RenderedConnection focusable.
BenHenning Apr 23, 2025
4479b82
Merge branch 'add-focus-manager-callbacks-and-improvements' into make…
BenHenning Apr 23, 2025
2637736
fix: Ensure Block paths are focusable.
BenHenning Apr 23, 2025
49192ba
chore: Fix line comment.
BenHenning Apr 23, 2025
917c4b6
Merge branch 'make-workspace-focusable' into make-toolbox-and-flyout-…
BenHenning Apr 23, 2025
d276dbc
chore: reduce branching.
BenHenning Apr 24, 2025
c819130
Merge branch 'make-toolbox-and-flyout-focusable' into make-fields-foc…
BenHenning Apr 24, 2025
90fdde2
feat: make drop down & widget divs focusable.
BenHenning Apr 24, 2025
7c2f705
chore: undo breaking field changes.
BenHenning Apr 24, 2025
9726389
chore: some more clean-ups after removals.
BenHenning Apr 24, 2025
1094787
feat: fix field node retrieval.
BenHenning Apr 24, 2025
082a6ef
chore: lint fixes.
BenHenning Apr 24, 2025
0006a45
Merge branch 'make-fields-focusable' into make-connections-focusable
BenHenning Apr 24, 2025
7f14372
feat: add remaining connection support
BenHenning Apr 24, 2025
59198db
chore: lint fixes.
BenHenning Apr 24, 2025
93603d3
Merge branch 'make-connections-focusable' into test-prototype-focus-m…
BenHenning Apr 24, 2025
8df19a7
chore: lint fixes for better diffing.
BenHenning Apr 24, 2025
129e417
chore: remove unnecessary field ephemeral focus.
BenHenning Apr 24, 2025
a346a92
fix: remove unnecessary shadow check.
BenHenning Apr 24, 2025
4ed61bf
Merge branch 'make-workspace-focusable' into make-toolbox-and-flyout-…
BenHenning Apr 24, 2025
898c5a4
Merge branch 'make-toolbox-and-flyout-focusable' into make-fields-foc…
BenHenning Apr 24, 2025
fdf4b4f
Merge branch 'make-fields-focusable' into make-connections-focusable
BenHenning Apr 24, 2025
26a54df
Merge branch 'make-connections-focusable' into test-prototype-focus-m…
BenHenning Apr 24, 2025
80c8859
chore: add braces.
BenHenning Apr 24, 2025
b3bd5e7
Merge branch 'rc/v12.0.0' into make-workspace-focusable
BenHenning Apr 24, 2025
1f0cefc
Merge branch 'make-workspace-focusable' into make-toolbox-and-flyout-…
BenHenning Apr 24, 2025
c2384c6
chore: empty commit to make CI pass.
BenHenning Apr 24, 2025
57391a7
Merge branch 'rc/v12.0.0' into make-toolbox-and-flyout-focusable
BenHenning Apr 24, 2025
8057051
Merge branch 'make-toolbox-and-flyout-focusable' into make-fields-foc…
BenHenning Apr 24, 2025
ad96cad
Merge branch 'make-fields-focusable' into make-connections-focusable
BenHenning Apr 24, 2025
78941e7
Merge branch 'make-connections-focusable' into test-prototype-focus-m…
BenHenning Apr 24, 2025
23c1b8f
fix: CSS issues.
BenHenning Apr 24, 2025
28d5057
fix: ensure click & focus sync.
BenHenning Apr 25, 2025
3e6a995
chore: remove a bunch of unused code.
BenHenning Apr 28, 2025
8251654
Merge branch 'rc/v12.0.0' into test-prototype-focus-manager-incorpora…
BenHenning May 15, 2025
96f033e
Merge branch 'develop' into test-prototype-focus-manager-incorporation
BenHenning May 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {ICopyable} from './interfaces/i_copyable.js';
import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import {IIcon} from './interfaces/i_icon.js';
import * as internalConstants from './internal_constants.js';
import {MarkerManager} from './marker_manager.js';
Expand Down Expand Up @@ -74,7 +76,8 @@ export class BlockSvg
IBoundedElement,
ICopyable<BlockCopyData>,
IDraggable,
IDeletable
IDeletable,
IFocusableNode
{
/**
* Constant for identifying rows that are to be rendered inline.
Expand Down Expand Up @@ -208,6 +211,7 @@ export class BlockSvg

// Expose this block's ID on its top-level SVG group.
this.svgGroup.setAttribute('data-id', this.id);
svgPath.id = this.id;

this.doInit_();
}
Expand Down Expand Up @@ -1767,4 +1771,24 @@ export class BlockSvg
);
}
}

getFocusableElement(): HTMLElement | SVGElement {
return this.pathObject.svgPath;
}

getFocusableTree(): IFocusableTree {
return this.workspace;
}

onNodeFocus(): void {
if (!this.isShadow()) {
common.setSelected(this);
}
}

onNodeBlur(): void {
if (common.getSelected() === this) {
common.setSelected(null);
}
}
}
6 changes: 4 additions & 2 deletions core/blockly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ import {
} from './interfaces/i_draggable.js';
import {IDragger} from './interfaces/i_dragger.js';
import {IFlyout} from './interfaces/i_flyout.js';
import {IFocusableNode} from './interfaces/i_focusable_node.js';
import {IFocusableTree} from './interfaces/i_focusable_tree.js';
import {IFocusableNode, isFocusableNode} from './interfaces/i_focusable_node.js';
import {IFocusableTree, isFocusableTree} from './interfaces/i_focusable_tree.js';
import {IHasBubble, hasBubble} from './interfaces/i_has_bubble.js';
import {IIcon, isIcon} from './interfaces/i_icon.js';
import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
Expand Down Expand Up @@ -619,6 +619,8 @@ export {
isCopyable,
isDeletable,
isDraggable,
isFocusableNode,
isFocusableTree,
isIcon,
isObservable,
isPaster,
Expand Down
4 changes: 4 additions & 0 deletions core/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {Input} from './inputs/input.js';
import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js';
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
import * as blocks from './serialization/blocks.js';
import { idGenerator } from './utils.js';
import * as Xml from './xml.js';

/**
Expand Down Expand Up @@ -55,6 +56,8 @@ export class Connection implements IASTNodeLocationWithBlock {
/** DOM representation of a shadow block, or null if none. */
private shadowDom: Element | null = null;

id: string;

/**
* Horizontal location of this connection.
*
Expand All @@ -80,6 +83,7 @@ export class Connection implements IASTNodeLocationWithBlock {
public type: number,
) {
this.sourceBlock_ = source;
this.id = `${source.id}_connection_${idGenerator.getNextUniqueId()}`;
}

/**
Expand Down
24 changes: 16 additions & 8 deletions core/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ let content = `

.blocklyHighlightedConnectionPath {
fill: none;
stroke: #fc3;
stroke-width: 4px;
// stroke: #fc3;
// stroke-width: 4px;
}

.blocklyPathLight {
Expand All @@ -160,6 +160,11 @@ let content = `
stroke-width: 1;
}

.blocklySelected {
stroke: #ffa200;
stroke-width: 5;
}

.blocklySelected>.blocklyPathLight {
display: none;
}
Expand Down Expand Up @@ -464,8 +469,8 @@ input[type=number] {
}

.blocklyMenuSeparator {
background-color: #ccc;
height: 1px;
background-color: #ccc;
height: 1px;
border: 0;
margin-left: 4px;
margin-right: 4px;
Expand Down Expand Up @@ -496,11 +501,14 @@ input[type=number] {
}

.blocklyActiveFocus {
outline-color: #2ae;
outline-width: 2px;
stroke: #ffa200;
stroke-width: 3;
outline-color: #ffa200;
}
.blocklyPassiveFocus {
outline-color: #3fdfff;
outline-width: 1.5px;
stroke: #ffa200;
stroke-dasharray: 5 3;
stroke-width: 3;
outline-color: #ffa200;
}
`;
37 changes: 34 additions & 3 deletions core/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import * as eventUtils from './events/utils.js';
import type {Input} from './inputs/input.js';
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
import type {IRegistrable} from './interfaces/i_registrable.js';
import {ISerializable} from './interfaces/i_serializable.js';
Expand All @@ -42,7 +44,8 @@ import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
import * as utilsXml from './utils/xml.js';
import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import {WorkspaceSvg} from './workspace_svg.js';
import * as idGenerator from './utils/idgenerator.js';

/**
* A function that is called to validate changes to the field's value before
Expand Down Expand Up @@ -72,7 +75,8 @@ export abstract class Field<T = any>
IASTNodeLocationWithBlock,
IKeyboardAccessible,
IRegistrable,
ISerializable
ISerializable,
IFocusableNode
{
/**
* To overwrite the default value which is set in **Field**, directly update
Expand Down Expand Up @@ -191,6 +195,8 @@ export abstract class Field<T = any>
*/
SERIALIZABLE = false;

private id_: string | null = null;

/**
* @param value The initial value of the field.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
Expand Down Expand Up @@ -255,6 +261,7 @@ export abstract class Field<T = any>
throw Error('Field already bound to a block');
}
this.sourceBlock_ = block;
this.id_ = `${block.id}_field_${idGenerator.getNextUniqueId()}`;
}

/**
Expand Down Expand Up @@ -298,7 +305,12 @@ export abstract class Field<T = any>
// Field has already been initialized once.
return;
}
this.fieldGroup_ = dom.createSvgElement(Svg.G, {});
const id = this.id_;
if (!id) throw new Error('Expected ID to be defined prior to init.');
this.fieldGroup_ = dom.createSvgElement(Svg.G, {
'tabindex': '-1',
'id': id
});
if (!this.isVisible()) {
this.fieldGroup_.style.display = 'none';
}
Expand Down Expand Up @@ -1401,6 +1413,25 @@ export abstract class Field<T = any>
}
}

getFocusableElement(): HTMLElement | SVGElement {
if (!this.fieldGroup_) {
throw Error("This field currently has no representative DOM element.");
}
return this.fieldGroup_;
}

getFocusableTree(): IFocusableTree {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
return block.workspace as WorkspaceSvg;
}

onNodeFocus(): void {}

onNodeBlur(): void {}

/**
* Subclasses should reimplement this method to construct their Field
* subclass from a JSON arg object.
Expand Down
9 changes: 8 additions & 1 deletion core/field_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {Size} from './utils/size.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import { getFocusManager } from './focus_manager.js';

/**
* Supported types for FieldInput subclasses.
Expand Down Expand Up @@ -351,6 +352,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
* keyboards).
*/
private showPromptEditor() {
const returnFocusCallback = getFocusManager().takeEphemeralFocus(document.body);
dialog.prompt(
Msg['CHANGE_VALUE_TITLE'],
this.getText(),
Expand All @@ -360,6 +362,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
this.setValue(this.getValueFromEditorText_(text));
}
this.onFinishEditing_(this.value_);
returnFocusCallback();
},
);
}
Expand All @@ -374,10 +377,14 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
if (!block) {
throw new UnattachedFieldError();
}
const returnFocusCallback = getFocusManager().takeEphemeralFocus(document.body);
WidgetDiv.show(
this,
block.RTL,
this.widgetDispose_.bind(this),
() => {
this.widgetDispose_();
returnFocusCallback();
},
this.workspace_,
);
this.htmlInput_ = this.widgetCreate_() as HTMLInputElement;
Expand Down
69 changes: 68 additions & 1 deletion core/flyout_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// Former goog.module ID: Blockly.Flyout

import {BlockSvg} from './block_svg.js';
import { IFocusableNode, isFocusableNode } from './interfaces/i_focusable_node.js';
import { IFocusableTree } from './interfaces/i_focusable_tree.js';
import * as browserEvents from './browser_events.js';
import {ComponentManager} from './component_manager.js';
import {DeleteArea} from './delete_area.js';
Expand All @@ -32,18 +34,20 @@ import {SEPARATOR_TYPE} from './separator_flyout_inflater.js';
import * as blocks from './serialization/blocks.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import { FocusableTreeTraverser } from './utils/focusable_tree_traverser.js';
import * as idGenerator from './utils/idgenerator.js';
import {Svg} from './utils/svg.js';
import * as toolbox from './utils/toolbox.js';
import * as Variables from './variables.js';
import {WorkspaceSvg} from './workspace_svg.js';
import { getFocusManager } from './focus_manager.js';

/**
* Class for a flyout.
*/
export abstract class Flyout
extends DeleteArea
implements IAutoHideable, IFlyout
implements IAutoHideable, IFlyout, IFocusableNode
{
/**
* Position the flyout.
Expand Down Expand Up @@ -303,6 +307,7 @@ export abstract class Flyout
// hide/show code will set up proper visibility and size later.
this.svgGroup_ = dom.createSvgElement(tagName, {
'class': 'blocklyFlyout',
'tabindex': '0',
});
this.svgGroup_.style.display = 'none';
this.svgBackground_ = dom.createSvgElement(
Expand All @@ -317,6 +322,9 @@ export abstract class Flyout
this.workspace_
.getThemeManager()
.subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity');

getFocusManager().registerTree(this);

return this.svgGroup_;
}

Expand Down Expand Up @@ -398,6 +406,7 @@ export abstract class Flyout
if (this.svgGroup_) {
dom.removeNode(this.svgGroup_);
}
getFocusManager().unregisterTree(this);
}

/**
Expand Down Expand Up @@ -961,4 +970,62 @@ export abstract class Flyout

return null;
}

getFocusableElement(): HTMLElement | SVGElement {
if (!this.svgGroup_) throw new Error('Flyout DOM is not yet created.');
return this.svgGroup_;
// return this.workspace_.getFocusableElement();
}

getFocusableTree(): IFocusableTree {
return this;
// return this.workspace_.getFocusableTree();
}

onNodeFocus(): void {
// this.workspace_.onNodeFocus();
}

onNodeBlur(): void {
// this.workspace_.onNodeBlur();
}

getRootFocusableNode(): IFocusableNode {
return this;
// return this.workspace_.getRootFocusableNode();
}

getRestoredFocusableNode(previousNode: IFocusableNode | null): IFocusableNode | null {
return null;
}

getNestedTrees(): Array<IFocusableTree> {
return [this.workspace_];
// return this.workspace_.getNestedTrees();
}

onTreeFocus(node: IFocusableNode, previousTree: IFocusableTree | null): void {
// this.workspace_.onTreeFocus(node);
}

onTreeBlur(nextTree: IFocusableTree | null): void {
const toolbox = this.targetWorkspace.getToolbox();
// If focus is moving to either the toolbox or the flyout's workspace, do
// not close the flyout. For anything else, do close it.
if (toolbox && nextTree === toolbox) return;
if (nextTree == this.workspace_) return;
if (toolbox) toolbox.clearSelection();
this.autoHide(false);
// this.workspace_.onTreeBlur();
}

lookUpFocusableNode(id: string): IFocusableNode | null {
return null;
// return this.workspace_.lookUpFocusableNode(id);
// TODO: This may violate the cross-subtree boundary (since flyout contains a workspace that is itself a tree).
// return this.getContents()
// .filter((item) => isFocusableNode(item))
// .map((item) => item as unknown as IFocusableNode)
// .find((node) => node.getFocusableElement().id == id) ?? null;
}
}
Loading