Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b1d3d6d
feat: Clean up a11y node hierarchy.
BenHenning Oct 30, 2025
a7f7810
Merge branch 'add-screen-reader-support-experimental' into clean-up-n…
BenHenning Nov 4, 2025
f81e2b2
fix: Fix CI failures.
BenHenning Nov 12, 2025
cc5d002
Merge branch 'add-screen-reader-support-experimental' into clean-up-n…
BenHenning Nov 26, 2025
7e1b4d5
Merge branch 'add-screen-reader-support-experimental' into clean-up-n…
BenHenning Dec 4, 2025
18200c9
Merge branch 'add-screen-reader-support-experimental' into clean-up-n…
BenHenning Dec 8, 2025
eeaff73
chore: Simplify connectiong gating logic.
BenHenning Dec 8, 2025
5e28067
chore: Reduce complexity.
BenHenning Dec 9, 2025
4a6f102
Merge branch 'add-screen-reader-support-experimental' into clean-up-n…
BenHenning Dec 11, 2025
c7e074f
feat: Make Flyout a menu.
BenHenning Dec 11, 2025
14cd72c
Merge branch 'add-screen-reader-support-experimental' into clean-up-n…
BenHenning Dec 11, 2025
45206fa
Merge branch 'clean-up-node-hierarchy' into make-flyout-a-menu
BenHenning Dec 11, 2025
8dacb41
fix: Broken tests due to connection change.
BenHenning Dec 12, 2025
c19eaa5
chore: Lint fixes.
BenHenning Dec 12, 2025
71a9678
fix: Fix output connections in C-shaped blocks.
BenHenning Dec 12, 2025
6660963
Merge branch 'clean-up-node-hierarchy' into make-flyout-a-menu
BenHenning Dec 12, 2025
84f78e2
chore: Undo changes from #9449.
BenHenning Dec 12, 2025
fa925e8
chore: Re-add BlockSvg changes.
BenHenning Dec 12, 2025
a6c2744
chore: Remove unnecessary testing code.
BenHenning Dec 12, 2025
dc750a1
fix: Attempt to use list instead of menu.
BenHenning Dec 12, 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
10 changes: 8 additions & 2 deletions core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,16 @@ export class BlockSvg
aria.setState(
this.getFocusableElement(),
aria.State.LABEL,
this.computeAriaLabel(),
!this.isInFlyout
? this.computeAriaLabel()
: this.computeAriaLabelForFlyoutBlock(),
);
}

private computeAriaLabelForFlyoutBlock(): string {
return `${this.computeAriaLabel(true)}, block`;
}

