Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grids a11y: FilterBuilder - not accessible at all #28143

Merged
merged 9 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
104 changes: 104 additions & 0 deletions e2e/testcafe-devextreme/tests/filterBuilder/filterBuilderA11y.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* eslint-disable @typescript-eslint/no-misused-promises */

import FilterBuilder from 'devextreme-testcafe-models/filterBuilder';
import { createWidget } from '../../helpers/createWidget';
import url from '../../helpers/getPageUrl';
import { fields, filter } from './data';
import { a11yCheck } from '../../helpers/accessibility/utils';

fixture`Filter Builder Accessibility Tests`.page(
url(__dirname, '../container.html'),
);

const elements = [
{
name: 'Root Element',
role: 'tree',
label: 'Filter builder',
selector: (filterBuilder) => filterBuilder.getRootElement(),
},
{
name: 'Group Item',
role: 'treeitem',
label: 'Group item',
selector: (filterBuilder) => filterBuilder.getGroupItem(),
},
{
name: 'Operation Button',
role: 'combobox',
label: 'Operation',
selector: (filterBuilder) => filterBuilder.getOperationButton(1),
},
{
name: 'Add Button',
role: 'combobox',
label: 'Add',
selector: (filterBuilder) => filterBuilder.getAddButton(1),
},
{
name: 'Remove Condition Button',
role: 'button',
label: 'Remove condition',
selector: (filterBuilder) => filterBuilder.getRemoveButton(0),
},
{
name: 'Remove Group Button',
role: 'button',
label: 'Remove group',
selector: (filterBuilder) => filterBuilder.getRemoveButton(1),
},
{
name: 'Item Field',
role: 'combobox',
label: 'Item field',
selector: (filterBuilder) => filterBuilder.getItem('field', 2),
},
{
name: 'Item Operation',
role: 'combobox',
label: 'Item operation',
selector: (filterBuilder) => filterBuilder.getItem('operation', 2),
},
{
name: 'Item Value',
role: 'button',
label: 'Item value',
selector: (filterBuilder) => filterBuilder.getItem('value', 2),
},
];

elements.forEach(({
name, role, label, selector,
}) => {
test(`Filter Builder - ${name} has correct ARIA attributes`, async (t) => {
const filterBuilder = new FilterBuilder('#parentContainer');
const elementSelector = selector(filterBuilder);
const labelValue = ['button', 'combobox'].includes(role) && await elementSelector.innerText ? 'title' : 'aria-label';

await t
.expect(elementSelector.getAttribute('role'))
.eql(role)
.expect(elementSelector.getAttribute(`${labelValue}`))
.eql(label);
}).before(async () => {
await createWidget('dxFilterBuilder', {
fields,
value: filter,
});
});
});

