From 241a281594aa15f7df187db444ad6caaf8ed7df9 Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Thu, 27 Aug 2026 17:56:34 -0700 Subject: [PATCH 1/3] fix(react-headless-components-preview): provide MenuSplitGroup's context renderMenuSplitGroup falls back to menuSplitGroupContextDefaultValue when no contexts argument is supplied, and MenuSplitGroup supplies none. useIsInMenuSplitGroup compares against that default by identity, so it returns false for every descendant of a headless MenuSplitGroup. The visible effect is in useMenuItemBase's useIconAndCheckmarkAlignment: a submenu trigger inside a split group is supposed to drop its icon and checkmark gutters, and instead keeps them, widening one half of the group by the gutter plus the row gap whenever the surrounding MenuList sets hasIcons or hasCheckmarks. Pass the state's own context value so the identity differs from the default. The setMultiline it carries stays a no-op: the multiline attribute remains a styling concern for consumers, as documented on the hook. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i --- .../components/Menu/MenuSplitGroup/MenuSplitGroup.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx index 71c98981325439..a7d4f4ecc03162 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx @@ -8,7 +8,15 @@ import type { MenuSplitGroupProps } from '@fluentui/react-menu'; export const MenuSplitGroup: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useMenuSplitGroup(props, ref); - return renderMenuSplitGroup(state); + // useIsInMenuSplitGroup compares the provided value against the module-level default BY IDENTITY, + // so supplying a contexts argument is what lets a descendant tell it is inside a split group. The + // memo keeps that identity stable across renders; setMultiline stays the documented no-op. + const contexts = React.useMemo( + () => ({ menuSplitGroup: { setMultiline: state.setMultiline } }), + [state.setMultiline], + ); + + return renderMenuSplitGroup(state, contexts); }); MenuSplitGroup.displayName = 'MenuSplitGroup'; From 591c6a7747f23d12d8e82c52bbf6d4697b8e9b5a Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Mon, 31 Aug 2026 13:14:47 -0700 Subject: [PATCH 2/3] chore: add change file Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Aj9uA3rCVgosnh2zNn8qkc --- ...nents-preview-b0360dff-28bf-4dcc-9d25-d07a2ae536ec.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-headless-components-preview-b0360dff-28bf-4dcc-9d25-d07a2ae536ec.json diff --git a/change/@fluentui-react-headless-components-preview-b0360dff-28bf-4dcc-9d25-d07a2ae536ec.json b/change/@fluentui-react-headless-components-preview-b0360dff-28bf-4dcc-9d25-d07a2ae536ec.json new file mode 100644 index 00000000000000..66e3096c206a61 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-b0360dff-28bf-4dcc-9d25-d07a2ae536ec.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: MenuSplitGroup provides its context to descendants so useIsInMenuSplitGroup can return true", + "packageName": "@fluentui/react-headless-components-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} From 5e8a57c998013656fa1fc3b6c8187c8e1b146552 Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Tue, 1 Sep 2026 13:21:45 -0700 Subject: [PATCH 3/3] refactor(react-headless-components-preview): extract and export useMenuSplitGroupContextValues --- ...-b0360dff-28bf-4dcc-9d25-d07a2ae536ec.json | 2 +- .../library/etc/menu.api.md | 10 ++++++ .../library/src/components/Menu/Menu.test.tsx | 36 +++++++++++++++++++ .../Menu/MenuSplitGroup/MenuSplitGroup.tsx | 11 ++---- .../components/Menu/MenuSplitGroup/index.ts | 2 ++ .../useMenuSplitGroupContextValues.ts | 31 ++++++++++++++++ .../library/src/components/Menu/index.ts | 8 ++++- .../library/src/menu.ts | 2 ++ 8 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/useMenuSplitGroupContextValues.ts diff --git a/change/@fluentui-react-headless-components-preview-b0360dff-28bf-4dcc-9d25-d07a2ae536ec.json b/change/@fluentui-react-headless-components-preview-b0360dff-28bf-4dcc-9d25-d07a2ae536ec.json index 66e3096c206a61..b3625c629bca1f 100644 --- a/change/@fluentui-react-headless-components-preview-b0360dff-28bf-4dcc-9d25-d07a2ae536ec.json +++ b/change/@fluentui-react-headless-components-preview-b0360dff-28bf-4dcc-9d25-d07a2ae536ec.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "fix: MenuSplitGroup provides its context to descendants so useIsInMenuSplitGroup can return true", + "comment": "fix: MenuSplitGroup provides its context to descendants via a new exported useMenuSplitGroupContextValues hook", "packageName": "@fluentui/react-headless-components-preview", "email": "array.knight@gmail.com", "dependentChangeType": "patch" diff --git a/packages/react-components/react-headless-components-preview/library/etc/menu.api.md b/packages/react-components/react-headless-components-preview/library/etc/menu.api.md index d5795fe2182ec6..47225a7d4c8ab4 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/menu.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/menu.api.md @@ -191,6 +191,13 @@ export type MenuProps = Omit & { // @public (undocumented) export const MenuSplitGroup: ForwardRefComponent; +// @public +export type MenuSplitGroupContextValues = { + menuSplitGroup: { + setMultiline: (multiline: boolean) => void; + }; +}; + export { MenuSplitGroupProps } export { MenuSplitGroupSlots } @@ -279,6 +286,9 @@ export const useMenuPopover: (props: MenuPopoverProps, ref: React_2.Ref) => MenuSplitGroupState; +// @public +export const useMenuSplitGroupContextValues: (state: MenuSplitGroupState) => MenuSplitGroupContextValues; + // @public (undocumented) export const useMenuTrigger: (props: MenuTriggerProps) => MenuTriggerState; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx index d5dc4f9b44fed9..be5399657cf518 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx @@ -13,6 +13,7 @@ import { MenuItemSwitch } from './MenuItemSwitch/MenuItemSwitch'; import { MenuDivider } from './MenuDivider/MenuDivider'; import { MenuGroup } from './MenuGroup/MenuGroup'; import { MenuGroupHeader } from './MenuGroupHeader/MenuGroupHeader'; +import { MenuSplitGroup } from './MenuSplitGroup/MenuSplitGroup'; describe('Menu', () => { it('renders trigger and surface children when open', () => { @@ -502,4 +503,39 @@ describe('Menu', () => { expect(header.getAttribute('id')).toBeTruthy(); }); }); + + describe('MenuSplitGroup', () => { + // Regression test for https://github.com/microsoft/fluentui/issues/36651 — MenuSplitGroup + // rendered without a contexts argument, so `useIsInMenuSplitGroup` could never return true and + // descendants kept the icon gutter that a split-group trigger is supposed to drop. + it('provides split group context so the trigger half drops the icon gutter', () => { + const { getByTestId } = render( + + + + + + + + Outside + + + + Split main + + Chevron + + + + , + ); + + expect(getByTestId('split-group')).toHaveAttribute('role', 'group'); + + // Outside the split group: default icon gutter + submenu indicator + content = 3 spans. + expect(getByTestId('outside-item').querySelectorAll('span')).toHaveLength(3); + // Inside the split group the trigger half suppresses the icon gutter: indicator + content. + expect(getByTestId('split-trigger').querySelectorAll('span')).toHaveLength(2); + }); + }); }); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx index a7d4f4ecc03162..2b7c7fb507558e 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx @@ -3,20 +3,15 @@ import * as React from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import { useMenuSplitGroup } from './useMenuSplitGroup'; +import { useMenuSplitGroupContextValues } from './useMenuSplitGroupContextValues'; import { renderMenuSplitGroup } from './renderMenuSplitGroup'; import type { MenuSplitGroupProps } from '@fluentui/react-menu'; export const MenuSplitGroup: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useMenuSplitGroup(props, ref); - // useIsInMenuSplitGroup compares the provided value against the module-level default BY IDENTITY, - // so supplying a contexts argument is what lets a descendant tell it is inside a split group. The - // memo keeps that identity stable across renders; setMultiline stays the documented no-op. - const contexts = React.useMemo( - () => ({ menuSplitGroup: { setMultiline: state.setMultiline } }), - [state.setMultiline], - ); + const contextValues = useMenuSplitGroupContextValues(state); - return renderMenuSplitGroup(state, contexts); + return renderMenuSplitGroup(state, contextValues); }); MenuSplitGroup.displayName = 'MenuSplitGroup'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/index.ts index 4c8a5cf3d09f46..496dc867919fef 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/index.ts @@ -1,4 +1,6 @@ export { MenuSplitGroup } from './MenuSplitGroup'; export { useMenuSplitGroup } from './useMenuSplitGroup'; +export { useMenuSplitGroupContextValues } from './useMenuSplitGroupContextValues'; +export type { MenuSplitGroupContextValues } from './useMenuSplitGroupContextValues'; export { renderMenuSplitGroup } from './renderMenuSplitGroup'; export type { MenuSplitGroupProps, MenuSplitGroupSlots, MenuSplitGroupState } from '@fluentui/react-menu'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/useMenuSplitGroupContextValues.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/useMenuSplitGroupContextValues.ts new file mode 100644 index 00000000000000..70bc7c62cd874e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/useMenuSplitGroupContextValues.ts @@ -0,0 +1,31 @@ +'use client'; + +import * as React from 'react'; +import type { MenuSplitGroupState } from '@fluentui/react-menu'; + +/** + * Context values shared with the descendants of a MenuSplitGroup. + * + * Mirrors `@fluentui/react-menu`'s `MenuSplitGroupContextValues`, which that package does not + * export from its public API. + */ +export type MenuSplitGroupContextValues = { + menuSplitGroup: { + setMultiline: (multiline: boolean) => void; + }; +}; + +/** + * Builds the context value shared with the descendants of a MenuSplitGroup. + * + * `useIsInMenuSplitGroup` compares the provided value against the module-level default by + * identity, so providing one is what lets a descendant tell it is inside a split group. The memo + * keeps that identity stable across renders; `setMultiline` stays the documented no-op. + */ +export const useMenuSplitGroupContextValues = (state: MenuSplitGroupState): MenuSplitGroupContextValues => { + const { setMultiline } = state; + + const menuSplitGroup = React.useMemo(() => ({ setMultiline }), [setMultiline]); + + return { menuSplitGroup }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/index.ts index c236c4f3a82f2f..3d0a3baa804627 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/index.ts @@ -45,5 +45,11 @@ export type { MenuGroupProps, MenuGroupSlots, MenuGroupState, MenuGroupContextVa export { MenuGroupHeader, useMenuGroupHeader, renderMenuGroupHeader } from './MenuGroupHeader'; export type { MenuGroupHeaderProps, MenuGroupHeaderSlots, MenuGroupHeaderState } from '@fluentui/react-menu'; -export { MenuSplitGroup, useMenuSplitGroup, renderMenuSplitGroup } from './MenuSplitGroup'; +export { + MenuSplitGroup, + useMenuSplitGroup, + useMenuSplitGroupContextValues, + renderMenuSplitGroup, +} from './MenuSplitGroup'; +export type { MenuSplitGroupContextValues } from './MenuSplitGroup'; export type { MenuSplitGroupProps, MenuSplitGroupSlots, MenuSplitGroupState } from '@fluentui/react-menu'; diff --git a/packages/react-components/react-headless-components-preview/library/src/menu.ts b/packages/react-components/react-headless-components-preview/library/src/menu.ts index 108d8ff6180565..10f1fb4a538596 100644 --- a/packages/react-components/react-headless-components-preview/library/src/menu.ts +++ b/packages/react-components/react-headless-components-preview/library/src/menu.ts @@ -41,6 +41,7 @@ export { renderMenuGroupHeader, MenuSplitGroup, useMenuSplitGroup, + useMenuSplitGroupContextValues, renderMenuSplitGroup, } from './components/Menu'; export type { @@ -85,4 +86,5 @@ export type { MenuSplitGroupProps, MenuSplitGroupSlots, MenuSplitGroupState, + MenuSplitGroupContextValues, } from './components/Menu';