Skip to content

Commit 695e071

Browse files
authored
fix: improve fallback behavior for custom input labels (#9942)
* fix: refactor custom input labels to simplify * fix: handle bad field image config from block factory * chore: remove stray log * fix: fix move mode labels * fix: dont use numbered inputs for dummy and end row inputs * chore: fix test
1 parent 2656863 commit 695e071

10 files changed

Lines changed: 245 additions & 70 deletions

File tree

packages/blockly/core/block_aria_composer.ts

Lines changed: 86 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,18 @@ export enum ConnectionPreposition {
5050
* The returned label will be specialized based on whether the block is part of a
5151
* flyout.
5252
*
53+
* Custom input labels (from {@link Input.setAriaLabelProvider}) are not included
54+
* here; they are used only in move-mode disambiguation and parent-input context
55+
* via {@link Input.getAriaLabelText}.
56+
*
5357
* @internal
5458
* @param block The block for which an ARIA representation should be created.
5559
* @param verbosity How much detail to include in the description.
56-
* @param useCustomInputLabels Whether to use custom labels for inputs, if they
57-
* exist. We don't want to do this when just reading a block's label, but do
58-
* want to in other scenarios such as move mode.
5960
* @returns The ARIA representation for the specified block.
6061
*/
6162
export function computeAriaLabel(
6263
block: BlockSvg,
6364
verbosity = Verbosity.STANDARD,
64-
useCustomInputLabels = true,
6565
) {
6666
if (block.isSimpleReporter()) {
6767
// special case for full-block field blocks.
@@ -73,7 +73,7 @@ export function computeAriaLabel(
7373
return [
7474
verbosity >= Verbosity.STANDARD && getBeginStackLabel(block),
7575
getParentInputLabel(block),
76-
...getInputLabels(block, verbosity, useCustomInputLabels),
76+
...getInputLabels(block, verbosity),
7777
verbosity === Verbosity.LOQUACIOUS && getParentToolboxCategoryLabel(block),
7878
verbosity >= Verbosity.STANDARD && getDisabledLabel(block),
7979
verbosity >= Verbosity.STANDARD && getCollapsedLabel(block),
@@ -197,6 +197,10 @@ export function computeFieldRowLabel(
197197
* statement input of the parent block (in this case, the label
198198
* would be redundant with the parent block's label)
199199
*
200+
* For statement inputs without their own field labels, labels from other
201+
* inputs in the same statement section are included (via
202+
* {@link getInputLabelsSubset}), consistent with move-target disambiguation.
203+
*
200204
* For statement inputs, the resolved label (whether custom or fallback) is
201205
* wrapped in the "Begin %1" prefix so the readout indicates that the child
202206
* block starts the body of the statement input.
@@ -235,7 +239,14 @@ function getParentInputLabel(block: BlockSvg) {
235239
return undefined;
236240
}
237241

238-
inputLabel = computeFieldRowLabel(parentInput, true);
242+
const sectionLabels = getInputLabelsSubset(
243+
parentBlock as BlockSvg,
244+
parentInput,
245+
);
246+
if (!sectionLabels.length) {
247+
return undefined;
248+
}
249+
inputLabel = sectionLabels.join(', ');
239250
}
240251

241252
if (parentInput.type === inputTypes.STATEMENT) {
@@ -272,21 +283,18 @@ function getBeginStackLabel(block: BlockSvg) {
272283
* their contents are returned as a single item in the array per top-level
273284
* input.
274285
*
275-
* Generally, if a custom label for an input is provided, that is preferred.
276-
* However, we do not surface the custom labels when simply reading the text of
277-
* the block. They are used as supplementary information for situations like
278-
* move mode or when an input itself is focused.
286+
* Uses derived labels only (field row text and connected block content via
287+
* {@link Input.getLabel}). Custom input labels are not included; see
288+
* {@link Input.getAriaLabelText} for move-mode and parent-input usage.
279289
*
280290
* @internal
281291
* @param block The block to retrieve a list of field/input labels for.
282-
* @param verbosity
283-
* @param useCustomLabels whether to use the custom label for an input, if it's present.
292+
* @param verbosity How much detail to include in each input label.
284293
* @returns A list of field/input labels for the given block.
285294
*/
286295
export function getInputLabels(
287296
block: BlockSvg,
288297
verbosity = Verbosity.STANDARD,
289-
useCustomLabels = true,
290298
): string[] {
291299
const visibleInputs = block.inputList.filter((input) => input.isVisible());
292300
let inputsToLabel = visibleInputs;
@@ -306,15 +314,13 @@ export function getInputLabels(
306314
}
307315
}
308316

309-
return inputsToLabel.map((input) => {
310-
const customLabel = useCustomLabels ? input.getAriaLabelText() : null;
311-
return customLabel ?? input.getLabel(verbosity, useCustomLabels);
312-
});
317+
return inputsToLabel.map((input) => input.getLabel(verbosity));
313318
}
314319

315320
/**
316-
* Returns a subset of labels for inputs on the given block, ending at the
317-
* specified input.
321+
* Returns a subset of derived labels for inputs on the given block, ending at
322+
* the specified input. Used to disambiguate move targets and connection
323+
* highlights when no custom label is set.
318324
*
319325
* The subset is determined based on the input type:
320326
* - For non-statement inputs, only the label for the given input is returned.
@@ -323,41 +329,74 @@ export function getInputLabels(
323329
* begins immediately after the previous statement input, or at the start of
324330
* the block if none exists.
325331
*
332+
* Label resolution (see also {@link computeMoveConnectionLabel}):
333+
* 1. Custom labels ({@link Input.getAriaLabelText}) are handled by callers, not here.
334+
* 2. Derived labels from {@link Input.getLabel} (field row + child blocks).
335+
* 3. Numbered fallback ({@link Msg.INPUT_LABEL_INDEX}) when tier 2 is empty.
336+
* For the statement target input, the fallback is omitted if any earlier
337+
* input in the subset already produced a label.
338+
*
326339
* @internal
327340
* @param block The block to retrieve a list of field/input labels for.
328341
* @param input The input that defines the end of the subset.
329342
* @returns A list of field/input labels for the given block.
330343
*/
331-
export function getInputLabelsSubset(
332-
block: BlockSvg,
333-
input: Input,
334-
includeFallbackLabels = true,
335-
): string[] {
344+
export function getInputLabelsSubset(block: BlockSvg, input: Input): string[] {
336345
const inputIndex = block.inputList.indexOf(input);
337346
if (inputIndex === -1) {
338347
throw new Error(
339348
`Input with name "${input.name}" not found on block with id "${block.id}".`,
340349
);
341350
}
351+
const isStatementTarget = input.type === inputTypes.STATEMENT;
342352

343-
const startIndex =
344-
input.type === inputTypes.STATEMENT
345-
? findStartOfStatementSection(block.inputList, inputIndex)
346-
: inputIndex;
353+
const startIndex = isStatementTarget
354+
? findStartOfStatementSection(block.inputList, inputIndex)
355+
: inputIndex;
347356

348-
return block.inputList
357+
// For statement inputs, we include all visible inputs from the start
358+
// of the current statement section up to and including the target input.
359+
// For non-statement inputs, this will just be the target input itself.
360+
const inputsInSubset = block.inputList
349361
.slice(startIndex, inputIndex + 1)
350-
.filter((input) => input.isVisible())
351-
.map(
352-
(input) =>
353-
input.getLabel(Verbosity.TERSE, false) ||
354-
(includeFallbackLabels
355-
? Msg['INPUT_LABEL_INDEX'].replace(
356-
'%1',
357-
(input.getIndex() + 1).toString(),
358-
)
359-
: undefined),
360-
)
362+
.filter((subsetInput) => subsetInput.isVisible());
363+
364+
// The derived labels are based on the field row and any connected child
365+
// blocks.
366+
const derivedLabels = inputsInSubset.map((subsetInput) =>
367+
subsetInput.getLabel(Verbosity.TERSE),
368+
);
369+
370+
// For statement inputs, we only include the fallback label ("input %1")
371+
// for the target input if no preceding input in the subset has a label.
372+
// This prevents, e.g., "else" statement inputs from being read as "else, input 2".
373+
const precedingLabelsProvideContext =
374+
isStatementTarget && derivedLabels.slice(0, -1).some((label) => !!label);
375+
376+
return derivedLabels
377+
.map((label, index) => {
378+
if (label) {
379+
return label;
380+
}
381+
const subsetInput = inputsInSubset[index];
382+
// Dummy and end-row inputs are not connection inputs; getIndex() is -1
383+
// and would produce a misleading "input 0" fallback label.
384+
if (
385+
subsetInput.type === inputTypes.DUMMY ||
386+
subsetInput.type === inputTypes.END_ROW
387+
) {
388+
return undefined;
389+
}
390+
const isStatementTargetInput =
391+
isStatementTarget && index === derivedLabels.length - 1;
392+
if (isStatementTargetInput && precedingLabelsProvideContext) {
393+
return undefined;
394+
}
395+
return Msg['INPUT_LABEL_INDEX'].replace(
396+
'%1',
397+
(subsetInput.getIndex() + 1).toString(),
398+
);
399+
})
361400
.filter((label) => label !== undefined);
362401
}
363402

@@ -468,7 +507,13 @@ function getAnnouncementTemplate(preposition: ConnectionPreposition): string {
468507
}
469508

470509
/**
471-
* Returns a label for a connection includes either a block label, input label or both.
510+
* Returns a label for a connection that includes either a block label, input
511+
* label, or both.
512+
*
513+
* Input label resolution:
514+
* 1. Custom label from {@link Input.getAriaLabelText} when set.
515+
* 2. Otherwise derived labels from {@link getInputLabelsSubset} (field row,
516+
* child blocks, and numbered fallbacks as needed).
472517
*
473518
* @param conn The connection to generate a label for.
474519
* @param baseLabel An optional block label to include in the returned string.
@@ -483,8 +528,6 @@ function computeMoveConnectionLabel(
483528

484529
let inputLabel = input.getAriaLabelText();
485530

486-
// If the input doesn't have a custom ARIA label, compute one using the labels from
487-
// nearby fields.
488531
if (!inputLabel) {
489532
const labels = getInputLabelsSubset(conn.getSourceBlock(), input);
490533
if (!labels.length) return baseLabel;

packages/blockly/core/block_svg.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,7 +2034,7 @@ export class BlockSvg
20342034
* @returns An accessibility description of this block.
20352035
*/
20362036
getAriaLabel(verbosity: aria.Verbosity) {
2037-
return computeAriaLabel(this, verbosity, false);
2037+
return computeAriaLabel(this, verbosity);
20382038
}
20392039

20402040
/**
@@ -2051,7 +2051,7 @@ export class BlockSvg
20512051
block = block.getNextBlock();
20522052
}
20532053
if (count <= 1) {
2054-
return computeAriaLabel(this, aria.Verbosity.TERSE, false);
2054+
return computeAriaLabel(this, aria.Verbosity.TERSE);
20552055
}
20562056

20572057
const labelTemplate = Msg['BLOCK_LABEL_STACK_BLOCKS'];

packages/blockly/core/dragging/block_drag_strategy.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1277,7 +1277,6 @@ export class BlockDragStrategy implements IDragStrategy {
12771277
* @param block The block to re-disable, if applicable.
12781278
*/
12791279
private redisableAllDraggedBlocks(block: BlockSvg) {
1280-
console.log('redisable');
12811280
const oldUndo = eventUtils.getRecordUndo();
12821281
eventUtils.setRecordUndo(false);
12831282
block.getDescendants(false).forEach((descendant) => {

packages/blockly/core/field_image.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,18 @@ export class FieldImage extends Field<string> {
113113

114114
if (config) {
115115
this.configure_(config);
116+
} else if (isFieldImageConfig(alt)) {
117+
// Block Factory and some hand-written blocks pass a config object as the
118+
// fourth argument instead of using the seventh `config` parameter.
119+
// This is wrong, and typescript will complain about it, but handle it
120+
// for backwards compatibility.
121+
this.configure_(alt);
116122
} else {
117123
this.flipRtl = !!flipRtl;
118-
this.altText = parsing.replaceMessageReferences(alt) || '';
124+
this.altText =
125+
typeof alt === 'string'
126+
? parsing.replaceMessageReferences(alt) || ''
127+
: '';
119128
}
120129
this.setValue(parsing.replaceMessageReferences(src));
121130
}
@@ -372,6 +381,22 @@ export class FieldImage extends Field<string> {
372381
}
373382
}
374383

384+
/**
385+
* Returns whether a value is a FieldImage config object passed in place of alt
386+
* text (e.g. `{alt: '*', flipRtl: false}`). You shouldn't do this on purpose,
387+
* but the block factory generates block definitions in this format.
388+
*
389+
* @param value The value to test.
390+
*/
391+
function isFieldImageConfig(value: unknown): value is FieldImageConfig {
392+
return (
393+
typeof value === 'object' &&
394+
value !== null &&
395+
!Array.isArray(value) &&
396+
('alt' in value || 'flipRtl' in value)
397+
);
398+
}
399+
375400
fieldRegistry.register('field_image', FieldImage);
376401

377402
FieldImage.prototype.DEFAULT_VALUE = '';

packages/blockly/core/inputs/input.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -399,13 +399,14 @@ export class Input {
399399
}
400400

401401
/**
402-
* Returns an accessibility label describing this input, including the labels
403-
* of any fields on the input and the labels of any connected blocks, to help
404-
* disambiguate this input from others on the same block.
402+
* Returns a derived accessibility label for this input: field row text plus
403+
* labels of any connected child blocks. Does not include custom labels from
404+
* {@link getAriaLabelText}; those are used in move-mode and parent-input
405+
* context only.
405406
*
406407
* @internal
407408
*/
408-
getLabel(verbosity = Verbosity.STANDARD, useCustomLabels = true): string {
409+
getLabel(verbosity = Verbosity.STANDARD): string {
409410
if (!this.isVisible()) return '';
410411

411412
const labels = computeFieldRowLabel(this, false, verbosity);
@@ -414,11 +415,7 @@ export class Input {
414415
const childBlock = this.connection.targetBlock();
415416
if (childBlock && !childBlock.isInsertionMarker()) {
416417
labels.push(
417-
getInputLabels(
418-
childBlock as BlockSvg,
419-
verbosity,
420-
useCustomLabels,
421-
).join(', '),
418+
getInputLabels(childBlock as BlockSvg, verbosity).join(', '),
422419
);
423420
}
424421
}

packages/blockly/core/rendered_connection.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -355,14 +355,11 @@ export class RenderedConnection
355355

356356
// Use the custom label for an input if it exists, otherwise use the
357357
// "field row" approach to get the default label for the input.
358-
// Don't include the "input 1" fallback for default labels, since
359-
// the input is already being described as a statement or value input.
360358
const parentInputLabel =
361359
parentInput?.getAriaLabelText() ??
362360
getInputLabelsSubset(
363361
parentInput.getSourceBlock() as BlockSvg,
364362
parentInput,
365-
false,
366363
).join(', ');
367364
if (this.type === ConnectionType.NEXT_STATEMENT) {
368365
aria.setState(

packages/blockly/core/shortcut_items.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -807,11 +807,7 @@ export function registerFocusToolbox() {
807807
*/
808808
export function registerReadInformation() {
809809
const announceBlockInformation = (block: BlockSvg) => {
810-
const description = computeAriaLabel(
811-
block,
812-
aria.Verbosity.LOQUACIOUS,
813-
false,
814-
);
810+
const description = computeAriaLabel(block, aria.Verbosity.LOQUACIOUS);
815811
aria.announceDynamicAriaState(description);
816812
};
817813

@@ -898,14 +894,12 @@ export function registerReadExtendedInformation() {
898894
}
899895

900896
if (startBlock !== block) {
901-
toAnnounce.push(
902-
computeAriaLabel(startBlock, aria.Verbosity.TERSE, false),
903-
);
897+
toAnnounce.push(computeAriaLabel(startBlock, aria.Verbosity.TERSE));
904898
}
905899

906900
let parent = startBlock.getParent();
907901
while (parent) {
908-
toAnnounce.push(computeAriaLabel(parent, aria.Verbosity.TERSE, false));
902+
toAnnounce.push(computeAriaLabel(parent, aria.Verbosity.TERSE));
909903
parent = parent.getParent();
910904
}
911905

@@ -917,7 +911,7 @@ export function registerReadExtendedInformation() {
917911
toAnnounce.push(
918912
Msg['CURRENT_BLOCK_ANNOUNCEMENT'].replace(
919913
'%1',
920-
computeAriaLabel(block, aria.Verbosity.TERSE, false),
914+
computeAriaLabel(block, aria.Verbosity.TERSE),
921915
),
922916
);
923917
}

packages/blockly/demos/blockfactory/factory_utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ FactoryUtils.getFieldsJs_ = function(block) {
458458
}
459459
break;
460460
case 'field_image':
461-
// Result: new Blockly.FieldImage('http://...', 80, 60, '*')
461+
// Result: new Blockly.FieldImage('http://...', 80, 60, {alt: '*', flipRtl: false})
462462
var src = JSON.stringify(block.getFieldValue('SRC'));
463463
var width = Number(block.getFieldValue('WIDTH'));
464464
var height = Number(block.getFieldValue('HEIGHT'));

0 commit comments

Comments
 (0)