diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts index 310e9e0184..558c160ecb 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts @@ -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 }); diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts index 9d36ee72d3..52ac07551a 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts @@ -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) { + 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; + } & Record; + + const elementProps = el.props as { + className?: string; + style?: Record; + }; + + return cloneElement(el, { + ...rest, + className: mergeClassNames(elementProps.className, className), + style: { + ...elementProps.style, + fontSize: legacyFontSizeMap[fontSize], + ...style, }, - el, - ); + }); }; this.#components.set(key, component); return component;