refactor: Make INavigable extend IFocusableNode. (#9033)

This commit is contained in:
Aaron Dodson
2025-05-12 15:46:27 -07:00
committed by GitHub
parent 77bfa5b572
commit a1be83bad8
16 changed files with 135 additions and 72 deletions

View File

@@ -1824,15 +1824,6 @@ export class BlockSvg
return true;
}
/**
* Returns whether or not this block can be navigated to via the keyboard.
*
* @returns True if this block is keyboard navigable, otherwise false.
*/
isNavigable() {
return true;
}
/**
* Returns this block's class.
*

View File

@@ -1385,20 +1385,6 @@ export abstract class Field<T = any>
);
}
/**
* Returns whether or not this field is accessible by keyboard navigation.
*
* @returns True if this field is keyboard accessible, otherwise false.
*/
isNavigable() {
return (
this.isClickable() &&
this.isCurrentlyEditable() &&
!(this.getSourceBlock()?.isSimpleReporter() && this.isFullBlockField()) &&
this.getParentInput().isVisible()
);
}
/**
* Returns this field's class.
*

View File

@@ -413,16 +413,6 @@ export class FlyoutButton
return true;
}
/**
* Returns whether or not this button is accessible through keyboard
* navigation.
*
* @returns True if this button is keyboard accessible, otherwise false.
*/
isNavigable() {
return true;
}
/**
* Returns this button's class.
*

View File

@@ -5,6 +5,8 @@
*/
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import type {INavigable} from './interfaces/i_navigable.js';
import {Rect} from './utils/rect.js';
@@ -12,7 +14,7 @@ import {Rect} from './utils/rect.js';
* Representation of a gap between elements in a flyout.
*/
export class FlyoutSeparator
implements IBoundedElement, INavigable<FlyoutSeparator>
implements IBoundedElement, INavigable<FlyoutSeparator>, IFocusableNode
{
private x = 0;
private y = 0;
@@ -75,6 +77,27 @@ export class FlyoutSeparator
getClass() {
return FlyoutSeparator;
}
/** See IFocusableNode.getFocusableElement. */
getFocusableElement(): HTMLElement | SVGElement {
throw new Error('Cannot be focused');
}
/** See IFocusableNode.getFocusableTree. */
getFocusableTree(): IFocusableTree {
throw new Error('Cannot be focused');
}
/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {}
/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {}
/** See IFocusableNode.canBeFocused. */
canBeFocused(): boolean {
return false;
}
}
/**

View File

@@ -4,24 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {IFocusableNode} from './i_focusable_node.js';
/**
* Represents a UI element which can be navigated to using the keyboard.
*/
export interface INavigable<T> {
/**
* Returns whether or not this specific instance should be reachable via
* keyboard navigation.
*
* Implementors should generally return true, unless there are circumstances
* under which this item should be skipped while using keyboard navigation.
* Common examples might include being disabled, invalid, readonly, or purely
* a visual decoration. For example, while Fields are navigable, non-editable
* fields return false, since they cannot be interacted with when focused.
*
* @returns True if this element should be included in keyboard navigation.
*/
isNavigable(): boolean;
export interface INavigable<T> extends IFocusableNode {
/**
* Returns the class of this instance.
*

View File

@@ -43,4 +43,18 @@ export interface INavigationPolicy<T> {
* there is none.
*/
getPreviousSibling(current: T): INavigable<any> | null;
/**
* Returns whether or not the given instance should be reachable via keyboard
* navigation.
*
* Implementors should generally return true, unless there are circumstances
* under which this item should be skipped while using keyboard navigation.
* Common examples might include being disabled, invalid, readonly, or purely
* a visual decoration. For example, while Fields are navigable, non-editable
* fields return false, since they cannot be interacted with when focused.
*
* @returns True if this element should be included in keyboard navigation.
*/
isNavigable(current: T): boolean;
}

View File

@@ -114,4 +114,14 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
}
return block.outputConnection;
}
/**
* Returns whether or not the given block can be navigated to.
*
* @param current The instance to check for navigability.
* @returns True if the given block can be focused.
*/
isNavigable(current: BlockSvg): boolean {
return current.canBeFocused();
}
}

View File

@@ -166,4 +166,14 @@ export class ConnectionNavigationPolicy
}
return block.outputConnection;
}
/**
* Returns whether or not the given connection can be navigated to.
*
* @param current The instance to check for navigability.
* @returns True if the given connection can be focused.
*/
isNavigable(current: RenderedConnection): boolean {
return current.canBeFocused();
}
}

View File

