fix: Block-level ARIA labels no longer include clickable image descri… (#9844)

* fix: Block-level ARIA labels no longer include clickable image descriptions

* fixed merge conflicts, added/updated tests
This commit is contained in:
lizschwab
2026-05-12 13:38:31 -07:00
committed by GitHub
parent 83f2c9be36
commit e32ef656bd
3 changed files with 54 additions and 6 deletions
+5 -3
View File
@@ -366,9 +366,11 @@ export abstract class Field<T = any>
* 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.
+35
View File
@@ -315,6 +315,36 @@ export class FieldImage extends Field<string> {
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<string> {
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;
}
}
@@ -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');
});
});
});
});