Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{

@github-actions github-actions Bot Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Menu 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu.Nested Submenus Small Viewport Stacked.nested menu.chromium.png 1008 Changed
vr-tests-react-components/Menu.Nested Submenus Small Viewport Flipped.nested menu.chromium.png 699 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default - RTL.submenus open.chromium.png 404 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default.submenus open.chromium.png 413 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 747 Changed
vr-tests-react-components/Positioning.Positioning end.chromium.png 910 Changed
vr-tests-react-components/ProgressBar converged 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png 110 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png 160 Changed
vr-tests-react-components/TagPicker 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - Dark Mode.disabled input hover.chromium.png 658 Changed
vr-tests-react-components/TagPicker.disabled.disabled input hover.chromium.png 677 Changed

There were 2 duplicate changes discarded. Check the build logs for more information.

"type": "minor",
"comment": "feat: expose useSplitButtonBase_unstable hook",
"packageName": "@fluentui/react-button",
"email": "vgenaev@gmail.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,25 @@ export const renderSplitButton_unstable: (state: SplitButtonState) => JSXElement
// @public
export const SplitButton: ForwardRefComponent<SplitButtonProps>;

// @public
export type SplitButtonBaseProps = ComponentProps<SplitButtonBaseSlots> & Omit<ButtonBaseProps, 'root' | 'as'> & Omit<MenuButtonBaseProps, 'root' | 'as'>;

// @public (undocumented)
export type SplitButtonBaseSlots = {
root: NonNullable<Slot<'div'>>;
menuButton?: Slot<MenuButtonBaseProps>;
primaryActionButton?: Slot<ButtonBaseProps>;
};

// @public
export type SplitButtonBaseState = Omit<ComponentState<SplitButtonBaseSlots>, 'components' | 'menuButton' | 'primaryActionButton'> & Required<Pick<SplitButtonBaseProps, 'disabled' | 'disabledFocusable' | 'iconPosition'>> & {
components: {
root: 'div';
};
menuButton: MenuButtonBaseProps;
primaryActionButton: ButtonBaseProps;
};

// @public (undocumented)
export const splitButtonClassNames: SlotClassNames<SplitButtonSlots>;

Expand Down Expand Up @@ -189,7 +208,10 @@ export const useMenuButtonBase_unstable: (props: MenuButtonBaseProps, ref: React
export const useMenuButtonStyles_unstable: (state: MenuButtonState) => MenuButtonState;

// @public
export const useSplitButton_unstable: (props: SplitButtonProps, ref: React_2.Ref<HTMLButtonElement | HTMLAnchorElement>) => SplitButtonState;
export const useSplitButton_unstable: (props: SplitButtonProps, ref: React_2.Ref<HTMLDivElement>) => SplitButtonState;

// @public
export const useSplitButtonBase_unstable: (props: SplitButtonBaseProps, ref: React_2.Ref<HTMLDivElement>) => SplitButtonBaseState;

// @public (undocumented)
export const useSplitButtonStyles_unstable: (state: SplitButtonState) => SplitButtonState;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
export type { SplitButtonProps, SplitButtonSlots, SplitButtonState } from './components/SplitButton/index';
export type {
SplitButtonBaseProps,
SplitButtonBaseSlots,
SplitButtonBaseState,
SplitButtonProps,
SplitButtonSlots,
SplitButtonState,
} from './components/SplitButton/index';
export {
SplitButton,
renderSplitButton_unstable,
splitButtonClassNames,
useSplitButtonStyles_unstable,
useSplitButton_unstable,
useSplitButtonBase_unstable,
} from './components/SplitButton/index';
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import userEvent from '@testing-library/user-event';
import { isSlot, SLOT_ELEMENT_TYPE_SYMBOL } from '@fluentui/react-utilities';
import type { SlotRenderFunction } from '@fluentui/react-utilities';
import { isConformant } from '../../testing/isConformant';
import { SplitButton } from './SplitButton';
import { Button } from '../Button/Button';
import type { ButtonProps } from '../Button/Button.types';
import { MenuButton } from '../MenuButton/MenuButton';
import { useSplitButton_unstable } from './useSplitButton';
import type { MenuButtonProps } from '../MenuButton/MenuButton.types';
import { useSplitButtonBase_unstable, useSplitButton_unstable } from './useSplitButton';
import type { SplitButtonProps } from './SplitButton.types';

describe('SplitButton', () => {
Expand All @@ -34,6 +37,27 @@ describe('SplitButton', () => {
expect(menuButton).toBeTruthy();
});

it('preserves default children for a primary action button render function', () => {
const renderPrimaryActionButton: SlotRenderFunction<ButtonProps> = (_Component, slotProps) => slotProps.children;
const { getByText } = render(
<SplitButton primaryActionButton={{ children: renderPrimaryActionButton }}>This is a button</SplitButton>,
);

expect(getByText('This is a button')).toBeTruthy();
});

it('preserves undefined children for a menu button render function', () => {
const renderMenuButton: SlotRenderFunction<MenuButtonProps> = (_Component, slotProps) => {
expect(slotProps.children).toBeUndefined();
return <button type="button" />;
};
const { getAllByRole } = render(
<SplitButton menuButton={{ children: renderMenuButton }}>This is a button</SplitButton>,
);

expect(getAllByRole('button')).toHaveLength(2);
});

it('primary action button and menu button can both be focused', () => {
const { getAllByRole } = render(<SplitButton>This is a button</SplitButton>);
const [primaryActionButton, menuButton] = getAllByRole('button');
Expand Down Expand Up @@ -212,4 +236,55 @@ describe('SplitButton', () => {
expect(primaryActionButton[SLOT_ELEMENT_TYPE_SYMBOL]).toBe(Button);
expect(menuButton[SLOT_ELEMENT_TYPE_SYMBOL]).toBe(MenuButton);
});

it('normalizes base child props without slot metadata and preserves user overrides', () => {
const { result } = renderHook(() =>
useSplitButtonBase_unstable(
{
children: 'This is a button',
disabled: true,
disabledFocusable: true,
icon: 'Test Icon',
iconPosition: 'after',
menuButton: { disabled: false },
primaryActionButton: { disabled: false, id: 'custom-primary-action' },
},
React.createRef<HTMLDivElement>(),
),
);

expect(result.current.components).toEqual({ root: 'div' });
expect(result.current).toMatchObject({
disabled: true,
disabledFocusable: true,
iconPosition: 'after',
});
expect(isSlot(result.current.menuButton)).toBe(false);
expect(isSlot(result.current.primaryActionButton)).toBe(false);
expect(result.current.menuButton).toMatchObject({
disabled: false,
disabledFocusable: true,
'aria-labelledby': 'custom-primary-action',
});
expect(result.current.primaryActionButton).toMatchObject({
children: 'This is a button',
disabled: false,
disabledFocusable: true,
icon: 'Test Icon',
iconPosition: 'after',
id: 'custom-primary-action',
});
expect(result.current.root).not.toHaveProperty('disabled');
expect(result.current.root).not.toHaveProperty('disabledFocusable');
expect(result.current.root).not.toHaveProperty('icon');
expect(result.current.root).not.toHaveProperty('iconPosition');
});

it('does not label the menu button with a primary action that is not rendered', () => {
const { getByRole } = render(
<SplitButton primaryActionButton={null}>This primary action is not rendered</SplitButton>,
);

expect(getByRole('button').getAttribute('aria-labelledby')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts';
* SplitButtons are a grouping of two interactive surfaces where interacting with the first one triggers a primary
* action, while interacting with the second one opens a menu with secondary actions.
*/
export const SplitButton: ForwardRefComponent<SplitButtonProps> = React.forwardRef((props, ref) => {
const state = useSplitButton_unstable(props, ref);
export const SplitButton: ForwardRefComponent<SplitButtonProps> = React.forwardRef<HTMLDivElement, SplitButtonProps>(
(props, ref) => {
const state = useSplitButton_unstable(props, ref);

useSplitButtonStyles_unstable(state);
useSplitButtonStyles_unstable(state);

useCustomStyleHook_unstable('useSplitButtonStyles_unstable')(state);
useCustomStyleHook_unstable('useSplitButtonStyles_unstable')(state);

return renderSplitButton_unstable(state);
// Casting is required due to lack of distributive union to support unions on @types/react
}) as ForwardRefComponent<SplitButtonProps>;
return renderSplitButton_unstable(state);
// Casting is required due to lack of distributive union to support unions on @types/react
},
) as ForwardRefComponent<SplitButtonProps>;

SplitButton.displayName = 'SplitButton';
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Button } from '../Button/Button';
import type { MenuButton } from '../MenuButton/MenuButton';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { ButtonProps, ButtonState } from '../Button/Button.types';
import type { MenuButtonProps, MenuButtonState } from '../MenuButton/MenuButton.types';
import type { ButtonBaseProps, ButtonProps, ButtonState } from '../Button/Button.types';
import type { MenuButtonBaseProps, MenuButtonProps, MenuButtonState } from '../MenuButton/MenuButton.types';

export type SplitButtonSlots = {
/**
Expand All @@ -27,3 +27,41 @@ export type SplitButtonProps = ComponentProps<SplitButtonSlots> &
export type SplitButtonState = ComponentState<SplitButtonSlots> &
Omit<ButtonState, 'components' | 'iconOnly' | 'root'> &
Omit<MenuButtonState, 'components' | 'iconOnly' | 'root'>;

export type SplitButtonBaseSlots = {
/**
* Root of the component that wraps the primary action button and menu button.
*/
root: NonNullable<Slot<'div'>>;

/**
* Button that opens menu with secondary actions in SplitButton.
*/
menuButton?: Slot<MenuButtonBaseProps>;
/**
* Button to perform primary action in SplitButton.
*/
primaryActionButton?: Slot<ButtonBaseProps>;
};

/**
* SplitButton props without the `appearance`/`size`/`shape` styling props, for headless usage.
*/
export type SplitButtonBaseProps = ComponentProps<SplitButtonBaseSlots> &
Omit<ButtonBaseProps, 'root' | 'as'> &
Omit<MenuButtonBaseProps, 'root' | 'as'>;

/**
* SplitButton state without the `appearance`/`size`/`shape` styling props, for headless usage.
*/
export type SplitButtonBaseState = Omit<
ComponentState<SplitButtonBaseSlots>,
'components' | 'menuButton' | 'primaryActionButton'
> &
Required<Pick<SplitButtonBaseProps, 'disabled' | 'disabledFocusable' | 'iconPosition'>> & {
components: {
root: 'div';
};
menuButton: MenuButtonBaseProps;
primaryActionButton: ButtonBaseProps;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
export { SplitButton } from './SplitButton';
export type { SplitButtonProps, SplitButtonSlots, SplitButtonState } from './SplitButton.types';
export type {
SplitButtonBaseProps,
SplitButtonBaseSlots,
SplitButtonBaseState,
SplitButtonProps,
SplitButtonSlots,
SplitButtonState,
} from './SplitButton.types';
export { renderSplitButton_unstable } from './renderSplitButton';
export { useSplitButton_unstable } from './useSplitButton';
export { useSplitButton_unstable, useSplitButtonBase_unstable } from './useSplitButton';
export { splitButtonClassNames, useSplitButtonStyles_unstable } from './useSplitButtonStyles.styles';
Loading
Loading