Skip to content

Commit

Permalink
List: Improve aria for collapsible groups
Browse files Browse the repository at this point in the history
  • Loading branch information
marker-dao authored Nov 8, 2024
1 parent b6e3868 commit 7583a24
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 22 deletions.
4 changes: 2 additions & 2 deletions e2e/testcafe-devextreme/tests/accessibility/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const configurationWithSimpleItems: Configuration = {

testAccessibility(configurationWithSimpleItems);

const groupedItems = [[
const groupedItems = [
{
key: 'Mr. John Heart',
items: ['Choose between PPO and HMO Health Plan', 'Google AdWords Strategy', 'New Brochures', 'Update NDA Agreement', 'Review Product Recall Report by Engineering Team'],
Expand Down Expand Up @@ -64,7 +64,7 @@ const groupedItems = [[
key: 'Dr. Kent Samuelson',
items: ['Update Sales Strategy Documents', 'Review Revenue Projections', 'Refund Request Template'],
},
]];
];

const options: Options<Properties> = {
dataSource: [groupedItems],
Expand Down
93 changes: 73 additions & 20 deletions packages/devextreme/js/__internal/ui/list/m_list.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,14 +704,20 @@ export const ListBase = CollectionWidget.inherit({
_collapseGroupHandler($group, toggle) {
const deferred = Deferred();

if ($group.hasClass(LIST_GROUP_COLLAPSED_CLASS) === toggle) {
const $groupHeader = $group.children(`.${LIST_GROUP_HEADER_CLASS}`);
const collapsed = $group.hasClass(LIST_GROUP_COLLAPSED_CLASS);

this._updateGroupHeaderAriaExpanded($groupHeader, collapsed);

if (collapsed === toggle) {
return deferred.resolve();
}

const $groupBody = $group.children(`.${LIST_GROUP_BODY_CLASS}`);

const startHeight = getOuterHeight($groupBody);

let endHeight = 0;

if (startHeight === 0) {
setHeight($groupBody, 'auto');
endHeight = getOuterHeight($groupBody);
Expand Down Expand Up @@ -766,18 +772,17 @@ export const ListBase = CollectionWidget.inherit({
},

_setListAria() {
const { items, allowItemDeleting } = this.option();
const { items, allowItemDeleting, collapsibleGroups } = this.option();

const label = allowItemDeleting
? messageLocalization.format('dxList-listAriaLabel-deletable')
: messageLocalization.format('dxList-listAriaLabel');

const listArea = items?.length ? {
role: 'listbox',
label,
} : {
role: undefined,
label: undefined,
const shouldSetAria = items?.length && !collapsibleGroups;

const listArea = {
role: shouldSetAria ? 'listbox' : undefined,
label: shouldSetAria ? label : undefined,
};

this.setAria(listArea, this._$listContainer);
Expand Down Expand Up @@ -852,27 +857,67 @@ export const ListBase = CollectionWidget.inherit({
}
},

_renderGroup(index, group) {
const $groupElement = $('<div>')
.addClass(LIST_GROUP_CLASS)
.appendTo(this._getItemsContainer());
_setGroupAria($group, groupHeaderId): void {
const { collapsibleGroups } = this.option();

const id = `dx-${new Guid().toString()}`;
const groupAria = {
role: 'group',
role: collapsibleGroups ? undefined : 'group',
// eslint-disable-next-line spellcheck/spell-checker
labelledby: collapsibleGroups ? undefined : groupHeaderId,
};

this.setAria(groupAria, $group);
},

_updateGroupHeaderAriaExpanded($groupHeader, expanded): void {
this.setAria({ expanded }, $groupHeader);
},

_setGroupHeaderAria($groupHeader, listGroupBodyId): void {
const { collapsibleGroups } = this.option();

const groupHeaderAria = {
role: collapsibleGroups ? 'button' : undefined,
expanded: collapsibleGroups ? true : undefined,
controls: collapsibleGroups ? listGroupBodyId : undefined,
};

this.setAria(groupHeaderAria, $groupHeader);
},

_setGroupBodyAria($groupBody, groupHeaderId): void {
const { collapsibleGroups } = this.option();

const groupHeaderAria = {
role: collapsibleGroups ? 'listbox' : undefined,
// eslint-disable-next-line spellcheck/spell-checker
labelledby: id,
labelledby: collapsibleGroups ? groupHeaderId : undefined,
};

this.setAria(groupAria, $groupElement);
this.setAria(groupHeaderAria, $groupBody);
},

_renderGroup(index, group) {
const $groupElement = $('<div>')
.addClass(LIST_GROUP_CLASS)
.appendTo(this._getItemsContainer());

const groupHeaderId = `dx-${new Guid().toString()}`;

const $groupHeaderElement = $('<div>')
.addClass(LIST_GROUP_HEADER_CLASS)
.attr('id', id)
.attr('id', groupHeaderId)
.appendTo($groupElement);

const groupTemplateName = this.option('groupTemplate');
const groupTemplate = this._getTemplate(group.template || groupTemplateName, group, index, $groupHeaderElement);
const { groupTemplate: templateName } = this.option();

const groupTemplate = this._getTemplate(
group.template || templateName,
group,
index,
$groupHeaderElement,
);

const renderArgs = {
index,
itemData: group,
Expand All @@ -887,9 +932,13 @@ export const ListBase = CollectionWidget.inherit({

this._renderingGroupIndex = index;

const groupBodyId = `dx-${new Guid().toString()}`;

const $groupBody = $('<div>')
.addClass(LIST_GROUP_BODY_CLASS)
.attr('id', groupBodyId)
.appendTo($groupElement);

// @ts-expect-error
each(groupItemsGetter(group) || [], (itemIndex, item) => {
this._renderItem({ group: index, item: itemIndex }, item, $groupBody);
Expand All @@ -900,6 +949,10 @@ export const ListBase = CollectionWidget.inherit({
groupIndex: index,
groupData: group,
});

this._setGroupAria($groupElement, groupHeaderId);
this._setGroupHeaderAria($groupHeaderElement, groupBodyId);
this._setGroupBodyAria($groupBody, groupHeaderId);
},

downInkRippleHandler(e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4628,4 +4628,183 @@ QUnit.module('Accessibility', () => {
$switchableButton = $(`.${SWITCHABLE_DELETE_BUTTON_CLASS}`);
checkButtonAttributes(assert, $switchableButton);
});

[true, false].forEach(collapsibleGroups => {
QUnit.test(`Grouped list items element should have correct aria if collapsibleGroups: ${collapsibleGroups}`, function(assert) {
const $element = $('#list').dxList({
dataSource: [{ key: 1, items: [1] }],
grouped: true,
collapsibleGroups,
});

const $listItems = $element.find(`.${LIST_ITEMS_CLASS}`);

const expectedListItemsAria = {
role: collapsibleGroups ? undefined : 'listbox',
'aria-label': collapsibleGroups ? undefined : 'Items',
};

assert.strictEqual($listItems.attr('role'), expectedListItemsAria.role, 'role is correct');
assert.strictEqual($listItems.attr('aria-label'), expectedListItemsAria['aria-label'], 'aria-label is correct');
});

QUnit.test(`Group element should have correct aria if collapsibleGroups: ${collapsibleGroups}`, function(assert) {
const $element = $('#list').dxList({
dataSource: [{ key: 1, items: [1] }],
grouped: true,
collapsibleGroups,
});

const $groupElement = $element.find(`.${LIST_GROUP_CLASS}`);

const expectedGroupAria = {
role: collapsibleGroups ? undefined : 'group',
'aria-labelledby': collapsibleGroups ? false : true,
};

assert.strictEqual($groupElement.attr('role'), expectedGroupAria.role, 'role is correct');
assert.strictEqual(!!$groupElement.attr('aria-labelledby'), expectedGroupAria['aria-labelledby'], 'aria-labelledby is correct');
});

QUnit.test(`Group header element should have correct aria if collapsibleGroups: ${collapsibleGroups}`, function(assert) {
const $element = $('#list').dxList({
dataSource: [{ key: 1, items: [1] }],
grouped: true,
collapsibleGroups,
});

const $groupHeader = $element.find(`.${LIST_GROUP_HEADER_CLASS}`);

const expectedGroupHeaderAria = {
role: collapsibleGroups ? 'button' : undefined,
'aria-expanded': collapsibleGroups ? 'true' : undefined,
'aria-controls': collapsibleGroups ? true : false,
};

assert.strictEqual($groupHeader.attr('role'), expectedGroupHeaderAria.role, 'role is correct');
assert.strictEqual($groupHeader.attr('aria-expanded'), expectedGroupHeaderAria['aria-expanded'], 'aria-expanded is correct');
assert.strictEqual(!!$groupHeader.attr('aria-controls'), expectedGroupHeaderAria['aria-controls'], 'aria-controls is correct');
});

QUnit.test(`Group body element should have correct aria if collapsibleGroups: ${collapsibleGroups}`, function(assert) {
const $element = $('#list').dxList({
dataSource: [{ key: 1, items: [1] }],
grouped: true,
collapsibleGroups,
});

const $groupBody = $element.find(`.${LIST_GROUP_BODY_CLASS}`);

const expectedGroupBodyAria = {
role: collapsibleGroups ? 'listbox' : undefined,
'aria-labelledby': collapsibleGroups ? true : false,
};

assert.strictEqual($groupBody.attr('role'), expectedGroupBodyAria.role, 'role is correct');
assert.strictEqual(!!$groupBody.attr('aria-labelledby'), expectedGroupBodyAria['aria-labelledby'], 'aria-labelledby is correct');
});

QUnit.test(`Grouped list items element should have correct aria if collapsibleGroups is changed in runtime, init value: ${collapsibleGroups}`, function(assert) {
const $element = $('#list').dxList({
dataSource: [{ key: 1, items: [1] }],
grouped: true,
collapsibleGroups,
});
const instance = $element.dxList('instance');

instance.option('collapsibleGroups', !collapsibleGroups);

const $listItems = $element.find(`.${LIST_ITEMS_CLASS}`);

const expectedListItemsAria = {
role: collapsibleGroups ? 'listbox' : undefined,
'aria-label': collapsibleGroups ? 'Items' : undefined,
};

assert.strictEqual($listItems.attr('role'), expectedListItemsAria.role, 'role is correct');
assert.strictEqual($listItems.attr('aria-label'), expectedListItemsAria['aria-label'], 'aria-label is correct');
});

QUnit.test(`Group element should have correct aria if collapsibleGroups is changed in runtime, init value: ${collapsibleGroups}`, function(assert) {
const $element = $('#list').dxList({
dataSource: [{ key: 1, items: [1] }],
grouped: true,
collapsibleGroups,
});
const instance = $element.dxList('instance');

instance.option('collapsibleGroups', !collapsibleGroups);

const $groupElement = $element.find(`.${LIST_GROUP_CLASS}`);

const expectedGroupAria = {
role: collapsibleGroups ? 'group' : undefined,
'aria-labelledby': collapsibleGroups ? true : false,
};

assert.strictEqual($groupElement.attr('role'), expectedGroupAria.role, 'role is correct');
assert.strictEqual(!!$groupElement.attr('aria-labelledby'), expectedGroupAria['aria-labelledby'], 'aria-labelledby is correct');
});

QUnit.test(`Group header element should have correct aria if collapsibleGroups is changed in runtime, init value: ${collapsibleGroups}`, function(assert) {
const $element = $('#list').dxList({
dataSource: [{ key: 1, items: [1] }],
grouped: true,
collapsibleGroups,
});
const instance = $element.dxList('instance');

instance.option('collapsibleGroups', !collapsibleGroups);

const $groupHeader = $element.find(`.${LIST_GROUP_HEADER_CLASS}`);

const expectedGroupHeaderAria = {
role: collapsibleGroups ? undefined : 'button',
'aria-expanded': collapsibleGroups ? undefined : 'true',
'aria-controls': collapsibleGroups ? false : true,
};

assert.strictEqual($groupHeader.attr('role'), expectedGroupHeaderAria.role, 'role is correct');
assert.strictEqual($groupHeader.attr('aria-expanded'), expectedGroupHeaderAria['aria-expanded'], 'aria-expanded is correct');
assert.strictEqual(!!$groupHeader.attr('aria-controls'), expectedGroupHeaderAria['aria-controls'], 'aria-controls is correct');
});

QUnit.test(`Group body element should have correct aria if collapsibleGroups is changed in runtime, init value: ${collapsibleGroups}`, function(assert) {
const $element = $('#list').dxList({
dataSource: [{ key: 1, items: [1] }],
grouped: true,
collapsibleGroups,
});
const instance = $element.dxList('instance');

instance.option('collapsibleGroups', !collapsibleGroups);

const $groupBody = $element.find(`.${LIST_GROUP_BODY_CLASS}`);

const expectedGroupBodyAria = {
role: collapsibleGroups ? undefined : 'listbox',
'aria-labelledby': collapsibleGroups ? false : true,
};

assert.strictEqual($groupBody.attr('role'), expectedGroupBodyAria.role, 'role is correct');
assert.strictEqual(!!$groupBody.attr('aria-labelledby'), expectedGroupBodyAria['aria-labelledby'], 'aria-labelledby is correct');
});
});

QUnit.test('Group header element should have correct aria-expanded after click if collapsibleGroups: true', function(assert) {
const $element = $('#list').dxList({
dataSource: [{ key: 1, items: [1] }],
grouped: true,
collapsibleGroups: true,
});

const $groupHeader = $element.find(`.${LIST_GROUP_HEADER_CLASS}`);
assert.strictEqual($groupHeader.attr('aria-expanded'), 'true', 'aria-expanded is correct');

$groupHeader.trigger('dxclick');
assert.strictEqual($groupHeader.attr('aria-expanded'), 'false', 'aria-expanded is changed');

$groupHeader.trigger('dxclick');
assert.strictEqual($groupHeader.attr('aria-expanded'), 'true', 'aria-expanded is changed');
});
});

0 comments on commit 7583a24

Please sign in to comment.