Skip to content

Improve simulator accessibility#6783

Closed
microbit-robert wants to merge 16 commits intomicrosoft:masterfrom
microbit-matt-hillsdon:sim-sr
Closed

Improve simulator accessibility#6783
microbit-robert wants to merge 16 commits intomicrosoft:masterfrom
microbit-matt-hillsdon:sim-sr

Conversation

@microbit-robert
Copy link
Copy Markdown
Contributor

@microbit-robert microbit-robert commented Apr 20, 2026

This PR introduces changes to improve the screen reader and keyboard controls behaviour of the simulator as well as some refactoring and de-duplication of keyboard handling code.

  • CSS tweaks to ensure that focus outlines are only visible for keyboard users (focus-visible) and not mouse users
  • Pin values have been decoupled from gradient values and their real values (0..1 or 0..1023) are used in the markup
  • Pins can be in the following modes for both keyboard and mouse controls (keyboard and mouse behaviour has been aligned):
    • touch with role="button" (no slider role or slider keyboard controls are active in this mode)
    • input (digital or analog) with role="slider" and readonly attributes - pressing arrow keys etc at this point will provide feedback via the live region that the pin is readonly
    • output (digital or analogue) with role="slider" and active slider controls (value can be changed)
  • Simplify the pin and shake aria-labels
    • shake the board -> shake
    • P0, ANALOG IN -> Pin 0 (etc)
  • Initialise the live region as soon as possible
    • The live region needs to exist before text content is added / changed, otherwise screen readers may not output anything
  • Remove uses of setLiveContent for sensor value changes and hide sensor value text from screen readers to avoid values being output multiple times
    • Screen readers already provide feedback for slider values and value changes
  • Slider values no longer loop/wrap (including the compass) when using keyboard controls
  • Sliders implement arrow key keyboard controls as well as Home (min value) / End (max value) and Page Up / Down (increment in steps of range / 10)

The compass control is currently not accessible as it is trying to be both a button and slider which cannot be supported simultaneously. See #6784

This PR is expected to work with microsoft/pxt#11275

@abchatra
Copy link
Copy Markdown
Collaborator

@microbit-robert is it possible to split this PR into smaller set of PRs. It is easy to manage smaller PR and review if the changes are unrelated. Reverting is also easier. Best is to keep cleanup's in its own PR.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves simulator accessibility and keyboard interaction behavior for the micro:bit simulator UI, with an emphasis on better screen reader semantics and more consistent slider/key handling across controls.

Changes:

  • Adjusts focus styling (via :focus-visible) and pointer interaction styling for simulator elements.
  • Refactors pin/sensor UI to separate visual gradient values from actual numeric values and updates ARIA roles/attributes accordingly.
  • Consolidates and expands keyboard slider handling (arrows, Home/End, PageUp/PageDown) and reduces live-region duplication.
Show a summary per file
File Description
sim/visuals/microbit.ts Updates simulator SVG styling, ARIA roles/labels, live region initialization, and consolidates keyboard handling logic for multiple controls.
sim/state/edgeconnector.ts Changes digital pin read/write value semantics to use 0/1 instead of 0/1023-style scaling.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (1)

sim/visuals/microbit.ts:1440

  • Mouse interaction for pins still writes pin.value on a 0..1023 scale regardless of pin mode (pin.value = ... * 1023). With the new digital semantics elsewhere (aria-valuemax=1, digitalWritePin sets 0/1, and updatePin treats any >0 as 1), this makes digital pins almost impossible to set to 0 with the mouse and can leave pin.value outside the slider range. Update this handler to set digital pin values as 0/1 (e.g. based on a threshold) and only use 0..1023 for analog pins.
                        if (pin.mode & PinFlags.Input && !(pin.mode & PinFlags.Touch)) {
                            let cursor = svg.cursorPoint(pt, this.element, ev);
                            let v = (400 - cursor.y) / 40 * 1023
                            pin.value = Math.max(0, Math.min(1023, Math.floor(v)));
                        }
  • Files reviewed: 2/2 changed files
  • Comments generated: 6

