fix(packages): handle menu child mutations (#1739)

This commit is contained in:
Sam Potts
2026-06-30 10:20:07 +10:00
committed by GitHub
parent a6d30a9e61
commit 9ab7adefb7
7 changed files with 163 additions and 2 deletions
@@ -12,6 +12,7 @@ import {
getRootPositionOptions,
isEventWithinElement,
isMenuNavigationKey,
observeMenuViewContent,
resolveOffsets,
resolvePositioningBoundary,
syncMenuViewRoot,
@@ -242,6 +243,18 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
syncMenuViewRoot(internalRef.current, activeSubMenuId !== null);
}, [isSubmenu, state.open, activeSubMenuId]);
useLayoutEffect(() => {
if (isSubmenu) return;
if (!state.open) return;
const contentElement = internalRef.current;
if (!contentElement) return;
return observeMenuViewContent(contentElement, () => {
syncMenuViewRoot(contentElement, activeSubMenuId !== null);
});
}, [isSubmenu, state.open, activeSubMenuId]);
useLayoutEffect(() => {
if (!isSubmenu) return;
@@ -331,6 +331,48 @@ function expectNoMenuStateAttrs(element: HTMLElement): void {
}
}
function createRect(width: number, height: number): DOMRect {
return {
x: 0,
y: 0,
width,
height,
top: 0,
right: width,
bottom: height,
left: 0,
toJSON: () => ({}),
} as DOMRect;
}
function mockMenuViewSize(element: HTMLElement, getHeight: () => number): void {
element.getBoundingClientRect = vi.fn(() => createRect(160, getHeight()));
Object.defineProperty(element, 'scrollWidth', {
configurable: true,
get: () => 160,
});
Object.defineProperty(element, 'scrollHeight', {
configurable: true,
get: getHeight,
});
}
function DynamicMenuFixture({ showCaptions }: { showCaptions: boolean }) {
return (
<MenuRoot defaultOpen>
<MenuTrigger>Settings</MenuTrigger>
<MenuContent data-testid="content">
<MenuView data-testid="root-view">
<MenuItem>Speed</MenuItem>
{showCaptions ? <MenuItem>Captions</MenuItem> : null}
</MenuView>
</MenuContent>
</MenuRoot>
);
}
describe('MenuContent', () => {
it('scopes menu state data attributes to content elements', async () => {
render(
@@ -457,6 +499,20 @@ describe('MenuContent', () => {
expect(screen.getByTestId('root-view').hasAttribute('data-menu-view')).toBe(true);
});
it('remeasures an open root view when menu items are added', async () => {
const { rerender } = render(<DynamicMenuFixture showCaptions={false} />);
const content = screen.getByTestId('content');
const rootView = screen.getByTestId('root-view');
mockMenuViewSize(rootView, () => rootView.children.length * 20);
rerender(<DynamicMenuFixture showCaptions />);
await waitFor(() => {
expect(content.style.getPropertyValue('--media-menu-height')).toBe('40px');
});
});
it('forces layout while the submenu starting style is applied', async () => {
const startingStyleMeasurements: boolean[] = [];
const getBoundingClientRect = HTMLElement.prototype.getBoundingClientRect;