mirror of
https://github.com/google/blockly.git
synced 2026-05-01 01:20:13 +02:00
feat: ARIA for other field classes (#9772)
* feat: ARIA for other field classes * fix: code review
This commit is contained in:
@@ -117,12 +117,12 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
||||
this.type === 'variables_get' ||
|
||||
this.type === 'variables_get_reporter'
|
||||
) {
|
||||
const name = this.getField('VAR')!.getText();
|
||||
const renameOption = {
|
||||
text: Msg['RENAME_VARIABLE'],
|
||||
text: Msg['RENAME_VARIABLE'].replace('%1', name),
|
||||
enabled: true,
|
||||
callback: renameOptionCallbackFactory(this),
|
||||
};
|
||||
const name = this.getField('VAR')!.getText();
|
||||
const deleteOption = {
|
||||
text: Msg['DELETE_VARIABLE'].replace('%1', name),
|
||||
enabled: true,
|
||||
|
||||
@@ -117,12 +117,12 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
||||
this.type === 'variables_get_dynamic' ||
|
||||
this.type === 'variables_get_reporter_dynamic'
|
||||
) {
|
||||
const name = this.getField('VAR')!.getText();
|
||||
const renameOption = {
|
||||
text: Msg['RENAME_VARIABLE'],
|
||||
text: Msg['RENAME_VARIABLE'].replace('%1', name),
|
||||
enabled: true,
|
||||
callback: renameOptionCallbackFactory(this),
|
||||
};
|
||||
const name = this.getField('VAR')!.getText();
|
||||
const deleteOption = {
|
||||
text: Msg['DELETE_VARIABLE'].replace('%1', name),
|
||||
enabled: true,
|
||||
|
||||
@@ -373,7 +373,7 @@ export abstract class Field<T = any>
|
||||
* @param includeTypeInfo Whether to include the field's type information in
|
||||
* the returned label, if available.
|
||||
*/
|
||||
computeAriaLabel(includeTypeInfo: boolean = false): string {
|
||||
computeAriaLabel(includeTypeInfo: boolean = true): string {
|
||||
const ariaTypeName = includeTypeInfo ? this.getAriaTypeName() : null;
|
||||
let ariaValue = this.getAriaValue();
|
||||
if (ariaValue === null || ariaValue === '') {
|
||||
|
||||
@@ -16,6 +16,8 @@ import './events/events_block_change.js';
|
||||
|
||||
import {Field, FieldConfig, FieldValidator} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {Msg} from './msg.js';
|
||||
import {aria} from './utils.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
|
||||
type BoolString = 'TRUE' | 'FALSE';
|
||||
@@ -111,6 +113,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
const textElement = this.getTextElement();
|
||||
dom.addClass(this.fieldGroup_!, 'blocklyCheckboxField');
|
||||
textElement.style.display = this.value_ ? 'block' : 'none';
|
||||
this.recomputeAriaContext();
|
||||
}
|
||||
|
||||
override render_() {
|
||||
@@ -170,6 +173,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
if (this.textElement_) {
|
||||
this.textElement_.style.display = this.value_ ? 'block' : 'none';
|
||||
}
|
||||
this.recomputeAriaContext();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,6 +217,39 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
return !!value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an ARIA-friendly label representation of this field's type.
|
||||
*
|
||||
* Implementations are responsible for, and encouraged to, return a localized
|
||||
* version of the ARIA representation of the field's type.
|
||||
*
|
||||
* @returns An ARIA representation of the field's type or a default if it is
|
||||
* unspecified.
|
||||
*/
|
||||
override getAriaTypeName(): string {
|
||||
return this.ariaTypeName || Msg['ARIA_TYPE_FIELD_CHECKBOX'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an ARIA-friendly label representation of this field's value.
|
||||
*
|
||||
* Implementations are responsible for, and encouraged to, return a localized
|
||||
* version of the ARIA representation of the field's value.
|
||||
*
|
||||
* The FieldCheckbox implementation is not used for the actual ARIA label of
|
||||
* the field, since the checked state is already included in the ARIA checked
|
||||
* state, but it is used for the ARIA label of its source block.
|
||||
*
|
||||
* @returns An ARIA representation of the field's text.
|
||||
*/
|
||||
override getAriaValue(): string | null {
|
||||
// return null;
|
||||
const checked = this.convertValueToBool(this.value_);
|
||||
return checked
|
||||
? Msg['FIELD_LABEL_CHECKBOX_CHECKED']
|
||||
: Msg['FIELD_LABEL_CHECKBOX_UNCHECKED'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a FieldCheckbox from a JSON arg object.
|
||||
*
|
||||
@@ -228,6 +265,31 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
// 'override' the static fromJson method.
|
||||
return new this(options.checked, undefined, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the ARIA role and label for this field.
|
||||
*/
|
||||
protected recomputeAriaContext(): void {
|
||||
const focusableElement = this.getClickTarget_();
|
||||
if (!focusableElement) return;
|
||||
|
||||
if (this.getSourceBlock()?.isInFlyout) {
|
||||
aria.setState(focusableElement, aria.State.HIDDEN, true);
|
||||
return;
|
||||
}
|
||||
|
||||
aria.setState(focusableElement, aria.State.HIDDEN, false);
|
||||
aria.setRole(focusableElement, aria.Role.CHECKBOX);
|
||||
const checked = this.convertValueToBool(this.value_);
|
||||
aria.setState(focusableElement, aria.State.CHECKED, checked);
|
||||
|
||||
// Checkbox fields do not use this.computeAriaLabel(), because the
|
||||
// included 'checked' or 'not checked' state in the ARIA label would
|
||||
// be redundant with the ARIA checked state.
|
||||
const label = this.getAriaTypeName();
|
||||
|
||||
aria.setState(focusableElement, aria.State.LABEL, label);
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_checkbox', FieldCheckbox);
|
||||
|
||||
@@ -884,7 +884,7 @@ export class FieldDropdown extends Field<string> {
|
||||
*
|
||||
* @returns An ARIA representation of the field's text.
|
||||
*/
|
||||
override getAriaValue(): string | null {
|
||||
override getAriaValue(): string {
|
||||
// Note: This fallback is effectively unreachable since computeOptionAriaLabel
|
||||
// always returns a non-empty string for non-separator options. It exists as a
|
||||
// defensive safeguard.
|
||||
@@ -920,7 +920,7 @@ export class FieldDropdown extends Field<string> {
|
||||
/**
|
||||
* Recomputes the ARIA role and label for this field.
|
||||
*/
|
||||
private recomputeAriaContext(): void {
|
||||
protected recomputeAriaContext(): void {
|
||||
const focusableElement = this.getFocusableElement();
|
||||
if (!focusableElement) return;
|
||||
|
||||
@@ -934,7 +934,7 @@ export class FieldDropdown extends Field<string> {
|
||||
// editing mode that can be activated.
|
||||
aria.setRole(focusableElement, aria.Role.BUTTON);
|
||||
|
||||
const label = this.computeAriaLabel(false);
|
||||
const label = this.computeAriaLabel(true);
|
||||
|
||||
aria.setState(focusableElement, aria.State.LABEL, label);
|
||||
aria.setState(focusableElement, aria.State.HASPOPUP, 'listbox');
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
import {Field, FieldConfig} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {Msg} from './msg.js';
|
||||
import {aria} from './utils.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as parsing from './utils/parsing.js';
|
||||
import {Size} from './utils/size.js';
|
||||
@@ -157,6 +159,8 @@ export class FieldImage extends Field<string> {
|
||||
if (this.clickHandler) {
|
||||
this.imageElement.style.cursor = 'pointer';
|
||||
}
|
||||
|
||||
this.recomputeAriaContext();
|
||||
}
|
||||
|
||||
override updateSize_() {}
|
||||
@@ -186,6 +190,7 @@ export class FieldImage extends Field<string> {
|
||||
if (this.imageElement) {
|
||||
this.imageElement.setAttributeNS(dom.XLINK_NS, 'xlink:href', this.value_);
|
||||
}
|
||||
this.recomputeAriaContext();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,6 +288,59 @@ export class FieldImage extends Field<string> {
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an ARIA-friendly label representation of this field's type.
|
||||
*
|
||||
* Implementations are responsible for, and encouraged to, return a localized
|
||||
* version of the ARIA representation of the field's type.
|
||||
*
|
||||
* @returns An ARIA representation of the field's type or a default if it is
|
||||
* unspecified.
|
||||
*/
|
||||
override getAriaTypeName(): string | null {
|
||||
return this.ariaTypeName || Msg['ARIA_TYPE_FIELD_IMAGE'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an ARIA-friendly label representation of this field's value.
|
||||
*
|
||||
* Implementations are responsible for, and encouraged to, return a localized
|
||||
* version of the ARIA representation of the field's value.
|
||||
*
|
||||
* @returns An ARIA representation of the field's text, or null if no text is
|
||||
* currently defined or known for the field.
|
||||
*/
|
||||
override getAriaValue(): string | null {
|
||||
return this.altText || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the ARIA role and label for this field.
|
||||
*/
|
||||
protected recomputeAriaContext(): void {
|
||||
const focusableElement = this.getClickTarget_();
|
||||
if (!focusableElement) return;
|
||||
|
||||
const isInFlyout = this.getSourceBlock()?.isInFlyout;
|
||||
if (isInFlyout) {
|
||||
aria.setState(focusableElement, aria.State.HIDDEN, true);
|
||||
return;
|
||||
}
|
||||
|
||||
aria.setState(focusableElement, aria.State.HIDDEN, false);
|
||||
// The button role is intended to indicate to users that the field has an
|
||||
// editing mode that can be activated. The presentation role is used to
|
||||
// prevent screen readers from reading the content or its descendants.
|
||||
// Only clickable image fields are navigable.
|
||||
aria.setRole(
|
||||
focusableElement,
|
||||
this.isClickable() ? aria.Role.BUTTON : aria.Role.PRESENTATION,
|
||||
);
|
||||
|
||||
const label = this.computeAriaLabel(true);
|
||||
aria.setState(focusableElement, aria.State.LABEL, label);
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_image', FieldImage);
|
||||
|
||||
@@ -839,7 +839,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
|
||||
/**
|
||||
* Recomputes the ARIA role and label for this field.
|
||||
*/
|
||||
private recomputeAriaContext(): void {
|
||||
protected recomputeAriaContext(): void {
|
||||
const focusableElement = this.getClickTarget_();
|
||||
if (!focusableElement) return;
|
||||
|
||||
@@ -853,7 +853,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
|
||||
// editing mode that can be activated.
|
||||
aria.setRole(focusableElement, aria.Role.BUTTON);
|
||||
|
||||
let label = this.computeAriaLabel(false);
|
||||
let label = this.computeAriaLabel(true);
|
||||
|
||||
if (this.isCurrentlyEditable?.()) {
|
||||
label = Msg['FIELD_LABEL_EDIT_PREFIX'].replace('%1', label);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import {Field, FieldConfig} from './field.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {aria} from './utils.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as parsing from './utils/parsing.js';
|
||||
|
||||
@@ -76,6 +77,7 @@ export class FieldLabel extends Field<string> {
|
||||
}
|
||||
if (this.fieldGroup_) {
|
||||
dom.addClass(this.fieldGroup_, 'blocklyLabelField');
|
||||
aria.setState(this.fieldGroup_, aria.State.HIDDEN, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from './field_input.js';
|
||||
import * as fieldRegistry from './field_registry.js';
|
||||
import {Msg} from './msg.js';
|
||||
import * as aria from './utils/aria.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
|
||||
/**
|
||||
@@ -300,11 +299,9 @@ export class FieldNumber extends FieldInput<number> {
|
||||
// Set the accessibility state
|
||||
if (this.min_ > -Infinity) {
|
||||
htmlInput.min = `${this.min_}`;
|
||||
aria.setState(htmlInput, aria.State.VALUEMIN, this.min_);
|
||||
}
|
||||
if (this.max_ < Infinity) {
|
||||
htmlInput.max = `${this.max_}`;
|
||||
aria.setState(htmlInput, aria.State.VALUEMAX, this.max_);
|
||||
}
|
||||
return htmlInput;
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@ export class FieldVariable extends FieldDropdown {
|
||||
];
|
||||
}
|
||||
options.push([
|
||||
Msg['RENAME_VARIABLE'],
|
||||
Msg['RENAME_VARIABLE'].replace('%1', name),
|
||||
internalConstants.RENAME_VARIABLE_ID,
|
||||
]);
|
||||
if (Msg['DELETE_VARIABLE']) {
|
||||
@@ -617,6 +617,18 @@ export class FieldVariable extends FieldDropdown {
|
||||
|
||||
return options;
|
||||
}
|
||||
/**
|
||||
* Gets an ARIA-friendly label representation of this field's value.
|
||||
*
|
||||
* Implementations are responsible for, and encouraged to, return a localized
|
||||
* version of the ARIA representation of the field's value.
|
||||
*
|
||||
* @returns An ARIA representation of the field's text.
|
||||
*/
|
||||
override getAriaValue(): string {
|
||||
// Example: 'Variable "i"'
|
||||
return Msg['FIELD_LABEL_VARIABLE'].replace('%1', super.getAriaValue());
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_variable', FieldVariable);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
|
||||
"lastupdated": "2026-04-24 15:03:55.288228",
|
||||
"lastupdated": "2026-04-29 08:57:47.670420",
|
||||
"locale": "en",
|
||||
"messagedocumentation" : "qqq"
|
||||
},
|
||||
@@ -29,7 +29,7 @@
|
||||
"UNDO": "Undo",
|
||||
"REDO": "Redo",
|
||||
"CHANGE_VALUE_TITLE": "Change value:",
|
||||
"RENAME_VARIABLE": "Rename variable...",
|
||||
"RENAME_VARIABLE": "Rename the '%1' variable",
|
||||
"RENAME_VARIABLE_TITLE": "Rename all '%1' variables to:",
|
||||
"NEW_VARIABLE": "Create variable...",
|
||||
"NEW_STRING_VARIABLE": "Create string variable...",
|
||||
@@ -488,6 +488,11 @@
|
||||
"ARIA_TYPE_FIELD_TEXT_INPUT": "text",
|
||||
"ARIA_TYPE_FIELD_NUMBER": "number",
|
||||
"ARIA_TYPE_FIELD_DROPDOWN": "dropdown",
|
||||
"ARIA_TYPE_FIELD_IMAGE": "image",
|
||||
"ARIA_TYPE_FIELD_CHECKBOX": "checkbox",
|
||||
"FIELD_LABEL_EDIT_PREFIX": "Edit %1",
|
||||
"FIELD_LABEL_OPTION_INDEX": "Option %1"
|
||||
"FIELD_LABEL_OPTION_INDEX": "Option %1",
|
||||
"FIELD_LABEL_CHECKBOX_CHECKED": "Checked",
|
||||
"FIELD_LABEL_CHECKBOX_UNCHECKED": "Not checked",
|
||||
"FIELD_LABEL_VARIABLE": "Variable '%1'"
|
||||
}
|
||||
|
||||
@@ -495,6 +495,11 @@
|
||||
"ARIA_TYPE_FIELD_TEXT_INPUT": "ARIA type name for a text input field, used by screen readers to identify the type of field.",
|
||||
"ARIA_TYPE_FIELD_NUMBER": "ARIA type name for a number field, used by screen readers to identify the type of field.",
|
||||
"ARIA_TYPE_FIELD_DROPDOWN": "ARIA type name for a dropdown field, used by screen readers to identify the type of field.",
|
||||
"ARIA_TYPE_FIELD_IMAGE": "ARIA type name of an image field, used by screen readers to identify the type of field.",
|
||||
"ARIA_TYPE_FIELD_CHECKBOX": "ARIA type name of an checkbox field, used by screen readers to identify the type of field.",
|
||||
"FIELD_LABEL_EDIT_PREFIX": "Label for an editable field, used by screen readers to identify fields that can be edited by the user. Placeholder corresponds to the label of the field's value. \n\nParameters:\n* %1 - the label of the field's value \n\nExamples:\n* 'Edit 5'\n* 'Edit item'",
|
||||
"FIELD_LABEL_OPTION_INDEX": "Label for an unlabeled dropdown field option, used by screen readers to identify options in a dropdown field. Placeholder corresponds to the index of the option in the dropdown, starting at 1. \n\nParameters:\n* %1 - the index of the option in the dropdown, starting at 1 \n\nExamples:\n* 'Option 1'\n* 'Option 2'"
|
||||
"FIELD_LABEL_OPTION_INDEX": "Label for an unlabeled dropdown field option, used by screen readers to identify options in a dropdown field. Placeholder corresponds to the index of the option in the dropdown, starting at 1. \n\nParameters:\n* %1 - the index of the option in the dropdown, starting at 1 \n\nExamples:\n* 'Option 1'\n* 'Option 2'",
|
||||
"FIELD_LABEL_CHECKBOX_CHECKED": "Label for a checked checkbox field, used by screen readers to identify the state of a checkbox field.",
|
||||
"FIELD_LABEL_CHECKBOX_UNCHECKED": "Label for an unchecked checkbox field, used by screen readers to identify the state of a checkbox field.",
|
||||
"FIELD_LABEL_VARIABLE": "Label for a variable field option, used by screen readers to identify the options in a variable dropdown field. \n\nParameters:\n* %1 - the name of the variable represented by the option \n\nExamples:\n* 'Variable 'item''\n* 'Variable 'x''"
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ Blockly.Msg.REDO = 'Redo';
|
||||
Blockly.Msg.CHANGE_VALUE_TITLE = 'Change value:';
|
||||
/** @type {string} */
|
||||
/// dropdown choice - When the user clicks on a variable block, this is one of the dropdown menu choices. It is used to rename the current variable. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu].
|
||||
Blockly.Msg.RENAME_VARIABLE = 'Rename variable...';
|
||||
Blockly.Msg.RENAME_VARIABLE = 'Rename the "%1" variable';
|
||||
/** @type {string} */
|
||||
/// prompt - Prompts the user to enter the new name for the selected variable. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu].\n\nParameters:\n* %1 - the name of the variable to be renamed.
|
||||
Blockly.Msg.RENAME_VARIABLE_TITLE = 'Rename all "%1" variables to:';
|
||||
@@ -1940,6 +1940,12 @@ Blockly.Msg.ARIA_TYPE_FIELD_NUMBER = 'number';
|
||||
/// ARIA type name for a dropdown field, used by screen readers to identify the type of field.
|
||||
Blockly.Msg.ARIA_TYPE_FIELD_DROPDOWN = 'dropdown';
|
||||
/** @type {string} */
|
||||
/// ARIA type name of an image field, used by screen readers to identify the type of field.
|
||||
Blockly.Msg.ARIA_TYPE_FIELD_IMAGE = 'image';
|
||||
/** @type {string} */
|
||||
/// ARIA type name of an checkbox field, used by screen readers to identify the type of field.
|
||||
Blockly.Msg.ARIA_TYPE_FIELD_CHECKBOX = 'checkbox';
|
||||
/** @type {string} */
|
||||
/// Label for an editable field, used by screen readers to identify fields that can be edited by the user. Placeholder corresponds to the label of the field's value.
|
||||
/// \n\nParameters:\n* %1 - the label of the field's value
|
||||
/// \n\nExamples:\n* "Edit 5"\n* "Edit item"
|
||||
@@ -1948,4 +1954,15 @@ Blockly.Msg.FIELD_LABEL_EDIT_PREFIX = 'Edit %1';
|
||||
/// Label for an unlabeled dropdown field option, used by screen readers to identify options in a dropdown field. Placeholder corresponds to the index of the option in the dropdown, starting at 1.
|
||||
/// \n\nParameters:\n* %1 - the index of the option in the dropdown, starting at 1
|
||||
/// \n\nExamples:\n* "Option 1"\n* "Option 2"
|
||||
Blockly.Msg.FIELD_LABEL_OPTION_INDEX = 'Option %1';
|
||||
Blockly.Msg.FIELD_LABEL_OPTION_INDEX = 'Option %1';
|
||||
/** @type {string} */
|
||||
/// Label for a checked checkbox field, used by screen readers to identify the state of a checkbox field.
|
||||
Blockly.Msg.FIELD_LABEL_CHECKBOX_CHECKED = 'Checked';
|
||||
/** @type {string} */
|
||||
/// Label for an unchecked checkbox field, used by screen readers to identify the state of a checkbox field.
|
||||
Blockly.Msg.FIELD_LABEL_CHECKBOX_UNCHECKED = 'Not checked';
|
||||
/** @type {string} */
|
||||
/// Label for a variable field option, used by screen readers to identify the options in a variable dropdown field.
|
||||
/// \n\nParameters:\n* %1 - the name of the variable represented by the option
|
||||
/// \n\nExamples:\n* 'Variable "item"'\n* 'Variable "x"'
|
||||
Blockly.Msg.FIELD_LABEL_VARIABLE = 'Variable "%1"';
|
||||
@@ -293,4 +293,50 @@ suite('Checkbox Fields', function () {
|
||||
this.assertValue(false);
|
||||
});
|
||||
});
|
||||
|
||||
suite('ARIA', function () {
|
||||
setup(function () {
|
||||
this.workspace = Blockly.inject('blocklyDiv', {
|
||||
renderer: 'geras',
|
||||
});
|
||||
this.block = this.workspace.newBlock('test_fields_checkbox');
|
||||
this.field = this.block.getField('CHECKBOX');
|
||||
this.block.initSvg();
|
||||
this.block.render();
|
||||
|
||||
this.focusableElement = this.field.getClickTarget_();
|
||||
});
|
||||
test('Block has field type name in ARIA label', function () {
|
||||
const blockLabel = this.block.getAriaLabel();
|
||||
assert.include(blockLabel, 'checkbox');
|
||||
});
|
||||
test('Field ARIA label is type name', function () {
|
||||
const fieldLabel = this.focusableElement.getAttribute('aria-label');
|
||||
assert.equal(fieldLabel, 'checkbox');
|
||||
});
|
||||
test('Field does not include value in ARIA label', function () {
|
||||
const fieldLabel = this.focusableElement.getAttribute('aria-label');
|
||||
assert.isFalse(fieldLabel.toLowerCase().includes('checked'));
|
||||
});
|
||||
test('Hidden when in a flyout', function () {
|
||||
this.block.isInFlyout = true;
|
||||
// Force recompute of ARIA label.
|
||||
this.field.setValue(this.field.getValue());
|
||||
const ariaHidden = this.focusableElement.getAttribute('aria-hidden');
|
||||
assert.equal(ariaHidden, 'true');
|
||||
});
|
||||
test('Focusable element has role of checkbox', function () {
|
||||
const role = this.focusableElement.getAttribute('role');
|
||||
assert.equal(role, 'checkbox');
|
||||
});
|
||||
test('Focusable element has correct default ARIA checked state', function () {
|
||||
const ariaChecked = this.focusableElement.getAttribute('aria-checked');
|
||||
assert.equal(ariaChecked, 'true');
|
||||
});
|
||||
test('Focusable element updates ARIA checked state on setValue', function () {
|
||||
this.field.setValue(false);
|
||||
const ariaChecked = this.focusableElement.getAttribute('aria-checked');
|
||||
assert.equal(ariaChecked, 'false');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -345,6 +345,10 @@ suite('Dropdown Fields', function () {
|
||||
const blockLabel = this.block.getAriaLabel();
|
||||
assert.include(blockLabel, 'dropdown:');
|
||||
});
|
||||
test('Field has field type name in ARIA label', function () {
|
||||
const fieldLabel = this.focusableElement.getAttribute('aria-label');
|
||||
assert.include(fieldLabel, 'dropdown:');
|
||||
});
|
||||
test('Focusable element has role of button', function () {
|
||||
const role = this.focusableElement.getAttribute('role');
|
||||
assert.equal(role, 'button');
|
||||
@@ -479,7 +483,7 @@ suite('Dropdown Fields', function () {
|
||||
const label = this.field
|
||||
.getFocusableElement()
|
||||
.getAttribute('aria-label');
|
||||
assert.equal(label, 'A');
|
||||
assert.include(label, 'A');
|
||||
});
|
||||
test('Image ARIA label is prioritized over alt text', function () {
|
||||
this.field.dropdownCreate();
|
||||
@@ -487,7 +491,7 @@ suite('Dropdown Fields', function () {
|
||||
const label = this.field
|
||||
.getFocusableElement()
|
||||
.getAttribute('aria-label');
|
||||
assert.equal(label, 'Letter B');
|
||||
assert.include(label, 'Letter B');
|
||||
});
|
||||
});
|
||||
suite('Dropdown with HTMLElement options', function () {
|
||||
@@ -549,35 +553,35 @@ suite('Dropdown Fields', function () {
|
||||
const label = this.field
|
||||
.getFocusableElement()
|
||||
.getAttribute('aria-label');
|
||||
assert.equal(label, 'Explicit A label');
|
||||
assert.include(label, 'Explicit A label');
|
||||
});
|
||||
test('HTMLElement ariaLabel prioritized over other properties', function () {
|
||||
this.field.setValue('B');
|
||||
const label = this.field
|
||||
.getFocusableElement()
|
||||
.getAttribute('aria-label');
|
||||
assert.equal(label, 'Element ARIA');
|
||||
assert.include(label, 'Element ARIA');
|
||||
});
|
||||
test('HTMLElement title is used when ariaLabel is missing', function () {
|
||||
this.field.setValue('C');
|
||||
const label = this.field
|
||||
.getFocusableElement()
|
||||
.getAttribute('aria-label');
|
||||
assert.equal(label, 'Title text');
|
||||
assert.include(label, 'Title text');
|
||||
});
|
||||
test('HTMLElement innerText is used as final fallback', function () {
|
||||
this.field.setValue('D');
|
||||
const label = this.field
|
||||
.getFocusableElement()
|
||||
.getAttribute('aria-label');
|
||||
assert.equal(label, 'Inner text');
|
||||
assert.include(label, 'Inner text');
|
||||
});
|
||||
test('Empty label falls back to option index', function () {
|
||||
this.field.setValue('E');
|
||||
const label = this.field
|
||||
.getFocusableElement()
|
||||
.getAttribute('aria-label');
|
||||
assert.equal(label, 'Option 5');
|
||||
assert.include(label, 'Option 5');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -348,4 +348,55 @@ suite('Image Fields', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
suite('ARIA', function () {
|
||||
setup(function () {
|
||||
this.workspace = Blockly.inject('blocklyDiv', {
|
||||
renderer: 'geras',
|
||||
});
|
||||
});
|
||||
suite('Image without click handler', function () {
|
||||
setup(function () {
|
||||
this.block = this.workspace.newBlock('text');
|
||||
this.field = this.block.inputList[0].fieldRow[0];
|
||||
this.block.initSvg();
|
||||
this.block.render();
|
||||
this.focusableElement = this.field.getFocusableElement();
|
||||
});
|
||||
test('Block has field type name in ARIA label', function () {
|
||||
const blockLabel = this.block.getAriaLabel();
|
||||
assert.include(blockLabel, 'image:');
|
||||
});
|
||||
test('Field has field type name in ARIA label', function () {
|
||||
const fieldLabel = this.focusableElement.getAttribute('aria-label');
|
||||
assert.include(fieldLabel, 'image:');
|
||||
});
|
||||
test('Block has image alt text in ARIA label', function () {
|
||||
const blockLabel = this.block.getAriaLabel();
|
||||
assert.include(blockLabel, this.field.altText);
|
||||
});
|
||||
test('Focusable element has role of presentation', function () {
|
||||
const role = this.focusableElement.getAttribute('role');
|
||||
assert.equal(role, 'presentation');
|
||||
});
|
||||
test('Hidden when in a flyout', function () {
|
||||
this.block.isInFlyout = true;
|
||||
// Force recompute of ARIA label.
|
||||
this.field.setValue(this.field.getValue());
|
||||
const ariaHidden = this.focusableElement.getAttribute('aria-hidden');
|
||||
assert.equal(ariaHidden, 'true');
|
||||
});
|
||||
});
|
||||
suite('Image with click handler', function () {
|
||||
test('Focusable element has role of button', function () {
|
||||
const block = this.workspace.newBlock('test_images_clickhandler');
|
||||
const field = block.getField('IMAGE');
|
||||
block.initSvg();
|
||||
block.render();
|
||||
|
||||
const focusableElement = field.getFocusableElement();
|
||||
const role = focusableElement.getAttribute('role');
|
||||
assert.equal(role, 'button');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,4 +223,20 @@ suite('Label Fields', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('ARIA', function () {
|
||||
test('Is hidden', function () {
|
||||
const workspace = Blockly.inject('blocklyDiv', {
|
||||
renderer: 'geras',
|
||||
});
|
||||
const block = workspace.newBlock('text_print');
|
||||
const field = block.inputList[0].fieldRow[0];
|
||||
block.initSvg();
|
||||
block.render();
|
||||
|
||||
const focusableElement = field.getFocusableElement();
|
||||
const ariaHidden = focusableElement.getAttribute('aria-hidden');
|
||||
assert.equal(ariaHidden, 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -518,6 +518,10 @@ suite('Number Fields', function () {
|
||||
const blockLabel = this.block.getAriaLabel();
|
||||
assert.include(blockLabel, 'number:');
|
||||
});
|
||||
test('Field has field type name in ARIA label', function () {
|
||||
const fieldLabel = this.focusableElement.getAttribute('aria-label');
|
||||
assert.include(fieldLabel, 'number:');
|
||||
});
|
||||
test('Focusable element has role of button', function () {
|
||||
const role = this.focusableElement.getAttribute('role');
|
||||
assert.equal(role, 'button');
|
||||
|
||||
@@ -609,6 +609,10 @@ suite('Text Input Fields', function () {
|
||||
const blockLabel = this.block.getAriaLabel();
|
||||
assert.include(blockLabel, 'text:');
|
||||
});
|
||||
test('Field has field type name in ARIA label', function () {
|
||||
const fieldLabel = this.focusableElement.getAttribute('aria-label');
|
||||
assert.include(fieldLabel, 'text:');
|
||||
});
|
||||
test('Focusable element has role of button', function () {
|
||||
const role = this.focusableElement.getAttribute('role');
|
||||
assert.equal(role, 'button');
|
||||
|
||||
@@ -204,9 +204,17 @@ suite('Variable Fields', function () {
|
||||
for (let i = 0, option; (option = expectedVarOptions[i]); i++) {
|
||||
assert.deepEqual(dropdownOptions[i], option);
|
||||
}
|
||||
assert.include(dropdownOptions[dropdownOptions.length - 2][0], 'Rename');
|
||||
const varName = fieldVariable.getText();
|
||||
|
||||
assert.include(dropdownOptions[dropdownOptions.length - 1][0], 'Delete');
|
||||
const renameLabel = dropdownOptions[dropdownOptions.length - 2][0];
|
||||
const deleteLabel = dropdownOptions[dropdownOptions.length - 1][0];
|
||||
|
||||
// Expect the rename and delete options to include the variable name.
|
||||
assert.include(renameLabel, 'Rename');
|
||||
assert.include(renameLabel, varName);
|
||||
|
||||
assert.include(deleteLabel, 'Delete');
|
||||
assert.include(deleteLabel, varName);
|
||||
};
|
||||
test('Contains variables created before field', function () {
|
||||
this.workspace.getVariableMap().createVariable('name1', '', 'id1');
|
||||
@@ -641,4 +649,75 @@ suite('Variable Fields', function () {
|
||||
assert.equal(variable.getId(), 'id2');
|
||||
});
|
||||
});
|
||||
suite('ARIA', function () {
|
||||
setup(function () {
|
||||
this.workspace = Blockly.inject('blocklyDiv', {
|
||||
renderer: 'geras',
|
||||
});
|
||||
this.block = this.workspace.newBlock('variables_set');
|
||||
this.field = this.block.getField('VAR');
|
||||
this.block.initSvg();
|
||||
this.block.render();
|
||||
|
||||
this.focusableElement = this.field.getFocusableElement();
|
||||
});
|
||||
test('Block has dropdown field type name and "Variable" qualifier in ARIA label', function () {
|
||||
const blockLabel = this.block.getAriaLabel();
|
||||
assert.include(blockLabel, 'dropdown:');
|
||||
});
|
||||
test('Field has dropdown field type name and "Variable" qualifier in ARIA label', function () {
|
||||
const fieldLabel = this.focusableElement.getAttribute('aria-label');
|
||||
assert.include(fieldLabel, 'dropdown:');
|
||||
assert.include(fieldLabel, 'Variable');
|
||||
});
|
||||
test('Focusable element has role of button', function () {
|
||||
const role = this.focusableElement.getAttribute('role');
|
||||
assert.equal(role, 'button');
|
||||
});
|
||||
test('Hidden when in a flyout', function () {
|
||||
this.block.isInFlyout = true;
|
||||
// Force recompute of ARIA label.
|
||||
this.field.setValue(this.field.getValue());
|
||||
const ariaHidden = this.focusableElement.getAttribute('aria-hidden');
|
||||
assert.equal(ariaHidden, 'true');
|
||||
});
|
||||
test('Does not have aria-expanded when dropdown is closed', function () {
|
||||
const ariaExpanded = this.focusableElement.getAttribute('aria-expanded');
|
||||
assert.equal(ariaExpanded, 'false');
|
||||
});
|
||||
test('Has aria-expanded when dropdown is open', function () {
|
||||
this.field.showEditor_();
|
||||
const ariaExpanded = this.focusableElement.getAttribute('aria-expanded');
|
||||
assert.equal(ariaExpanded, 'true');
|
||||
this.workspace.hideChaff();
|
||||
});
|
||||
test('Has aria-haspopup of listbox', function () {
|
||||
const ariaHasPopup = this.focusableElement.getAttribute('aria-haspopup');
|
||||
assert.equal(ariaHasPopup, 'listbox');
|
||||
});
|
||||
test('Has aria-controls that matches the ID of the dropdown menu', function () {
|
||||
this.field.showEditor_();
|
||||
const ariaControls = this.focusableElement.getAttribute('aria-controls');
|
||||
const menuId = this.field.menu_.id;
|
||||
assert.equal(ariaControls, menuId);
|
||||
this.workspace.hideChaff();
|
||||
});
|
||||
test('Has placeholder ARIA label by default', function () {
|
||||
const label = this.focusableElement.getAttribute('aria-label');
|
||||
assert.include(label, 'item');
|
||||
});
|
||||
test('New selected option updates ARIA label', function () {
|
||||
const initialLabel = this.focusableElement.getAttribute('aria-label');
|
||||
assert.include(initialLabel, 'item');
|
||||
|
||||
const newVariable = this.workspace
|
||||
.getVariableMap()
|
||||
.createVariable('newVar');
|
||||
this.field.getOptions(false); // Invalidate cached options.
|
||||
this.field.setValue(newVariable.getId());
|
||||
|
||||
const updatedLabel = this.focusableElement.getAttribute('aria-label');
|
||||
assert.include(updatedLabel, 'newVar');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user