From 19d22550abd68df7a99aa9c0db1d017379057f1c Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Wed, 16 Oct 2024 09:50:12 +0200 Subject: [PATCH] [TreeView] Fix usage of the `aria-selected` attribute (#14991) --- .../x-tree-view/src/TreeItem/TreeItem.tsx | 22 +++++++++---------- .../useTreeViewSelection.test.tsx | 17 ++++++++++---- .../src/useTreeItem2/useTreeItem2.ts | 20 ++++++++--------- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx index 6cc7d7cbec99..898f14212517 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx @@ -199,7 +199,7 @@ export const TreeItem = React.forwardRef(function TreeItem( icons: contextIcons, runItemPlugins, items: { disabledItemsFocusable, indentationAtItemLevel }, - selection: { multiSelect }, + selection: { disableSelection }, expansion: { expansionTrigger }, treeId, instance, @@ -358,17 +358,17 @@ export const TreeItem = React.forwardRef(function TreeItem( }); const icon = Icon ? : null; - let ariaSelected; - if (multiSelect) { - ariaSelected = selected; - } else if (selected) { - /* single-selection trees unset aria-selected on un-selected items. - * - * If the tree does not support multiple selection, aria-selected - * is set to true for the selected item and it is not present on any other item in the tree. - * Source: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ - */ + // https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ + let ariaSelected: boolean | undefined; + if (selected) { + // - each selected node has aria-selected set to true. ariaSelected = true; + } else if (disableSelection || disabled) { + // - if the tree contains nodes that are not selectable, aria-selected is not present on those nodes. + ariaSelected = undefined; + } else { + // - all nodes that are selectable but not selected have aria-selected set to false. + ariaSelected = false; } function handleFocus(event: React.FocusEvent) { diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx index 0f80094dc69a..448178c94601 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx @@ -712,12 +712,12 @@ describeTreeView<[UseTreeViewSelectionSignature, UseTreeViewExpansionSignature]> // This `describe` only tests basics scenarios, more complex scenarios are tested in this file's other `describe`. describe('aria-selected item attribute', () => { describe('single selection', () => { - it('should not have the attribute `aria-selected=false` if not selected', () => { + it('should have the attribute `aria-selected=false` if not selected', () => { const view = render({ items: [{ id: '1' }, { id: '2' }], }); - expect(view.getItemRoot('1')).not.to.have.attribute('aria-selected'); + expect(view.getItemRoot('1')).to.have.attribute('aria-selected', 'false'); }); it('should have the attribute `aria-selected=true` if selected', () => { @@ -750,14 +750,23 @@ describeTreeView<[UseTreeViewSelectionSignature, UseTreeViewExpansionSignature]> expect(view.getItemRoot('1')).to.have.attribute('aria-selected', 'true'); }); - it('should have the attribute `aria-selected=false` if disabledSelection is true', () => { + it('should not have the attribute `aria-selected=false` if disabledSelection is true', () => { const view = render({ multiSelect: true, items: [{ id: '1' }, { id: '2' }], disableSelection: true, }); - expect(view.getItemRoot('1')).to.have.attribute('aria-selected', 'false'); + expect(view.getItemRoot('1')).not.to.have.attribute('aria-selected'); + }); + + it('should not have the attribute `aria-selected=false` if the item is disabled', () => { + const view = render({ + multiSelect: true, + items: [{ id: '1', disabled: true }, { id: '2' }], + }); + + expect(view.getItemRoot('1')).not.to.have.attribute('aria-selected'); }); }); }); diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts index a8b8fff491e4..674f7380b39f 100644 --- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts +++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts @@ -37,7 +37,7 @@ export const useTreeItem2 = < const { runItemPlugins, items: { onItemClick, disabledItemsFocusable, indentationAtItemLevel }, - selection: { multiSelect, disableSelection, checkboxSelection }, + selection: { disableSelection, checkboxSelection }, expansion: { expansionTrigger }, treeId, instance, @@ -194,17 +194,17 @@ export const useTreeItem2 = < ...extractEventHandlers(externalProps), }; + // https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ let ariaSelected: boolean | undefined; - if (multiSelect) { - ariaSelected = status.selected; - } else if (status.selected) { - /* single-selection trees unset aria-selected on un-selected items. - * - * If the tree does not support multiple selection, aria-selected - * is set to true for the selected item and it is not present on any other item in the tree. - * Source: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ - */ + if (status.selected) { + // - each selected node has aria-selected set to true. ariaSelected = true; + } else if (disableSelection || status.disabled) { + // - if the tree contains nodes that are not selectable, aria-selected is not present on those nodes. + ariaSelected = undefined; + } else { + // - all nodes that are selectable but not selected have aria-selected set to false. + ariaSelected = false; } const props: UseTreeItem2RootSlotPropsFromUseTreeItem = {