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..b3625c629bca1f --- /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 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 71c98981325439..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,12 +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); - return renderMenuSplitGroup(state); + const contextValues = useMenuSplitGroupContextValues(state); + + 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';