diff --git a/packages/blockly/core/field.ts b/packages/blockly/core/field.ts index 2b48b91f8..a04b58134 100644 --- a/packages/blockly/core/field.ts +++ b/packages/blockly/core/field.ts @@ -366,9 +366,11 @@ export abstract class Field * checkboxes represent their checked/non-checked status (i.e. value) through * a separate ARIA property. * - * It's not expected that this method, under normal operations, returns an empty - * string. If the field's value is empty then it will return a localized - * placeholder indicating that its value is empty. + * If the field's value is empty then it will return a localized placeholder + * indicating that its value is empty. If this method returns an empty string, + * the output will be ignored when composing the block-level ARIA label. Make + * sure you want your label hidden from screenreaders before returning an + * empty string. * * @param includeTypeInfo Whether to include the field's type information in * the returned label, if available. diff --git a/packages/blockly/core/field_image.ts b/packages/blockly/core/field_image.ts index 7c34ac215..4ed467b0e 100644 --- a/packages/blockly/core/field_image.ts +++ b/packages/blockly/core/field_image.ts @@ -315,6 +315,36 @@ export class FieldImage extends Field { return this.altText || null; } + /** + * 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. + * + * Returns an empty string on clickable images (buttons), as we do not want to + * include image buttons on the block-level ARIA label. When the button is + * focused the label is set in recomputeAriaContext below. + * + * @param includeTypeInfo Whether to include the field's type information in + * the returned label, if available. + */ + override computeAriaLabel(includeTypeInfo: boolean): string { + return this.isClickable() ? '' : super.computeAriaLabel(includeTypeInfo); + } + /** * Customizes label and sets additional aria state. */ @@ -333,6 +363,11 @@ export class FieldImage extends Field { aria.clearState(focusableElement, aria.State.LABEL); return false; } + // For clickable images we need to set the label to the alt text here as + // we have overridden the computeAriaLabel to return an empty string. This + // will set it at the element level. + const label = this.getAriaValue() || ''; + aria.setState(focusableElement, aria.State.LABEL, label); return true; } } diff --git a/packages/blockly/tests/mocha/field_image_test.js b/packages/blockly/tests/mocha/field_image_test.js index 6de4e9bf4..2c317f218 100644 --- a/packages/blockly/tests/mocha/field_image_test.js +++ b/packages/blockly/tests/mocha/field_image_test.js @@ -379,7 +379,7 @@ suite('Image Fields', function () { }); }); suite('Image with click handler', function () { - test('Field has field type name in ARIA label', function () { + test('Field has alt text ARIA label', function () { const block = this.workspace.newBlock('test_images_clickhandler'); const field = block.getField('IMAGE'); block.initSvg(); @@ -387,9 +387,9 @@ suite('Image Fields', function () { const focusableElement = field.getFocusableElement(); const fieldLabel = focusableElement.getAttribute('aria-label'); - assert.include(fieldLabel, 'image:'); + assert.include(fieldLabel, 'image with click handler'); }); - test('Focusable element has role of button', function () { + test('Focusable element has role of button', function () { const block = this.workspace.newBlock('test_images_clickhandler'); const field = block.getField('IMAGE'); block.initSvg(); @@ -399,6 +399,17 @@ suite('Image Fields', function () { const role = focusableElement.getAttribute('role'); assert.equal(role, 'button'); }); + test('Block omits image button from ARIA label', function () { + const block = this.workspace.newBlock('test_images_clickhandler'); + const field = block.getField('IMAGE'); + block.initSvg(); + block.render(); + + const blockFocusableElement = block.getFocusableElement(); + const blockLabel = blockFocusableElement.getAttribute('aria-label'); + assert.notInclude(blockLabel, 'Image:'); + assert.notInclude(blockLabel, 'image with click handler'); + }); }); }); });