Skip to content
Merged
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
67 changes: 49 additions & 18 deletions packages/blockly/core/block_aria_composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,24 @@ export function computeFieldRowLabel(
}

/**
* Returns a description of the parent statement input a block is attached to.
* When a block is connected to a statement input, the input's field row label
* will be prepended to the block's description to indicate that the block
* begins a clause in its parent block.
* Returns a description of the parent input a block is attached to.
* When a block is connected to an input, the input's label will sometimes
* be prepended to the block's description.
*
* If an input has a custom label, the custom label will be prepended
* to the first child block connected to that input.
*
* If an input does not have a custom label, the input's fallback
* label determined from the field row will be prepended to the
* child block's label only if the following are true:
* - the parent block has at least one statement input
* - the child block in question is not attached to the first
* statement input of the parent block (in this case, the label
* would be redundant with the parent block's label)
*
* For statement inputs, the resolved label (whether custom or fallback) is
* wrapped in the "Begin %1" prefix so the readout indicates that the child
* block starts the body of the statement input.
*
* @internal
* @param block The block to generate a parent input label for.
Expand All @@ -168,24 +182,41 @@ function getParentInputLabel(block: BlockSvg) {
const parentInput = (
block.outputConnection ?? block.previousConnection
)?.targetConnection?.getParentInput();
const parentBlock = parentInput?.getSourceBlock();
if (!parentInput) return undefined;

const parentBlock = parentInput.getSourceBlock();
if (parentBlock.isInsertionMarker()) return undefined;

// parentInput is only non-null when this block is directly attached to the
// input (i.e. it is the first child block in that input). A custom label
// is always prepended for the first child; a fallback label from the field
// row is only used in select circumstances.
let inputLabel: string | string[];
const customLabel = parentInput.getAriaLabelText();
if (customLabel) {
inputLabel = customLabel;
} else {
if (!parentBlock.statementInputCount) return undefined;

if (parentBlock?.isInsertionMarker()) return undefined;
if (!parentBlock?.statementInputCount) return undefined;
const firstStatementInput = parentBlock.inputList.find(
(i) => i.type === inputTypes.STATEMENT,
);
// The first statement input in a block has no field row label as it would
// be duplicative of the block's label.
if (parentInput === firstStatementInput) {
return undefined;
}

const firstStatementInput = parentBlock.inputList.find(
(i) => i.type === inputTypes.STATEMENT,
);
// The first statement input in a block has no field row label as it would
// be duplicative of the block's label.
if (!parentInput || parentInput === firstStatementInput) {
return undefined;
inputLabel = computeFieldRowLabel(parentInput, true);
}

const parentInputLabel = computeFieldRowLabel(parentInput, true);
return parentInput.type === inputTypes.STATEMENT
? Msg['BLOCK_LABEL_BEGIN_PREFIX'].replace('%1', parentInputLabel.join(' '))
: parentInputLabel;
if (parentInput.type === inputTypes.STATEMENT) {
const labelText = Array.isArray(inputLabel)
? inputLabel.join(' ')
: inputLabel;
return Msg['BLOCK_LABEL_BEGIN_PREFIX'].replace('%1', labelText);
}
return inputLabel;
}

/**
Expand Down
73 changes: 71 additions & 2 deletions packages/blockly/tests/mocha/aria_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ suite('ARIA', function () {
assert.notInclude(label, 'Begin stack');
});

test('Nested statement blocks in first statement input do not include their parent input in their label', function () {
test('Statement blocks in first statement input do not include their parent input in their label', function () {
const ifBlock = this.makeBlock('controls_ifelse');
const printBlock = this.makeBlock('text_print');
ifBlock.getInput('IF0').connection.connect(printBlock.previousConnection);
Expand All @@ -382,7 +382,7 @@ suite('ARIA', function () {
assert.isFalse(label.startsWith('Begin do'));
});

test('Nested statement blocks in subsequent statement inputs include their parent input in their label', function () {
test('Statement blocks in subsequent statement inputs include their parent input in their label', function () {
const ifBlock = this.makeBlock('controls_ifelse');
const printBlock = this.makeBlock('text_print');
ifBlock
Expand All @@ -395,6 +395,75 @@ suite('ARIA', function () {
assert.isTrue(label.startsWith('Begin else'));
});

test('A custom statement input label is wrapped in the "Begin" prefix', function () {
const ifBlock = this.makeBlock('controls_ifelse');
ifBlock.getInput('ELSE').setAriaLabelProvider('otherwise do');
const printBlock = this.makeBlock('text_print');
ifBlock
.getInput('ELSE')
.connection.connect(printBlock.previousConnection);
const label = Blockly.utils.aria.getState(
printBlock.getFocusableElement(),
Blockly.utils.aria.State.LABEL,
);
assert.include(label, 'Begin otherwise do');
});

test('A custom label on the first statement input is prepended to its child block label', function () {
const ifBlock = this.makeBlock('controls_ifelse');
ifBlock.getInput('DO0').setAriaLabelProvider('then do');
const printBlock = this.makeBlock('text_print');
ifBlock.getInput('DO0').connection.connect(printBlock.previousConnection);
const label = Blockly.utils.aria.getState(
printBlock.getFocusableElement(),
Blockly.utils.aria.State.LABEL,
);
assert.include(label, 'Begin then do');
});

test('A custom input label is only used for the first child block in a statement input stack', function () {
const ifBlock = this.makeBlock('controls_ifelse');
ifBlock.getInput('ELSE').setAriaLabelProvider('otherwise do');
const firstPrintBlock = this.makeBlock('text_print');
ifBlock
.getInput('ELSE')
.connection.connect(firstPrintBlock.previousConnection);
const secondPrintBlock = this.makeBlock('text_print');
firstPrintBlock.nextConnection.connect(
secondPrintBlock.previousConnection,
);
const subsequentLabel = Blockly.utils.aria.getState(
secondPrintBlock.getFocusableElement(),
Blockly.utils.aria.State.LABEL,
);
assert.notInclude(subsequentLabel, 'otherwise do');
});

test('A custom input label is prepended to the child block of a value input', function () {
const ifBlock = this.makeBlock('controls_ifelse');
ifBlock.getInput('IF0').setAriaLabelProvider('condition');
const boolBlock = this.makeBlock('logic_boolean');
ifBlock.getInput('IF0').connection.connect(boolBlock.outputConnection);
const label = Blockly.utils.aria.getState(
boolBlock.getFocusableElement(),
Blockly.utils.aria.State.LABEL,
);
assert.include(label, 'condition');
});

test('A block connected to a value input without a custom label does not include the input label', function () {
const negateBlock = this.makeBlock('logic_negate');
const boolBlock = this.makeBlock('logic_boolean');
negateBlock
.getInput('BOOL')
.connection.connect(boolBlock.outputConnection);
const label = Blockly.utils.aria.getState(
boolBlock.getFocusableElement(),
Blockly.utils.aria.State.LABEL,
);
assert.notInclude(label, 'not');
});

test('Disabled blocks indicate that in their label', function () {
const block = this.makeBlock('text_print');
let label = Blockly.utils.aria.getState(
Expand Down
Loading