Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
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
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
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
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
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
c75aea7
feat: Make WorkspaceSvg and BlockSvg focusable (#8916)
BenHenning Apr 24, 2025
d4883f5
feat: Make toolbox and flyout focusable (#8920)
BenHenning Apr 24, 2025
2a1a8b3
Merge branch 'make-toolbox-and-flyout-focusable-roll-forward' into ma…
BenHenning Apr 29, 2025
e5abf72
fix: Remove CSS for active/passive focus.
BenHenning Apr 30, 2025
e849e0c
Merge branch 'make-workspace-focusable-roll-forward' into make-toolbo…
BenHenning Apr 30, 2025
585c950
feat: Make FlyoutButton focusable.
BenHenning Apr 30, 2025
a520554
Merge branch 'make-toolbox-and-flyout-focusable-roll-forward' into ma…
BenHenning Apr 30, 2025
d6dcc4b
fix: Actually make FlyoutButton focusable.
BenHenning Apr 30, 2025
a9cf3d7
chore: lint fixes.
BenHenning Apr 30, 2025
f18670a
chore: Use strict equals.
BenHenning Apr 30, 2025
34970cc
chore: Empty commit to re-trigger CI.
BenHenning Apr 30, 2025
b11aa43
Merge branch 'make-toolbox-and-flyout-focusable-roll-forward' into ma…
BenHenning Apr 30, 2025
3cf8fb8
chore: Add field doc.
BenHenning Apr 30, 2025
e3a6f98
Merge branch 'rc/v12.0.0' into make-workspace-focusable-roll-forward
BenHenning Apr 30, 2025
b738b4a
Merge branch 'make-workspace-focusable-roll-forward' into make-toolbo…
BenHenning Apr 30, 2025
ef6f661
Merge branch 'rc/v12.0.0' into make-toolbox-and-flyout-focusable-roll…
BenHenning Apr 30, 2025
b493147
Merge branch 'make-toolbox-and-flyout-focusable-roll-forward' into ma…
BenHenning Apr 30, 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
7 changes: 7 additions & 0 deletions core/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const defaultPrompt = function (
defaultValue: string,
callback: (result: string | null) => void,
) {
// NOTE TO DEVELOPER: Ephemeral focus doesn't need to be taken for the native
// window prompt since it prevents focus from changing while open.
callback(window.prompt(message, defaultValue));
};

Expand Down Expand Up @@ -116,6 +118,11 @@ export function prompt(
/**
* Sets the function to be run when Blockly.dialog.prompt() is called.
*
* **Important**: When overridding this, be aware that non-native prompt
* experiences may require managing ephemeral focus in FocusManager. This isn't
* needed for the native window prompt because it prevents focus from being
* changed while open.
*
* @param promptFunction The function to be run, or undefined to restore the
* default implementation.
* @see Blockly.dialog.prompt
Expand Down
14 changes: 14 additions & 0 deletions core/dropdowndiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import type {BlockSvg} from './block_svg.js';
import * as common from './common.js';
import type {Field} from './field.js';
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
import * as dom from './utils/dom.js';
import * as math from './utils/math.js';
import {Rect} from './utils/rect.js';
Expand Down Expand Up @@ -82,6 +83,9 @@ let owner: Field | null = null;
/** Whether the dropdown was positioned to a field or the source block. */
let positionToField: boolean | null = null;

/** Callback to FocusManager to return ephemeral focus when the div closes. */
let returnEphemeralFocus: ReturnEphemeralFocus | null = null;

/**
* Dropdown bounds info object used to encapsulate sizing information about a
* bounding element (bounding box and width/height).
Expand Down Expand Up @@ -338,6 +342,8 @@ export function show<T>(
dom.addClass(div, renderedClassName);
dom.addClass(div, themeClassName);

returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);

// When we change `translate` multiple times in close succession,
// Chrome may choose to wait and apply them all at once.
// Since we want the translation to initial X, Y to be immediate,
Expand Down Expand Up @@ -623,6 +629,10 @@ export function hide() {
animateOutTimer = setTimeout(function () {
hideWithoutAnimation();
}, ANIMATION_TIME * 1000);
if (returnEphemeralFocus) {
returnEphemeralFocus();
returnEphemeralFocus = null;
}
if (onHide) {
onHide();
onHide = null;
Expand All @@ -638,6 +648,10 @@ export function hideWithoutAnimation() {
clearTimeout(animateOutTimer);
}

if (returnEphemeralFocus) {
returnEphemeralFocus();
returnEphemeralFocus = null;
}
if (onHide) {
onHide();
onHide = null;
Expand Down
42 changes: 39 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 @@ -34,6 +36,7 @@ import type {KeyboardShortcut} from './shortcut_registry.js';
import * as Tooltip from './tooltip.js';
import type {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import * as idGenerator from './utils/idgenerator.js';
import * as parsing from './utils/parsing.js';
import {Rect} from './utils/rect.js';
import {Size} from './utils/size.js';
Expand All @@ -42,7 +45,7 @@ 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';

/**
* 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,9 @@ export abstract class Field<T = any>
*/
SERIALIZABLE = false;

/** The unique ID of this field. */
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 +262,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 +306,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 +1414,29 @@ export abstract class Field<T = any>
}
}

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

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

/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {}

/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {}

/**
* Subclasses should reimplement this method to construct their Field
* subclass from a JSON arg object.
Expand Down
15 changes: 13 additions & 2 deletions core/widgetdiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import * as common from './common.js';
import {Field} from './field.js';
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
import * as dom from './utils/dom.js';
import type {Rect} from './utils/rect.js';
import type {Size} from './utils/size.js';
Expand All @@ -34,6 +35,9 @@ let themeClassName = '';
/** The HTML container for popup overlays (e.g. editor widgets). */
let containerDiv: HTMLDivElement | null;

/** Callback to FocusManager to return ephemeral focus when the div closes. */
let returnEphemeralFocus: ReturnEphemeralFocus | null = null;

/**
* Returns the HTML container for editor widgets.
*
Expand Down Expand Up @@ -110,6 +114,7 @@ export function show(
if (themeClassName) {
dom.addClass(div, themeClassName);
}
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
}

/**
Expand All @@ -126,8 +131,14 @@ export function hide() {
div.style.display = 'none';
div.style.left = '';
div.style.top = '';
if (dispose) dispose();
dispose = null;
if (returnEphemeralFocus) {
returnEphemeralFocus();
returnEphemeralFocus = null;
}
if (dispose) {
dispose();
dispose = null;
}
div.textContent = '';

if (rendererClassName) {
Expand Down
12 changes: 12 additions & 0 deletions core/workspace_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2709,6 +2709,18 @@ export class WorkspaceSvg
}
}

const fieldIndicatorIndex = id.indexOf('_field_');
if (fieldIndicatorIndex !== -1) {
const blockId = id.substring(0, fieldIndicatorIndex);
const block = this.getBlockById(blockId);
if (block) {
for (const field of block.getFields()) {
if (field.getFocusableElement().id === id) return field;
}
}
return null;
}

return this.getBlockById(id) as IFocusableNode;
}

Expand Down
Loading