Skip to content

Commit acdad98

Browse files
authored
refactor!: Use navigation rulesets instead of ASTNode to control keyboard navigation. (#8992)
* feat: Add interfaces for keyboard navigation. * feat: Add the Navigator. * feat: Make core types conform to INavigable. * feat: Require FlyoutItems elements to be INavigable. * feat: Add navigation policies for built-in types. * refactor: Convert Marker and LineCursor to operate on INavigables instead of ASTNodes. * chore: Delete dead code in ASTNode. * fix: Fix the tests. * chore: Assuage the linter. * fix: Fix advanced build/tests. * chore: Restore ASTNode tests. * refactor: Move isNavigable() validation into Navigator. * refactor: Exercise navigation instead of ASTNode. * chore: Rename astnode_test.js to navigation_test.js. * chore: Enable the navigation tests. * fix: Fix bug when retrieving the first child of an empty workspace.
1 parent a3b3ea7 commit acdad98

37 files changed

+1941
-1708
lines changed

core/block_flyout_inflater.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater {
7070
block.getDescendants(false).forEach((b) => (b.isInFlyout = true));
7171
this.addBlockListeners(block);
7272

73-
return new FlyoutItem(block, BLOCK_TYPE, true);
73+
return new FlyoutItem(block, BLOCK_TYPE);
7474
}
7575

7676
/**

core/block_svg.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js';
4848
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
4949
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
5050
import {IIcon} from './interfaces/i_icon.js';
51+
import type {INavigable} from './interfaces/i_navigable.js';
5152
import * as internalConstants from './internal_constants.js';
5253
import {MarkerManager} from './marker_manager.js';
5354
import {Msg} from './msg.js';
@@ -80,7 +81,8 @@ export class BlockSvg
8081
ICopyable<BlockCopyData>,
8182
IDraggable,
8283
IDeletable,
83-
IFocusableNode
84+
IFocusableNode,
85+
INavigable<BlockSvg>
8486
{
8587
/**
8688
* Constant for identifying rows that are to be rendered inline.
@@ -1886,4 +1888,25 @@ export class BlockSvg
18861888
common.setSelected(null);
18871889
}
18881890
}
1891+
1892+
/**
1893+
* Returns whether or not this block can be navigated to via the keyboard.
1894+
*
1895+
* @returns True if this block is keyboard navigable, otherwise false.
1896+
*/
1897+
isNavigable() {
1898+
return true;
1899+
}
1900+
1901+
/**
1902+
* Returns this block's class.
1903+
*
1904+
* Used by keyboard navigation to look up the rules for navigating from this
1905+
* block.
1906+
*
1907+
* @returns This block's class.
1908+
*/
1909+
getClass() {
1910+
return BlockSvg;
1911+
}
18891912
}

core/blockly.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,16 @@ Names.prototype.populateProcedures = function (
433433
};
434434
// clang-format on
435435

436+
export * from './interfaces/i_navigable.js';
437+
export * from './interfaces/i_navigation_policy.js';
438+
export * from './keyboard_nav/block_navigation_policy.js';
439+
export * from './keyboard_nav/connection_navigation_policy.js';
440+
export * from './keyboard_nav/field_navigation_policy.js';
441+
export * from './keyboard_nav/flyout_button_navigation_policy.js';
442+
export * from './keyboard_nav/flyout_navigation_policy.js';
443+
export * from './keyboard_nav/flyout_separator_navigation_policy.js';
444+
export * from './keyboard_nav/workspace_navigation_policy.js';
445+
export * from './navigator.js';
436446
export * from './toast.js';
437447

438448
// Re-export submodules that no longer declareLegacyNamespace.

core/button_flyout_inflater.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class ButtonFlyoutInflater implements IFlyoutInflater {
3333
);
3434
button.show();
3535

36-
return new FlyoutItem(button, BUTTON_TYPE, true);
36+
return new FlyoutItem(button, BUTTON_TYPE);
3737
}
3838

