Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
238 changes: 197 additions & 41 deletions core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@
/** Whether this block is currently being dragged. */
private dragging = false;

public currentConnectionCandidate: RenderedConnection | null = null;

Check failure on line 172 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22.x)

All declarations of 'currentConnectionCandidate' must have identical modifiers.

Check failure on line 172 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20.x)

All declarations of 'currentConnectionCandidate' must have identical modifiers.

Check failure on line 172 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.x)

All declarations of 'currentConnectionCandidate' must have identical modifiers.

/**
* The location of the top left of this block (in workspace coordinates)
Expand Down Expand Up @@ -216,15 +216,18 @@
this.svgGroup.setAttribute('data-id', this.id);

// The page-wide unique ID of this Block used for focusing.
this.svgGroup.id = idGenerator.getNextUniqueId();
svgPath.id = idGenerator.getNextUniqueId();

// TODO: Figure out how to make this work better with trying to reduce redundant announcements.
// aria.setState(svgPath, aria.State.LIVE, 'off');
// aria.setState(svgPath, aria.State.ATOMIC, true);
aria.setState(svgPath, aria.State.ROLEDESCRIPTION, 'block');
aria.setRole(svgPath, aria.Role.TREEITEM);
this.recomputeAriaLabel();
aria.setRole(svgPath, this.getAriaRole());
svgPath.tabIndex = -1;
this.currentConnectionCandidate = null;

this.doInit_();
aria.setState(this.getFocusableElement(), aria.State.LABEL, this.getAriaLabel());

Check failure on line 230 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `this.getFocusableElement(),·aria.State.LABEL,·this.getAriaLabel()` with `⏎······this.getFocusableElement(),⏎······aria.State.LABEL,⏎······this.getAriaLabel(),⏎····`
}

private recomputeAriaLabel() {
Expand Down Expand Up @@ -257,7 +260,7 @@
return fieldLabels.join(' ');
}

collectSiblingBlocks(surroundParent: BlockSvg | null): BlockSvg[] {

Check failure on line 263 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22.x)

Duplicate function implementation.

Check failure on line 263 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20.x)

Duplicate function implementation.

Check failure on line 263 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.x)

Duplicate function implementation.
// NOTE TO DEVELOPERS: it's very important that these are NOT sorted. The
// returned list needs to be relatively stable for consistency block indexes
// read out to users via screen readers.
Expand All @@ -276,7 +279,7 @@
}
}

computeLevelInWorkspace(): number {

Check failure on line 282 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22.x)

Duplicate function implementation.

Check failure on line 282 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20.x)

Duplicate function implementation.

Check failure on line 282 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.x)

Duplicate function implementation.
const surroundParent = this.getSurroundParent();
return surroundParent ? surroundParent.computeLevelInWorkspace() + 1 : 0;
}
Expand Down Expand Up @@ -339,6 +342,75 @@
aria.setState(this.getFocusableElement(), aria.State.SELECTED, false);
}

private getParentAriaGroup(): Element {
const surroundingParent = this.getSurroundParent();
if (surroundingParent) {
surroundingParent.ensureAriaConnectionToParent();

const parentGroupElem = surroundingParent.svgGroup;
if (aria.getRole(parentGroupElem) !== aria.Role.GROUP) {
aria.setRole(parentGroupElem, aria.Role.GROUP);
// If the parent isn't already a group, set it up as one and add it to
// its parent.
BlockSvg.addAriaOwner(surroundingParent.getFocusableElement(), parentGroupElem);

Check failure on line 355 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `surroundingParent.getFocusableElement(),·parentGroupElem` with `⏎··········surroundingParent.getFocusableElement(),⏎··········parentGroupElem,⏎········`
}
return parentGroupElem;
} else return this.workspace.getFocusableElement();
}

private ensureAriaConnectionToParent() {
// TODO: This needs to be centrally managed and set up to work across all types of workspace mutations.
// TODO: Figure out if we ever need to set aria-owns for the groups for the tree items, or just the groups themselves.
// It seems that https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-navigation/ only does the latter.
BlockSvg.addAriaOwner(this.getParentAriaGroup(), this.getFocusableElement());

Check failure on line 365 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `this.getParentAriaGroup(),·this.getFocusableElement()` with `⏎······this.getParentAriaGroup(),⏎······this.getFocusableElement(),⏎····`
}

// TODO: Support deregistering.
private static addAriaOwner(treeItemElement: Element, groupElement: Element) {
const ariaChildren = treeItemElement.getAttribute('aria-owns')?.split(' ') ?? [];

Check failure on line 370 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / lint

Insert `⏎·····`
ariaChildren.push(groupElement.id);
treeItemElement.setAttribute('aria-owns', [... new Set(ariaChildren)].join(' '));

Check failure on line 372 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `'aria-owns',·[...·new·Set(ariaChildren)].join('·')` with `⏎······'aria-owns',⏎······[...new·Set(ariaChildren)].join('·'),⏎····`
}

// TODO: Do this efficiently (probably centrally).
private recomputeAriaTreeItemDetailsRecursively() {
const elem = this.getFocusableElement();
const connection = this.currentConnectionCandidate;
let childPosition: number;
let parentsChildCount: number;
let hierarchyDepth: number;
if (connection) {
// If the block is being inserted into a new location, the position is hypothetical.
// TODO: Figure out how to deal with output connections.
let surroundParent: BlockSvg | null;
let siblingBlocks: BlockSvg[];
if (connection.type === ConnectionType.INPUT_VALUE) {
surroundParent = connection.sourceBlock_;
siblingBlocks = this.collectSiblingBlocks(surroundParent);
// The block is being added as a child since it's input.
// TODO: Figure out how to compute the correct position.
childPosition = 1;
} else {
surroundParent = connection.sourceBlock_.getSurroundParent();
siblingBlocks = this.collectSiblingBlocks(surroundParent);
// The block is being added after the connected block.
childPosition = siblingBlocks.indexOf(connection.sourceBlock_) + 2;
}
parentsChildCount = siblingBlocks.length + 1;
hierarchyDepth = surroundParent ? surroundParent.computeLevelInWorkspace() + 1 : 1;

Check failure on line 400 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `·?·surroundParent.computeLevelInWorkspace()·+·1` with `⏎········?·surroundParent.computeLevelInWorkspace()·+·1⏎·······`
} else {
const surroundParent = this.getSurroundParent();
const siblingBlocks = this.collectSiblingBlocks(surroundParent);
childPosition = siblingBlocks.indexOf(this) + 1;
parentsChildCount = siblingBlocks.length;
hierarchyDepth = this.computeLevelInWorkspace() + 1;
}
elem.setAttribute('aria-posinset', `${childPosition}`);
elem.setAttribute('aria-setsize', `${parentsChildCount}`);
elem.setAttribute('aria-level', `${hierarchyDepth}`);
this.getChildren(false).forEach((block) => block.recomputeAriaTreeItemDetailsRecursively());

Check failure on line 411 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `·block.recomputeAriaTreeItemDetailsRecursively()` with `⏎······block.recomputeAriaTreeItemDetailsRecursively(),⏎····`
}

/**
* Sets the parent of this block to be a new block or null.
*
Expand All @@ -355,6 +427,35 @@
super.setParent(newParent);
dom.stopTextWidthCache();

// ATTEMPT 3
// this.ensureAriaConnectionToParent();

// ATTEMPT 2 of tree item
// const surroundingParent = this.getSurroundParent();
// if (surroundingParent) {
// const elem = surroundingParent.getFocusableElement();
// const parentGroupElem = surroundingParent.svgGroup;
// if (aria.getRole(parentGroupElem) !== aria.Role.GROUP) {
// // If the parent isn't already a group, then it needs to be set up as
// // one.
// aria.setRole(parentGroupElem, aria.Role.GROUP);
// const grandparent = surroundingParent.getSurroundParent();
// if (grandparent) {
// // Update the grandparent to own this group.
// } else {
// // Update the workspace to own this group since it's top-level.
// const workspaceRoot = this.workspace.getFocusableElement();
// const ariaChildren = elem.getAttribute('aria-owns')?.split(' ') ?? [];
// ariaChildren.push(elem.id);
// elem.setAttribute('aria-owns', [... new Set(ariaChildren)].join(' '));
// }
// }

// const ariaChildren = workspaceRoot.getAttribute('aria-owns')?.split(' ') ?? [];
// ariaChildren.push(this.getFocusableElement().id);
// elem.setAttribute('aria-owns', [... new Set(ariaChildren)].join(' '));
// }

const svgRoot = this.getSvgRoot();

// Bail early if workspace is clearing, or we aren't rendered.
Expand Down Expand Up @@ -408,7 +509,8 @@

this.applyColour();

this.workspace.recomputeAriaTree();
// ATTEMPT 4 (full cheating without custom English gen).
this.workspace.getTopBlocks(false).forEach((block) => (block as BlockSvg).recomputeAriaTreeItemDetailsRecursively());

Check failure on line 513 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `.getTopBlocks(false).forEach((block)·=>·(block·as·BlockSvg).recomputeAriaTreeItemDetailsRecursively()` with `⏎······.getTopBlocks(false)⏎······.forEach((block)·=>⏎········(block·as·BlockSvg).recomputeAriaTreeItemDetailsRecursively(),⏎······`
}

/**
Expand Down Expand Up @@ -1840,18 +1942,17 @@
/** Starts a drag on the block. */
startDrag(e?: PointerEvent): void {
this.dragStrategy.startDrag(e);
const dragStrategy = this.dragStrategy as BlockDragStrategy;
const candidate = dragStrategy.connectionCandidate?.neighbour ?? null;
this.currentConnectionCandidate = candidate;
this.currentConnectionCandidate = (this.dragStrategy as BlockDragStrategy).connectionCandidate?.neighbour ?? null;

Check failure on line 1945 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `·(this.dragStrategy·as·BlockDragStrategy).connectionCandidate?.neighbour·??` with `⏎······(this.dragStrategy·as·BlockDragStrategy).connectionCandidate?.neighbour·??⏎·····`
this.announceDynamicAriaState(true, false);
}

