From f2af7015c3a2703c2bba92caf57f19d603cde029 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Nov 2025 00:12:07 +0000 Subject: [PATCH 1/3] feat: Expand single field block labeling. --- core/block.ts | 32 +++++++++++++++----- core/block_svg.ts | 14 ++------- core/contextmenu_items.ts | 8 +---- core/field.ts | 4 +-- core/keyboard_nav/field_navigation_policy.ts | 5 +-- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/core/block.ts b/core/block.ts index af44facda5d..e11708220f1 100644 --- a/core/block.ts +++ b/core/block.ts @@ -962,16 +962,34 @@ export class Block { } /** - * @returns True if this block is a value block with a single editable field. + * @returns True if this block is a value block with a full block field. + * @param mustBeEditable Whether the evaluated field must be editable. * @internal */ - isSimpleReporter(): boolean { - if (!this.outputConnection) return false; + isSimpleReporter(mustBeFullBlock: boolean = false, mustBeEditable: boolean = false): boolean { + return this.getSingletonFullBlockField(mustBeFullBlock, mustBeEditable) !== null; + } - for (const input of this.inputList) { - if (input.connection || input.fieldRow.length > 1) return false; - } - return true; + /** + * Determies and returns the only field of this block, or null if there isn't + * one and this block can't be considered a simple reporter. Null will also be + * returned if the singleton block doesn't match additional criteria, if set, + * such as being full-block or editable. + * + * @param mustBeFullBlock Whether the returned field must be 'full-block'. + * @param mustBeEditable Whether the returned field must be editable. + * @returns The only full-block, maybe editable field of this block, or null. + * @internal + */ + getSingletonFullBlockField(mustBeFullBlock: boolean, mustBeEditable: boolean): Field | null { + if (!this.outputConnection) return null; + for (const input of this.inputList) if (input.connection) return null; + const matchingFields = Array.from(this.getFields()).filter((field) => { + if (mustBeFullBlock && !field.isFullBlockField()) return false; + if (mustBeEditable && !field.isCurrentlyEditable()) return false; + return true; + }); + return matchingFields.length === 1 ? matchingFields[0] : null; } /** diff --git a/core/block_svg.ts b/core/block_svg.ts index d312f418a43..50f76e50fc1 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -233,10 +233,7 @@ export class BlockSvg * @internal */ recomputeAriaLabel() { - if (this.isSimpleReporter()) { - const field = Array.from(this.getFields())[0]; - if (field.isFullBlockField() && field.isCurrentlyEditable()) return; - } + if (this.isSimpleReporter(true, true)) return; aria.setState( this.getFocusableElement(), @@ -1964,13 +1961,8 @@ export class BlockSvg /** See IFocusableNode.getFocusableElement. */ getFocusableElement(): HTMLElement | SVGElement { - if (this.isSimpleReporter()) { - const field = Array.from(this.getFields())[0]; - if (field && field.isFullBlockField() && field.isCurrentlyEditable()) { - return field.getFocusableElement(); - } - } - return this.pathObject.svgPath; + const singletonField = this.getSingletonFullBlockField(true, true); + return singletonField?.getFocusableElement() ?? this.pathObject.svgPath; } /** See IFocusableNode.getFocusableTree. */ diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts index 001a3c58e25..764758322b7 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -26,12 +26,6 @@ import {Coordinate} from './utils/coordinate.js'; import * as svgMath from './utils/svg_math.js'; import type {WorkspaceSvg} from './workspace_svg.js'; -function isFullBlockField(block?: BlockSvg) { - if (!block || !block.isSimpleReporter()) return false; - const firstField = block.getFields().next().value; - return firstField?.isFullBlockField(); -} - /** * Option to undo previous action. */ @@ -377,7 +371,7 @@ export function registerComment() { // Either block already has a comment so let us remove it, // or the block isn't just one full-block field block, which // shouldn't be allowed to have comments as there's no way to read them. - (block.hasIcon(CommentIcon.TYPE) || !isFullBlockField(block)) + (block.hasIcon(CommentIcon.TYPE) || !block.isSimpleReporter(true)) ) { return 'enabled'; } diff --git a/core/field.ts b/core/field.ts index ccbd3442275..b27b5e0da63 100644 --- a/core/field.ts +++ b/core/field.ts @@ -331,9 +331,7 @@ export abstract class Field this.applyColour(); const id = - this.isFullBlockField() && - this.isCurrentlyEditable() && - this.sourceBlock_?.isSimpleReporter() + this.sourceBlock_?.isSimpleReporter(true, true) ? idGenerator.getNextUniqueId() : `${this.sourceBlock_?.id}_field_${idGenerator.getNextUniqueId()}`; this.fieldGroup_.setAttribute('id', id); diff --git a/core/keyboard_nav/field_navigation_policy.ts b/core/keyboard_nav/field_navigation_policy.ts index f9df406c22c..fb332a1cf85 100644 --- a/core/keyboard_nav/field_navigation_policy.ts +++ b/core/keyboard_nav/field_navigation_policy.ts @@ -65,10 +65,7 @@ export class FieldNavigationPolicy implements INavigationPolicy> { current.canBeFocused() && current.isVisible() && (current.isClickable() || current.isCurrentlyEditable()) && - !( - current.getSourceBlock()?.isSimpleReporter() && - current.isFullBlockField() - ) && + !current.getSourceBlock()?.isSimpleReporter(true, true) && current.getParentInput().isVisible() ); } From f9d8af3d12f4935613301cf5b1a8e7e4ea351462 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Nov 2025 00:22:01 +0000 Subject: [PATCH 2/3] chore: Lint fixes. --- core/block.ts | 14 +++++++++++--- core/field.ts | 7 +++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/block.ts b/core/block.ts index e11708220f1..c50b5ae1c3a 100644 --- a/core/block.ts +++ b/core/block.ts @@ -966,8 +966,13 @@ export class Block { * @param mustBeEditable Whether the evaluated field must be editable. * @internal */ - isSimpleReporter(mustBeFullBlock: boolean = false, mustBeEditable: boolean = false): boolean { - return this.getSingletonFullBlockField(mustBeFullBlock, mustBeEditable) !== null; + isSimpleReporter( + mustBeFullBlock: boolean = false, + mustBeEditable: boolean = false, + ): boolean { + return ( + this.getSingletonFullBlockField(mustBeFullBlock, mustBeEditable) !== null + ); } /** @@ -981,7 +986,10 @@ export class Block { * @returns The only full-block, maybe editable field of this block, or null. * @internal */ - getSingletonFullBlockField(mustBeFullBlock: boolean, mustBeEditable: boolean): Field | null { + getSingletonFullBlockField( + mustBeFullBlock: boolean, + mustBeEditable: boolean, + ): Field | null { if (!this.outputConnection) return null; for (const input of this.inputList) if (input.connection) return null; const matchingFields = Array.from(this.getFields()).filter((field) => { diff --git a/core/field.ts b/core/field.ts index b27b5e0da63..3bb3d5caf40 100644 --- a/core/field.ts +++ b/core/field.ts @@ -330,10 +330,9 @@ export abstract class Field this.initModel(); this.applyColour(); - const id = - this.sourceBlock_?.isSimpleReporter(true, true) - ? idGenerator.getNextUniqueId() - : `${this.sourceBlock_?.id}_field_${idGenerator.getNextUniqueId()}`; + const id = this.sourceBlock_?.isSimpleReporter(true, true) + ? idGenerator.getNextUniqueId() + : `${this.sourceBlock_?.id}_field_${idGenerator.getNextUniqueId()}`; this.fieldGroup_.setAttribute('id', id); } From 11d6a1a5045942eb61c66e58a090514931c6f4c5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 2 Dec 2025 20:12:37 +0000 Subject: [PATCH 3/3] chore: Doc updates (addresses reviewer comment). --- core/block.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/block.ts b/core/block.ts index c50b5ae1c3a..faa0ae324eb 100644 --- a/core/block.ts +++ b/core/block.ts @@ -963,6 +963,7 @@ export class Block { /** * @returns True if this block is a value block with a full block field. + * @param mustBeFullBlock Whether the evaluated field must be 'full-block'. * @param mustBeEditable Whether the evaluated field must be editable. * @internal */ @@ -976,7 +977,7 @@ export class Block { } /** - * Determies and returns the only field of this block, or null if there isn't + * Determines and returns the only field of this block, or null if there isn't * one and this block can't be considered a simple reporter. Null will also be * returned if the singleton block doesn't match additional criteria, if set, * such as being full-block or editable.