Fix scroll jumping

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-10-12 17:24:24 +01:00
parent fbb4abef52
commit 85faee0f69
3 changed files with 76 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': patch
---
Fix scroll jumping when opening menu in Backstage UI.
@@ -412,3 +412,38 @@ export const VirtualizedMaxHeight: Story = {
);
},
};
export const WithScroll: Story = {
args: {
children: null,
},
decorators: [
Story => (
<div style={{ height: '2000px', overflow: 'auto' }}>
<Story />
</div>
),
],
render: () => (
<MenuTrigger>
<Button aria-label="Menu">Menu</Button>
<Menu>
<MenuItem>Edit</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuItem>Rename</MenuItem>
<MenuSeparator />
<MenuItem iconStart={<RiShareBoxLine />}>Share</MenuItem>
<MenuItem iconStart={<RiChat1Line />}>Feedback</MenuItem>
<MenuSeparator />
<SubmenuTrigger>
<MenuItem iconStart={<RiSettingsLine />}>Settings</MenuItem>
<Menu placement="right top">
<MenuItem>Edit</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuItem>Rename</MenuItem>
</Menu>
</SubmenuTrigger>
</Menu>
</MenuTrigger>
),
};
+36 -1
View File
@@ -33,6 +33,7 @@ import {
RouterProvider,
Virtualizer,
ListLayout,
OverlayTriggerStateContext,
} from 'react-aria-components';
import { useStyles } from '../../hooks/useStyles';
import type {
@@ -54,6 +55,7 @@ import {
} from '@remixicon/react';
import { useNavigate, useHref } from 'react-router-dom';
import { isExternalLink } from '../../utils/isExternalLink';
import { useRef, useEffect, useContext } from 'react';
// The height will be used for virtualized menus. It should match the size set in CSS for each menu item.
const rowHeight = 32;
@@ -86,6 +88,33 @@ export const Menu = (props: MenuProps<object>) => {
const { classNames } = useStyles('Menu');
const navigate = useNavigate();
let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined');
const popoverRef = useRef<HTMLDivElement>(null);
const state = useContext(OverlayTriggerStateContext);
// Custom click-outside handler for non-modal popovers
useEffect(() => {
if (!state?.isOpen) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
// Check if click is outside the popover
if (popoverRef.current && !popoverRef.current.contains(target)) {
// Check if click is on a trigger button or submenu
const isOnTrigger = (target as Element).closest('[data-trigger]');
const isOnSubmenu = (target as Element).closest('[role="menu"]');
if (!isOnTrigger && !isOnSubmenu) {
state.close();
}
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [state]);
const menuContent = (
<RAMenu
@@ -97,7 +126,13 @@ export const Menu = (props: MenuProps<object>) => {
);
return (
<RAPopover className={classNames.popover} placement={placement}>
<RAPopover
ref={popoverRef}
className={classNames.popover}
placement={placement}
isNonModal={true}
isKeyboardDismissDisabled={false}
>
<RouterProvider navigate={navigate} useHref={useHref}>
{virtualized ? (
<Virtualizer