Comment thread sim/visuals/microbit.ts
Comment on lines +1642 to +1646
const commonKeyHandler = (e: KeyboardEvent, currentValue: number, min: number, max: number): number | undefined => {
const key = e.key;
if (isHandledKey(key)) {
e.preventDefault();
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

commonKeyHandler assumes currentValue is already within [min,max]. If it isn't (e.g. digital pins can still hold 1023 via pull-up or mouse input), ArrowDown/PageDown can return values > max and require many key presses to get back into range. Clamp currentValue into [min,max] at the start (and/or clamp the returned value on every branch) so sliders always behave consistently.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@microbit-robert microbit-robert Apr 22, 2026

Choose a reason for hiding this comment

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

This clamp was implemented in the split apart PRs.

Comment thread sim/visuals/microbit.ts Outdated
if (isHandledKey(key)) {
if (!(pinMode & PinFlags.Input)) {
e.preventDefault();
accessibility.setLiveContent(pxsim.localization.lf("This input is read only"));
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The live-region message "This input is read only" is emitted when the pin is not PinFlags.Input (i.e. when user interaction is blocked). The wording is confusing/misleading for screen reader users; consider changing it to something like "This pin is read-only" (and/or include the pin name) so it matches the actual condition.

Suggested change
accessibility.setLiveContent(pxsim.localization.lf("This input is read only"));
accessibility.setLiveContent(pxsim.localization.lf("This pin is read-only"));

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Improve aria-live message 0c965eb

Comment thread sim/visuals/microbit.ts
Comment on lines +1030 to +1032
"fill": "rgba(0,0,0,0)",
// Allows screen reader users to interact with board properly.
"role": "application"
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Setting role="application" on the root <svg> is generally discouraged because it can change screen reader navigation modes and override expected semantics for all descendants. If the goal is to improve interaction, prefer more specific roles (e.g. leave the SVG as-is and rely on the interactive child elements’ roles), or apply role="application" only to a dedicated focusable interaction surface with a clear aria-label and keyboard handling.

Suggested change
"fill": "rgba(0,0,0,0)",
// Allows screen reader users to interact with board properly.
"role": "application"
"fill": "rgba(0,0,0,0)"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Screen readers don't always respect the role attribute on SVG elements. While running NVDA on Windows and using the current live version of MakeCode, keyboard interaction with the sliders work, but you are unable to trigger a button press on Button A or Button B for example using Space or Enter, even though these elements have role="button" on them. There may be a more nuanced way to address this, but role="application" is one fix. This only impacts the micro:bit simulator and I'm not sure there is much in the way of screen reader semantics that will be lost here. Thoughts welcome.

Comment thread sim/visuals/microbit.ts
Comment on lines +147 to 151
*:focus-visible .sim-button-outer,
.sim-shake:focus-visible,
.sim-thermometer:focus-visible {
outline: 5px solid white;
stroke: black;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Focus outlines were moved to :focus-visible in the main stylesheet, but the high-contrast stylesheet (MB_HIGHCONTRAST) still uses :focus selectors. In high-contrast mode this likely reintroduces focus styling for mouse users, undermining the intended keyboard-only outline behavior. Consider updating the high-contrast focus selectors to :focus-visible (or adding a fallback that preserves keyboard-only outlines).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated high-contrast focus selectors to focus-visible 294e6f6

if (!pin) return -1;
pin.mode = PinFlags.Digital | PinFlags.Input;
return pin.value > 100 ? 1 : 0;
return pin.value > 0 ? 1 : 0;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

digitalReadPin now treats any non-zero pin.value as 1. Since digital pin values can still be on a 0..1023 scale (e.g. from pull settings and current mouse handling in the simulator), this effectively makes almost all non-zero values read as HIGH, which is a behavior change from the previous threshold-based mapping. Either ensure digital pin values are always normalized to 0/1 everywhere, or keep a threshold appropriate for the underlying value range (e.g. midpoint of 0..1023).

Suggested change
return pin.value > 0 ? 1 : 0;
return pin.value >= 512 ? 1 : 0;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@microbit-robert microbit-robert Apr 22, 2026

Choose a reason for hiding this comment

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

This change was backed out in the split up PRs.

Comment thread sim/visuals/microbit.ts Outdated
state.compassState.heading++;
if (state.compassState.heading < 0) state.compassState.heading += 360;
if (state.compassState.heading >= 360) state.compassState.heading %= 360;
const value = commonKeyHandler(ev, state.compassState.heading, 0, 360);
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

commonKeyHandler is invoked with max=360 for compass heading, which allows the keyboard to set heading to 360 (e.g. via End or ArrowUp from 359). The rest of the code treats heading as degrees in the [0, 359] range, so 360° will display and be stored in globals. Clamp to 359 (or clamp/mod to [0, 359]) when handling keyboard input.

Suggested change
const value = commonKeyHandler(ev, state.compassState.heading, 0, 360);
const value = commonKeyHandler(ev, state.compassState.heading, 0, 359);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Clamp compass value to 0..359 when using keyboard controls 2fb12eb

@microbit-robert
Copy link
Copy Markdown
Contributor Author

@microbit-robert is it possible to split this PR into smaller set of PRs. It is easy to manage smaller PR and review if the changes are unrelated. Reverting is also easier. Best is to keep cleanup's in its own PR.

Yes, I'll address the copilot review here, but subsequently break this down into smaller PRs and close this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants