diff --git a/.changeset/polite-turkeys-run.md b/.changeset/polite-turkeys-run.md
new file mode 100644
index 0000000000..4722f53e64
--- /dev/null
+++ b/.changeset/polite-turkeys-run.md
@@ -0,0 +1,5 @@
+---
+'@leafygreen-ui/icon': patch
+---
+
+Fixed an issue where icons generated through `createIconComponent` were not passing in fill value correctly
diff --git a/packages/icon/src/Icon.spec.tsx b/packages/icon/src/Icon.spec.tsx
index 30246f9971..001f1fa1ae 100644
--- a/packages/icon/src/Icon.spec.tsx
+++ b/packages/icon/src/Icon.spec.tsx
@@ -9,6 +9,7 @@ import { typeIs } from '@leafygreen-ui/lib';
import EditIcon from './generated/Edit';
import { Size } from './glyphCommon';
+import { Icon } from './Icon';
import { isComponentGlyph } from './isComponentGlyph';
import { SVGR } from './types';
import { createGlyphComponent, createIconComponent, glyphs } from '.';
@@ -256,6 +257,18 @@ describe('packages/Icon/createIconComponent', () => {
});
});
+describe('packages/Icon/Icon', () => {
+ test('`fill` prop applies CSS color correctly', () => {
+ const { container } = render();
+ const svg = container.querySelector('svg');
+ expect(svg).toBeInTheDocument();
+ // The fill prop should be applied as a CSS color via emotion
+ // We check that the computed style has the correct color
+ const computedStyle = window.getComputedStyle(svg!);
+ expect(computedStyle.color).toBe('red');
+ });
+});
+
describe('Generated glyphs', () => {
test('Edit icon has displayName: "Edit"', () => {
expect(EditIcon.displayName).toBe('Edit');
diff --git a/packages/icon/src/Icon.stories.tsx b/packages/icon/src/Icon.stories.tsx
index aebdfbbd6e..d4ecea4e55 100644
--- a/packages/icon/src/Icon.stories.tsx
+++ b/packages/icon/src/Icon.stories.tsx
@@ -9,7 +9,7 @@ import { css } from '@leafygreen-ui/emotion';
import { palette } from '@leafygreen-ui/palette';
import { GlyphName } from './glyphs';
-import Icon, { glyphs, IconProps, Size } from '.';
+import Icon, { createIconComponent, glyphs, IconProps, Size } from '.';
const meta: StoryMetaType = {
title: 'Components/Display/Icon',
@@ -109,6 +109,18 @@ export const LiveExample: StoryObj = {
),
};
+export const Custom: StoryObj = {
+ parameters: {
+ controls: {
+ exclude: [...meta.parameters.controls!.exclude!, 'glyph'],
+ },
+ },
+ render: (args: Omit) => {
+ const CustomIcon = createIconComponent(glyphs);
+ return ;
+ },
+};
+
export const Generated: StoryObj = {
parameters: {
generate: {
diff --git a/packages/icon/src/createGlyphComponent.spec.tsx b/packages/icon/src/createGlyphComponent.spec.tsx
new file mode 100644
index 0000000000..94ee42edf1
--- /dev/null
+++ b/packages/icon/src/createGlyphComponent.spec.tsx
@@ -0,0 +1,274 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { createGlyphComponent } from './createGlyphComponent';
+import { Size } from './glyphCommon';
+import { isComponentGlyph } from './isComponentGlyph';
+import { TestSVGRGlyph, TestSVGRGlyphWithChildren } from './testUtils';
+
+describe('packages/Icon/createGlyphComponent', () => {
+ describe('basic functionality', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', TestSVGRGlyph);
+
+ test('returns a function', () => {
+ expect(typeof GlyphComponent).toBe('function');
+ });
+
+ test('returned component has the correct displayName', () => {
+ expect(GlyphComponent.displayName).toBe('TestGlyph');
+ });
+
+ test('returned component has the property `isGlyph`', () => {
+ expect(GlyphComponent).toHaveProperty('isGlyph');
+ expect(GlyphComponent.isGlyph).toBe(true);
+ });
+
+ test('returned component passes `isComponentGlyph`', () => {
+ expect(isComponentGlyph(GlyphComponent)).toBe(true);
+ });
+ });
+
+ describe('rendering', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', TestSVGRGlyph);
+
+ test('renders an SVG element', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toBeInTheDocument();
+ expect(glyph.nodeName.toLowerCase()).toBe('svg');
+ });
+
+ test('passes through additional props to the SVG element', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('data-custom', 'custom-value');
+ });
+ });
+
+ describe('size prop', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', TestSVGRGlyph);
+
+ test('applies numeric size to height and width', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('height', '24');
+ expect(glyph).toHaveAttribute('width', '24');
+ });
+
+ test('applies Size.Small correctly (14px)', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('height', '14');
+ expect(glyph).toHaveAttribute('width', '14');
+ });
+
+ test('applies Size.Default correctly (16px)', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('height', '16');
+ expect(glyph).toHaveAttribute('width', '16');
+ });
+
+ test('applies Size.Large correctly (20px)', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('height', '20');
+ expect(glyph).toHaveAttribute('width', '20');
+ });
+
+ test('applies Size.XLarge correctly (24px)', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('height', '24');
+ expect(glyph).toHaveAttribute('width', '24');
+ });
+
+ test('uses Size.Default (16px) when size prop is not provided', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('height', '16');
+ expect(glyph).toHaveAttribute('width', '16');
+ });
+ });
+
+ describe('fill prop', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', TestSVGRGlyph);
+
+ test('applies fill as CSS color via className', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ // Fill is applied as a CSS color via an emotion-generated class
+ expect(glyph).toHaveAttribute('class');
+ expect(glyph.className).not.toBe('');
+ });
+
+ test('does not apply fill style class when fill is not provided', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ // When no fill is provided, no emotion class should be applied
+ expect(glyph.classList.length).toBe(0);
+ });
+
+ test('applies fill alongside other props', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveClass('custom-class');
+ expect(glyph).toHaveAttribute('height', '32');
+ expect(glyph).toHaveAttribute('width', '32');
+ // Fill adds an emotion class in addition to custom-class
+ expect(glyph.classList.length).toBeGreaterThan(1);
+ });
+ });
+
+ describe('className prop', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', TestSVGRGlyph);
+
+ test('applies className to the SVG element', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveClass('my-custom-class');
+ });
+ });
+
+ describe('role prop', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', TestSVGRGlyph);
+
+ test('applies role="img" by default', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('role', 'img');
+ });
+
+ test('applies role="presentation" when specified', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('role', 'presentation');
+ expect(glyph).toHaveAttribute('aria-hidden', 'true');
+ });
+
+ test('logs a warning when an invalid role is provided', () => {
+ const consoleSpy = jest
+ .spyOn(console, 'warn')
+ .mockImplementation(() => {});
+
+ // @ts-expect-error - intentionally passing invalid role for testing
+ // eslint-disable-next-line jsx-a11y/aria-role
+ render();
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ "Please provide a valid role to this component. Valid options are 'img' and 'presentation'. If you'd like the Icon to be accessible to screen readers please use 'img', otherwise set the role to 'presentation'.",
+ );
+
+ consoleSpy.mockRestore();
+ });
+ });
+
+ describe('accessibility props', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', TestSVGRGlyph);
+
+ test('generates default aria-label from glyph name when no accessibility props provided', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('aria-label', 'Test Glyph Icon');
+ });
+
+ test('applies custom aria-label when provided', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('aria-label', 'My Custom Label');
+ });
+
+ test('applies aria-labelledby when provided', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('aria-labelledby', 'my-label-id');
+ });
+
+ test('sets aria-labelledby when title is provided', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ const ariaLabelledBy = glyph.getAttribute('aria-labelledby');
+ expect(ariaLabelledBy).not.toBeNull();
+ expect(ariaLabelledBy).toContain('icon-title');
+ });
+
+ test('combines title ID with aria-labelledby when both are provided', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('mock-glyph');
+
+ const ariaLabelledBy = glyph.getAttribute('aria-labelledby');
+ expect(ariaLabelledBy).toContain('external-label');
+ expect(ariaLabelledBy).toContain('icon-title');
+ });
+
+ test('sets the title ID when title is provided', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ const titleId = glyph.getAttribute('aria-labelledby');
+ expect(titleId).not.toBeNull();
+ expect(titleId).toContain('icon-title');
+ });
+
+ test('sets aria-hidden to true when role is presentation', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('aria-hidden', 'true');
+ });
+ });
+
+ describe('title prop', () => {
+ const GlyphComponent = createGlyphComponent(
+ 'TestGlyph',
+ TestSVGRGlyphWithChildren,
+ );
+
+ test('does not include title in children when title is not provided', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph-with-children');
+ const titleElement = glyph.querySelector('title');
+ expect(titleElement).not.toBeInTheDocument();
+ });
+ });
+
+ describe('combined props', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', TestSVGRGlyph);
+
+ test('applies all props correctly together', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('mock-glyph');
+
+ expect(glyph).toHaveAttribute('height', '32');
+ expect(glyph).toHaveAttribute('width', '32');
+ expect(glyph).toHaveClass('combined-class');
+ expect(glyph).toHaveAttribute('aria-label', 'Combined Icon');
+ // Fill adds an emotion class in addition to combined-class
+ expect(glyph.classList.length).toBeGreaterThan(1);
+ });
+
+ test('applies size enum with className and role', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('mock-glyph');
+
+ expect(glyph).toHaveAttribute('height', '20');
+ expect(glyph).toHaveAttribute('width', '20');
+ expect(glyph).toHaveClass('accessible-class');
+ expect(glyph).toHaveAttribute('role', 'presentation');
+ expect(glyph).toHaveAttribute('aria-hidden', 'true');
+ });
+ });
+});
diff --git a/packages/icon/src/createGlyphComponent.tsx b/packages/icon/src/createGlyphComponent.tsx
index 8910c75b59..0b3b19ee3a 100644
--- a/packages/icon/src/createGlyphComponent.tsx
+++ b/packages/icon/src/createGlyphComponent.tsx
@@ -34,6 +34,8 @@ export function createGlyphComponent(
color: ${fill};
`;
+ // Note: We do not currently support title prop for custom glyphs. TODO: LG-5828
+
const renderedSize = typeof size === 'number' ? size : sizeMap[size];
if (!(role === 'img' || role === 'presentation')) {
@@ -60,7 +62,7 @@ export function createGlyphComponent(
['aria-labelledby']: ariaLabelledby,
})}
{...rest}
- />
+ >
);
};
diff --git a/packages/icon/src/createIconComponent.spec.tsx b/packages/icon/src/createIconComponent.spec.tsx
new file mode 100644
index 0000000000..450f5f1cc4
--- /dev/null
+++ b/packages/icon/src/createIconComponent.spec.tsx
@@ -0,0 +1,395 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { createGlyphComponent } from './createGlyphComponent';
+import { createIconComponent } from './createIconComponent';
+import * as generatedGlyphs from './generated';
+import { Size } from './glyphCommon';
+import { isComponentGlyph } from './isComponentGlyph';
+import {
+ AnotherCustomGlyph,
+ createTestSVGRComponent,
+ CustomSVGRGlyph,
+} from './testUtils';
+
+// Create glyph components from the SVGR components
+const customGlyphs = {
+ CustomGlyph: createGlyphComponent('CustomGlyph', CustomSVGRGlyph),
+ AnotherGlyph: createGlyphComponent('AnotherGlyph', AnotherCustomGlyph),
+};
+
+describe('packages/Icon/createIconComponent', () => {
+ describe('basic functionality', () => {
+ const IconComponent = createIconComponent(customGlyphs);
+
+ test('returns a function', () => {
+ expect(typeof IconComponent).toBe('function');
+ });
+
+ test('returned function has the displayName: "Icon"', () => {
+ expect(IconComponent.displayName).toBe('Icon');
+ });
+
+ test('returned function has the property: `isGlyph`', () => {
+ expect(IconComponent).toHaveProperty('isGlyph');
+ expect(IconComponent.isGlyph).toBeTruthy();
+ });
+
+ test('returned function passes `isComponentGlyph`', () => {
+ expect(isComponentGlyph(IconComponent)).toBeTruthy();
+ });
+ });
+
+ describe('rendering glyphs', () => {
+ const IconComponent = createIconComponent(customGlyphs);
+
+ test('renders the correct glyph when passed a valid glyph name', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toBeInTheDocument();
+ expect(glyph.nodeName.toLowerCase()).toBe('svg');
+ });
+
+ test('renders different glyphs based on glyph prop', () => {
+ const { rerender } = render();
+ expect(screen.getByTestId('custom-svgr-glyph')).toBeInTheDocument();
+
+ rerender();
+ expect(screen.getByTestId('another-custom-glyph')).toBeInTheDocument();
+ });
+
+ test('logs an error and renders nothing when glyph does not exist', () => {
+ const consoleSpy = jest
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ const { container } = render();
+
+ // Should log an error
+ expect(consoleSpy).toHaveBeenCalledWith(
+ 'Error in Icon',
+ 'Could not find glyph named "NonExistentGlyph" in the icon set.',
+ undefined,
+ );
+
+ // Should not render an SVG
+ const svg = container.querySelector('svg');
+ expect(svg).not.toBeInTheDocument();
+
+ consoleSpy.mockRestore();
+ });
+
+ test('suggests near match when glyph name has incorrect casing', () => {
+ const consoleSpy = jest
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ render();
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ 'Error in Icon',
+ 'Could not find glyph named "custom-glyph" in the icon set.',
+ 'Did you mean "CustomGlyph?"',
+ );
+
+ consoleSpy.mockRestore();
+ });
+ });
+
+ describe('custom SVG support', () => {
+ const RawSVGGlyph = createTestSVGRComponent('raw-svg-glyph');
+
+ const customSVGGlyphs = {
+ RawSVG: createGlyphComponent('RawSVG', RawSVGGlyph),
+ };
+
+ const IconComponent = createIconComponent(customSVGGlyphs);
+
+ test('renders custom SVG components correctly', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svg-glyph');
+ expect(glyph).toBeInTheDocument();
+ expect(glyph.nodeName.toLowerCase()).toBe('svg');
+ });
+
+ test('applies size prop to custom SVGs', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svg-glyph');
+ expect(glyph).toHaveAttribute('height', '32');
+ expect(glyph).toHaveAttribute('width', '32');
+ });
+
+ test('applies Size enum to custom SVGs', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svg-glyph');
+ expect(glyph).toHaveAttribute('height', '20');
+ expect(glyph).toHaveAttribute('width', '20');
+ });
+ });
+
+ describe('raw SVGR component support (auto-wrapped)', () => {
+ // Pass raw SVGR components directly without wrapping with createGlyphComponent
+ const RawSVGRGlyph = createTestSVGRComponent('raw-svgr-glyph');
+
+ const IconComponent = createIconComponent({ RawSVGR: RawSVGRGlyph });
+
+ test('automatically wraps raw SVGR components with createGlyphComponent', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svgr-glyph');
+ expect(glyph).toBeInTheDocument();
+ expect(glyph.nodeName.toLowerCase()).toBe('svg');
+ });
+
+ test('applies size prop to auto-wrapped SVGR components', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svgr-glyph');
+ expect(glyph).toHaveAttribute('height', '28');
+ expect(glyph).toHaveAttribute('width', '28');
+ });
+
+ test('applies Size enum to auto-wrapped SVGR components', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svgr-glyph');
+ expect(glyph).toHaveAttribute('height', '24');
+ expect(glyph).toHaveAttribute('width', '24');
+ });
+
+ test('applies className to auto-wrapped SVGR components', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svgr-glyph');
+ expect(glyph).toHaveClass('my-raw-class');
+ });
+
+ test('applies fill to auto-wrapped SVGR components', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svgr-glyph');
+ // Fill is applied as a CSS color via an emotion-generated class
+ expect(glyph.classList.length).toBeGreaterThan(0);
+ });
+
+ test('applies accessibility props to auto-wrapped SVGR components', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svgr-glyph');
+ expect(glyph).toHaveAttribute('role', 'img');
+ // Default aria-label is generated from glyph name
+ expect(glyph).toHaveAttribute('aria-label', 'Raw SVGR Icon');
+ });
+ });
+
+ describe('className prop', () => {
+ const IconComponent = createIconComponent(customGlyphs);
+
+ test('applies className to glyph SVG element', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveClass('custom-class');
+ });
+ });
+
+ describe('className prop with custom SVGs', () => {
+ const RawSVGGlyph = createTestSVGRComponent('raw-svg-for-class');
+
+ const customSVGGlyphs = {
+ RawSVG: createGlyphComponent('RawSVG', RawSVGGlyph),
+ };
+
+ const IconComponent = createIconComponent(customSVGGlyphs);
+
+ test('applies className to custom SVG components', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svg-for-class');
+ expect(glyph).toHaveClass('my-custom-class');
+ });
+ });
+
+ describe('size prop', () => {
+ const IconComponent = createIconComponent(customGlyphs);
+
+ test('applies numeric size to glyph', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('height', '24');
+ expect(glyph).toHaveAttribute('width', '24');
+ });
+
+ test('applies Size.Small correctly', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('height', '14');
+ expect(glyph).toHaveAttribute('width', '14');
+ });
+
+ test('applies Size.Default correctly', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('height', '16');
+ expect(glyph).toHaveAttribute('width', '16');
+ });
+
+ test('applies Size.Large correctly', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('height', '20');
+ expect(glyph).toHaveAttribute('width', '20');
+ });
+
+ test('applies Size.XLarge correctly', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('height', '24');
+ expect(glyph).toHaveAttribute('width', '24');
+ });
+
+ test('uses default size when size prop is not provided', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('height', '16');
+ expect(glyph).toHaveAttribute('width', '16');
+ });
+ });
+
+ describe('accessibility props', () => {
+ const IconComponent = createIconComponent(customGlyphs);
+
+ test('applies role="img" by default', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('role', 'img');
+ });
+
+ test('applies role="presentation" when specified', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('role', 'presentation');
+ expect(glyph).toHaveAttribute('aria-hidden', 'true');
+ });
+
+ test('generates default aria-label when no accessibility props provided', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('aria-label', 'Custom Glyph Icon');
+ });
+
+ test('applies custom aria-label when provided', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('aria-label', 'My Custom Label');
+ });
+
+ test('applies aria-labelledby when provided', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveAttribute('aria-labelledby', 'my-label-id');
+ });
+ });
+
+ describe('fill prop', () => {
+ const IconComponent = createIconComponent(customGlyphs);
+
+ test('applies fill as CSS color via className', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ // Fill is applied as a CSS color via an emotion-generated class
+ expect(glyph).toHaveAttribute('class');
+ expect(glyph.classList.length).toBeGreaterThan(0);
+ });
+
+ test('applies fill alongside className', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveClass('custom-class');
+ // Fill adds an emotion class in addition to custom-class
+ expect(glyph.classList.length).toBeGreaterThan(1);
+ });
+ });
+
+ describe('fill prop with generated glyphs', () => {
+ const IconComponent = createIconComponent(generatedGlyphs);
+
+ test('applies fill as CSS color to generated glyph', () => {
+ render();
+ const glyph = screen.getByRole('img');
+ // Fill is applied as a CSS color via an emotion-generated class
+ expect(glyph).toHaveAttribute('class');
+ expect(glyph.classList.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('combined props with generated glyphs', () => {
+ const IconComponent = createIconComponent(generatedGlyphs);
+
+ test('applies all props correctly together', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByRole('img');
+
+ expect(glyph).toHaveAttribute('height', '24');
+ expect(glyph).toHaveAttribute('width', '24');
+ expect(glyph).toHaveClass('combined-class');
+
+ // Check title
+ const titleElement = glyph.querySelector('title');
+ expect(titleElement).toBeInTheDocument();
+ expect(titleElement?.textContent).toBe('Combined Title');
+
+ // Check aria-labelledby points to title
+ expect(glyph.getAttribute('aria-labelledby')).toBe(titleElement?.id);
+
+ // Fill adds an emotion class in addition to combined-class
+ expect(glyph.classList.length).toBeGreaterThan(1);
+ });
+ });
+
+ describe('combined props with custom SVGR glyphs', () => {
+ const IconComponent = createIconComponent(customGlyphs);
+
+ test('applies size, className, and fill together', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+
+ expect(glyph).toHaveAttribute('height', '32');
+ expect(glyph).toHaveAttribute('width', '32');
+ expect(glyph).toHaveClass('combined-custom-class');
+ // Fill adds an emotion class in addition to combined-custom-class
+ expect(glyph.classList.length).toBeGreaterThan(1);
+ });
+
+ test('applies accessibility props with className', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+
+ expect(glyph).toHaveClass('accessible-class');
+ expect(glyph).toHaveAttribute('aria-label', 'Accessible Custom Icon');
+ });
+ });
+});
diff --git a/packages/icon/src/createIconComponent.tsx b/packages/icon/src/createIconComponent.tsx
index 7c76f7e53c..43540b8da6 100644
--- a/packages/icon/src/createIconComponent.tsx
+++ b/packages/icon/src/createIconComponent.tsx
@@ -1,7 +1,10 @@
import React from 'react';
import kebabCase from 'lodash/kebabCase';
+import mapValues from 'lodash/mapValues';
+import { createGlyphComponent } from './createGlyphComponent';
import { Size } from './glyphCommon';
+import { isComponentGlyph } from './isComponentGlyph';
import { LGGlyph } from './types';
// We omit size here because we map string values for size to numbers in this component.
@@ -27,8 +30,18 @@ type GlyphObject = Record;
export function createIconComponent(
glyphs: G,
) {
+ const allGlyphsAreComponents = Object.values(glyphs).every(isComponentGlyph);
+ const glyphDict = allGlyphsAreComponents
+ ? glyphs
+ : mapValues(glyphs, (val, key) => {
+ // We do not currently check for valid SVG files. TODO: LG-5827
+ if (isComponentGlyph(val)) return val;
+
+ return createGlyphComponent(key, val);
+ });
+
const Icon = ({ glyph, ...rest }: IconProps) => {
- const SVGComponent = glyphs[glyph];
+ const SVGComponent = glyphDict[glyph];
if (SVGComponent) {
return ;
diff --git a/packages/icon/src/testUtils.tsx b/packages/icon/src/testUtils.tsx
new file mode 100644
index 0000000000..813c22cd3f
--- /dev/null
+++ b/packages/icon/src/testUtils.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import { SVGR } from './types';
+
+/**
+ * Creates a mock SVGR component for testing purposes.
+ */
+export const createTestSVGRComponent = (testId: string): SVGR.Component => {
+ const TestComponent: SVGR.Component = ({ children, ...props }) => (
+
+ );
+
+ return TestComponent;
+};
+
+// Pre-built mock components for common test scenarios
+export const TestSVGRGlyph = createTestSVGRComponent('mock-glyph');
+export const TestSVGRGlyphWithChildren = createTestSVGRComponent(
+ 'mock-glyph-with-children',
+);
+export const CustomSVGRGlyph = createTestSVGRComponent('custom-svgr-glyph');
+export const AnotherCustomGlyph = createTestSVGRComponent(
+ 'another-custom-glyph',
+);