Merge pull request #33524 from backstage/bui-menu-fix

This commit is contained in:
Charles de Dreuille
2026-03-23 15:48:14 +00:00
committed by GitHub
4 changed files with 75 additions and 97 deletions
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Simplified the `Menu` component's item structure by removing the inner wrapper element and applying styles directly to the menu item, improving DOM clarity.
**Affected components:** Menu
+48 -44
View File
@@ -69,51 +69,11 @@
box-sizing: border-box;
overflow: auto;
min-width: 150px;
box-sizing: border-box;
outline: none;
padding-block: var(--bui-space-1);
}
.bui-MenuItem {
padding-inline: var(--bui-space-1);
display: block;
&[data-focus-visible] {
outline: none;
}
&[data-focus-visible] {
outline: 2px solid var(--bui-ring);
outline-offset: -2px;
}
&[data-focused] .bui-MenuItemWrapper {
background: var(--bui-bg-neutral-2);
color: var(--bui-fg-primary);
}
&[data-open] .bui-MenuItemWrapper {
background: var(--bui-bg-neutral-2);
color: var(--bui-fg-primary);
}
&[data-color='danger'] .bui-MenuItemWrapper {
color: var(--bui-fg-danger);
}
&[data-color='danger'][data-focused] .bui-MenuItemWrapper {
background: var(--bui-bg-danger);
color: var(--bui-fg-danger);
}
&[data-has-submenu] {
& > .bui-MenuItemWrapper > .bui-MenuItemArrow {
display: block;
}
}
}
.bui-MenuItemWrapper {
display: flex;
align-items: center;
justify-content: space-between;
@@ -121,17 +81,61 @@
padding-inline: var(--bui-space-2);
border-radius: var(--bui-radius-2);
outline: none;
cursor: default;
color: var(--bui-fg-primary);
font-size: var(--bui-font-size-3);
gap: var(--bui-space-6);
cursor: pointer;
margin-inline: var(--bui-space-1);
&[data-focus-visible] {
outline: 2px solid var(--bui-ring);
outline-offset: -2px;
}
&[data-focused] {
background: var(--bui-bg-neutral-2);
color: var(--bui-fg-primary);
}
&[data-open] {
background: var(--bui-bg-neutral-2);
color: var(--bui-fg-primary);
}
&[data-color='danger'] {
color: var(--bui-fg-danger);
}
&[data-color='danger'][data-focused] {
background: var(--bui-bg-danger);
color: var(--bui-fg-danger);
}
&[data-has-submenu] {
& > .bui-MenuItemArrow {
display: block;
}
}
}
.bui-MenuItemListBox {
padding-inline: var(--bui-space-1);
display: block;
display: flex;
align-items: center;
height: 2rem;
padding-inline: var(--bui-space-2);
border-radius: var(--bui-radius-2);
outline: none;
color: var(--bui-fg-primary);
font-size: var(--bui-font-size-3);
cursor: pointer;
&:hover .bui-MenuItemWrapper {
&[data-focus-visible] {
outline: 2px solid var(--bui-ring);
outline-offset: -2px;
}
&[data-hovered],
&[data-focused] {
background: var(--bui-bg-neutral-2);
color: var(--bui-fg-primary);
}
+20 -51
View File
@@ -308,63 +308,34 @@ export const MenuItem = (props: MenuItemProps) => {
);
const { classes, iconStart, children, href } = ownProps;
const handleAction = () => {
if (href) {
const text =
restProps['aria-label'] ?? getNodeText(children) ?? String(href);
analytics.captureEvent('click', text, {
attributes: { to: String(href) },
});
}
};
// External links open in new tab via window.open instead of client-side routing
if (href && !isInternalLink(href)) {
return (
<RAMenuItem
className={classes.root}
{...dataAttributes}
textValue={typeof children === 'string' ? children : undefined}
{...restProps}
onAction={() => {
restProps.onAction?.();
handleAction();
window.open(href, '_blank', 'noopener,noreferrer');
}}
>
<div className={classes.itemWrapper}>
<div className={classes.itemContent}>
{iconStart}
{children}
</div>
<div className={classes.itemArrow}>
<RiArrowRightSLine />
</div>
</div>
</RAMenuItem>
);
}
const isExternal = href && !isInternalLink(href);
return (
<RAMenuItem
className={classes.root}
{...dataAttributes}
href={href}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
textValue={typeof children === 'string' ? children : undefined}
{...restProps}
onAction={() => {
restProps.onAction?.();
handleAction();
if (href) {
const text =
restProps['aria-label'] ?? getNodeText(children) ?? String(href);
analytics.captureEvent('click', text, {
attributes: { to: String(href) },
});
}
}}
>
<div className={classes.itemWrapper}>
<div className={classes.itemContent}>
{iconStart}
{children}
</div>
<div className={classes.itemArrow}>
<RiArrowRightSLine />
</div>
<div className={classes.itemContent}>
{iconStart}
{children}
</div>
<div className={classes.itemArrow}>
<RiArrowRightSLine />
</div>
</RAMenuItem>
);
@@ -384,13 +355,11 @@ export const MenuListBoxItem = (props: MenuListBoxItemProps) => {
className={classes.root}
{...restProps}
>
<div className={classes.itemWrapper}>
<div className={classes.itemContent}>
<div className={classes.check}>
<RiCheckLine />
</div>
{children}
<div className={classes.itemContent}>
<div className={classes.check}>
<RiCheckLine />
</div>
{children}
</div>
</RAListBoxItem>
);
@@ -100,7 +100,6 @@ export const MenuItemDefinition = defineComponent<MenuItemOwnProps>()({
styles,
classNames: {
root: 'bui-MenuItem',
itemWrapper: 'bui-MenuItemWrapper',
itemContent: 'bui-MenuItemContent',
itemArrow: 'bui-MenuItemArrow',
},
@@ -121,7 +120,6 @@ export const MenuListBoxItemDefinition =
styles,
classNames: {
root: 'bui-MenuItemListBox',
itemWrapper: 'bui-MenuItemWrapper',
itemContent: 'bui-MenuItemContent',
check: 'bui-MenuItemListBoxCheck',
},