// TODO: Since it's event-driven, this can probably be replaced with just locals.
private currentConnectionCandidate: RenderedConnection | null = null;

Check failure on line 1950 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22.x)

All declarations of 'currentConnectionCandidate' must have identical modifiers.

Check failure on line 1950 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22.x)

Duplicate identifier 'currentConnectionCandidate'.

Check failure on line 1950 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20.x)

All declarations of 'currentConnectionCandidate' must have identical modifiers.

Check failure on line 1950 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20.x)

Duplicate identifier 'currentConnectionCandidate'.

Check failure on line 1950 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.x)

All declarations of 'currentConnectionCandidate' must have identical modifiers.

Check failure on line 1950 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.x)

Duplicate identifier 'currentConnectionCandidate'.

/** Drags the block to the given location. */
drag(newLoc: Coordinate, e?: PointerEvent): void {
this.dragStrategy.drag(newLoc, e);
const dragStrategy = this.dragStrategy as BlockDragStrategy;
const candidate = dragStrategy.connectionCandidate?.neighbour ?? null;
this.currentConnectionCandidate = candidate;
this.currentConnectionCandidate = (this.dragStrategy as BlockDragStrategy).connectionCandidate?.neighbour ?? null;

Check failure on line 1955 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `·(this.dragStrategy·as·BlockDragStrategy).connectionCandidate?.neighbour·??` with `⏎······(this.dragStrategy·as·BlockDragStrategy).connectionCandidate?.neighbour·??⏎·····`
this.announceDynamicAriaState(true, false, newLoc);
}