3939
/**

core/field.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_w
2828
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
2929
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
3030
import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
31+
import type {INavigable} from './interfaces/i_navigable.js';
3132
import type {IRegistrable} from './interfaces/i_registrable.js';
3233
import {ISerializable} from './interfaces/i_serializable.js';
3334
import {MarkerManager} from './marker_manager.js';
@@ -76,7 +77,8 @@ export abstract class Field<T = any>
7677
IKeyboardAccessible,
7778
IRegistrable,
7879
ISerializable,
79-
IFocusableNode
80+
IFocusableNode,
81+
INavigable<Field<T>>
8082
{
8183
/**
8284
* To overwrite the default value which is set in **Field**, directly update
@@ -1452,6 +1454,30 @@ export abstract class Field<T = any>
14521454
`Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`,
14531455
);
14541456
}
1457+
1458+
/**
1459+
* Returns whether or not this field is accessible by keyboard navigation.
1460+
*
1461+
* @returns True if this field is keyboard accessible, otherwise false.
1462+
*/
1463+
isNavigable() {
1464+
return (
1465+
this.isClickable() &&
1466+
this.isCurrentlyEditable() &&
1467+
!(this.getSourceBlock()?.isSimpleReporter() && this.isFullBlockField()) &&
1468+
this.getParentInput().isVisible()
1469+
);
1470+
}
1471+
1472+
/**
1473+
* Returns this field's class.
1474+
*
1475+
* Used by keyboard navigation to look up the rules for navigating from this
1476+
* field. Must be implemented by subclasses.
1477+
*
1478+
* @returns This field's class.
1479+
*/
1480+
abstract getClass(): new (...args: any) => Field<T>;
14551481
}
14561482

14571483
/**

core/field_checkbox.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,18 @@ export class FieldCheckbox extends Field<CheckboxBool> {
228228
// 'override' the static fromJson method.
229229
return new this(options.checked, undefined, options);
230230
}
231+
232+
/**
233+
* Returns this field's class.
234+
*
235+
* Used by keyboard navigation to look up the rules for navigating from this
236+
* field.
237+
*
238+
* @returns This field's class.
239+
*/
240+
getClass() {
241+
return FieldCheckbox;
242+
}
231243
}
232244

233245
fieldRegistry.register('field_checkbox', FieldCheckbox);

core/field_dropdown.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,18 @@ export class FieldDropdown extends Field<string> {
796796
throw TypeError('Found invalid FieldDropdown options.');
797797
}
798798
}
799+
800+
/**
801+
* Returns this field's class.
802+
*
803+
* Used by keyboard navigation to look up the rules for navigating from this
804+
* field.
805+
*
806+
* @returns This field's class.
807+
*/
808+
getClass() {
809+
return FieldDropdown;
810+
}
799811
}
800812

801813
/**

core/field_image.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,18 @@ export class FieldImage extends Field<string> {
273273
options,
274274
);
275275
}
276+
277+
/**
278+
* Returns this field's class.
279+
*
280+
* Used by keyboard navigation to look up the rules for navigating from this
281+
* field.
282+
*
283+
* @returns This field's class.
284+
*/
285+
getClass() {
286+
return FieldImage;
287+
}
276288
}
277289

278290
fieldRegistry.register('field_image', FieldImage);

core/field_label.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,18 @@ export class FieldLabel extends Field<string> {
126126
// the static fromJson method.
127127
return new this(text, undefined, options);
128128
}
129+
130+
/**
131+
* Returns this field's class.
132+
*
133+
* Used by keyboard navigation to look up the rules for navigating from this
134+
* field.
135+
*
136+
* @returns This field's class.
137+
*/
138+
getClass() {
139+
return FieldLabel;
140+
}
129141
}
130142

131143
fieldRegistry.register('field_label', FieldLabel);

core/field_number.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,18 @@ export class FieldNumber extends FieldInput<number> {
341341
options,
342342
);
343343
}
344+
345+
/**
346+
* Returns this field's class.
347+
*
348+
* Used by keyboard navigation to look up the rules for navigating from this
349+
* field.
350+
*
351+
* @returns This field's class.
352+
*/
353+
getClass() {
354+
return FieldNumber;
355+
}
344356
}
345357

346358
fieldRegistry.register('field_number', FieldNumber);

0 commit comments

Comments
 (0)