@@ -88,4 +88,23 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
}
return null;
}
/**
* Returns whether or not the given field can be navigated to.
*
* @param current The instance to check for navigability.
* @returns True if the given field can be focused and navigated to.
*/
isNavigable(current: Field<any>): boolean {
return (
current.canBeFocused() &&
current.isClickable() &&
current.isCurrentlyEditable() &&
!(
current.getSourceBlock()?.isSimpleReporter() &&
current.isFullBlockField()
) &&
current.getParentInput().isVisible()
);
}
}

View File

@@ -53,4 +53,14 @@ export class FlyoutButtonNavigationPolicy
getPreviousSibling(_current: FlyoutButton): INavigable<unknown> | null {
return null;
}
/**
* Returns whether or not the given flyout button can be navigated to.
*
* @param current The instance to check for navigability.
* @returns True if the given flyout button can be focused.
*/
isNavigable(current: FlyoutButton): boolean {
return current.canBeFocused();
}
}

View File

@@ -88,4 +88,14 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
return flyoutContents[index].getElement();
}
/**
* Returns whether or not the given flyout item can be navigated to.
*
* @param current The instance to check for navigability.
* @returns True if the given flyout item can be focused.
*/
isNavigable(current: T): boolean {
return this.policy.isNavigable(current);
}
}

View File

@@ -30,4 +30,14 @@ export class FlyoutSeparatorNavigationPolicy
getPreviousSibling(_current: FlyoutSeparator): INavigable<unknown> | null {
return null;
}
/**
* Returns whether or not the given flyout separator can be navigated to.
*
* @param _current The instance to check for navigability.
* @returns False.
*/
isNavigable(_current: FlyoutSeparator): boolean {
return false;
}
}

View File

@@ -63,4 +63,14 @@ export class WorkspaceNavigationPolicy
getPreviousSibling(_current: WorkspaceSvg): INavigable<unknown> | null {
return null;
}
/**
* Returns whether or not the given workspace can be navigated to.
*
* @param current The instance to check for navigability.
* @returns True if the given workspace can be focused.
*/
isNavigable(current: WorkspaceSvg): boolean {
return current.canBeFocused();
}
}

View File

@@ -59,7 +59,9 @@ export class Navigator {
const result = this.get(current)?.getFirstChild(current);
if (!result) return null;
// If the child isn't navigable, don't traverse into it; check its peers.
if (!result.isNavigable()) return this.getNextSibling(result);
if (!this.get(result)?.isNavigable(result)) {
return this.getNextSibling(result);
}
return result;
}
@@ -72,7 +74,7 @@ export class Navigator {
getParent<T extends INavigable<T>>(current: T): INavigable<any> | null {
const result = this.get(current)?.getParent(current);
if (!result) return null;
if (!result.isNavigable()) return this.getParent(result);
if (!this.get(result)?.isNavigable(result)) return this.getParent(result);
return result;
}
@@ -85,7 +87,9 @@ export class Navigator {
getNextSibling<T extends INavigable<T>>(current: T): INavigable<any> | null {
const result = this.get(current)?.getNextSibling(current);
if (!result) return null;
if (!result.isNavigable()) return this.getNextSibling(result);
if (!this.get(result)?.isNavigable(result)) {
return this.getNextSibling(result);
}
return result;
}
@@ -100,7 +104,9 @@ export class Navigator {
): INavigable<any> | null {
const result = this.get(current)?.getPreviousSibling(current);
if (!result) return null;
if (!result.isNavigable()) return this.getPreviousSibling(result);
if (!this.get(result)?.isNavigable(result)) {
return this.getPreviousSibling(result);
}
return result;
}
}

View File

@@ -665,15 +665,6 @@ export class RenderedConnection
| null as SVGElement | null;
}
/**
* Returns whether or not this connection is keyboard-navigable.
*
* @returns True.
*/
isNavigable() {
return true;
}
/**
* Returns this connection's class for keyboard navigation.
*

View File

@@ -2728,7 +2728,11 @@ export class WorkspaceSvg
if (this.isFlyout && flyout) {
for (const flyoutItem of flyout.getContents()) {
const elem = flyoutItem.getElement();
if (isFocusableNode(elem) && elem.getFocusableElement().id === id) {
if (
isFocusableNode(elem) &&
elem.canBeFocused() &&
elem.getFocusableElement().id === id
) {
return elem;
}
}
@@ -2817,15 +2821,6 @@ export class WorkspaceSvg
return WorkspaceSvg;
}
/**
* Returns whether or not this workspace is keyboard-navigable.
*
* @returns True.
*/
isNavigable() {
return true;
}
/**
* Returns an object responsible for coordinating movement of focus between
* items on this workspace in response to keyboard navigation commands.