Expand Down Expand Up @@ -1919,10 +2020,12 @@
/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {
this.select();
aria.setState(this.pathObject.svgPath, aria.State.SELECTED, true);
}

/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {
aria.setState(this.pathObject.svgPath, aria.State.SELECTED, false);
this.unselect();
}

Expand All @@ -1931,53 +2034,106 @@
return true;
}

/**
* Announces the current dynamic state of the specified block, if any.
*
* An example of dynamic state is whether the block is currently being moved,
* and in what way. These states aren't represented through ARIA directly, so
* they need to be determined and announced using an ARIA live region
* (see aria.announceDynamicAriaState).
*
* @param block The block whose dynamic state should maybe be announced.
* @param isMoving Whether the specified block is currently being moved.
* @param isCanceled Whether the previous movement operation has been canceled.
* @param newLoc The new location the block is moving to (if unconstrained).
*/
private announceDynamicAriaState(
isMoving: boolean,
isCanceled: boolean,
newLoc?: Coordinate,
) {
/** See IFocusableNode.getAriaRole. */
getAriaRole(): aria.Role | null {
return aria.Role.TREEITEM;
}

private announceDynamicAriaState(isMoving: boolean, isCanceled: boolean, newLoc?: Coordinate) {
// this.recomputeAriaTreeItemDetailsRecursively();
// const label = this.getAriaLabel();
// aria.setState(this.getFocusableElement(), aria.State.LABEL, label);
const connection = this.currentConnectionCandidate;
const ariaAnnouncementSpan = document.getElementById('blocklyAriaAnnounce');
if (!ariaAnnouncementSpan) return;
if (isCanceled) {
aria.announceDynamicAriaState('Canceled movement');
ariaAnnouncementSpan.innerHTML = 'Canceled movement';
return;
}
if (!isMoving) return;
if (this.currentConnectionCandidate) {
if (connection) {
// const newParentBlock = newConnection?.sourceBlock_ as BlockSvg | undefined;
// const newSurroundBlock = newParentBlock?.getSurroundParent();
// console.log('@@@@@ drag: go after:',newParentBlock?.getAriaLabel(), 'go under:',newSurroundBlock?.getAriaLabel());

// TODO: Figure out general detachment.
// TODO: Figure out how to deal with output connections.
const surroundParent = this.currentConnectionCandidate.sourceBlock_;
let surroundParent: BlockSvg | null = connection.sourceBlock_;
const announcementContext = [];
announcementContext.push('Moving'); // TODO: Specialize for inserting?
// NB: Old code here doesn't seem to handle parents correctly.
if (this.currentConnectionCandidate.type === ConnectionType.INPUT_VALUE) {
announcementContext.push('to', 'input');
if (connection.type === ConnectionType.INPUT_VALUE) {
// surroundParent = connection.sourceBlock_;
announcementContext.push('to','input','of');
} else {
announcementContext.push('to', 'child');
}
if (surroundParent) {
announcementContext.push('of', surroundParent.computeAriaLabel());
// surroundParent = connection.sourceBlock_.getSurroundParent();
announcementContext.push('to','child','of');
}
announcementContext.push(surroundParent.getAriaLabel());

// if (this.workspace.getMovingBlock() === this) {
// fieldLabels.push('Moving');
// }

// If the block is currently being moved, announce the new block label so that the user understands where it is now.
// TODO: Figure out how much recomputeAriaTreeItemDetailsRecursively needs to anticipate position if it won't be reannounced, and how much of that context should be included in the liveannouncement.
aria.announceDynamicAriaState(announcementContext.join(' '));
ariaAnnouncementSpan.innerHTML = announcementContext.join(' ');
} else if (newLoc) {
// The block is being freely dragged.
aria.announceDynamicAriaState(
`Moving unconstrained to coordinate x ${Math.round(newLoc.x)} and y ${Math.round(newLoc.y)}.`,
);
ariaAnnouncementSpan.innerHTML = `Moving unconstrained to coordinate x ${Math.round(newLoc.x)} and y ${Math.round(newLoc.y)}.`;
}
}

/** See IFocusableNode.getAriaLabel. */
getAriaLabel(): string {
// TODO: Blocks probably need to define their aria label as part of their block definition, but
// it can be guessed based on its field labels.

if (this.isShadow()) {
// Shadow blocks are best represented directly by their field since they
// effectively operate like a field does for keyboard navigation purposes.
const field = Array.from(this.getFields())[0];
return field.getAriaLabel();
}

// TODO: Localize this (is it even possible?).
const fieldLabels = [];
for (const field of this.getFields()) {
if (field instanceof FieldLabel) {
fieldLabels.push(field.getText());
}
}
// const siblingBlocks = this.collectSiblingBlocks();
// fieldLabels.push('Block');
// fieldLabels.push(`${siblingBlocks.indexOf(this) + 1}`);
// fieldLabels.push('of');
// fieldLabels.push(`${siblingBlocks.length}`);
// fieldLabels.push('Level');
// fieldLabels.push(`${this.computeLevelInWorkspace() + 1}`);
return fieldLabels.join(' ');
}

private collectSiblingBlocks(surroundParent: BlockSvg | null): BlockSvg[] {

Check failure on line 2116 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22.x)

Duplicate function implementation.

Check failure on line 2116 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20.x)

Duplicate function implementation.

Check failure on line 2116 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.x)

Duplicate function implementation.
// NOTE TO DEVELOPERS: it's very important that these are NOT sorted. The
// returned list needs to be relatively stable for consistency block indexes
// read out to users via screen readers.
if (surroundParent) {
// Start from the first sibling and iterate in navigation order.
const firstSibling: BlockSvg = surroundParent.getChildren(false)[0];
const siblings: BlockSvg[] = [firstSibling];
let nextSibling: BlockSvg | null = firstSibling;
while (nextSibling = nextSibling.getNextBlock()) {
siblings.push(nextSibling);
}
return siblings;
} else {
// For top-level blocks, simply return those from the workspace.
return this.workspace.getTopBlocks(false);
}
}

