Skip to content

fix: Announce the workspace to VoiceOver via a focus target node#10087

Merged
gonfunko merged 4 commits into
RaspberryPiFoundation:mainfrom
microbit-matt-hillsdon:workspace-focus-node
Jun 29, 2026
Merged

fix: Announce the workspace to VoiceOver via a focus target node#10087
gonfunko merged 4 commits into
RaspberryPiFoundation:mainfrom
microbit-matt-hillsdon:workspace-focus-node

Conversation

@microbit-matt-hillsdon

@microbit-matt-hillsdon microbit-matt-hillsdon commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

VoiceOver doesn't announce a region when focus moves into it from a node already inside that region, so focusing the workspace region directly (e.g. pressing "W" from a block) was silent.

Introduce WorkspaceFocusTarget: an ordinary focusable node that represents the workspace as a whole. The workspace's selection ring (which already represents the workspace being the active node) doubles as this target, gaining role=figure, aria-roledescription="workspace" and the stack count. Focusing the workspace now lands here instead of the region, so the move is announced. The region keeps a short, stable label ("Blocks workspace.") as enclosing context, and the stack count moves off the region onto the target.

  • W shortcut and fresh-entry (getRestoredFocusableNode) focus the target.
  • Add WorkspaceFocusTargetNavigationPolicy so navigating in reaches blocks.
  • Block/comment navigation is unchanged.

The basics

The details

Resolves

Fixes #9885

Proposed Changes

This restructures what takes the focus when moving to the workspace to be a node inside the workspace region even in the workspace case so we always get readout.

We considered some alternatives and welcome the Blockly team's input here:

  • aria-live, but the delay in response feels unacceptable and contrasts poorly with other similar actions (e.g. t, arrow nav) especially if you're on a block with longer readout
  • Scoping this change to just Mac. This seems possible but adds complexity.
  • Calling the new node something other than workspace to better differentiate it from the region, e.g. "workspace summary". It seems hard to find something that works together with the region (case 1 below) and standalone (case 2)

New readout:

  1. entering workspace from outside: "0 stacks of blocks, workspace, Blocks workspace, region"
    • note: pressing 'w' from a block just inserted from the flyout also seems to count as a region
      change in at least NVDA + VoiceOver before and after this change
    • the duplication is a bit worse than the current readout but not too bad
  2. entering workspace from a block via W: "1 stack of blocks, workspace"

When testing this we were surprised to find that 'W' in a mutator workspace focuses the main workspace. I expected it to behave with the same scoping as 'T'. But from the tests this behaviour is clearly intentional (and FWIW has no impact on MakeCode). This is relevant because if 'W' did focus mutator workspaces the same design is harder to pull off there: stack count is not part of the readout there so there's less of a case for a separate node. Not an issue as-is but flagging for awareness.

Note that strings change here, not sure of the impact on v13.

Reason for Changes

Test Coverage

Documentation

Additional Information

VoiceOver doesn't announce a region when focus moves into it from a node
already inside that region, so focusing the workspace region directly
(e.g. pressing "W" from a block) was silent.

Introduce WorkspaceFocusTarget: an ordinary focusable node that represents
the workspace as a whole. The workspace's selection ring (which already
represents the workspace being the active node) doubles as this target,
gaining role=figure, aria-roledescription="workspace" and the stack count.
Focusing the workspace now lands here instead of the region, so the move is
announced. The region keeps a short, stable label ("Blocks workspace.") as
enclosing context, and the stack count moves off the region onto the target.

- W shortcut and fresh-entry (getRestoredFocusableNode) focus the target.
- Add WorkspaceFocusTargetNavigationPolicy so navigating in reaches blocks.
- Block/comment navigation is unchanged.
@microbit-matt-hillsdon microbit-matt-hillsdon requested a review from a team as a code owner June 29, 2026 10:59
@github-actions github-actions Bot added the PR: fix Fixes a bug label Jun 29, 2026
@github-actions github-actions Bot added PR: fix Fixes a bug and removed PR: fix Fixes a bug labels Jun 29, 2026

@gonfunko gonfunko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you find that the additional navigation policy was actually necessary? Experimentally it seems to work without it, and I'd generally expect that to be the case (the navigator does some funny business to make cross-stack navigation work that, as a side effect, makes nav work a bit different at the root)

@microbit-matt-hillsdon

microbit-matt-hillsdon commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

@gonfunko huh, I'd convinced myself we did need this but I agree it makes no difference. Left/right doesn't work either way, up/down works. 4adc0fc.

@microbit-matt-hillsdon

Copy link
Copy Markdown
Collaborator Author

As mentioned in chat, totally understand if this approach isn't merged, I'd love a better plan.

@gonfunko gonfunko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love this, but it does work and none of the other hacks I've thought of (focus something else before focusing the workspace, adding nonbreaking spaces to the label on focus) do. The closest alternative I found was just returning the focus ring element as the workspace's getFocusableElement(), but that seemed to confuse the focus manager (I think it may want the focusable tree's focusable element to actually contain the focusable elements of children of the tree). If you want to explore that feel free (main benefit would be avoiding the focus target wrapper class), but I can live with this as-is particularly since the navigation policy bit wasn't needed. LMK what your preference is!

@@ -0,0 +1,64 @@
/**
* @license
* Copyright 2025 Google LLC

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2026 Raspberry Pi Foundation

@microbit-matt-hillsdon

Copy link
Copy Markdown
Collaborator Author

The closest alternative I found was just returning the focus ring element as the workspace's getFocusableElement()

I'm not optimistic about this. What about e.g. clicking the workspace background? And I also expect trouble from code like this.

@gonfunko

Copy link
Copy Markdown
Contributor

The closest alternative I found was just returning the focus ring element as the workspace's getFocusableElement()

I'm not optimistic about this. What about e.g. clicking the workspace background? And I also expect trouble from code like this.

Yeah, I think that's part of what was causing trouble. I'll go ahead and merge this then - thanks for figuring this all out!

@gonfunko gonfunko merged commit 9734a63 into RaspberryPiFoundation:main Jun 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: fix Fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[13.0.0-beta.4] Workspace aria-label issues (VoiceOver only?)

2 participants