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
20 changes: 8 additions & 12 deletions packages/blockly/core/block_aria_composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ export function configureAriaRole(block: BlockSvg) {
* `lookback` attribute is specified, all of the fields on the row immediately
* above the Input will be used instead.
*
* Empty field labels are excluded because they don't provide useful context.
* Fields should generally have a helpful label, but there are exceptions, such
* as when empty label fields are used to control the layout of a block.
*
* @internal
* @param input The Input to compute a description/context label for.
* @param lookback If true, will use labels for fields on the previous row if
Expand All @@ -146,7 +150,7 @@ export function computeFieldRowLabel(
return computeFieldRowLabel(inputs[index - 1], lookback, verbosity);
}
}
return fieldRowLabel;
return fieldRowLabel.filter((label) => !!label);
}

/**
Expand Down Expand Up @@ -249,11 +253,7 @@ export function getInputLabels(
* @param input The input that defines the end of the subset.
* @returns A list of field/input labels for the given block.
*/
export function getInputLabelsSubset(
block: BlockSvg,
input: Input,
verbosity = Verbosity.STANDARD,
): string[] {
function getInputLabelsSubset(block: BlockSvg, input: Input): string[] {
Comment thread
lizschwab marked this conversation as resolved.
const inputIndex = block.inputList.indexOf(input);
if (inputIndex === -1) {
throw new Error(
Expand All @@ -271,7 +271,7 @@ export function getInputLabelsSubset(
.filter((input) => input.isVisible())
.map(
(input) =>
input.getLabel(verbosity) ||
input.getLabel(Verbosity.TERSE) ||
Msg['INPUT_LABEL_INDEX'].replace(
'%1',
(input.getIndex() + 1).toString(),
Expand Down Expand Up @@ -404,11 +404,7 @@ function computeMoveConnectionLabel(
// If the input doesn't have a custom ARIA label, compute one using the labels from
// nearby fields.
if (!inputLabel) {
const labels = getInputLabelsSubset(
conn.getSourceBlock(),
input,
Verbosity.TERSE,
);
const labels = getInputLabelsSubset(conn.getSourceBlock(), input);
if (!labels.length) return baseLabel;

inputLabel = labels.join(', ');
Expand Down
36 changes: 36 additions & 0 deletions packages/blockly/core/field_label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,42 @@ export class FieldLabel extends Field<string> {
}
}

/**
* Computes a descriptive ARIA label to represent this field with configurable
* verbosity.
*
* A 'verbose' label includes type information, if available, whereas a
* non-verbose label only contains the field's value.
*
* Note that this will always return the latest representation of the field's
* label which may differ from any previously set ARIA label for the field
* itself. Implementations are largely responsible for ensuring that the
* field's ARIA label is set correctly at relevant moments in the field's
* lifecycle (such as when its value changes).
*
* Finally, it is never guaranteed that implementations use the label returned
* by this method for their actual ARIA label. Some implementations may rely
* on other contexts to convey information like the field's value. Example:
* checkboxes represent their checked/non-checked status (i.e. value) through
* a separate ARIA property.
*
* Unlike other built-in fields, FieldLabel does return an empty string when its
* value is empty. This is because empty labels are sometimes used for layout
* purposes.
*
* @param includeTypeInfo Whether to include the field's type information in
* the returned label, if available.
*/
override computeAriaLabel(includeTypeInfo: boolean = true): string {
const ariaTypeName = includeTypeInfo ? this.getAriaTypeName() : null;
const ariaValue = this.getAriaValue() ?? '';

if (ariaTypeName) {
return `${ariaTypeName}: ${ariaValue}`;
}
return ariaValue;
}

/**
* Ensure that the input value casts to a valid string.
*
Expand Down
11 changes: 8 additions & 3 deletions packages/blockly/core/inputs/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,12 +418,17 @@ export class Input {
}

/**
* Returns the index of this input on its source block.
* Returns the index of this input, excluding inputs without connections, on its
* source block.
*
* @internal
*/
getIndex(): number {
const inputs = this.getSourceBlock().inputList;
return inputs.indexOf(this);
const noConnectionInputTypes = [inputTypes.DUMMY, inputTypes.END_ROW];
const allInputs = this.getSourceBlock().inputList;
const allConnectionInputs = allInputs.filter(
(input) => !noConnectionInputTypes.includes(input.type),
);
return allConnectionInputs.indexOf(this);
}
}
44 changes: 44 additions & 0 deletions packages/blockly/tests/mocha/keyboard_movement_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1403,6 +1403,50 @@ suite('Keyboard-driven movement', function () {

cancelMove(this.workspace);
});
test('ignores dummy inputs when disambiguating', function () {
const subListBlock = this.workspace.newBlock('lists_getSublist');
subListBlock.initSvg();
subListBlock.render();
const mathBlock = this.workspace.newBlock('math_number');
mathBlock.initSvg();
mathBlock.render();

Blockly.getFocusManager().focusNode(mathBlock);
startMove(this.workspace);
this.clock.tick(10);
this.moveAndAssert(
moveRight,
['Moving', 'to', 'list, get sub-list from', 'input 2'],
['input 3'],
);
this.moveAndAssert(
moveRight,
['Moving', 'to', 'list, get sub-list from', 'input 3'],
['input 4'],
);

cancelMove(this.workspace);
});
test('ignores end row inputs when disambiguating', function () {
const compare = this.workspace.newBlock('logic_compare');
compare.appendDummyInput('END_ROW');
compare.moveInputBefore('END_ROW', 'A');
compare.initSvg();
compare.render();
const boolean = this.workspace.newBlock('logic_boolean');
boolean.initSvg();
boolean.render();

Blockly.getFocusManager().focusNode(boolean);
startMove(this.workspace);
this.clock.tick(10);
this.moveAndAssert(
moveRight,
['Moving', 'to', 'input 1', '='],
[this.getBlockLabel(boolean)],
);
cancelMove(this.workspace);
});
});
});

Expand Down