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
3 changes: 3 additions & 0 deletions packages/blockly/core/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2165,6 +2165,9 @@ export class Block {
input.setAlign(alignment);
}
}
if (element['ariaLabelText']) {
input.setAriaLabelProvider(element['ariaLabelText']);
}
return input;
}

Expand Down
6 changes: 5 additions & 1 deletion packages/blockly/core/block_aria_composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,11 @@ export function getInputLabels(
): string[] {
return block.inputList
.filter((input) => input.isVisible())
.map((input) => input.getLabel(verbosity));
.map((input) =>
input.getAriaLabelText() !== null
? input.getAriaLabelText()!
: input.getLabel(verbosity),
);
}

/**
Expand Down
43 changes: 43 additions & 0 deletions packages/blockly/core/inputs/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@ import type {Field} from '../field.js';
import * as fieldRegistry from '../field_registry.js';
import {RenderedConnection} from '../rendered_connection.js';
import {Verbosity} from '../utils/aria.js';
import * as parsing from '../utils/parsing.js';
import {Align} from './align.js';
import {inputTypes} from './input_types.js';

/**
* Represents a string or a function that returns a string which can be used as a
* custom ARIA string to represent an Input, or null if the default fallback should
* be used. See setAriaLabelProvider for more context.
*/
export type AriaLabelProvider = ((input: Input) => string | null) | string;

/** Class for an input with optional fields. */
export class Input {
fieldRow: Field[] = [];
Expand All @@ -35,6 +43,9 @@ export class Input {
/** Is the input visible? */
private visible = true;

/** The AriaLabelProvider */
private ariaLabelProvider: AriaLabelProvider | null = null;

public readonly type: inputTypes = inputTypes.CUSTOM;

public connection: Connection | null = null;
Expand Down Expand Up @@ -273,6 +284,38 @@ export class Input {
}
}

/**
* Sets a custom ARIA label provider for this input, or null if it should be reset
* to use the default method.
*
* Inputs do not compute ARIA contexts directly, so the set provider will be used
* in select cases when the Input needs to be represented (such as for parts of a
* block label or for connections). Note that overriding this provider will not
* recompute any already constructed ARIA labels, and it cannot be assumed that the
* provider will be called any particular number of times during label
* recomputation. As such, implementations should make sure to provide a
* deterministic and idempotent ARIA representation each time the provider is
* called for a given input. It's also fine to reuse providers across multiple
* Input implementations.
*/
setAriaLabelProvider(provider: AriaLabelProvider | null) {
this.ariaLabelProvider = provider;
}

/**
* Returns the string from the custom ARIA label provider set, or null if the default label (from the field row) should
* be used. See setAriaLabelProvider for more context.
*/
getAriaLabelText(): string | null {
if (!this.ariaLabelProvider) {
return null;
} else if (typeof this.ariaLabelProvider === 'string') {
return parsing.replaceMessageReferences(this.ariaLabelProvider);
} else {
return this.ariaLabelProvider(this);
}
}

/**
* Initializes the fields on this input for a headless block.
*
Expand Down
76 changes: 76 additions & 0 deletions packages/blockly/tests/mocha/input_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import {assert} from '../../node_modules/chai/index.js';
import {createRenderedBlock} from './test_helpers/block_definitions.js';
import {
sharedTestSetup,
sharedTestTeardown,
Expand Down Expand Up @@ -293,4 +294,79 @@ suite('Inputs', function () {
assert.deepEqual(this.dummy.fieldRow, [this.b, this.c]);
});
});
suite('ARIA', function () {
setup(function () {
Blockly.defineBlocksWithJsonArray([
{
'type': 'row_block',
'message0': '%1',
'args0': [
{
'type': 'input_value',
'name': 'INPUT',
},
],
'output': null,
},
]);
});
test('Set input ARIA Label Provider', function () {
const customLabel = 'custom ARIA label';
// Using a text input as it will return a default ARIA label
this.block
.appendValueInput('NAME')
.appendField(new Blockly.FieldTextInput('text'), 'NAME')
.setAriaLabelProvider((input) => customLabel);

const label = this.block.getAriaLabel();

assert.include(label, customLabel);
assert.notInclude(label, 'text');
});
test('Set input ARIA Label Provider from JSON', function () {
const customLabel = 'custom ARIA label';
Blockly.defineBlocksWithJsonArray([
{
'type': 'input_aria_block',
'message0': '%1 %2',
'args0': [
{
'type': 'field_input',
'name': 'NAME',
'text': 'text',
},
{
'type': 'input_value',
'name': 'NAME',
'ariaLabelText': customLabel,
},
],
},
]);

this.block = this.workspace.newBlock('input_aria_block');
const label = this.block.getAriaLabel();

assert.include(label, customLabel);
});
test('Set input ARIA Label Provider to null', function () {
const blockA = createRenderedBlock(this.workspace, 'row_block');
const blockB = createRenderedBlock(this.workspace, 'row_block');

blockA
.appendValueInput('NAME')
.appendField(new Blockly.FieldTextInput('text'), 'NAME')
.setAriaLabelProvider(null);
blockB
.appendValueInput('NAME')
.appendField(new Blockly.FieldTextInput('text'), 'NAME');

const labelA = blockA.getAriaLabel();
const labelB = blockB.getAriaLabel();

// The label should be the same between a block created with a null
// AriaLabelProvider and without setting the provider (the default label)
assert.equal(labelA, labelB);
});
});
});