Skip to content

Commit ebd8bc6

Browse files
authored
FilterBuilder - dialog is missing the programmatic name (T1279092) (DevExpress#30062)
1 parent ced4781 commit ebd8bc6

File tree

4 files changed

+118
-39
lines changed

4 files changed

+118
-39
lines changed

e2e/testcafe-devextreme/tests/filterBuilder/filterBuilderA11y.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fixture`Filter Builder Accessibility Tests`.page(
1313
const elements = [
1414
{
1515
name: 'Root Element',
16-
role: 'tree',
16+
role: 'group',
1717
label: 'Filter builder',
1818
selector: (filterBuilder) => filterBuilder.getRootElement(),
1919
},
@@ -80,6 +80,30 @@ elements.forEach(({
8080
.eql(role)
8181
.expect(elementSelector.getAttribute(`${labelValue}`))
8282
.eql(label);
83+
84+
const ariaControls = await elementSelector.getAttribute('aria-controls');
85+
86+
if (role === 'combobox' && ariaControls) {
87+
await t.click(elementSelector);
88+
89+
const popupTreeView = FilterBuilder.getPopupTreeView();
90+
const popupId = await popupTreeView.getAttribute('id');
91+
92+
await t
93+
.expect(ariaControls)
94+
.eql(popupId);
95+
96+
await a11yCheck(
97+
t,
98+
{
99+
rules: {
100+
// NOTE: color-contrast issues
101+
'color-contrast': { enabled: false },
102+
},
103+
},
104+
'#parentContainer',
105+
);
106+
}
83107
}).before(async () => {
84108
await createWidget('dxFilterBuilder', {
85109
fields,

packages/devextreme/js/__internal/filter_builder/m_filter_builder.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const ACTIVE_CLASS = 'dx-state-active';
5050
const FILTER_BUILDER_MENU_CUSTOM_OPERATION_CLASS = `${FILTER_BUILDER_CLASS}-menu-custom-operation`;
5151
const SOURCE = 'filterBuilder';
5252
const DISABLED_STATE_CLASS = 'dx-state-disabled';
53+
const OVERLAY_CONTENT_CLASS = 'dx-overlay-content';
54+
const TREEVIEW_NODE_CONTAINER = 'dx-treeview-node-container';
5355

5456
const TAB_KEY = 'tab';
5557
const ENTER_KEY = 'enter';
@@ -255,7 +257,7 @@ class FilterBuilder extends Widget<any> {
255257
// @ts-expect-error
256258
super._initMarkup();
257259

258-
this._addAriaAttributes(this.$element(), messageLocalization.format('dxFilterBuilder-filterAriaRootElement'), 'tree');
260+
this._addAriaAttributes(this.$element(), messageLocalization.format('dxFilterBuilder-filterAriaRootElement'), 'group');
259261
this._createGroupElementByCriteria(this._model)
260262
.appendTo(this.$element());
261263
}
@@ -292,7 +294,8 @@ class FilterBuilder extends Widget<any> {
292294
_createConditionElement(condition, parent, groupLevel?) {
293295
return $('<div>')
294296
.addClass(FILTER_BUILDER_GROUP_CLASS)
295-
.append(this._createConditionItem(condition, parent, groupLevel));
297+
.append(this._createConditionItem(condition, parent, groupLevel))
298+
.attr('role', 'group');
296299
}
297300

298301
_createGroupElementByCriteria(criteria, parent?, groupLevel = 0) {
@@ -327,8 +330,21 @@ class FilterBuilder extends Widget<any> {
327330
}, 'group').appendTo($groupItem);
328331
}
329332

330-
this._addAriaAttributes($groupItem, messageLocalization.format('dxFilterBuilder-filterAriaGroupItem'), 'treeitem', null, null, `${groupLevel + 1}`);
331-
this._addAriaAttributes($groupContent, '', 'group');
333+
let groupItemLevel = groupLevel;
334+
335+
if (groupLevel === 0) {
336+
this._addAriaAttributes($group, '', 'tree');
337+
groupItemLevel += 1;
338+
}
339+
340+
this._addAriaAttributes(
341+
$groupItem,
342+
messageLocalization.format('dxFilterBuilder-filterAriaGroupItem'),
343+
'treeitem',
344+
null,
345+
null,
346+
groupItemLevel,
347+
);
332348
$groupItem.attr('aria-owns', `${$guid}`);
333349

334350
this._createGroupOperationButton(criteria).appendTo($groupItem);
@@ -405,8 +421,11 @@ class FilterBuilder extends Widget<any> {
405421
};
406422
const position = rtlEnabled ? 'right' : 'left';
407423
const $button = this._createButton(options.caption);
424+
const $guid = new Guid();
425+
$button.attr('aria-controls', `${$guid}`);
408426

409427
extend(options.menu, {
428+
id: $guid,
410429
focusStateEnabled: true,
411430
selectionMode: 'single',
412431
onItemClick: menuOnItemClickWrapper(options.menu.onItemClick),
@@ -426,9 +445,13 @@ class FilterBuilder extends Widget<any> {
426445

427446
options.popup = {
428447
onShown(info) {
429-
const treeViewElement = $(info.component.content()).find('.dx-treeview');
430-
// @ts-expect-error dxElementWrapper doesn't contain widget creation methods types
431-
const treeView = treeViewElement.dxTreeView('instance');
448+
const treeViewContentElement = $(info.component.content());
449+
const treeViewElement = treeViewContentElement.find('.dx-treeview');
450+
451+
if (treeViewElement.length) {
452+
that._applyAccessibilityAttributes(treeViewElement);
453+
}
454+
432455
eventsEngine.on(treeViewElement, 'keyup keydown', (e) => {
433456
const keyName = normalizeKeyName(e);
434457

@@ -440,6 +463,9 @@ class FilterBuilder extends Widget<any> {
440463
}
441464
});
442465

466+
// @ts-expect-error dxElementWrapper doesn't contain widget creation methods types
467+
const treeView = treeViewElement.dxTreeView('instance');
468+
443469
treeView.focus();
444470
treeView.option('focusedElement', null);
445471
},
@@ -855,6 +881,8 @@ class FilterBuilder extends Widget<any> {
855881
const $menuContainer = $('<div>').appendTo(contentElement);
856882
// @ts-expect-error
857883
that._createComponent($menuContainer, TreeView, options.menu);
884+
885+
$menuContainer.attr('id', `${options.menu.id}`);
858886
// T852701
859887
this.repaint();
860888
},
@@ -885,6 +913,14 @@ class FilterBuilder extends Widget<any> {
885913
}
886914
});
887915
}
916+
917+
_applyAccessibilityAttributes($element) {
918+
const treeViewPopup = $element.closest(`.${OVERLAY_CONTENT_CLASS}`);
919+
treeViewPopup?.removeAttr('role');
920+
921+
const treeViewNode = treeViewPopup?.find?.(`.${TREEVIEW_NODE_CONTAINER}`);
922+
treeViewNode?.attr('role', 'presentation');
923+
}
888924
}
889925

890926
registerComponent('dxFilterBuilder', FilterBuilder);

packages/devextreme/testing/tests/DevExpress.ui.widgets/filterBuilderParts/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const FILTER_BUILDER_ITEM_VALUE_CLASS = FILTER_BUILDER_CLASS + '-item-val
55
export const FILTER_BUILDER_ITEM_VALUE_TEXT_CLASS = FILTER_BUILDER_CLASS + '-item-value-text';
66
export const FILTER_BUILDER_OVERLAY_CLASS = FILTER_BUILDER_CLASS + '-overlay';
77
export const FILTER_BUILDER_GROUP_CLASS = FILTER_BUILDER_CLASS + '-group';
8+
export const FILTER_BUILDER_GROUP_ITEM_CLASS = FILTER_BUILDER_CLASS + '-group-item';
89
export const FILTER_BUILDER_GROUP_OPERATION_CLASS = FILTER_BUILDER_CLASS + '-group-operation';
910
export const FILTER_BUILDER_IMAGE_ADD_CLASS = 'dx-icon-plus';
1011
export const FILTER_BUILDER_IMAGE_REMOVE_CLASS = 'dx-icon-remove';

packages/devextreme/testing/tests/DevExpress.ui.widgets/filterBuilderParts/markupTests.js

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fields from '../../../helpers/filterBuilderTestData.js';
33
import { shouldSkipOnMobile } from '../../../helpers/device.js';
44

55
import 'ui/filter_builder';
6+
import * as CLASSES from './constants.js';
67

78
const FILTER_BUILDER_GROUP_CONTENT_CLASS = 'dx-filterbuilder-group-content';
89

@@ -12,16 +13,18 @@ QUnit.test('markup init', function(assert) {
1213
}
1314

1415
const element = $('#container').dxFilterBuilder();
15-
const guid = element.find('.dx-filterbuilder-group-item').attr('aria-owns');
16+
const groupId = element.find(`.${CLASSES.FILTER_BUILDER_GROUP_ITEM_CLASS}`).attr('aria-owns');
17+
const operationId = element.find(`.${CLASSES.FILTER_BUILDER_GROUP_OPERATION_CLASS}`).attr('aria-controls');
18+
const actionId = element.find(`.${CLASSES.FILTER_BUILDER_IMAGE_ADD_CLASS}`).attr('aria-controls');
1619

1720
const $etalon = $(
1821
'<div id="container" class="dx-filterbuilder dx-widget">'
19-
+ '<div class="dx-filterbuilder-group">'
20-
+ '<div class="dx-filterbuilder-group-item" role="treeitem" aria-label="Group item" aria-level="1" aria-owns="' + guid + '">'
21-
+ '<div role="combobox" title="Operation" aria-haspopup="true" aria-expanded="false" class="dx-filterbuilder-text dx-filterbuilder-group-operation" tabindex="0">And</div>'
22-
+ '<div role="combobox" aria-label="Add" aria-haspopup="true" aria-expanded="false" class="dx-filterbuilder-action-icon dx-icon-plus dx-filterbuilder-action" tabindex="0"></div>'
22+
+ '<div class="dx-filterbuilder-group" role="tree">'
23+
+ '<div class="dx-filterbuilder-group-item" role="treeitem" aria-label="Group item" aria-level="1" aria-owns="' + groupId + '">'
24+
+ '<div aria-controls="' + operationId + '" role="combobox" title="Operation" aria-haspopup="true" aria-expanded="false" class="dx-filterbuilder-text dx-filterbuilder-group-operation" tabindex="0">And</div>'
25+
+ '<div aria-controls="' + actionId + '" role="combobox" aria-label="Add" aria-haspopup="true" aria-expanded="false" class="dx-filterbuilder-action-icon dx-icon-plus dx-filterbuilder-action" tabindex="0"></div>'
2326
+ '</div>'
24-
+ '<div class="dx-filterbuilder-group-content" id="' + guid + '" role="group"></div>'
27+
+ '<div class="dx-filterbuilder-group-content" id="' + groupId + '"></div>'
2528
+ '</div>'
2629
+ '</div>'
2730
);
@@ -59,28 +62,35 @@ QUnit.test('filter Content init by one condition', function(assert) {
5962
fields: fields,
6063
value: [[['CompanyName', '=', 'K&S Music'], 'Or'], 'And']
6164
});
62-
const guid = element.find('.dx-filterbuilder-group-item[aria-level="2"]').attr('aria-owns');
65+
66+
const groupElement = element.find('.dx-filterbuilder-group-item[aria-level="1"]').eq(1);
67+
const groupContent = element.find('.dx-filterbuilder-group-content').eq(1);
68+
const groupId = groupElement.attr('aria-owns');
69+
const operationId = groupElement.find(`.${CLASSES.FILTER_BUILDER_GROUP_OPERATION_CLASS}`).attr('aria-controls');
70+
const actionId = groupElement.find(`.${CLASSES.FILTER_BUILDER_IMAGE_ADD_CLASS}`).attr('aria-controls');
71+
const fieldId = groupContent.find(`.${CLASSES.FILTER_BUILDER_ITEM_FIELD_CLASS}`).attr('aria-controls');
72+
const itemOperationId = groupContent.find(`.${CLASSES.FILTER_BUILDER_ITEM_OPERATION_CLASS}`).attr('aria-controls');
6373

6474
const $etalon = $('<div/>').html(
6575
'<div class="dx-filterbuilder-group">'
66-
+ '<div class="dx-filterbuilder-group-item" role="treeitem" aria-label="Group item" aria-level="2" aria-owns="' + guid + '">'
67-
+ '<div class="dx-filterbuilder-action-icon dx-icon-remove dx-filterbuilder-action" tabindex="0" role="button" aria-label="Remove group"></div>'
68-
+ '<div role="combobox" title="Operation" aria-haspopup="true" aria-expanded="false" class="dx-filterbuilder-text dx-filterbuilder-group-operation" tabindex="0">Or</div>'
69-
+ '<div role="combobox" aria-label="Add" aria-haspopup="true" aria-expanded="false" class="dx-filterbuilder-action-icon dx-icon-plus dx-filterbuilder-action" tabindex="0"></div>'
70-
+ '</div>'
71-
+ '<div class="dx-filterbuilder-group-content" id="' + guid + '" role="group">'
72-
+ '<div class="dx-filterbuilder-group">'
73-
+ '<div class="dx-filterbuilder-group-item" role="treeitem" aria-level="2">'
74-
+ '<div class="dx-filterbuilder-action-icon dx-icon-remove dx-filterbuilder-action" tabindex="0" role="button" aria-label="Remove condition"></div>'
75-
+ '<div class="dx-filterbuilder-text dx-filterbuilder-item-field" tabindex="0" role="combobox" title="Item field" aria-haspopup="true" aria-expanded="false">Company Name</div>'
76-
+ '<div class="dx-filterbuilder-text dx-filterbuilder-item-operation" tabindex="0" role="combobox" title="Item operation" aria-haspopup="true" aria-expanded="false">Equals</div>'
77-
+ '<div class="dx-filterbuilder-text dx-filterbuilder-item-value">'
78-
+ '<div class="dx-filterbuilder-item-value-text" tabindex="0" role="button" title="Item value" aria-haspopup="true">K&amp;S Music</div>'
79-
+ '</div>'
76+
+ `<div class="dx-filterbuilder-group-item" role="treeitem" aria-label="Group item" aria-level="1" aria-owns="${groupId}">`
77+
+ '<div class="dx-filterbuilder-action-icon dx-icon-remove dx-filterbuilder-action" tabindex="0" role="button" aria-label="Remove group"></div>'
78+
+ `<div aria-controls="${operationId}" role="combobox" title="Operation" aria-haspopup="true" aria-expanded="false" class="dx-filterbuilder-text dx-filterbuilder-group-operation" tabindex="0">Or</div>`
79+
+ `<div aria-controls="${actionId}" role="combobox" aria-label="Add" aria-haspopup="true" aria-expanded="false" class="dx-filterbuilder-action-icon dx-icon-plus dx-filterbuilder-action" tabindex="0"></div>`
80+
+ '</div>'
81+
+ `<div class="dx-filterbuilder-group-content" id="${groupId}">`
82+
+ '<div class="dx-filterbuilder-group" role="group">'
83+
+ '<div class="dx-filterbuilder-group-item" role="treeitem" aria-level="2">'
84+
+ '<div class="dx-filterbuilder-action-icon dx-icon-remove dx-filterbuilder-action" tabindex="0" role="button" aria-label="Remove condition"></div>'
85+
+ `<div aria-controls="${fieldId}" class="dx-filterbuilder-text dx-filterbuilder-item-field" tabindex="0" role="combobox" title="Item field" aria-haspopup="true" aria-expanded="false">Company Name</div>`
86+
+ `<div aria-controls="${itemOperationId}" class="dx-filterbuilder-text dx-filterbuilder-item-operation" tabindex="0" role="combobox" title="Item operation" aria-haspopup="true" aria-expanded="false">Equals</div>`
87+
+ '<div class="dx-filterbuilder-text dx-filterbuilder-item-value">'
88+
+ '<div class="dx-filterbuilder-item-value-text" tabindex="0" role="button" title="Item value" aria-haspopup="true">K&amp;S Music</div>'
8089
+ '</div>'
8190
+ '</div>'
8291
+ '</div>'
8392
+ '</div>'
93+
+ '</div>'
8494
);
8595

8696
assert.equal(element.find('.' + FILTER_BUILDER_GROUP_CONTENT_CLASS).html(), $etalon.html());
@@ -91,33 +101,41 @@ QUnit.test('filter Content init by several conditions', function(assert) {
91101
return;
92102
}
93103

104+
const element = $('#container').dxFilterBuilder({
105+
fields: fields,
106+
value: [['CompanyName', '=', 'K&S Music'], 'or', ['Zipcode', '=', '98027']]
107+
});
108+
109+
const classLevel = 'dx-filterbuilder-group-item[aria-level="1"]';
110+
const groupItems = element.find(`.${FILTER_BUILDER_GROUP_CONTENT_CLASS} .${classLevel}`);
111+
const fieldId1 = groupItems.eq(0).find(`.${CLASSES.FILTER_BUILDER_ITEM_FIELD_CLASS}`).attr('aria-controls');
112+
const itemOperationId1 = groupItems.eq(0).find(`.${CLASSES.FILTER_BUILDER_ITEM_OPERATION_CLASS}`).attr('aria-controls');
113+
const fieldId2 = groupItems.eq(1).find(`.${CLASSES.FILTER_BUILDER_ITEM_FIELD_CLASS}`).attr('aria-controls');
114+
const itemOperationId2 = groupItems.eq(1).find(`.${CLASSES.FILTER_BUILDER_ITEM_OPERATION_CLASS}`).attr('aria-controls');
115+
94116
const $etalon = $('<div/>').html(
95-
'<div class="dx-filterbuilder-group">'
117+
'<div class="dx-filterbuilder-group" role="group">'
96118
+ '<div class="dx-filterbuilder-group-item" role="treeitem" aria-level="1">'
97119
+ '<div class="dx-filterbuilder-action-icon dx-icon-remove dx-filterbuilder-action" tabindex="0" role="button" aria-label="Remove condition"></div>'
98-
+ '<div class="dx-filterbuilder-text dx-filterbuilder-item-field" tabindex="0" role="combobox" title="Item field" aria-haspopup="true" aria-expanded="false">Company Name</div>'
99-
+ '<div class="dx-filterbuilder-text dx-filterbuilder-item-operation" tabindex="0" role="combobox" title="Item operation" aria-haspopup="true" aria-expanded="false">Equals</div>'
120+
+ `<div aria-controls="${fieldId1}" class="dx-filterbuilder-text dx-filterbuilder-item-field" tabindex="0" role="combobox" title="Item field" aria-haspopup="true" aria-expanded="false">Company Name</div>`
121+
+ `<div aria-controls="${itemOperationId1}" class="dx-filterbuilder-text dx-filterbuilder-item-operation" tabindex="0" role="combobox" title="Item operation" aria-haspopup="true" aria-expanded="false">Equals</div>`
100122
+ '<div class="dx-filterbuilder-text dx-filterbuilder-item-value">'
101123
+ '<div class="dx-filterbuilder-item-value-text" tabindex="0" role="button" title="Item value" aria-haspopup="true">K&amp;S Music</div>'
102124
+ '</div>'
103125
+ '</div>'
104126
+ '</div>'
105-
+ '<div class="dx-filterbuilder-group">'
127+
+ '<div class="dx-filterbuilder-group" role="group">'
106128
+ '<div class="dx-filterbuilder-group-item" role="treeitem" aria-level="1">'
107129
+ '<div class="dx-filterbuilder-action-icon dx-icon-remove dx-filterbuilder-action" tabindex="0" role="button" aria-label="Remove condition"></div>'
108-
+ '<div class="dx-filterbuilder-text dx-filterbuilder-item-field" tabindex="0" role="combobox" title="Item field" aria-haspopup="true" aria-expanded="false">Zipcode</div>'
109-
+ '<div class="dx-filterbuilder-text dx-filterbuilder-item-operation" tabindex="0" role="combobox" title="Item operation" aria-haspopup="true" aria-expanded="false">Equals</div>'
130+
+ `<div aria-controls="${fieldId2}" class="dx-filterbuilder-text dx-filterbuilder-item-field" tabindex="0" role="combobox" title="Item field" aria-haspopup="true" aria-expanded="false">Zipcode</div>`
131+
+ `<div aria-controls="${itemOperationId2}" class="dx-filterbuilder-text dx-filterbuilder-item-operation" tabindex="0" role="combobox" title="Item operation" aria-haspopup="true" aria-expanded="false">Equals</div>`
110132
+ '<div class="dx-filterbuilder-text dx-filterbuilder-item-value">'
111133
+ '<div class="dx-filterbuilder-item-value-text" tabindex="0" role="button" title="Item value" aria-haspopup="true">98027</div>'
112134
+ '</div>'
113135
+ '</div>'
114136
+ '</div>'
115137
);
116138

117-
const element = $('#container').dxFilterBuilder({
118-
fields: fields,
119-
value: [['CompanyName', '=', 'K&S Music'], 'or', ['Zipcode', '=', '98027']]
120-
});
121139
assert.equal(element.find('.' + FILTER_BUILDER_GROUP_CONTENT_CLASS).html(), $etalon.html());
122140
});
123141

0 commit comments

Comments
 (0)