From 12ce7fc41b57c3d440fdb1db5b6b543bf7fbf270 Mon Sep 17 00:00:00 2001 From: Alexander Kozlovskiy Date: Tue, 24 Oct 2023 04:33:41 -0500 Subject: [PATCH] TreeView: fix axe accessibility issues (#25860) --- .../ui.hierarchical_collection_widget.js | 12 ++++++++- .../js/ui/tree_view/ui.tree_view.base.js | 2 ++ .../contextMenu.tests.js | 21 +++++++++++++++ .../menu.markup.tests.js | 22 ++++++++++++++++ .../treeView.markup.tests.js | 26 +++++++++++++++++++ 5 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/ui/hierarchical_collection/ui.hierarchical_collection_widget.js b/packages/devextreme/js/ui/hierarchical_collection/ui.hierarchical_collection_widget.js index a6d17f3931b5..945f4b2ad336 100644 --- a/packages/devextreme/js/ui/hierarchical_collection/ui.hierarchical_collection_widget.js +++ b/packages/devextreme/js/ui/hierarchical_collection/ui.hierarchical_collection_widget.js @@ -105,7 +105,17 @@ const HierarchicalCollectionWidget = CollectionWidget.inherit({ }, _getIconContainer: function(itemData) { - return itemData.icon ? getImageContainer(itemData.icon) : undefined; + if(!itemData.icon) { + return undefined; + } + + const $imageContainer = getImageContainer(itemData.icon); + + if($imageContainer.is('img')) { + $imageContainer.attr('alt', itemData.text ?? `${this.NAME} item icon`); + } + + return $imageContainer; }, _getTextContainer: function(itemData) { diff --git a/packages/devextreme/js/ui/tree_view/ui.tree_view.base.js b/packages/devextreme/js/ui/tree_view/ui.tree_view.base.js index 9f975b807c4b..d71d6b1da9ca 100644 --- a/packages/devextreme/js/ui/tree_view/ui.tree_view.base.js +++ b/packages/devextreme/js/ui/tree_view/ui.tree_view.base.js @@ -631,6 +631,8 @@ const TreeViewBase = HierarchicalCollectionWidget.inherit({ direction: this.option('scrollDirection'), useKeyboard: false }); + + this.setAria('role', 'treeitem', this._scrollable.$element()); }, _renderNodeContainer: function($parent) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/contextMenu.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/contextMenu.tests.js index 6f480a335996..e68f6ab9e536 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/contextMenu.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/contextMenu.tests.js @@ -27,6 +27,7 @@ QUnit.testStart(() => { const DX_CONTEXT_MENU_CLASS = 'dx-context-menu'; const DX_MENU_ITEM_CLASS = 'dx-menu-item'; +const DX_ICON_CLASS = 'dx-icon'; const DX_MENU_ITEM_CONTENT_CLASS = 'dx-menu-item-content'; const DX_MENU_PHONE_CLASS = 'dx-menu-phone-overlay'; const DX_MENU_ITEM_SELECTED_CLASS = 'dx-menu-item-selected'; @@ -255,6 +256,26 @@ QUnit.module('Rendering', moduleConfig, () => { assert.notOk(instance._keyboardListenerId); }); + + QUnit.test('ContextMenu icon image should have alt attribute with item text if it specified', function(assert) { + const instance = new ContextMenu(this.$element, { + items: [{ text: 'Item text', icon: 'some_icon.jpg' }], + visible: true, + }); + const $icon = $(instance.itemsContainer()).find(`.${DX_MENU_ITEM_CLASS} .${DX_ICON_CLASS}`); + + assert.strictEqual($icon.attr('alt'), 'Item text'); + }); + + QUnit.test('ContextMenu icon image should have alt attribute with "dxContextMenu item icon" if item text is not specified', function(assert) { + const instance = new ContextMenu(this.$element, { + items: [{ icon: 'some_icon.jpg' }], + visible: true, + }); + const $icon = instance.itemsContainer().find(`.${DX_MENU_ITEM_CLASS} .${DX_ICON_CLASS}`); + + assert.strictEqual($icon.attr('alt'), 'dxContextMenu item icon'); + }); }); QUnit.module('Showing and hiding context menu', moduleConfig, () => { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/menu.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/menu.markup.tests.js index 5b7cfaeec679..ae08304192b8 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/menu.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/menu.markup.tests.js @@ -16,6 +16,8 @@ QUnit.testStart(() => { }); const DX_MENU_CLASS = 'dx-menu'; +const MENU_ITEM_CLASS = 'dx-menu-item'; +const ICON_CLASS = 'dx-icon'; const DX_MENU_ITEM_CLASS = DX_MENU_CLASS + '-item'; const DX_MENU_ITEM_SELECTED_CLASS = 'dx-menu-item-selected'; const DX_MENU_HORIZONTAL = 'dx-menu-horizontal'; @@ -72,6 +74,26 @@ QUnit.module('Menu rendering', { assert.ok(menu); assert.equal(root.length, 0, 'no root'); }); + + QUnit.test('Menu icon image should have alt attribute with item text if it specified', function(assert) { + const menu = createMenu({ + items: [{ text: 'Item text', icon: 'some_icon.jpg' }] + }); + + const $icon = menu.element.find(`.${MENU_ITEM_CLASS} .${ICON_CLASS}`); + + assert.strictEqual($icon.attr('alt'), 'Item text'); + }); + + QUnit.test('Menu icon image should have alt attribute with "dxMenu item icon" if item text is not specified', function(assert) { + const menu = createMenu({ + items: [{ icon: 'some_icon.jpg' }] + }); + + const $icon = menu.element.find(`.${MENU_ITEM_CLASS} .${ICON_CLASS}`); + + assert.strictEqual($icon.attr('alt'), 'dxMenu item icon'); + }); }); QUnit.module('Menu - selection', { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.markup.tests.js index c4ff3afd1b7d..58353a983701 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.markup.tests.js @@ -10,10 +10,12 @@ QUnit.testStart(function() { import 'ui/tree_view'; const WIDGET_CLASS = 'dx-treeview'; +const SCROLLABLE_CLASS = 'dx-scrollable'; const NODE_CONTAINER_CLASS = 'dx-treeview-node-container'; const OPENED_NODE_CONTAINER_CLASS = 'dx-treeview-node-container-opened'; const NODE_CLASS = 'dx-treeview-node'; const ITEM_CLASS = 'dx-treeview-item'; +const ICON_CLASS = 'dx-icon'; const SELECTED_STATE_CLASS = 'dx-state-selected'; const ITEM_WITH_CHECKBOX_CLASS = 'dx-treeview-item-with-checkbox'; const ITEM_WITHOUT_CHECKBOX_CLASS = 'dx-treeview-item-without-checkbox'; @@ -56,6 +58,12 @@ QUnit.module('aria accessibility', { assert.equal(this.$element.attr('role'), 'tree', 'role is correct'); }); + QUnit.test('scrollable should have role treeitem attribute', function(assert) { + const $scrollable = this.$element.find('.' + SCROLLABLE_CLASS); + + assert.equal($scrollable.attr('role'), 'treeitem', 'role is correct'); + }); + QUnit.test('aria role for items', function(assert) { const $node = this.$element.find('.' + NODE_CLASS); assert.equal($node.attr('role'), 'treeitem', 'role is correct'); @@ -604,5 +612,23 @@ QUnit.module('markup', { assert.ok($selectAll.hasClass('dx-checkbox-indeterminate')); }); + + QUnit.test('TreeView icon image should have alt attribute with item text if it specified', function(assert) { + const $treeView = initTree({ + items: [{ text: 'Item text', icon: 'some_icon.jpg' }] + }); + const $icon = $treeView.find(`.${ITEM_CLASS} .${ICON_CLASS}`); + + assert.strictEqual($icon.attr('alt'), 'Item text'); + }); + + QUnit.test('TreeView icon image should have alt attribute with "dxTreeView item icon" if item text is not specified', function(assert) { + const $treeView = initTree({ + items: [{ icon: 'some_icon.jpg' }] + }); + const $icon = $treeView.find(`.${ITEM_CLASS} .${ICON_CLASS}`); + + assert.strictEqual($icon.attr('alt'), 'dxTreeView item icon'); + }); });