Skip to content
Open
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
18 changes: 9 additions & 9 deletions packages/blockly/core/block_aria_composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,18 @@ export enum ConnectionPreposition {
* @internal
* @param block The block for which an ARIA representation should be created.
* @param verbosity How much detail to include in the description.
* @param fullBlockFieldLabel An optional override for input labels for full-block fields
* @returns The ARIA representation for the specified block.
*/
export function computeAriaLabel(
block: BlockSvg,
verbosity = Verbosity.STANDARD,
fullBlockFieldLabel: string | undefined = undefined,
) {
if (block.isSimpleReporter()) {
// special case for full-block field blocks.
const field = block.getFullBlockField();
if (field) {
return field.computeAriaLabel(verbosity >= Verbosity.STANDARD);
}
}
return [
verbosity >= Verbosity.STANDARD && getBeginStackLabel(block),
getParentInputLabel(block),
...getInputLabels(block, verbosity),
...getInputLabels(block, verbosity, fullBlockFieldLabel),
verbosity === Verbosity.LOQUACIOUS && getParentToolboxCategoryLabel(block),
verbosity >= Verbosity.STANDARD && getDisabledLabel(block),
verbosity >= Verbosity.STANDARD && getCollapsedLabel(block),
Expand Down Expand Up @@ -271,7 +266,7 @@ function getParentInputLabel(block: BlockSvg) {
* @returns Text indicating that the block begins a stack, or undefined if it
* does not.
*/
function getBeginStackLabel(block: BlockSvg) {
export function getBeginStackLabel(block: BlockSvg) {
// Don't include the "begin stack" label for blocks that are moving
// or blocks in the flyout
if (block.isInFlyout || block.isDragging()) return undefined;
Expand All @@ -295,12 +290,17 @@ function getBeginStackLabel(block: BlockSvg) {
* @internal
* @param block The block to retrieve a list of field/input labels for.
* @param verbosity How much detail to include in each input label.
* @param fullBlockFieldLabel An optional override for full-block fields.
* @returns A list of field/input labels for the given block.
*/
export function getInputLabels(
block: BlockSvg,
verbosity = Verbosity.STANDARD,
fullBlockFieldLabel: string | undefined = undefined,
): string[] {
if (fullBlockFieldLabel) {
return [fullBlockFieldLabel];
}
const visibleInputs = block.inputList.filter((input) => input.isVisible());
let inputsToLabel = visibleInputs;

Expand Down
10 changes: 9 additions & 1 deletion packages/blockly/core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ export class BlockSvg
}

this.applyColour();
this.recomputeAriaContext();
}

/**
Expand Down Expand Up @@ -791,6 +792,9 @@ export class BlockSvg
} else {
common.draggingConnections.length = 0;
this.removeClass('blocklyDragging');
if (this.getFullBlockField()) {
this.recomputeAriaContext();
}
}
// Recurse through all blocks attached under this one.
for (let i = 0; i < this.childBlocks_.length; i++) {
Expand Down Expand Up @@ -2038,7 +2042,11 @@ export class BlockSvg
* Updates the ARIA label, role and roledescription for this block.
*/
private recomputeAriaContext() {
if (this.getFullBlockField()) return;
const fullBlockField = this.getFullBlockField();
if (fullBlockField) {
fullBlockField.recomputeAriaContext();
return;
}
aria.setState(
this.getFocusableElement(),
aria.State.LABEL,
Expand Down
2 changes: 1 addition & 1 deletion packages/blockly/core/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1518,7 +1518,7 @@ export abstract class Field<T = any>
*
* @returns true if the element is in the accessibility tree, false if the aria state is hidden
*/
protected recomputeAriaContext(): boolean {
recomputeAriaContext(): boolean {
let focusableElement;
try {
focusableElement = this.getFocusableElement();
Expand Down
10 changes: 9 additions & 1 deletion packages/blockly/core/field_dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
// Former goog.module ID: Blockly.FieldDropdown

import {computeAriaLabel} from './block_aria_composer.js';
import type {BlockSvg} from './block_svg.js';
import * as dropDownDiv from './dropdowndiv.js';
import {
Expand Down Expand Up @@ -940,7 +941,14 @@ export class FieldDropdown extends Field<string> {
if (!shouldCustomize) return false;

const focusableElement = this.getFocusableElement();
const label = this.computeAriaLabel(true);
let label = this.computeAriaLabel(true);
if (this.isFullBlockField()) {
// Full block fields get a more detailed label that includes the block's label
label = computeAriaLabel(this.getSourceBlock() as BlockSvg).replace(
this.computeAriaLabel(false),
label,

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.

This admittedly feels a bit janky, but it's needed because getInputLabels will use includeTypeInfo = false for standard verbosity.

);
}

aria.setState(focusableElement, aria.State.LABEL, label);
aria.setState(focusableElement, aria.State.HASPOPUP, 'listbox');
Expand Down
35 changes: 33 additions & 2 deletions packages/blockly/core/field_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';

import {computeAriaLabel, getBeginStackLabel} from './block_aria_composer.js';
import {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as bumpObjects from './bump_objects.js';
Expand All @@ -32,6 +33,7 @@ import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import {Msg} from './msg.js';
import * as renderManagement from './render_management.js';
import * as aria from './utils/aria.js';
import {Verbosity} from './utils/aria.js';
import * as dom from './utils/dom.js';
import {Size} from './utils/size.js';
import * as userAgent from './utils/useragent.js';
Expand Down Expand Up @@ -855,8 +857,37 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
const focusableElement = this.getFocusableElement();

let label = this.computeAriaLabel(true);
if (this.isCurrentlyEditable() && !this.getSourceBlock()?.isInFlyout) {
label = Msg['FIELD_LABEL_EDIT_PREFIX'].replace('%1', label);
const requiresEditableLabel =
this.isCurrentlyEditable() && !this.getSourceBlock()?.isInFlyout;

if (!this.isFullBlockField()) {
if (requiresEditableLabel) {
label = Msg['FIELD_LABEL_EDIT_PREFIX'].replace('%1', label);
}
} else {
// Full block fields get a more detailed label that includes the block's label
const fullBlockLabel = computeAriaLabel(
this.getSourceBlock() as BlockSvg,
Verbosity.STANDARD,
label,
);
if (requiresEditableLabel) {
const labels = fullBlockLabel.split(', ');
const beginStackLabel = getBeginStackLabel(
this.getSourceBlock() as BlockSvg,
);

// Insert "Edit" after "Begin stack" if found, otherwise at start.

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.

This is vibes based, but it felt odd to read/hear "Edit Begin stack, number:10..."

const beginStackLabelIndex =
beginStackLabel === undefined ? -1 : labels.indexOf(beginStackLabel);
const insertIndex =
beginStackLabelIndex === -1 ? 0 : beginStackLabelIndex + 1;
labels[insertIndex] = Msg['FIELD_LABEL_EDIT_PREFIX'].replace(
'%1',
labels[insertIndex] ?? '',
);
label = labels.join(', ');
}
}
aria.setState(focusableElement, aria.State.LABEL, label);
return true;
Expand Down
93 changes: 93 additions & 0 deletions packages/blockly/tests/mocha/field_dropdown_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -580,5 +580,98 @@ suite('Dropdown Fields', function () {
assert.include(label, 'Option 5');
});
});
suite('Full block fields', function () {
setup(function () {
this.workspace = Blockly.inject('blocklyDiv', {
renderer: 'zelos',
});
this.block = this.workspace.newBlock('variables_get');
this.block.initSvg();
this.block.render();
this.field = this.block.getField('VAR');
});
teardown(function () {
workspaceTeardown.call(this, this.workspace);
});

test('Top block ARIA label includes "Begin stack" label before dropdown field label', function () {
const labels = this.block
.getFocusableElement()
.getAttribute('aria-label')
.split(', ');

const expectedBeginStackLabel = 'Begin stack';
const expectedFieldLabel = "dropdown: Variable 'item'";
assert.include(labels, expectedBeginStackLabel);
assert.include(labels, expectedFieldLabel);
assert.isTrue(
labels.indexOf(expectedBeginStackLabel) <
labels.indexOf(expectedFieldLabel),
);
});

test('Connect to parent updates ARIA label with parent input label', function () {
const parentBlock = this.workspace.newBlock('controls_repeat_ext');
parentBlock.initSvg();
parentBlock.render();

this.block.outputConnection.connect(
parentBlock.getInput('TIMES').connection,
);

const labels = this.block
.getFocusableElement()
.getAttribute('aria-label')
.split(', ');

const expectedInputLabel = 'number of times to repeat';
const expectedFieldLabel = "dropdown: Variable 'item'";
assert.include(labels, expectedInputLabel);
assert.include(labels, expectedFieldLabel);
assert.isTrue(
labels.indexOf(expectedInputLabel) <
labels.indexOf(expectedFieldLabel),
);
assert.notInclude(labels, 'Begin stack');
});
test('Disconnect from parent updates ARIA label with Begin stack', function () {
const parentBlock = this.workspace.newBlock('controls_repeat_ext');
parentBlock.initSvg();
parentBlock.render();
this.block.outputConnection.connect(
parentBlock.getInput('TIMES').connection,
);
this.block.outputConnection.disconnect();

const label = this.block
.getFocusableElement()
.getAttribute('aria-label');
assert.include(label, 'Begin stack');
assert.notInclude(label, 'number of times to repeat');
});
test('Disconnect during drag updates ARIA label after drag ends', function () {
const parentBlock = this.workspace.newBlock('controls_repeat_ext');
parentBlock.initSvg();
parentBlock.render();
this.block.outputConnection.connect(
parentBlock.getInput('TIMES').connection,
);

this.block.setDragging(true);
this.block.outputConnection.disconnect();

const labelWhileDragging = this.block
.getFocusableElement()
.getAttribute('aria-label');
assert.notInclude(labelWhileDragging, 'Begin stack');

this.block.setDragging(false);

const labelAfterDrag = this.block
.getFocusableElement()
.getAttribute('aria-label');
assert.include(labelAfterDrag, 'Begin stack');
});
});
});
});
89 changes: 89 additions & 0 deletions packages/blockly/tests/mocha/field_number_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -551,5 +551,94 @@ suite('Number Fields', function () {
const updatedLabel = this.focusableElement.getAttribute('aria-label');
assert.isTrue(updatedLabel.includes('1'));
});
suite('Full block fields', function () {
setup(function () {
this.workspace = Blockly.inject('blocklyDiv', {
renderer: 'zelos',
});
this.block = this.workspace.newBlock('math_number');
this.field = this.block.getField('NUM');
this.block.initSvg();
this.block.render();
});
teardown(function () {
workspaceTeardown.call(this, this.workspace);
});
test('Top block ARIA label includes "Begin stack" label before expected field label', function () {
const labels = this.block
.getFocusableElement()
.getAttribute('aria-label')
.split(', ');

const expectedBeginStackLabel = 'Begin stack';
const expectedFieldLabel = 'Edit number: 0';
assert.include(labels, expectedBeginStackLabel);
assert.include(labels, expectedFieldLabel);
assert.isTrue(
labels.indexOf(expectedBeginStackLabel) <
labels.indexOf(expectedFieldLabel),
);
});
test('Connect to parent updates ARIA label with parent input label', function () {
const parentBlock = this.workspace.newBlock('controls_repeat_ext');
parentBlock.initSvg();
parentBlock.render();
this.block.outputConnection.connect(
parentBlock.getInput('TIMES').connection,
);
const labels = this.block
.getFocusableElement()
.getAttribute('aria-label')
.split(', ');

const expectedInputLabel = 'Edit number of times to repeat';
const expectedFieldLabel = 'number: 0';
assert.include(labels, expectedInputLabel);
assert.include(labels, expectedFieldLabel);
assert.isTrue(
labels.indexOf(expectedInputLabel) <
labels.indexOf(expectedFieldLabel),
);
assert.notInclude(labels, 'Begin stack');
});
test('Disconnect from parent updates ARIA label with Begin stack', function () {
const parentBlock = this.workspace.newBlock('controls_repeat_ext');
parentBlock.initSvg();
parentBlock.render();
this.block.outputConnection.connect(
parentBlock.getInput('TIMES').connection,
);
this.block.outputConnection.disconnect();

const label = this.block
.getFocusableElement()
.getAttribute('aria-label');
assert.include(label, 'Begin stack');
assert.notInclude(label, 'number of times to repeat');
});
test('Disconnect during drag updates ARIA label after drag ends', function () {
const parentBlock = this.workspace.newBlock('controls_repeat_ext');
parentBlock.initSvg();
parentBlock.render();
this.block.outputConnection.connect(
parentBlock.getInput('TIMES').connection,
);

this.block.setDragging(true);
this.block.outputConnection.disconnect();

const labelWhileDragging = this.block
.getFocusableElement()
.getAttribute('aria-label');
assert.notInclude(labelWhileDragging, 'Begin stack');

this.block.setDragging(false);

const labelAfterDrag = this.block
.getFocusableElement()
.getAttribute('aria-label');
assert.include(labelAfterDrag, 'Begin stack');
});
});
});
});
Loading