private computeLevelInWorkspace(): number {

Check failure on line 2135 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22.x)

Duplicate function implementation.

Check failure on line 2135 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20.x)

Duplicate function implementation.

Check failure on line 2135 in core/block_svg.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 18.x)

Duplicate function implementation.
const surroundParent = this.getSurroundParent();
return surroundParent ? surroundParent.computeLevelInWorkspace() + 1 : 0;
}
}
11 changes: 11 additions & 0 deletions core/bubbles/bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {IHasBubble} from '../interfaces/i_has_bubble.js';
import {ISelectable} from '../interfaces/i_selectable.js';
import {ContainerRegion} from '../metrics_manager.js';
import {Scrollbar} from '../scrollbar.js';
import { aria } from '../utils.js';
import {Coordinate} from '../utils/coordinate.js';
import * as dom from '../utils/dom.js';
import * as idGenerator from '../utils/idgenerator.js';
Expand Down Expand Up @@ -719,6 +720,16 @@ export abstract class Bubble implements IBubble, ISelectable, IFocusableNode {
return true;
}

/** See IFocusableNode.getAriaRole. */
getAriaRole(): aria.Role | null {
return aria.Role.GROUP;
}

/** See IFocusableNode.getAriaLabel. */
getAriaLabel(): string {
return 'Bubble';
}

/**
* Returns the object that owns/hosts this bubble, if any.
*/
Expand Down
11 changes: 11 additions & 0 deletions core/comments/comment_bar_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import { aria } from '../utils.js';
import {Rect} from '../utils/rect.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
import type {RenderedWorkspaceComment} from './rendered_workspace_comment.js';
Expand Down Expand Up @@ -102,4 +103,14 @@ export abstract class CommentBarButton implements IFocusableNode {
canBeFocused() {
return this.isVisible();
}

/** See IFocusableNode.getAriaRole. */
getAriaRole(): aria.Role | null {
return aria.Role.BUTTON;
}

/** See IFocusableNode.getAriaLabel. */
getAriaLabel(): string {
return 'DoNotDefine?';
}
}
Loading
Loading