test('Filter Builder - ARIA Attributes axe test', async (t) => {
const filterBuilder = new FilterBuilder('#container');

await t
.expect(filterBuilder.isReady())
.ok();

await a11yCheck(t, {}, '#container');
}).before(async () => {
await createWidget('dxFilterBuilder', {
fields,
value: filter,
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable max-classes-per-file */
import registerComponent from '@js/core/component_registrator';
import domAdapter from '@js/core/dom_adapter';
import Guid from '@js/core/guid';
import $ from '@js/core/renderer';
import { when } from '@js/core/utils/deferred';
import { extend } from '@js/core/utils/extend';
Expand Down Expand Up @@ -256,14 +257,45 @@ class FilterBuilder extends Widget<any> {
this.$element().addClass(FILTER_BUILDER_CLASS);
// @ts-expect-error
super._initMarkup();

this._addAriaAttributes(this.$element(), messageLocalization.format('dxFilterBuilder-filterAriaRootElement'), 'tree');
this._createGroupElementByCriteria(this._model)
.appendTo(this.$element());
}

_createConditionElement(condition, parent) {
_addAriaAttributes($element, ariaLabel, role, hasPopup?, hasExpanded?, ariaLevel?) {
if (!$element || !$element.length) return;

const attributes = { role };

if (ariaLabel) {
if ($element.text().length > 0) {
// @ts-expect-error title attr
attributes.title = ariaLabel;
} else {
attributes['aria-label'] = ariaLabel;
}
}

if (isDefined(hasPopup)) {
attributes['aria-haspopup'] = `${hasPopup}`;
}

if (isDefined(hasExpanded)) {
attributes['aria-expanded'] = `${hasExpanded}`;
}

if (isDefined(ariaLevel)) {
attributes['aria-level'] = `${ariaLevel}`;
}

$element.attr(attributes);
}

_createConditionElement(condition, parent, groupLevel?) {
return $('<div>')
.addClass(FILTER_BUILDER_GROUP_CLASS)
.append(this._createConditionItem(condition, parent));
.append(this._createConditionItem(condition, parent, groupLevel));
}

_createGroupElementByCriteria(criteria, parent?, groupLevel = 0) {
Expand All @@ -277,26 +309,31 @@ class FilterBuilder extends Widget<any> {
this._createGroupElementByCriteria(innerCriteria, criteria, groupLevel + 1)
.appendTo($groupContent);
} else if (isCondition(innerCriteria)) {
this._createConditionElement(innerCriteria, criteria)
this._createConditionElement(innerCriteria, criteria, `${groupLevel + 1}`)
.appendTo($groupContent);
}
}
return $group;
}

_createGroupElement(criteria, parent, groupLevel) {
const $guid = new Guid();
const $groupItem = $('<div>').addClass(FILTER_BUILDER_GROUP_ITEM_CLASS);
const $groupContent = $('<div>').addClass(FILTER_BUILDER_GROUP_CONTENT_CLASS);
const $groupContent = $('<div>').addClass(FILTER_BUILDER_GROUP_CONTENT_CLASS).attr('id', `${$guid}`);
const $group = $('<div>').addClass(FILTER_BUILDER_GROUP_CLASS).append($groupItem).append($groupContent);

if (parent != null) {
this._createRemoveButton(() => {
removeItem(parent, criteria);
$group.remove();
this._updateFilter();
}).appendTo($groupItem);
}, 'group').appendTo($groupItem);
}

this._addAriaAttributes($groupItem, messageLocalization.format('dxFilterBuilder-filterAriaGroupItem'), 'treeitem', null, null, `${groupLevel + 1}`);
this._addAriaAttributes($groupContent, '', 'group');
$groupItem.attr('aria-owns', `${$guid}`);

this._createGroupOperationButton(criteria).appendTo($groupItem);

this._createAddButton(() => {
Expand All @@ -308,7 +345,7 @@ class FilterBuilder extends Widget<any> {
const field = this.option('fields')[0];
const newCondition = createCondition(field, this._customOperations);
addItem(newCondition, criteria);
this._createConditionElement(newCondition, criteria).appendTo($groupContent);
this._createConditionElement(newCondition, criteria, groupLevel + 1).appendTo($groupContent);
this._updateFilter();
}, groupLevel).appendTo($groupItem);

Expand Down Expand Up @@ -345,6 +382,9 @@ class FilterBuilder extends Widget<any> {
cssClass: FILTER_BUILDER_GROUP_OPERATIONS_CLASS,
},
});

this._addAriaAttributes($operationButton, messageLocalization.format('dxFilterBuilder-filterAriaOperationButton'), 'combobox', true, false);

return $operationButton.addClass(FILTER_BUILDER_ITEM_TEXT_CLASS)
.addClass(FILTER_BUILDER_GROUP_OPERATION_CLASS)
.attr('tabindex', 0);
Expand All @@ -354,7 +394,7 @@ class FilterBuilder extends Widget<any> {
const that = this;
const removeMenu = function () {
// @ts-expect-error
that.$element().find(`.${ACTIVE_CLASS}`).removeClass(ACTIVE_CLASS);
that.$element().find(`.${ACTIVE_CLASS}`).removeClass(ACTIVE_CLASS).attr('aria-expanded', 'false');
// @ts-expect-error
that.$element().find('.dx-overlay .dx-treeview').remove();
// @ts-expect-error
Expand All @@ -377,7 +417,7 @@ class FilterBuilder extends Widget<any> {
selectionMode: 'single',
onItemClick: menuOnItemClickWrapper(options.menu.onItemClick),
onHiding() {
$button.removeClass(ACTIVE_CLASS);
$button.removeClass(ACTIVE_CLASS).attr('aria-expanded', 'false');
},
position: {
my: `${position} top`, at: `${position} bottom`, offset: '0 1', of: $button, collision: 'flip',
Expand Down Expand Up @@ -414,7 +454,7 @@ class FilterBuilder extends Widget<any> {
this._subscribeOnClickAndEnterKey($button, () => {
removeMenu();
that._createPopupWithTreeView(options, that.$element());
$button.addClass(ACTIVE_CLASS);
$button.addClass(ACTIVE_CLASS).attr('aria-expanded', 'true');
});
return $button;
}
Expand Down Expand Up @@ -463,6 +503,7 @@ class FilterBuilder extends Widget<any> {
}).addClass(FILTER_BUILDER_ITEM_TEXT_CLASS)
.addClass(FILTER_BUILDER_ITEM_OPERATION_CLASS)
.attr('tabindex', 0);
this._addAriaAttributes($operationButton, messageLocalization.format('dxFilterBuilder-filterAriaItemOperation'), 'combobox', true, false);

return $operationButton;
}
Expand Down Expand Up @@ -517,14 +558,18 @@ class FilterBuilder extends Widget<any> {
.addClass(FILTER_BUILDER_ITEM_FIELD_CLASS)
.attr('tabindex', 0);

this._addAriaAttributes($fieldButton, messageLocalization.format('dxFilterBuilder-filterAriaItemField'), 'combobox', true, false);

return $fieldButton;
}

_createConditionItem(condition, parent) {
_createConditionItem(condition, parent, groupLevel?) {
const $item = $('<div>').addClass(FILTER_BUILDER_GROUP_ITEM_CLASS);
const fields = this._getNormalizedFields();
const field = getField(condition[0], fields);

this._addAriaAttributes($item, '', 'treeitem', null, null, groupLevel);

this._createRemoveButton(() => {
removeItem(parent, condition);
const isSingleChild = $item.parent().children().length === 1;
Expand All @@ -534,7 +579,7 @@ class FilterBuilder extends Widget<any> {
$item.remove();
}
this._updateFilter();
}).appendTo($item);
}, 'condition').appendTo($item);
this._createFieldButtonWithMenu(fields, condition, field).appendTo($item);
this._createOperationAndValueButtons(condition, field, $item);
return $item;
Expand All @@ -554,12 +599,16 @@ class FilterBuilder extends Widget<any> {
}));
}

_createRemoveButton(handler) {
_createRemoveButton(handler, type?) {
const $removeButton = $('<div>')
.addClass(FILTER_BUILDER_IMAGE_CLASS)
.addClass(FILTER_BUILDER_IMAGE_REMOVE_CLASS)
.addClass(FILTER_BUILDER_ACTION_CLASS)
.attr('tabindex', 0);
if (type) {
const removeMessage = (messageLocalization.format as any)('dxFilterBuilder-filterAriaRemoveButton', type);
this._addAriaAttributes($removeButton, removeMessage, 'button');
}
this._subscribeOnClickAndEnterKey($removeButton, handler);
return $removeButton;
}
Expand Down Expand Up @@ -588,6 +637,9 @@ class FilterBuilder extends Widget<any> {
},
});
}

