From 0147f132766f72fc35f83f33a7f3ed2a0bcb5353 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Thu, 9 Jul 2026 13:50:00 +0200 Subject: [PATCH 01/15] fix(react-tree): expose tree selection control to assistive technologies --- .../TreeItemLayout/useTreeItemLayout.tsx | 14 ++++++++++++-- .../useTreeItemPersonaLayout.ts | 7 ++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index bccb8bfe85fcf..e75b65086dc82 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -9,6 +9,7 @@ import { useEventCallback, elementContains, useControllableState, + useId, } from '@fluentui/react-utilities'; import { useTreeItemContext_unstable, useTreeContext_unstable } from '../../contexts'; import type { @@ -66,6 +67,11 @@ export const useTreeItemLayout_unstable = ( const checked = useTreeItemContext_unstable(ctx => ctx.checked); const isBranch = useTreeItemContext_unstable(ctx => ctx.itemType === 'branch'); + // Id of the item content, used to give the selection control an accessible name so that + // assistive technologies which target the visible control directly (e.g. Voice Access) can + // reach it. See https://aka.ms/MAS4.3.1 + const mainContentId = useId('fui-TreeItemLayout__main-'); + // FIXME: Asserting is required here, as converting this to RefObject on context type would be a breaking change // eslint-disable-next-line react-hooks/refs assertIsRefObject(treeItemRef); @@ -233,7 +239,11 @@ export const useTreeItemLayout_unstable = ( }, ), iconBefore: slot.optional(iconBefore, { elementType: 'div' }), - main: slot.always(main, { elementType: 'div' }), + main: slot.always(main, { + // Only expose an id when there is a selector that needs to reference it for its name. + defaultProps: { id: selectionMode !== 'none' ? mainContentId : undefined }, + elementType: 'div', + }), iconAfter: slot.optional(iconAfter, { elementType: 'div' }), aside: !isActionsVisible ? slot.optional(props.aside, { elementType: 'div' }) : undefined, actions, @@ -243,7 +253,7 @@ export const useTreeItemLayout_unstable = ( defaultProps: { checked, tabIndex: -1, - 'aria-hidden': true, + 'aria-labelledby': mainContentId, ref: selectionRef, // casting here to a union between checkbox and radio // since ref is not present on the selector signature diff --git a/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.ts b/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.ts index 9a338ee629b3c..f5b8c8c318616 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.ts +++ b/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.ts @@ -52,7 +52,12 @@ export const useTreeItemPersonaLayout_unstable = ( selector: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType, }, avatarSize: treeAvatarSize[size], - main: slot.always(main, { defaultProps: { children }, elementType: 'div' }), + // Preserve the id assigned by useTreeItemLayout so the selector's `aria-labelledby` + // (which names the selection control) keeps pointing at the primary content. + main: slot.always(main, { + defaultProps: { children, id: treeItemLayoutState.main.id }, + elementType: 'div', + }), media: slot.always(media, { elementType: 'div' }), description: slot.optional(description, { elementType: 'div' }), }; From 41cc16c881bbd8b9c6a931b77c76b0417016f222 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Fri, 10 Jul 2026 10:06:24 +0200 Subject: [PATCH 02/15] fix(react-tree): add unit tests --- .../TreeItemLayout/TreeItemLayout.test.tsx | 31 ++++++++++++++++++- .../TreeItemPersonaLayout.test.tsx | 19 +++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/TreeItemLayout.test.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/TreeItemLayout.test.tsx index 0b4a449272b4a..f42f71a051f35 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/TreeItemLayout.test.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/TreeItemLayout.test.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { TreeItemLayout } from './TreeItemLayout'; import { isConformant } from '../../testing/isConformant'; import { TreeItemProvider } from '../../contexts'; +import { Tree } from '../../Tree'; +import { TreeItem } from '../TreeItem/TreeItem'; const Wrapper: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( { const result = render(Default TreeItemLayout); expect(result.container).toMatchSnapshot(); }); + + // Regression tests for the accessibility fix that exposes the selection control to assistive + // technologies (e.g. Voice Access). The selector must not be `aria-hidden` (so it is found by + // role) and must have an accessible name derived from the item content. See MAS 4.3.1. + it('exposes the multiselect selection control with an accessible name', () => { + render( + + + Item 1 + + , + ); + + expect(screen.getByRole('checkbox', { name: 'Item 1' })).toBeTruthy(); + }); + + it('exposes the single-selection control with an accessible name', () => { + render( + + + Item 1 + + , + ); + + expect(screen.getByRole('radio', { name: 'Item 1' })).toBeTruthy(); + }); }); diff --git a/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx b/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx index 468ebed0c371c..de412aaf2f3b9 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { TreeItemPersonaLayout } from './TreeItemPersonaLayout'; import { isConformant } from '../../testing/isConformant'; import { TreeItemProvider } from '../../contexts'; +import { Tree } from '../../Tree'; +import { TreeItem } from '../TreeItem/TreeItem'; const Wrapper: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( { const result = render(Default TreeItemPersonaLayout); expect(result.container).toMatchSnapshot(); }); + + // Regression test: the persona layout shares the selection control via TreeItemLayout, so it + // must also expose a named selector. The content id is carried onto its `main` slot so the + // selector's `aria-labelledby` resolves here too. See MAS 4.3.1. + it('exposes the selection control with an accessible name', () => { + render( + + + Jane Doe + + , + ); + + expect(screen.getByRole('checkbox', { name: 'Jane Doe' })).toBeTruthy(); + }); }); From 26b0e06bde0e9f2d484ff14b405eed7d0fe60b6c Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Fri, 10 Jul 2026 10:24:21 +0200 Subject: [PATCH 03/15] fix(react-tree): move unit test to appropriate location --- .../library/src/components/Tree/Tree.test.tsx | 40 ++++++++++++++++++- .../TreeItemLayout/TreeItemLayout.test.tsx | 31 +------------- .../TreeItemPersonaLayout.test.tsx | 19 +-------- 3 files changed, 41 insertions(+), 49 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx index 4f5bc730ece6c..24967b74e7b5f 100644 --- a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx +++ b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { Tree } from './Tree'; import { TreeItem } from '../TreeItem/TreeItem'; +import { TreeItemLayout } from '../TreeItemLayout/TreeItemLayout'; +import { TreeItemPersonaLayout } from '../TreeItemPersonaLayout/TreeItemPersonaLayout'; describe('Tree', () => { isConformant({ @@ -37,4 +39,40 @@ describe('Tree', () => { expect(handleOpenChange).toHaveBeenNthCalledWith(2, expect.any(Object), expect.objectContaining({ open: false })); }); + + describe('selection control accessibility', () => { + it.each([ + { + case: 'multiselect', + selectionMode: 'multiselect', + role: 'checkbox', + layout: Item 1, + accessibleName: 'Item 1', + }, + { + case: 'single', + selectionMode: 'single', + role: 'radio', + layout: Item 1, + accessibleName: 'Item 1', + }, + { + case: 'TreeItemPersonaLayout', + selectionMode: 'multiselect', + role: 'checkbox', + layout: Jane Doe, + accessibleName: 'Jane Doe', + }, + ] as const)('exposes a named $role selector ($case)', ({ selectionMode, role, layout, accessibleName }) => { + render( + + + {layout} + + , + ); + + expect(screen.getByRole(role, { name: accessibleName })).toBeTruthy(); + }); + }); }); diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/TreeItemLayout.test.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/TreeItemLayout.test.tsx index f42f71a051f35..0b4a449272b4a 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/TreeItemLayout.test.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/TreeItemLayout.test.tsx @@ -1,10 +1,8 @@ import * as React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { TreeItemLayout } from './TreeItemLayout'; import { isConformant } from '../../testing/isConformant'; import { TreeItemProvider } from '../../contexts'; -import { Tree } from '../../Tree'; -import { TreeItem } from '../TreeItem/TreeItem'; const Wrapper: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( { const result = render(Default TreeItemLayout); expect(result.container).toMatchSnapshot(); }); - - // Regression tests for the accessibility fix that exposes the selection control to assistive - // technologies (e.g. Voice Access). The selector must not be `aria-hidden` (so it is found by - // role) and must have an accessible name derived from the item content. See MAS 4.3.1. - it('exposes the multiselect selection control with an accessible name', () => { - render( - - - Item 1 - - , - ); - - expect(screen.getByRole('checkbox', { name: 'Item 1' })).toBeTruthy(); - }); - - it('exposes the single-selection control with an accessible name', () => { - render( - - - Item 1 - - , - ); - - expect(screen.getByRole('radio', { name: 'Item 1' })).toBeTruthy(); - }); }); diff --git a/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx b/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx index de412aaf2f3b9..468ebed0c371c 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx @@ -1,10 +1,8 @@ import * as React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { TreeItemPersonaLayout } from './TreeItemPersonaLayout'; import { isConformant } from '../../testing/isConformant'; import { TreeItemProvider } from '../../contexts'; -import { Tree } from '../../Tree'; -import { TreeItem } from '../TreeItem/TreeItem'; const Wrapper: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( { const result = render(Default TreeItemPersonaLayout); expect(result.container).toMatchSnapshot(); }); - - // Regression test: the persona layout shares the selection control via TreeItemLayout, so it - // must also expose a named selector. The content id is carried onto its `main` slot so the - // selector's `aria-labelledby` resolves here too. See MAS 4.3.1. - it('exposes the selection control with an accessible name', () => { - render( - - - Jane Doe - - , - ); - - expect(screen.getByRole('checkbox', { name: 'Jane Doe' })).toBeTruthy(); - }); }); From bea47ecbae15d29dbb5a4c1d37d6a965da489642 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Fri, 10 Jul 2026 10:32:09 +0200 Subject: [PATCH 04/15] fix(react-tree): remove comments and add change file --- ...ui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json | 7 +++++++ .../src/components/TreeItemLayout/useTreeItemLayout.tsx | 4 ---- .../TreeItemPersonaLayout/useTreeItemPersonaLayout.ts | 2 -- 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 change/@fluentui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json diff --git a/change/@fluentui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json b/change/@fluentui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json new file mode 100644 index 0000000000000..d22eed77a1ec1 --- /dev/null +++ b/change/@fluentui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Tree selection checkbox/radio is now exposed with an accessible name so Voice Access users can select items.", + "packageName": "@fluentui/react-tree", + "email": "paulmardling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index e75b65086dc82..84f018955de7c 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -67,9 +67,6 @@ export const useTreeItemLayout_unstable = ( const checked = useTreeItemContext_unstable(ctx => ctx.checked); const isBranch = useTreeItemContext_unstable(ctx => ctx.itemType === 'branch'); - // Id of the item content, used to give the selection control an accessible name so that - // assistive technologies which target the visible control directly (e.g. Voice Access) can - // reach it. See https://aka.ms/MAS4.3.1 const mainContentId = useId('fui-TreeItemLayout__main-'); // FIXME: Asserting is required here, as converting this to RefObject on context type would be a breaking change @@ -240,7 +237,6 @@ export const useTreeItemLayout_unstable = ( ), iconBefore: slot.optional(iconBefore, { elementType: 'div' }), main: slot.always(main, { - // Only expose an id when there is a selector that needs to reference it for its name. defaultProps: { id: selectionMode !== 'none' ? mainContentId : undefined }, elementType: 'div', }), diff --git a/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.ts b/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.ts index f5b8c8c318616..73e0e0ee41392 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.ts +++ b/packages/react-components/react-tree/library/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.ts @@ -52,8 +52,6 @@ export const useTreeItemPersonaLayout_unstable = ( selector: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType, }, avatarSize: treeAvatarSize[size], - // Preserve the id assigned by useTreeItemLayout so the selector's `aria-labelledby` - // (which names the selection control) keeps pointing at the primary content. main: slot.always(main, { defaultProps: { children, id: treeItemLayoutState.main.id }, elementType: 'div', From c54547614ef84f58974f18e9494e044890e100a6 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Thu, 16 Jul 2026 12:28:59 +0200 Subject: [PATCH 05/15] Update change/@fluentui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json Co-authored-by: Victor Genaev --- ...luentui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/change/@fluentui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json b/change/@fluentui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json index d22eed77a1ec1..e67dfaa02ea17 100644 --- a/change/@fluentui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json +++ b/change/@fluentui-react-tree-48fe2b57-5a5f-4455-b90c-0810d0a2ba03.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "Tree selection checkbox/radio is now exposed with an accessible name so Voice Access users can select items.", + "comment": "fix: add accessible names to Tree selection controls", "packageName": "@fluentui/react-tree", "email": "paulmardling@microsoft.com", "dependentChangeType": "patch" From e7c44a747a906942e3f3b90518f383d05134b796 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Thu, 16 Jul 2026 12:55:25 +0200 Subject: [PATCH 06/15] fix(react-tree): preserve selection control labels --- .../library/src/components/Tree/Tree.test.tsx | 57 +++++++++++++++++++ .../TreeItemLayout/useTreeItemLayout.tsx | 43 +++++++------- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx index 24967b74e7b5f..b232c5e69ee7c 100644 --- a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx +++ b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx @@ -74,5 +74,62 @@ describe('Tree', () => { expect(screen.getByRole(role, { name: accessibleName })).toBeTruthy(); }); + + it.each([ + { + case: 'TreeItemLayout', + layout: Item 1, + mainId: 'custom-main', + accessibleName: 'Item 1', + }, + { + case: 'TreeItemPersonaLayout', + layout: Jane Doe, + mainId: 'custom-persona-main', + accessibleName: 'Jane Doe', + }, + ] as const)('preserves a consumer-provided main ID ($case)', ({ layout, mainId, accessibleName }) => { + render( + + + {layout} + + , + ); + + expect(screen.getByRole('checkbox', { name: accessibleName }).getAttribute('aria-labelledby')).toBe(mainId); + expect(document.getElementById(mainId)?.textContent).toBe(accessibleName); + }); + + it('preserves a consumer-provided selector aria-label', () => { + render( + + + Item 1 + + , + ); + + const selector = screen.getByRole('checkbox', { name: 'Custom selector label' }); + expect(selector.getAttribute('aria-label')).toBe('Custom selector label'); + expect(selector.hasAttribute('aria-labelledby')).toBe(false); + }); + + it('preserves a consumer-provided selector aria-labelledby', () => { + render( + <> + External selector label + + + Item 1 + + + , + ); + + expect(screen.getByRole('checkbox', { name: 'External selector label' }).getAttribute('aria-labelledby')).toBe( + 'external-selector-label', + ); + }); }); }); diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index 84f018955de7c..ee8e88058de8a 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -67,7 +67,27 @@ export const useTreeItemLayout_unstable = ( const checked = useTreeItemContext_unstable(ctx => ctx.checked); const isBranch = useTreeItemContext_unstable(ctx => ctx.itemType === 'branch'); - const mainContentId = useId('fui-TreeItemLayout__main-'); + const mainSlot = slot.always(main, { elementType: 'div' }); + const mainContentId = useId('fui-TreeItemLayout__main-', mainSlot.id); + + const selector = slot.optional(props.selector, { + renderByDefault: selectionMode !== 'none', + defaultProps: { + checked, + tabIndex: -1, + ref: selectionRef, + // casting here to a union between checkbox and radio + // since ref is not present on the selector signature + // FIXME: look into Slot type to see if we can make this work + } as CheckboxProps | RadioProps, + elementType: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType, + }); + if (selector) { + mainSlot.id = mainContentId; + if (!selector['aria-label'] && !selector['aria-labelledby']) { + selector['aria-labelledby'] = mainContentId; + } + } // FIXME: Asserting is required here, as converting this to RefObject on context type would be a breaking change // eslint-disable-next-line react-hooks/refs @@ -236,29 +256,12 @@ export const useTreeItemLayout_unstable = ( }, ), iconBefore: slot.optional(iconBefore, { elementType: 'div' }), - main: slot.always(main, { - defaultProps: { id: selectionMode !== 'none' ? mainContentId : undefined }, - elementType: 'div', - }), + main: mainSlot, iconAfter: slot.optional(iconAfter, { elementType: 'div' }), aside: !isActionsVisible ? slot.optional(props.aside, { elementType: 'div' }) : undefined, actions, expandIcon, - selector: slot.optional(props.selector, { - renderByDefault: selectionMode !== 'none', - defaultProps: { - checked, - tabIndex: -1, - 'aria-labelledby': mainContentId, - ref: selectionRef, - // casting here to a union between checkbox and radio - // since ref is not present on the selector signature - // FIXME: look into Slot type to see if we can make this work - } as CheckboxProps | RadioProps, - elementType: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType< - CheckboxProps | RadioProps - >, - }), + selector, }; }; From 345ccb51bf2341f52af2bb1e47c48e920b5fa027 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Mon, 20 Jul 2026 15:36:29 +0200 Subject: [PATCH 07/15] fix(react-tree): pass slot props during construction, not mutation Pass `ref` and `onBlur` to the actions slot as defaultProps during construction, rather than mutating the slot after creation. This follows Fluent UI best practices and addresses the code review feedback from @bsunderhus. Removed post-construction mutations on the actions slot and moved callback creation before slot initialization. --- .../TreeItemLayout/useTreeItemLayout.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index ee8e88058de8a..6137e10bddda9 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -178,16 +178,7 @@ export const useTreeItemLayout_unstable = ( expandIcon.ref = expandIconRefs; } const arrowNavigationProps = useArrowNavigationGroup({ circular: navigationMode === 'tree', axis: 'horizontal' }); - const actions = isActionsVisible - ? slot.optional(props.actions, { - defaultProps: { ...arrowNavigationProps, role: 'toolbar' }, - elementType: 'div', - }) - : undefined; - delete actions?.visible; - delete actions?.onVisibilityChange; - const actionsRefs = useMergedRefs(actions?.ref, actionsRef, actionsRefInternal); const handleActionsBlur = useEventCallback((event: React.FocusEvent) => { if (isResolvedShorthand(props.actions)) { props.actions.onBlur?.(event); @@ -200,10 +191,19 @@ export const useTreeItemLayout_unstable = ( } as Extract); setIsActionsVisible(isRelatedTargetFromActions); }); - if (actions) { - actions.ref = actionsRefs; - actions.onBlur = handleActionsBlur; - } + + const actionsRefs = useMergedRefs(actionsRef, actionsRefInternal); + const actions = isActionsVisible + ? slot.optional(props.actions, { + defaultProps: { + ...arrowNavigationProps, + role: 'toolbar', + ref: actionsRefs, + onBlur: handleActionsBlur, + }, + elementType: 'div', + }) + : undefined; const hasActions = Boolean(props.actions); From ea7ea78cb51bf5cfbd6c99ff9debee03f570e7f2 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Mon, 20 Jul 2026 15:44:17 +0200 Subject: [PATCH 08/15] fix(react-tree): pass all slot props during construction, not mutation Pass `ref`, `onBlur`, and other props to both the `expandIcon` and `actions` slots as defaultProps during construction, rather than mutating the slots after creation. This eliminates the post-construction mutation pattern and follows Fluent UI best practices, addressing the code review feedback from @bsunderhus. --- .../src/components/TreeItemLayout/useTreeItemLayout.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index 6137e10bddda9..b5d9e05040c44 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -165,18 +165,16 @@ export const useTreeItemLayout_unstable = ( [setIsActionsVisible, onActionVisibilityChange, treeItemRef, isNavigatingWithKeyboard, targetDocument], ); + const expandIconRefs = useMergedRefs(expandIconRef); const expandIcon = slot.optional(props.expandIcon, { renderByDefault: isBranch, defaultProps: { children: , 'aria-hidden': true, + ref: expandIconRefs, }, elementType: 'div', }); - const expandIconRefs = useMergedRefs(expandIcon?.ref, expandIconRef); - if (expandIcon) { - expandIcon.ref = expandIconRefs; - } const arrowNavigationProps = useArrowNavigationGroup({ circular: navigationMode === 'tree', axis: 'horizontal' }); const handleActionsBlur = useEventCallback((event: React.FocusEvent) => { From b4d0c834715ef1cf441293fd09a494159c98e522 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Mon, 20 Jul 2026 15:48:28 +0200 Subject: [PATCH 09/15] fix(react-tree): construct selector slot with conditional aria-labelledby Pass selector props during slot construction instead of mutating after. Preserve the conditional logic: only set aria-labelledby if the consumer didn't provide aria-label or aria-labelledby. Also set mainSlot.id unconditionally during construction. Update snapshots to reflect the id being added to the main slot. --- .../TreeItemLayout/useTreeItemLayout.tsx | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index b5d9e05040c44..5787f1a36c2ca 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -69,25 +69,28 @@ export const useTreeItemLayout_unstable = ( const mainSlot = slot.always(main, { elementType: 'div' }); const mainContentId = useId('fui-TreeItemLayout__main-', mainSlot.id); + mainSlot.id = mainContentId; + + const selectorDefaultProps: CheckboxProps | RadioProps = { + checked, + tabIndex: -1, + ref: selectionRef, + // casting here to a union between checkbox and radio + // since ref is not present on the selector signature + // FIXME: look into Slot type to see if we can make this work + }; + + // Only set aria-labelledby if the consumer didn't provide aria-label or aria-labelledby + const selectorShorthand = isResolvedShorthand(props.selector) ? props.selector : undefined; + if (!selectorShorthand?.['aria-label'] && !selectorShorthand?.['aria-labelledby']) { + selectorDefaultProps['aria-labelledby'] = mainContentId; + } const selector = slot.optional(props.selector, { renderByDefault: selectionMode !== 'none', - defaultProps: { - checked, - tabIndex: -1, - ref: selectionRef, - // casting here to a union between checkbox and radio - // since ref is not present on the selector signature - // FIXME: look into Slot type to see if we can make this work - } as CheckboxProps | RadioProps, + defaultProps: selectorDefaultProps, elementType: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType, }); - if (selector) { - mainSlot.id = mainContentId; - if (!selector['aria-label'] && !selector['aria-labelledby']) { - selector['aria-labelledby'] = mainContentId; - } - } // FIXME: Asserting is required here, as converting this to RefObject on context type would be a breaking change // eslint-disable-next-line react-hooks/refs From 6f2867daca00b275e6cc6a3ac1dff4ff8acde641 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Mon, 20 Jul 2026 17:04:31 +0200 Subject: [PATCH 10/15] Update packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx Co-authored-by: Bernardo Sunderhus --- .../TreeItemLayout/useTreeItemLayout.tsx | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index 5787f1a36c2ca..e526c471682d4 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -67,31 +67,29 @@ export const useTreeItemLayout_unstable = ( const checked = useTreeItemContext_unstable(ctx => ctx.checked); const isBranch = useTreeItemContext_unstable(ctx => ctx.itemType === 'branch'); - const mainSlot = slot.always(main, { elementType: 'div' }); - const mainContentId = useId('fui-TreeItemLayout__main-', mainSlot.id); - mainSlot.id = mainContentId; - - const selectorDefaultProps: CheckboxProps | RadioProps = { - checked, - tabIndex: -1, - ref: selectionRef, - // casting here to a union between checkbox and radio - // since ref is not present on the selector signature - // FIXME: look into Slot type to see if we can make this work - }; + const mainSlot = slot.always(main, { + defaultProps: { id: useId('fui-TreeItemLayout__main-') }, + elementType: 'div', + }); - // Only set aria-labelledby if the consumer didn't provide aria-label or aria-labelledby + // Link the selector to the main content unless the consumer provided aria-label or aria-labelledby const selectorShorthand = isResolvedShorthand(props.selector) ? props.selector : undefined; - if (!selectorShorthand?.['aria-label'] && !selectorShorthand?.['aria-labelledby']) { - selectorDefaultProps['aria-labelledby'] = mainContentId; - } + const selectorAriaLabelledBy = + selectorShorthand?.['aria-label'] || selectorShorthand?.['aria-labelledby'] ? undefined : mainSlot.id; const selector = slot.optional(props.selector, { renderByDefault: selectionMode !== 'none', - defaultProps: selectorDefaultProps, + defaultProps: { + checked, + tabIndex: -1, + ref: selectionRef, + 'aria-labelledby': selectorAriaLabelledBy, + // casting here to a union between checkbox and radio + // since ref is not present on the selector signature + // FIXME: look into Slot type to see if we can make this work + } as CheckboxProps | RadioProps, elementType: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType, }); - // FIXME: Asserting is required here, as converting this to RefObject on context type would be a breaking change // eslint-disable-next-line react-hooks/refs assertIsRefObject(treeItemRef); From cd49af775fff0e118f9ee5d90a8003ac5ceb335d Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Mon, 20 Jul 2026 17:40:57 +0200 Subject: [PATCH 11/15] fix(react-tree): avoid unused layout IDs --- .../components/TreeItemLayout/useTreeItemLayout.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index e526c471682d4..650897e5c9147 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -67,15 +67,13 @@ export const useTreeItemLayout_unstable = ( const checked = useTreeItemContext_unstable(ctx => ctx.checked); const isBranch = useTreeItemContext_unstable(ctx => ctx.itemType === 'branch'); - const mainSlot = slot.always(main, { - defaultProps: { id: useId('fui-TreeItemLayout__main-') }, - elementType: 'div', - }); + const mainShorthand = isResolvedShorthand(main) ? main : undefined; + const mainContentId = useId('fui-TreeItemLayout__main-', mainShorthand?.id); // Link the selector to the main content unless the consumer provided aria-label or aria-labelledby const selectorShorthand = isResolvedShorthand(props.selector) ? props.selector : undefined; const selectorAriaLabelledBy = - selectorShorthand?.['aria-label'] || selectorShorthand?.['aria-labelledby'] ? undefined : mainSlot.id; + selectorShorthand?.['aria-label'] || selectorShorthand?.['aria-labelledby'] ? undefined : mainContentId; const selector = slot.optional(props.selector, { renderByDefault: selectionMode !== 'none', @@ -90,6 +88,11 @@ export const useTreeItemLayout_unstable = ( } as CheckboxProps | RadioProps, elementType: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType, }); + const mainSlot = slot.always(main, { + defaultProps: { id: selector ? mainContentId : undefined }, + elementType: 'div', + }); + // FIXME: Asserting is required here, as converting this to RefObject on context type would be a breaking change // eslint-disable-next-line react-hooks/refs assertIsRefObject(treeItemRef); From 6044d7ea5df023b7cbd3a83d9f0d4b1812864aba Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Tue, 21 Jul 2026 10:54:31 +0200 Subject: [PATCH 12/15] test(react-tree): preserve layout IDs --- .../library/src/components/Tree/Tree.test.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx index b232c5e69ee7c..349b59aee3227 100644 --- a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx +++ b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx @@ -101,6 +101,24 @@ describe('Tree', () => { expect(document.getElementById(mainId)?.textContent).toBe(accessibleName); }); + it('preserves a consumer-provided layout ID', () => { + render( + + + Item 1 + + , + ); + + const layout = document.getElementById('custom-layout'); + const selector = screen.getByRole('checkbox', { name: 'Item 1' }); + const mainId = selector.getAttribute('aria-labelledby'); + + expect(layout?.classList.contains('fui-TreeItemLayout')).toBe(true); + expect(mainId).not.toBe('custom-layout'); + expect(document.getElementById(mainId ?? '')?.textContent).toBe('Item 1'); + }); + it('preserves a consumer-provided selector aria-label', () => { render( From a1d1d22dbdc5a88d15ee45ed29257e5c7dee3a15 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Tue, 21 Jul 2026 11:05:23 +0200 Subject: [PATCH 13/15] fix(react-tree): prioritize custom layout IDs --- .../react-tree/library/src/components/Tree/Tree.test.tsx | 4 ++-- .../src/components/TreeItemLayout/useTreeItemLayout.tsx | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx index 349b59aee3227..f609369e25698 100644 --- a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx +++ b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx @@ -115,8 +115,8 @@ describe('Tree', () => { const mainId = selector.getAttribute('aria-labelledby'); expect(layout?.classList.contains('fui-TreeItemLayout')).toBe(true); - expect(mainId).not.toBe('custom-layout'); - expect(document.getElementById(mainId ?? '')?.textContent).toBe('Item 1'); + expect(mainId).toBe('custom-layout'); + expect(document.querySelectorAll('#custom-layout')).toHaveLength(1); }); it('preserves a consumer-provided selector aria-label', () => { diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index 650897e5c9147..b436416b4009f 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -69,11 +69,12 @@ export const useTreeItemLayout_unstable = ( const mainShorthand = isResolvedShorthand(main) ? main : undefined; const mainContentId = useId('fui-TreeItemLayout__main-', mainShorthand?.id); + const selectorLabelId = props.id ?? mainContentId; - // Link the selector to the main content unless the consumer provided aria-label or aria-labelledby + // Link the selector to the layout ID or main content unless the consumer provided an accessible name const selectorShorthand = isResolvedShorthand(props.selector) ? props.selector : undefined; const selectorAriaLabelledBy = - selectorShorthand?.['aria-label'] || selectorShorthand?.['aria-labelledby'] ? undefined : mainContentId; + selectorShorthand?.['aria-label'] || selectorShorthand?.['aria-labelledby'] ? undefined : selectorLabelId; const selector = slot.optional(props.selector, { renderByDefault: selectionMode !== 'none', @@ -89,7 +90,7 @@ export const useTreeItemLayout_unstable = ( elementType: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType, }); const mainSlot = slot.always(main, { - defaultProps: { id: selector ? mainContentId : undefined }, + defaultProps: { id: selector && !props.id ? mainContentId : undefined }, elementType: 'div', }); From 365960865f6403048edbd7dcd798e2fb71f9eadb Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Tue, 21 Jul 2026 11:29:50 +0200 Subject: [PATCH 14/15] fix(react-tree): pass layout ID to main slot --- .../library/src/components/Tree/Tree.test.tsx | 9 ++++++--- .../TreeItemLayout/useTreeItemLayout.tsx | 16 ++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx index f609369e25698..dadb63af8f37f 100644 --- a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx +++ b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx @@ -105,18 +105,21 @@ describe('Tree', () => { render( - Item 1 + + Item 1 + , ); - const layout = document.getElementById('custom-layout'); const selector = screen.getByRole('checkbox', { name: 'Item 1' }); const mainId = selector.getAttribute('aria-labelledby'); + const main = document.getElementById('custom-layout'); - expect(layout?.classList.contains('fui-TreeItemLayout')).toBe(true); expect(mainId).toBe('custom-layout'); + expect(main?.classList.contains('fui-TreeItemLayout__main')).toBe(true); expect(document.querySelectorAll('#custom-layout')).toHaveLength(1); + expect(document.getElementById('lower-priority-main-id')).toBeNull(); }); it('preserves a consumer-provided selector aria-label', () => { diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index b436416b4009f..85f823bec819d 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -39,7 +39,7 @@ export const useTreeItemLayout_unstable = ( props: TreeItemLayoutProps, ref: React.Ref, ): TreeItemLayoutState => { - const { main, iconAfter, iconBefore } = props; + const { main, iconAfter, iconBefore, id, ...rootProps } = props; const layoutRef = useTreeItemContext_unstable(ctx => ctx.layoutRef); const selectionMode = useTreeContext_unstable(ctx => ctx.selectionMode); @@ -68,13 +68,13 @@ export const useTreeItemLayout_unstable = ( const isBranch = useTreeItemContext_unstable(ctx => ctx.itemType === 'branch'); const mainShorthand = isResolvedShorthand(main) ? main : undefined; - const mainContentId = useId('fui-TreeItemLayout__main-', mainShorthand?.id); - const selectorLabelId = props.id ?? mainContentId; + const mainContentId = useId('fui-TreeItemLayout__main-', id ?? mainShorthand?.id); + const mainWithResolvedId = id && mainShorthand ? { ...mainShorthand, id: mainContentId } : main; - // Link the selector to the layout ID or main content unless the consumer provided an accessible name + // Link the selector to the main content unless the consumer provided an accessible name const selectorShorthand = isResolvedShorthand(props.selector) ? props.selector : undefined; const selectorAriaLabelledBy = - selectorShorthand?.['aria-label'] || selectorShorthand?.['aria-labelledby'] ? undefined : selectorLabelId; + selectorShorthand?.['aria-label'] || selectorShorthand?.['aria-labelledby'] ? undefined : mainContentId; const selector = slot.optional(props.selector, { renderByDefault: selectionMode !== 'none', @@ -89,8 +89,8 @@ export const useTreeItemLayout_unstable = ( } as CheckboxProps | RadioProps, elementType: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType, }); - const mainSlot = slot.always(main, { - defaultProps: { id: selector && !props.id ? mainContentId : undefined }, + const mainSlot = slot.always(mainWithResolvedId, { + defaultProps: { id: selector || id ? mainContentId : undefined }, elementType: 'div', }); @@ -248,7 +248,7 @@ export const useTreeItemLayout_unstable = ( buttonContextValue: { size: 'small' }, root: slot.always( getIntrinsicElementProps('div', { - ...props, + ...rootProps, // FIXME: // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement` // but since it would be a breaking change to fix it, we are casting ref to it's proper type From 54375118b99beb9dfed98b3c1f58d29626a2e7bd Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Tue, 21 Jul 2026 11:46:11 +0200 Subject: [PATCH 15/15] fix(react-tree): preserve layout slot ownership --- .../library/src/components/Tree/Tree.test.tsx | 8 ++++---- .../components/TreeItemLayout/useTreeItemLayout.tsx | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx index dadb63af8f37f..39c3f68e16128 100644 --- a/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx +++ b/packages/react-components/react-tree/library/src/components/Tree/Tree.test.tsx @@ -114,12 +114,12 @@ describe('Tree', () => { const selector = screen.getByRole('checkbox', { name: 'Item 1' }); const mainId = selector.getAttribute('aria-labelledby'); - const main = document.getElementById('custom-layout'); + const layout = document.getElementById('custom-layout'); + const main = document.getElementById('lower-priority-main-id'); - expect(mainId).toBe('custom-layout'); + expect(layout?.classList.contains('fui-TreeItemLayout')).toBe(true); + expect(mainId).toBe('lower-priority-main-id'); expect(main?.classList.contains('fui-TreeItemLayout__main')).toBe(true); - expect(document.querySelectorAll('#custom-layout')).toHaveLength(1); - expect(document.getElementById('lower-priority-main-id')).toBeNull(); }); it('preserves a consumer-provided selector aria-label', () => { diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index 85f823bec819d..0804e495e46b0 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -39,7 +39,7 @@ export const useTreeItemLayout_unstable = ( props: TreeItemLayoutProps, ref: React.Ref, ): TreeItemLayoutState => { - const { main, iconAfter, iconBefore, id, ...rootProps } = props; + const { main, iconAfter, iconBefore } = props; const layoutRef = useTreeItemContext_unstable(ctx => ctx.layoutRef); const selectionMode = useTreeContext_unstable(ctx => ctx.selectionMode); @@ -68,8 +68,7 @@ export const useTreeItemLayout_unstable = ( const isBranch = useTreeItemContext_unstable(ctx => ctx.itemType === 'branch'); const mainShorthand = isResolvedShorthand(main) ? main : undefined; - const mainContentId = useId('fui-TreeItemLayout__main-', id ?? mainShorthand?.id); - const mainWithResolvedId = id && mainShorthand ? { ...mainShorthand, id: mainContentId } : main; + const mainContentId = useId('fui-TreeItemLayout__main-', mainShorthand?.id); // Link the selector to the main content unless the consumer provided an accessible name const selectorShorthand = isResolvedShorthand(props.selector) ? props.selector : undefined; @@ -89,8 +88,8 @@ export const useTreeItemLayout_unstable = ( } as CheckboxProps | RadioProps, elementType: (selectionMode === 'multiselect' ? Checkbox : Radio) as React.ElementType, }); - const mainSlot = slot.always(mainWithResolvedId, { - defaultProps: { id: selector || id ? mainContentId : undefined }, + const mainSlot = slot.always(main, { + defaultProps: { id: selector ? mainContentId : undefined }, elementType: 'div', }); @@ -248,7 +247,7 @@ export const useTreeItemLayout_unstable = ( buttonContextValue: { size: 'small' }, root: slot.always( getIntrinsicElementProps('div', { - ...rootProps, + ...props, // FIXME: // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement` // but since it would be a breaking change to fix it, we are casting ref to it's proper type