Preserve CSS sizing for translated system icons.

Keep the original icon element as the rendered root so legacy MUI-backed icons can still be styled through CSS like other Backstage UI icons.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-16 19:42:15 +01:00
parent c0ab3763e5
commit 80fed0e8f0
2 changed files with 56 additions and 19 deletions
@@ -73,12 +73,8 @@ describe('DefaultIconsApi', () => {
// @ts-expect-error testing runtime behavior
const result = icon({});
expect(result.type).toBe('span');
expect(result.props.style).toEqual({
display: 'inline-flex',
fontSize: '1.5rem',
lineHeight: 0,
});
expect(result.props.children).toBe(element);
expect(result.props.style).toEqual({ fontSize: '1.5rem' });
expect(result.props.children).toBe(element.props.children);
expect(api.getIcon('myIcon')).toBe(icon);
});
@@ -92,6 +88,26 @@ describe('DefaultIconsApi', () => {
expect(result.props.style.fontSize).toBe('1.25rem');
});
it('should forward runtime props to the original icon element', () => {
const element = createElement('svg', {
className: 'existing',
style: { color: 'red' },
});
const api = new DefaultIconsApi({ myIcon: element });
const icon = api.getIcon('myIcon');
// @ts-expect-error testing runtime behavior
const result = icon({ className: 'extra', style: { width: '2em' } });
expect(result.type).toBe('svg');
expect(result.props.className).toBe('existing extra');
expect(result.props.style).toEqual({
color: 'red',
fontSize: '1.5rem',
width: '2em',
});
});
it('should wrap null IconElement in a component for getIcon()', () => {
const api = new DefaultIconsApi({ empty: null });
@@ -19,7 +19,7 @@ import {
IconElement,
IconsApi,
} from '@backstage/frontend-plugin-api';
import { createElement, isValidElement } from 'react';
import { cloneElement, createElement, isValidElement } from 'react';
const legacyFontSizeMap = {
inherit: 'inherit',
@@ -28,6 +28,14 @@ const legacyFontSizeMap = {
large: '2.1875rem',
} as const;
function mergeClassNames(...classNames: Array<string | undefined>) {
const merged = classNames.filter(Boolean).join(' ');
if (merged) {
return merged;
}
return undefined;
}
/**
* Implementation for the {@link IconsApi}
*
@@ -75,23 +83,36 @@ export class DefaultIconsApi implements IconsApi {
if (el === undefined) {
return undefined;
}
component = ({ fontSize = 'medium' }) => {
component = props => {
if (el === null) {
return null;
}
return createElement(
// eslint-disable-next-line react/forbid-elements
'span',
{
style: {
display: 'inline-flex',
fontSize: legacyFontSizeMap[fontSize],
lineHeight: 0,
},
const {
fontSize = 'medium',
className,
style,
...rest
} = props as {
fontSize?: keyof typeof legacyFontSizeMap;
className?: string;
style?: Record<string, unknown>;
} & Record<string, unknown>;
const elementProps = el.props as {
className?: string;
style?: Record<string, unknown>;
};
return cloneElement(el, {
...rest,
className: mergeClassNames(elementProps.className, className),
style: {
...elementProps.style,
fontSize: legacyFontSizeMap[fontSize],
...style,
},
el,
);
});
};
this.#components.set(key, component);
return component;