diff --git a/change/@fluentui-react-button-de57007e-ed5d-4006-b38f-0715f6088ed4.json b/change/@fluentui-react-button-de57007e-ed5d-4006-b38f-0715f6088ed4.json
new file mode 100644
index 0000000000000..20094ddabdfe7
--- /dev/null
+++ b/change/@fluentui-react-button-de57007e-ed5d-4006-b38f-0715f6088ed4.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "fix: do not expose default icon in base hook",
+ "packageName": "@fluentui/react-button",
+ "email": "vgenaev@gmail.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/react-components/react-button/library/src/components/MenuButton/useMenuButton.test.tsx b/packages/react-components/react-button/library/src/components/MenuButton/useMenuButton.test.tsx
index d3fe07832432e..dd013f7df19a5 100644
--- a/packages/react-components/react-button/library/src/components/MenuButton/useMenuButton.test.tsx
+++ b/packages/react-components/react-button/library/src/components/MenuButton/useMenuButton.test.tsx
@@ -1,9 +1,11 @@
import * as React from 'react';
+import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import '@testing-library/jest-dom';
import { ButtonContextProvider } from '../../contexts/ButtonContext';
import type { ButtonContextValue } from '../../contexts/ButtonContext';
import { useMenuButton_unstable } from './useMenuButton';
+import { MenuButton } from './MenuButton';
const wrap = (contextValue: ButtonContextValue = {}): React.FC<{ children?: React.ReactNode }> => {
const Wrapper: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
@@ -19,10 +21,9 @@ describe('useMenuButton_unstable', () => {
expect(result.current.components).toEqual({ root: 'button', icon: 'span', menuIcon: 'span' });
});
- it('renders a menuIcon slot with a default chevron icon', () => {
- const { result } = renderHook(() => useMenuButton_unstable({}, React.createRef()));
- expect(result.current.menuIcon).toBeDefined();
- expect(React.isValidElement(result.current.menuIcon?.children)).toBe(true);
+ it('renders a menuIcon slot with a default icon', () => {
+ const result = render();
+ expect(result.container.querySelector('svg')).toBeInTheDocument();
});
it('preserves a user-provided menuIcon over the default chevron', () => {
@@ -33,6 +34,16 @@ describe('useMenuButton_unstable', () => {
expect(result.current.menuIcon?.children).toBe(customIcon);
});
+ it('renders the default chevron when menuIcon is explicitly undefined', () => {
+ const result = render();
+ expect(result.container.querySelector('svg')).toBeInTheDocument();
+ });
+
+ it('hides the menuIcon slot when menuIcon is null', () => {
+ const { result } = renderHook(() => useMenuButton_unstable({ menuIcon: null }, React.createRef()));
+ expect(result.current.menuIcon).toBeUndefined();
+ });
+
it('defaults aria-expanded to false when not provided', () => {
const { result } = renderHook(() => useMenuButton_unstable({}, React.createRef()));
expect(result.current.root['aria-expanded']).toBe(false);
diff --git a/packages/react-components/react-button/library/src/components/MenuButton/useMenuButton.tsx b/packages/react-components/react-button/library/src/components/MenuButton/useMenuButton.tsx
index 6ece2ab7c6c2c..44ffa53566b81 100644
--- a/packages/react-components/react-button/library/src/components/MenuButton/useMenuButton.tsx
+++ b/packages/react-components/react-button/library/src/components/MenuButton/useMenuButton.tsx
@@ -10,6 +10,10 @@ import type { MenuButtonBaseProps, MenuButtonBaseState, MenuButtonProps, MenuBut
/**
* Base hook for MenuButton.
*
+ * The `menuIcon` slot ships no icon of its own and only renders when a consumer
+ * provides one, so headless consumers can supply their own visuals. The styled
+ * `useMenuButton_unstable` adds the default chevron on top of this.
+ *
* @param props - User provided props to the MenuButton component.
* @param ref - User provided ref to be passed to the MenuButton component.
*/
@@ -40,10 +44,6 @@ export const useMenuButtonBase_unstable = (
},
menuIcon: slot.optional(menuIcon, {
- defaultProps: {
- children: ,
- },
- renderByDefault: true,
elementType: 'span',
}),
};
@@ -61,11 +61,18 @@ export const useMenuButton_unstable = (
ref: React.Ref,
): MenuButtonState => {
const { size: contextSize } = useButtonContext();
- const { appearance = 'secondary', shape = 'rounded', size = contextSize ?? 'medium', ...baseProps } = props;
+ const { appearance = 'secondary', menuIcon, shape = 'rounded', size = contextSize ?? 'medium', ...baseProps } = props;
const baseState = useMenuButtonBase_unstable(baseProps, ref);
return {
...baseState,
+ menuIcon: slot.optional(menuIcon, {
+ defaultProps: {
+ children: ,
+ },
+ renderByDefault: true,
+ elementType: 'span',
+ }),
appearance,
shape,
size,
diff --git a/packages/react-components/react-button/library/src/components/MenuButton/useMenuButtonBase.test.tsx b/packages/react-components/react-button/library/src/components/MenuButton/useMenuButtonBase.test.tsx
index 186d30b12acf7..80e42abb28dea 100644
--- a/packages/react-components/react-button/library/src/components/MenuButton/useMenuButtonBase.test.tsx
+++ b/packages/react-components/react-button/library/src/components/MenuButton/useMenuButtonBase.test.tsx
@@ -4,9 +4,18 @@ import '@testing-library/jest-dom';
import { useMenuButtonBase_unstable } from './useMenuButton';
describe('useMenuButtonBase_unstable', () => {
- it('returns a menuIcon slot by default', () => {
+ it('does not render the menuIcon slot when none is provided', () => {
const { result } = renderHook(() => useMenuButtonBase_unstable({}, React.createRef()));
+ expect(result.current.menuIcon).toBeUndefined();
+ });
+
+ it('renders the menuIcon slot only when one is provided, shipping no default icon', () => {
+ const customIcon = ;
+ const { result } = renderHook(() =>
+ useMenuButtonBase_unstable({ menuIcon: { children: customIcon } }, React.createRef()),
+ );
expect(result.current.menuIcon).toBeDefined();
+ expect(result.current.menuIcon?.children).toBe(customIcon);
});
it('forces aria-expanded to a boolean on root', () => {