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
27 changes: 13 additions & 14 deletions packages/blockly/core/block_aria_composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import type {BlockSvg} from './block_svg.js';
import {ConnectionType} from './connection_type.js';
import {FieldLabel} from './field_label.js';
import type {Input} from './inputs/input.js';
import {inputTypes} from './inputs/input_types.js';
import {
Expand Down Expand Up @@ -57,23 +56,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 @@ -143,11 +137,11 @@ export function computeFieldRowLabel(
const fieldRowLabel = input.fieldRow
.filter((field) => field.isVisible())
.flatMap((field, index, visibleFields) => {
const isFieldLabel = field instanceof FieldLabel;
const isFieldLabel = field.isLabelField();
if (isFieldLabel) {
if (
index < visibleFields.length - 1 &&
visibleFields[index + 1] instanceof FieldLabel
visibleFields[index + 1].isLabelField()
) {
// Both this item and the next item are FieldLabels. We want to
// combine these, so we add this one to the list for later handling.
Expand Down Expand Up @@ -271,7 +265,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 +289,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 Expand Up @@ -354,7 +353,7 @@ export function getInputLabels(
*/
function beginsWithFieldLabel(input: Input): boolean {
const visibleFields = input.fieldRow.filter((field) => field.isVisible());
return visibleFields.length > 0 && visibleFields[0] instanceof FieldLabel;
return visibleFields.length > 0 && visibleFields[0].isLabelField();
}

/**
Expand All @@ -372,7 +371,7 @@ function endsWithFieldLabel(input: Input): boolean {
const visibleFields = input.fieldRow.filter((field) => field.isVisible());
return (
visibleFields.length > 0 &&
visibleFields[visibleFields.length - 1] instanceof FieldLabel
visibleFields[visibleFields.length - 1].isLabelField()
);
}

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
24 changes: 22 additions & 2 deletions packages/blockly/core/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import './events/events_block_change.js';

import type {Block} from './block.js';
import {computeAriaLabel} from './block_aria_composer.js';
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as dropDownDiv from './dropdowndiv.js';
Expand Down Expand Up @@ -458,6 +459,16 @@ export abstract class Field<T = any>
return false;
}

/**
* Returns whether this field is a static text label (ex. FieldLabel).
* Used internally instead of `instanceof FieldLabel` to avoid circular imports.
*
* @internal
*/
isLabelField(): boolean {
return false;
}

/**
* Create a field border rect element. Not to be overridden by subclasses.
* Instead modify the result of the function inside initView, or create a
Expand Down Expand Up @@ -1518,7 +1529,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 Expand Up @@ -1560,7 +1571,16 @@ export abstract class Field<T = any>
// editing mode that can be activated.
aria.setRole(focusableElement, aria.Role.BUTTON);

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,
aria.Verbosity.STANDARD,
label,
);
}

aria.setState(focusableElement, aria.State.LABEL, label);
return true;
}
Expand Down
5 changes: 1 addition & 4 deletions packages/blockly/core/field_dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,16 +933,13 @@ export class FieldDropdown extends Field<string> {
}

/**
* Overrides the default label and sets additional aria state.
* Sets additional aria state.
*/
override recomputeAriaContext(): boolean {
const shouldCustomize = super.recomputeAriaContext();
if (!shouldCustomize) return false;

const focusableElement = this.getFocusableElement();
const label = this.computeAriaLabel(true);

aria.setState(focusableElement, aria.State.LABEL, label);
aria.setState(focusableElement, aria.State.HASPOPUP, 'listbox');
aria.setState(focusableElement, aria.State.EXPANDED, !!this.menu_);
return true;
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
4 changes: 4 additions & 0 deletions packages/blockly/core/field_label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export class FieldLabel extends Field<string> {
/** Text labels should not truncate. */
override maxDisplayLength = Infinity;

override isLabelField(): boolean {
return true;
}

/**
* @param value The initial value of the field. Should cast to a string.
* Defaults to an empty string if null or undefined. Also accepts
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');
});
});
});
});
Loading