Skip to content

Commit bcbf6f2

Browse files
sampottscursoragent
andcommitted
fix(core): sync menu viewport size when root menu opens
HTML menus register the viewport host before child views connect, so the initial syncRoot pass can miss the root panel and leave sizing CSS vars unset. Re-measure on open after child custom elements mount. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 379f769 commit bcbf6f2

2 files changed

Lines changed: 57 additions & 0 deletions

File tree

packages/core/src/dom/ui/menu/create-menu.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ export function createMenu(options: MenuOptions): MenuApi {
284284
cancelAnimationFrame(openRafId);
285285
openRafId = requestAnimationFrame(() => {
286286
openRafId = 0;
287+
// Root view children may connect after the menu host (HTML custom elements).
288+
// Re-measure on open so viewport CSS variables reflect the mounted panel.
289+
viewport?.syncRoot(getNavigationInput().current.stack.length > 0);
287290
// Guard against close() being called before the RAF fires — active
288291
// stays true during the closing animation, so also check status.
289292
if (!popover.input.current.active || popover.input.current.status === 'ending' || highlightedItem) return;

packages/core/src/dom/ui/menu/tests/create-menu.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,60 @@ describe('createMenu', () => {
112112
expect(menu.navigationInput.current.direction).toBe('forward');
113113
});
114114

115+
it('syncs viewport size when opening after the root view connects later than the menu host', async () => {
116+
const { menu } = createTestMenu();
117+
const content = document.createElement('div');
118+
const rootView = document.createElement('div');
119+
120+
content.style.setProperty('min-width', '11rem');
121+
document.body.append(content);
122+
menu.setContentElement(content);
123+
124+
expect(content.style.getPropertyValue('--media-menu-width')).toBe('');
125+
expect(content.style.getPropertyValue('--media-menu-height')).toBe('');
126+
127+
rootView.setAttribute('data-menu-view', '');
128+
rootView.setAttribute('data-menu-view-id', 'root');
129+
rootView.textContent = 'Speed\nCaptions';
130+
131+
function isMeasuringOpenRoot(): boolean {
132+
return (
133+
rootView.hasAttribute('data-open') &&
134+
rootView.style.getPropertyValue('display') === 'block' &&
135+
rootView.style.getPropertyValue('width') === 'max-content'
136+
);
137+
}
138+
139+
rootView.getBoundingClientRect = vi.fn(() =>
140+
isMeasuringOpenRoot()
141+
? { width: 180, height: 74, top: 0, left: 0, right: 180, bottom: 74, x: 0, y: 0, toJSON: () => ({}) }
142+
: { width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0, toJSON: () => ({}) }
143+
);
144+
145+
Object.defineProperty(rootView, 'scrollWidth', {
146+
configurable: true,
147+
get: () => (isMeasuringOpenRoot() ? 180 : 0),
148+
});
149+
150+
Object.defineProperty(rootView, 'scrollHeight', {
151+
configurable: true,
152+
get: () => (isMeasuringOpenRoot() ? 74 : 0),
153+
});
154+
155+
content.append(rootView);
156+
157+
menu.open();
158+
159+
await vi.waitFor(() => {
160+
expect(content.style.getPropertyValue('--media-menu-width')).toBe('180px');
161+
expect(content.style.getPropertyValue('--media-menu-height')).toBe('74px');
162+
});
163+
164+
menu.setContentElement(null);
165+
menu.destroy();
166+
content.remove();
167+
});
168+
115169
it('resets the root panel transition after close when a submenu was open', async () => {
116170
const onOpenChangeComplete = vi.fn();
117171
const { menu } = createTestMenu({ onOpenChangeComplete });

0 commit comments

Comments
 (0)