computeAriaLabel(
verbose: boolean = false,
minimal: boolean = false,
Expand Down Expand Up @@ -305,7 +311,7 @@ export class BlockSvg

private computeAriaRole() {
if (this.workspace.isFlyout) {
aria.setRole(this.pathObject.svgPath, aria.Role.TREEITEM);
aria.setRole(this.pathObject.svgPath, aria.Role.LISTITEM);
} else {
const roleDescription = this.getAriaRoleDescription();
aria.setState(
Expand Down
19 changes: 12 additions & 7 deletions core/field_checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,18 @@ export class FieldCheckbox extends Field<CheckboxBool> {

private recomputeAria() {
const element = this.getFocusableElement();
aria.setRole(element, aria.Role.CHECKBOX);
aria.setState(
element,
aria.State.LABEL,
this.getAriaTypeName() ?? 'Checkbox',
);
aria.setState(element, aria.State.CHECKED, !!this.value_);
const isInFlyout = this.getSourceBlock()?.workspace?.isFlyout || false;
if (!isInFlyout) {
aria.setRole(element, aria.Role.CHECKBOX);
aria.setState(
element,
aria.State.LABEL,
this.getAriaTypeName() ?? 'Checkbox',
);
aria.setState(element, aria.State.CHECKED, !!this.value_);
} else {
aria.setState(element, aria.State.HIDDEN, true);
}
}

override render_() {
Expand Down
20 changes: 12 additions & 8 deletions core/field_dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,21 @@ export class FieldDropdown extends Field<string> {

protected recomputeAria() {
if (!this.fieldGroup_) return; // There's no element to set currently.
const isInFlyout = this.getSourceBlock()?.workspace?.isFlyout || false;
const element = this.getFocusableElement();
aria.setRole(element, aria.Role.COMBOBOX);
aria.setState(element, aria.State.HASPOPUP, aria.Role.LISTBOX);
aria.setState(element, aria.State.EXPANDED, !!this.menu_);
if (this.menu_) {
aria.setState(element, aria.State.CONTROLS, this.menu_.id);
if (!isInFlyout) {
aria.setRole(element, aria.Role.COMBOBOX);
aria.setState(element, aria.State.HASPOPUP, aria.Role.LISTBOX);
aria.setState(element, aria.State.EXPANDED, !!this.menu_);
if (this.menu_) {
aria.setState(element, aria.State.CONTROLS, this.menu_.id);
} else {
aria.clearState(element, aria.State.CONTROLS);
}
aria.setState(element, aria.State.LABEL, super.computeAriaLabel(true));
} else {
aria.clearState(element, aria.State.CONTROLS);
aria.setState(element, aria.State.HIDDEN, true);
}

aria.setState(element, aria.State.LABEL, super.computeAriaLabel(true));
}

/**
Expand Down
5 changes: 3 additions & 2 deletions core/field_image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,14 @@ export class FieldImage extends Field<string> {
dom.addClass(this.fieldGroup_, 'blocklyImageField');
}

const isInFlyout = this.getSourceBlock()?.workspace?.isFlyout || false;
const element = this.getFocusableElement();
if (this.isClickable()) {
if (!isInFlyout && this.isClickable()) {
this.imageElement.style.cursor = 'pointer';
aria.setRole(element, aria.Role.BUTTON);
aria.setState(element, aria.State.LABEL, super.computeAriaLabel(true));
} else {
// The field isn't navigable unless it's clickable.
// The field isn't navigable unless it's clickable and outside the flyout.
aria.setRole(element, aria.Role.PRESENTATION);
}
}
Expand Down
10 changes: 7 additions & 3 deletions core/field_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,6 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
dom.addClass(this.fieldGroup_, 'blocklyInputField');
}

const element = this.getFocusableElement();
aria.setRole(element, aria.Role.BUTTON);
this.recomputeAriaLabel();
}

Expand All @@ -189,7 +187,13 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
protected recomputeAriaLabel() {
if (!this.fieldGroup_) return;
const element = this.getFocusableElement();
aria.setState(element, aria.State.LABEL, super.computeAriaLabel());
const isInFlyout = this.getSourceBlock()?.workspace?.isFlyout || false;
if (!isInFlyout) {
aria.setRole(element, aria.Role.BUTTON);
aria.setState(element, aria.State.LABEL, super.computeAriaLabel());
} else {
aria.setState(element, aria.State.HIDDEN, true);
}
}

override isFullBlockField(): boolean {
Expand Down
19 changes: 13 additions & 6 deletions core/flyout_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,13 @@ export class FlyoutButton
this.svgContainerGroup,
);

aria.setRole(this.svgContainerGroup, aria.Role.TREEITEM);
if (this.isFlyoutLabel) {
aria.setRole(this.svgContainerGroup, aria.Role.LISTITEM);
aria.setRole(this.svgContentGroup, aria.Role.PRESENTATION);
this.svgFocusableGroup = this.svgContainerGroup;
} else {
aria.setRole(this.svgContentGroup, aria.Role.BUTTON);
aria.setRole(this.svgContainerGroup, aria.Role.PRESENTATION);
aria.setRole(this.svgContentGroup, aria.Role.LISTITEM);
this.svgFocusableGroup = this.svgContentGroup;
}
this.svgFocusableGroup.id = this.id;
Expand Down Expand Up @@ -183,9 +184,7 @@ export class FlyoutButton
},
this.svgContentGroup,
);
if (!this.isFlyoutLabel) {
aria.setRole(svgText, aria.Role.PRESENTATION);
}
aria.setRole(svgText, aria.Role.PRESENTATION);
let text = parsing.replaceMessageReferences(this.text);
if (this.workspace.RTL) {
// Force text to be RTL by adding an RLM.
Expand All @@ -198,7 +197,15 @@ export class FlyoutButton
.getThemeManager()
.subscribe(this.svgText, 'flyoutForegroundColour', 'fill');
}
aria.setState(this.svgFocusableGroup, aria.State.LABEL, text);
if (this.isFlyoutLabel) {
aria.setState(this.svgFocusableGroup, aria.State.LABEL, text);
} else {
aria.setState(
this.svgFocusableGroup,
aria.State.LABEL,
`${text}, button`,
);
}

const fontSize = style.getComputedStyle(svgText, 'fontSize');
const fontWeight = style.getComputedStyle(svgText, 'fontWeight');
Expand Down
5 changes: 5 additions & 0 deletions core/flyout_item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export class FlyoutItem {
/**
* Creates a new FlyoutItem.
*
* Note that it's the responsibility of implementations to ensure that element
* is given the ARIA role LISTITEM and respects its expected constraints
* (which includes ensuring that no interactive elements are children of the
* item element--interactive elements themselves should be the LISTITEM).
*
* @param element The element that will be displayed in the flyout.
* @param type The type of element. Should correspond to the type of the
* flyout inflater that created this object.
Expand Down
3 changes: 3 additions & 0 deletions core/utils/aria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export enum Role {

// ARIA role for menu item elements.
MENUITEM = 'menuitem',

// ARIA role for option items that are children of combobox, listbox, menu,
// radiogroup, or tree elements.
OPTION = 'option',
Expand Down Expand Up @@ -55,6 +56,8 @@ export enum Role {
SPINBUTTON = 'spinbutton',
REGION = 'region',
GENERIC = 'generic',
LIST = 'list',
LISTITEM = 'listitem',
}

/**
Expand Down
4 changes: 2 additions & 2 deletions core/workspace_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,8 +804,8 @@ export class WorkspaceSvg
this.svgBubbleCanvas_ = this.layerManager.getBubbleLayer();

if (this.isFlyout) {
// Use the block canvas as the primary tree parent for flyout blocks.
aria.setRole(this.svgBlockCanvas_, aria.Role.TREE);
// Use the block canvas as the primary list for nesting.
aria.setRole(this.svgBlockCanvas_, aria.Role.LIST);
aria.setState(this.svgBlockCanvas_, aria.State.LABEL, ariaLabel);
} else {
browserEvents.conditionalBind(
Expand Down
Loading