this._addAriaAttributes($button, messageLocalization.format('dxFilterBuilder-filterAriaAddButton'), 'combobox', true, false);

return $button.addClass(FILTER_BUILDER_IMAGE_CLASS)
.addClass(FILTER_BUILDER_IMAGE_ADD_CLASS)
.addClass(FILTER_BUILDER_ACTION_CLASS)
Expand All @@ -601,6 +653,7 @@ class FilterBuilder extends Widget<any> {
.addClass(FILTER_BUILDER_ITEM_VALUE_TEXT_CLASS)
.attr('tabindex', 0)
.appendTo($container);
this._addAriaAttributes($text, messageLocalization.format('dxFilterBuilder-filterAriaItemValue'), 'button', true);
const value = item[2];

const customOperation = getCustomOperation(that._customOperations, item[1]);
Expand Down Expand Up @@ -768,7 +821,6 @@ class FilterBuilder extends Widget<any> {
const $valueButton = $('<div>')
.addClass(FILTER_BUILDER_ITEM_TEXT_CLASS)
.addClass(FILTER_BUILDER_ITEM_VALUE_CLASS);

this._createValueText(item, field, $valueButton);
return $valueButton;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/devextreme/js/localization/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,15 @@
"dxFilterBuilder-filterOperationBetween": "بين",
"dxFilterBuilder-filterOperationAnyOf": "أي من",
"dxFilterBuilder-filterOperationNoneOf": "ليس من",
"dxFilterBuilder-filterAriaRootElement": "Filter builder",
"dxFilterBuilder-filterAriaGroupLevel": "Level {0}",
"dxFilterBuilder-filterAriaGroupItem": "Group item",
"dxFilterBuilder-filterAriaOperationButton": "Operation",
"dxFilterBuilder-filterAriaAddButton": "Add",
"dxFilterBuilder-filterAriaRemoveButton": "Remove {0}",
"dxFilterBuilder-filterAriaItemField": "Item field",
"dxFilterBuilder-filterAriaItemOperation": "Item operation",
"dxFilterBuilder-filterAriaItemValue": "Item value",

"dxHtmlEditor-dialogColorCaption": "تغيير لون الخط",
"dxHtmlEditor-dialogBackgroundCaption": "تغيير لون الخلفية",
Expand Down
9 changes: 9 additions & 0 deletions packages/devextreme/js/localization/messages/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,15 @@
"dxFilterBuilder-filterOperationBetween": "És entre",
"dxFilterBuilder-filterOperationAnyOf": "És alguna de",
"dxFilterBuilder-filterOperationNoneOf": "No és cap",
"dxFilterBuilder-filterAriaRootElement": "Filter builder",
"dxFilterBuilder-filterAriaGroupLevel": "Level {0}",
"dxFilterBuilder-filterAriaGroupItem": "Group item",
"dxFilterBuilder-filterAriaOperationButton": "Operation",
"dxFilterBuilder-filterAriaAddButton": "Add",
"dxFilterBuilder-filterAriaRemoveButton": "Remove {0}",
"dxFilterBuilder-filterAriaItemField": "Item field",
"dxFilterBuilder-filterAriaItemOperation": "Item operation",
"dxFilterBuilder-filterAriaItemValue": "Item value",

"dxHtmlEditor-dialogColorCaption": "Canvia el color de la lletra",
"dxHtmlEditor-dialogBackgroundCaption": "Canvieu el color de fons",
Expand Down
9 changes: 9 additions & 0 deletions packages/devextreme/js/localization/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,15 @@
"dxFilterBuilder-filterOperationBetween": "Mezi",
"dxFilterBuilder-filterOperationAnyOf": "Libovolný z",
"dxFilterBuilder-filterOperationNoneOf": "Žádný z",
"dxFilterBuilder-filterAriaRootElement": "Filter builder",
"dxFilterBuilder-filterAriaGroupLevel": "Level {0}",
"dxFilterBuilder-filterAriaGroupItem": "Group item",
"dxFilterBuilder-filterAriaOperationButton": "Operation",
"dxFilterBuilder-filterAriaAddButton": "Add",
"dxFilterBuilder-filterAriaRemoveButton": "Remove {0}",
"dxFilterBuilder-filterAriaItemField": "Item field",
"dxFilterBuilder-filterAriaItemOperation": "Item operation",
"dxFilterBuilder-filterAriaItemValue": "Item value",

"dxHtmlEditor-dialogColorCaption": "Změna barvy písma",
"dxHtmlEditor-dialogBackgroundCaption": "Změna barvy pozadí",
Expand Down
9 changes: 9 additions & 0 deletions packages/devextreme/js/localization/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,15 @@
"dxFilterBuilder-filterOperationBetween": "Zwischen",
"dxFilterBuilder-filterOperationAnyOf": "Ist enthalten in",
"dxFilterBuilder-filterOperationNoneOf": "Ist nicht enthalten in",
"dxFilterBuilder-filterAriaRootElement": "Filter builder",
"dxFilterBuilder-filterAriaGroupLevel": "Level {0}",
"dxFilterBuilder-filterAriaGroupItem": "Group item",
"dxFilterBuilder-filterAriaOperationButton": "Operation",
"dxFilterBuilder-filterAriaAddButton": "Add",
"dxFilterBuilder-filterAriaRemoveButton": "Remove {0}",
"dxFilterBuilder-filterAriaItemField": "Item field",
"dxFilterBuilder-filterAriaItemOperation": "Item operation",
"dxFilterBuilder-filterAriaItemValue": "Item value",

"dxHtmlEditor-dialogColorCaption": "Schriftfarbe ändern",
"dxHtmlEditor-dialogBackgroundCaption": "Hintergrundfarbe ändern",
Expand Down
Loading
Loading