From 24769b32d532c1e623f4d3700e4f47086c25c1a2 Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:14:32 -0400 Subject: [PATCH 1/3] fix: restore passive focus on full-block fields --- packages/blockly/core/css.ts | 9 ++ packages/blockly/core/field.ts | 1 + .../tests/mocha/focus_indicator_test.ts | 97 +++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 packages/blockly/tests/mocha/focus_indicator_test.ts diff --git a/packages/blockly/core/css.ts b/packages/blockly/core/css.ts index 65158557b02..95200b7203b 100644 --- a/packages/blockly/core/css.ts +++ b/packages/blockly/core/css.ts @@ -596,6 +596,15 @@ input[type=number] { ) .blocklyPassiveFocus.blocklyField > .blocklyFieldRect, +/* Full-block fields use the block path for their passive focus indicator. */ +.blocklyKeyboardNavigation:not( + :has( + .blocklyDropDownDiv:focus-within, + .blocklyWidgetDiv:focus-within + ) + ) + .blocklyBlock:has(> .blocklyFullBlockField.blocklyPassiveFocus) + > .blocklyPath:not(.blocklyFlyout .blocklyPath), /* Icons with passive focus except when widget/dropdown div in use. */ .blocklyKeyboardNavigation:not( :has( diff --git a/packages/blockly/core/field.ts b/packages/blockly/core/field.ts index 2acd110c5fe..8e1902bf155 100644 --- a/packages/blockly/core/field.ts +++ b/packages/blockly/core/field.ts @@ -429,6 +429,7 @@ export abstract class Field // Since full-block fields can be focused from the workspace's tree, // they need IDs in the format that the workspace is expecting. if (this.isFullBlockField()) { + this.fieldGroup_.classList.add('blocklyFullBlockField'); this.id_ = idGenerator.getNextUniqueId(); } else { this.id_ = `${sourceBlockSvg.id}_field_${idGenerator.getNextUniqueId()}`; diff --git a/packages/blockly/tests/mocha/focus_indicator_test.ts b/packages/blockly/tests/mocha/focus_indicator_test.ts new file mode 100644 index 00000000000..d4e718aa2ac --- /dev/null +++ b/packages/blockly/tests/mocha/focus_indicator_test.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; +import { + DEFAULT_INJECT_OPTIONS, + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; +import {createKeyDownEvent} from './test_helpers/user_input.js'; + +suite('Field focus indicators', function () { + let workspace: Blockly.WorkspaceSvg; + + setup(function (this: Mocha.Context) { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + type: 'focus_reporter', + message0: '%1', + args0: [{type: 'field_input', name: 'TEXT', text: 'value'}], + output: null, + }, + { + type: 'focus_statement', + message0: 'value %1', + args0: [{type: 'field_input', name: 'TEXT', text: 'value'}], + previousStatement: null, + nextStatement: null, + }, + ]); + workspace = Blockly.inject('blocklyDiv', { + ...DEFAULT_INJECT_OPTIONS, + renderer: 'zelos', + toolbox: { + kind: 'categoryToolbox', + contents: [ + { + kind: 'category', + name: 'Fields', + contents: [{kind: 'block', type: 'focus_reporter'}], + }, + ], + }, + }); + Blockly.keyboardNavigationController.setIsActive(true); + }); + + teardown(function (this: Mocha.Context) { + Blockly.keyboardNavigationController.setIsActive(false); + sharedTestTeardown.call(this, workspace); + delete Blockly.Blocks['focus_reporter']; + delete Blockly.Blocks['focus_statement']; + }); + + for (const fullBlock of [true, false]) { + test(`${fullBlock ? 'full-block' : 'regular'} fields retain a passive indicator in the toolbox`, function () { + const block = workspace.newBlock( + fullBlock ? 'focus_reporter' : 'focus_statement', + ); + block.initSvg(); + block.render(); + const field = block.getField('TEXT'); + assert.isNotNull(field); + Blockly.getFocusManager().focusNode(fullBlock ? block : field); + + workspace + .getInjectionDiv() + .dispatchEvent(createKeyDownEvent(Blockly.utils.KeyCodes.T)); + + const indicator = fullBlock + ? block.pathObject.svgPath + : field.getFocusableElement().querySelector('.blocklyFieldRect'); + assert.isNotNull(indicator); + assert.match(getComputedStyle(indicator).strokeDasharray, /^5px,? 3px$/); + + Blockly.getFocusManager().focusNode(fullBlock ? block : field); + assert.notMatch( + getComputedStyle(indicator).strokeDasharray, + /^5px,? 3px$/, + ); + + workspace + .getInjectionDiv() + .dispatchEvent(createKeyDownEvent(Blockly.utils.KeyCodes.T)); + Blockly.keyboardNavigationController.setIsActive(false); + assert.notMatch( + getComputedStyle(indicator).strokeDasharray, + /^5px,? 3px$/, + ); + }); + } +}); From e32ea64e2284f109ad1a2372854fc04fc4015626 Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:46:31 -0400 Subject: [PATCH 2/3] fix: simplify passive field focus regression --- packages/blockly/core/field.ts | 2 +- .../tests/mocha/focus_indicator_test.ts | 57 +++++-------------- 2 files changed, 15 insertions(+), 44 deletions(-) diff --git a/packages/blockly/core/field.ts b/packages/blockly/core/field.ts index 8e1902bf155..b88dad42f16 100644 --- a/packages/blockly/core/field.ts +++ b/packages/blockly/core/field.ts @@ -429,7 +429,7 @@ export abstract class Field // Since full-block fields can be focused from the workspace's tree, // they need IDs in the format that the workspace is expecting. if (this.isFullBlockField()) { - this.fieldGroup_.classList.add('blocklyFullBlockField'); + dom.addClass(this.fieldGroup_, 'blocklyFullBlockField'); this.id_ = idGenerator.getNextUniqueId(); } else { this.id_ = `${sourceBlockSvg.id}_field_${idGenerator.getNextUniqueId()}`; diff --git a/packages/blockly/tests/mocha/focus_indicator_test.ts b/packages/blockly/tests/mocha/focus_indicator_test.ts index d4e718aa2ac..5fcca354a42 100644 --- a/packages/blockly/tests/mocha/focus_indicator_test.ts +++ b/packages/blockly/tests/mocha/focus_indicator_test.ts @@ -25,13 +25,6 @@ suite('Field focus indicators', function () { args0: [{type: 'field_input', name: 'TEXT', text: 'value'}], output: null, }, - { - type: 'focus_statement', - message0: 'value %1', - args0: [{type: 'field_input', name: 'TEXT', text: 'value'}], - previousStatement: null, - nextStatement: null, - }, ]); workspace = Blockly.inject('blocklyDiv', { ...DEFAULT_INJECT_OPTIONS, @@ -54,44 +47,22 @@ suite('Field focus indicators', function () { Blockly.keyboardNavigationController.setIsActive(false); sharedTestTeardown.call(this, workspace); delete Blockly.Blocks['focus_reporter']; - delete Blockly.Blocks['focus_statement']; }); - for (const fullBlock of [true, false]) { - test(`${fullBlock ? 'full-block' : 'regular'} fields retain a passive indicator in the toolbox`, function () { - const block = workspace.newBlock( - fullBlock ? 'focus_reporter' : 'focus_statement', - ); - block.initSvg(); - block.render(); - const field = block.getField('TEXT'); - assert.isNotNull(field); - Blockly.getFocusManager().focusNode(fullBlock ? block : field); - - workspace - .getInjectionDiv() - .dispatchEvent(createKeyDownEvent(Blockly.utils.KeyCodes.T)); + test('full-block fields show a passive outline when focus moves to the toolbox', function () { + const block = workspace.newBlock('focus_reporter'); + block.initSvg(); + block.render(); + Blockly.getFocusManager().focusNode(block); - const indicator = fullBlock - ? block.pathObject.svgPath - : field.getFocusableElement().querySelector('.blocklyFieldRect'); - assert.isNotNull(indicator); - assert.match(getComputedStyle(indicator).strokeDasharray, /^5px,? 3px$/); + workspace + .getInjectionDiv() + .dispatchEvent(createKeyDownEvent(Blockly.utils.KeyCodes.T)); - Blockly.getFocusManager().focusNode(fullBlock ? block : field); - assert.notMatch( - getComputedStyle(indicator).strokeDasharray, - /^5px,? 3px$/, - ); - - workspace - .getInjectionDiv() - .dispatchEvent(createKeyDownEvent(Blockly.utils.KeyCodes.T)); - Blockly.keyboardNavigationController.setIsActive(false); - assert.notMatch( - getComputedStyle(indicator).strokeDasharray, - /^5px,? 3px$/, - ); - }); - } + const dashArray = getComputedStyle( + block.pathObject.svgPath, + ).strokeDasharray; + assert.isNotEmpty(dashArray); + assert.notEqual(dashArray, 'none'); + }); }); From a879e14790b41c4df7730feaefb296e37803b367 Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:53:03 -0400 Subject: [PATCH 3/3] test: move passive focus regression into keyboard navigation suite --- .../tests/mocha/focus_indicator_test.ts | 68 ------------------- .../tests/mocha/keyboard_navigation_test.ts | 54 +++++++++++++++ 2 files changed, 54 insertions(+), 68 deletions(-) delete mode 100644 packages/blockly/tests/mocha/focus_indicator_test.ts diff --git a/packages/blockly/tests/mocha/focus_indicator_test.ts b/packages/blockly/tests/mocha/focus_indicator_test.ts deleted file mode 100644 index 5fcca354a42..00000000000 --- a/packages/blockly/tests/mocha/focus_indicator_test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @license - * Copyright 2026 Raspberry Pi Foundation - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as Blockly from '#core/blockly.js'; -import {assert} from 'chai'; -import { - DEFAULT_INJECT_OPTIONS, - sharedTestSetup, - sharedTestTeardown, -} from './test_helpers/setup_teardown.js'; -import {createKeyDownEvent} from './test_helpers/user_input.js'; - -suite('Field focus indicators', function () { - let workspace: Blockly.WorkspaceSvg; - - setup(function (this: Mocha.Context) { - sharedTestSetup.call(this); - Blockly.defineBlocksWithJsonArray([ - { - type: 'focus_reporter', - message0: '%1', - args0: [{type: 'field_input', name: 'TEXT', text: 'value'}], - output: null, - }, - ]); - workspace = Blockly.inject('blocklyDiv', { - ...DEFAULT_INJECT_OPTIONS, - renderer: 'zelos', - toolbox: { - kind: 'categoryToolbox', - contents: [ - { - kind: 'category', - name: 'Fields', - contents: [{kind: 'block', type: 'focus_reporter'}], - }, - ], - }, - }); - Blockly.keyboardNavigationController.setIsActive(true); - }); - - teardown(function (this: Mocha.Context) { - Blockly.keyboardNavigationController.setIsActive(false); - sharedTestTeardown.call(this, workspace); - delete Blockly.Blocks['focus_reporter']; - }); - - test('full-block fields show a passive outline when focus moves to the toolbox', function () { - const block = workspace.newBlock('focus_reporter'); - block.initSvg(); - block.render(); - Blockly.getFocusManager().focusNode(block); - - workspace - .getInjectionDiv() - .dispatchEvent(createKeyDownEvent(Blockly.utils.KeyCodes.T)); - - const dashArray = getComputedStyle( - block.pathObject.svgPath, - ).strokeDasharray; - assert.isNotEmpty(dashArray); - assert.notEqual(dashArray, 'none'); - }); -}); diff --git a/packages/blockly/tests/mocha/keyboard_navigation_test.ts b/packages/blockly/tests/mocha/keyboard_navigation_test.ts index 43ab200e993..86bfb0a0b4f 100644 --- a/packages/blockly/tests/mocha/keyboard_navigation_test.ts +++ b/packages/blockly/tests/mocha/keyboard_navigation_test.ts @@ -515,6 +515,60 @@ suite('Keyboard navigation on Fields', function () { }); }); +suite('Field focus indicators', function () { + let workspace: Blockly.WorkspaceSvg; + + setup(function (this: Mocha.Context) { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + type: 'focus_reporter', + message0: '%1', + args0: [{type: 'field_input', name: 'TEXT', text: 'value'}], + output: null, + }, + ]); + workspace = Blockly.inject('blocklyDiv', { + ...DEFAULT_INJECT_OPTIONS, + renderer: 'zelos', + toolbox: { + kind: 'categoryToolbox', + contents: [ + { + kind: 'category', + name: 'Fields', + contents: [{kind: 'block', type: 'focus_reporter'}], + }, + ], + }, + }); + Blockly.keyboardNavigationController.setIsActive(true); + }); + + teardown(function (this: Mocha.Context) { + Blockly.keyboardNavigationController.setIsActive(false); + sharedTestTeardown.call(this, workspace); + delete Blockly.Blocks['focus_reporter']; + }); + + test('full-block fields show a passive outline when focus moves to the toolbox', function () { + const block = workspace.newBlock('focus_reporter'); + block.initSvg(); + block.render(); + Blockly.getFocusManager().focusNode(block); + + workspace + .getInjectionDiv() + .dispatchEvent(createKeyDownEvent(Blockly.utils.KeyCodes.T)); + + const dashArray = getComputedStyle( + block.pathObject.svgPath, + ).strokeDasharray; + assert.isNotEmpty(dashArray); + assert.notEqual(dashArray, 'none'); + }); +}); + suite('Workspace comment navigation', function () { let workspace: Blockly.WorkspaceSvg; let commentId1: string;