Skip to content

Commit c8e0700

Browse files
authored
feat: use custom labels for block parent input labels (#9867)
1 parent 057356f commit c8e0700

2 files changed

Lines changed: 120 additions & 20 deletions

File tree

packages/blockly/core/block_aria_composer.ts

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,24 @@ export function computeFieldRowLabel(
154154
}
155155

156156
/**
157-
* Returns a description of the parent statement input a block is attached to.
158-
* When a block is connected to a statement input, the input's field row label
159-
* will be prepended to the block's description to indicate that the block
160-
* begins a clause in its parent block.
157+
* Returns a description of the parent input a block is attached to.
158+
* When a block is connected to an input, the input's label will sometimes
159+
* be prepended to the block's description.
160+
*
161+
* If an input has a custom label, the custom label will be prepended
162+
* to the first child block connected to that input.
163+
*
164+
* If an input does not have a custom label, the input's fallback
165+
* label determined from the field row will be prepended to the
166+
* child block's label only if the following are true:
167+
* - the parent block has at least one statement input
168+
* - the child block in question is not attached to the first
169+
* statement input of the parent block (in this case, the label
170+
* would be redundant with the parent block's label)
171+
*
172+
* For statement inputs, the resolved label (whether custom or fallback) is
173+
* wrapped in the "Begin %1" prefix so the readout indicates that the child
174+
* block starts the body of the statement input.
161175
*
162176
* @internal
163177
* @param block The block to generate a parent input label for.
@@ -168,24 +182,41 @@ function getParentInputLabel(block: BlockSvg) {
168182
const parentInput = (
169183
block.outputConnection ?? block.previousConnection
170184
)?.targetConnection?.getParentInput();
171-
const parentBlock = parentInput?.getSourceBlock();
185+
if (!parentInput) return undefined;
186+
187+
const parentBlock = parentInput.getSourceBlock();
188+
if (parentBlock.isInsertionMarker()) return undefined;
189+
190+
// parentInput is only non-null when this block is directly attached to the
191+
// input (i.e. it is the first child block in that input). A custom label
192+
// is always prepended for the first child; a fallback label from the field
193+
// row is only used in select circumstances.
194+
let inputLabel: string | string[];
195+
const customLabel = parentInput.getAriaLabelText();
196+
if (customLabel) {
197+
inputLabel = customLabel;
198+
} else {
199+
if (!parentBlock.statementInputCount) return undefined;
172200

173-
if (parentBlock?.isInsertionMarker()) return undefined;
174-
if (!parentBlock?.statementInputCount) return undefined;
201+
const firstStatementInput = parentBlock.inputList.find(
202+
(i) => i.type === inputTypes.STATEMENT,
203+
);
204+
// The first statement input in a block has no field row label as it would
205+
// be duplicative of the block's label.
206+
if (parentInput === firstStatementInput) {
207+
return undefined;
208+
}
175209

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

185-
const parentInputLabel = computeFieldRowLabel(parentInput, true);
186-
return parentInput.type === inputTypes.STATEMENT
187-
? Msg['BLOCK_LABEL_BEGIN_PREFIX'].replace('%1', parentInputLabel.join(' '))
188-
: parentInputLabel;
213+
if (parentInput.type === inputTypes.STATEMENT) {
214+
const labelText = Array.isArray(inputLabel)
215+
? inputLabel.join(' ')
216+
: inputLabel;
217+
return Msg['BLOCK_LABEL_BEGIN_PREFIX'].replace('%1', labelText);
218+
}
219+
return inputLabel;
189220
}
190221

191222
/**

packages/blockly/tests/mocha/aria_test.js

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ suite('ARIA', function () {
371371
assert.notInclude(label, 'Begin stack');
372372
});
373373

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

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

398+
test('A custom statement input label is wrapped in the "Begin" prefix', function () {
399+
const ifBlock = this.makeBlock('controls_ifelse');
400+
ifBlock.getInput('ELSE').setAriaLabelProvider('otherwise do');
401+
const printBlock = this.makeBlock('text_print');
402+
ifBlock
403+
.getInput('ELSE')
404+
.connection.connect(printBlock.previousConnection);
405+
const label = Blockly.utils.aria.getState(
406+
printBlock.getFocusableElement(),
407+
Blockly.utils.aria.State.LABEL,
408+
);
409+
assert.include(label, 'Begin otherwise do');
410+
});
411+
412+
test('A custom label on the first statement input is prepended to its child block label', function () {
413+
const ifBlock = this.makeBlock('controls_ifelse');
414+
ifBlock.getInput('DO0').setAriaLabelProvider('then do');
415+
const printBlock = this.makeBlock('text_print');
416+
ifBlock.getInput('DO0').connection.connect(printBlock.previousConnection);
417+
const label = Blockly.utils.aria.getState(
418+
printBlock.getFocusableElement(),
419+
Blockly.utils.aria.State.LABEL,
420+
);
421+
assert.include(label, 'Begin then do');
422+
});
423+
424+
test('A custom input label is only used for the first child block in a statement input stack', function () {
425+
const ifBlock = this.makeBlock('controls_ifelse');
426+
ifBlock.getInput('ELSE').setAriaLabelProvider('otherwise do');
427+
const firstPrintBlock = this.makeBlock('text_print');
428+
ifBlock
429+
.getInput('ELSE')
430+
.connection.connect(firstPrintBlock.previousConnection);
431+
const secondPrintBlock = this.makeBlock('text_print');
432+
firstPrintBlock.nextConnection.connect(
433+
secondPrintBlock.previousConnection,
434+
);
435+
const subsequentLabel = Blockly.utils.aria.getState(
436+
secondPrintBlock.getFocusableElement(),
437+
Blockly.utils.aria.State.LABEL,
438+
);
439+
assert.notInclude(subsequentLabel, 'otherwise do');
440+
});
441+
442+
test('A custom input label is prepended to the child block of a value input', function () {
443+
const ifBlock = this.makeBlock('controls_ifelse');
444+
ifBlock.getInput('IF0').setAriaLabelProvider('condition');
445+
const boolBlock = this.makeBlock('logic_boolean');
446+
ifBlock.getInput('IF0').connection.connect(boolBlock.outputConnection);
447+
const label = Blockly.utils.aria.getState(
448+
boolBlock.getFocusableElement(),
449+
Blockly.utils.aria.State.LABEL,
450+
);
451+
assert.include(label, 'condition');
452+
});
453+
454+
test('A block connected to a value input without a custom label does not include the input label', function () {
455+
const negateBlock = this.makeBlock('logic_negate');
456+
const boolBlock = this.makeBlock('logic_boolean');
457+
negateBlock
458+
.getInput('BOOL')
459+
.connection.connect(boolBlock.outputConnection);
460+
const label = Blockly.utils.aria.getState(
461+
boolBlock.getFocusableElement(),
462+
Blockly.utils.aria.State.LABEL,
463+
);
464+
assert.notInclude(label, 'not');
465+
});
466+
398467
test('Disabled blocks indicate that in their label', function () {
399468
const block = this.makeBlock('text_print');
400469
let label = Blockly.utils.aria.getState(

0 commit comments

Comments
 (0)