diff --git a/.changeset/polite-turkeys-run.md b/.changeset/polite-turkeys-run.md
new file mode 100644
index 0000000000..1157293e2e
--- /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..b04965dd36 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).toBeTruthy();
+ // 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..6694ac5c2d
--- /dev/null
+++ b/packages/icon/src/createGlyphComponent.spec.tsx
@@ -0,0 +1,295 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { createGlyphComponent } from './createGlyphComponent';
+import { Size } from './glyphCommon';
+import { isComponentGlyph } from './isComponentGlyph';
+import {
+ expectFillColor,
+ expectSize,
+ MockSVGRGlyph,
+ MockSVGRGlyphWithChildren,
+} from './testUtils';
+
+describe('packages/Icon/createGlyphComponent', () => {
+ describe('basic functionality', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', MockSVGRGlyph);
+
+ 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', MockSVGRGlyph);
+
+ 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', MockSVGRGlyph);
+
+ test('applies numeric size to height and width', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expectSize(glyph, '24');
+ });
+
+ test('applies Size.Small correctly (14px)', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expectSize(glyph, '14');
+ });
+
+ test('applies Size.Default correctly (16px)', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expectSize(glyph, '16');
+ });
+
+ test('applies Size.Large correctly (20px)', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expectSize(glyph, '20');
+ });
+
+ test('applies Size.XLarge correctly (24px)', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expectSize(glyph, '24');
+ });
+
+ test('uses Size.Default (16px) when size prop is not provided', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expectSize(glyph, '16');
+ });
+ });
+
+ describe('fill prop', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', MockSVGRGlyph);
+
+ test('applies fill as CSS color', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expectFillColor(glyph, 'red');
+ });
+
+ test('does not apply fill style when fill is not provided', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ // When no fill is provided, no fill-related class should be applied
+ // The glyph should still render without error
+ expect(glyph).toBeInTheDocument();
+ });
+
+ test('applies fill alongside other props', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveClass('custom-class');
+ expectSize(glyph, '32');
+ expectFillColor(glyph, 'blue');
+ });
+ });
+
+ describe('className prop', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', MockSVGRGlyph);
+
+ test('applies className to the SVG element', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveClass('my-custom-class');
+ });
+
+ test('applies multiple classNames to the SVG element', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveClass('class-one');
+ expect(glyph).toHaveClass('class-two');
+ });
+
+ test('applies className alongside fill style', () => {
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveClass('custom-class');
+ // fill applies a CSS class for the color style
+ const classList = Array.from(glyph.classList);
+ expect(classList.length).toBeGreaterThan(1);
+ });
+ });
+
+ describe('role prop', () => {
+ const GlyphComponent = createGlyphComponent('TestGlyph', MockSVGRGlyph);
+
+ 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', MockSVGRGlyph);
+
+ 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 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',
+ MockSVGRGlyphWithChildren,
+ );
+
+ 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', MockSVGRGlyph);
+
+ test('applies all props correctly together', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('mock-glyph');
+
+ expectSize(glyph, '32');
+ expect(glyph).toHaveClass('combined-class');
+ expect(glyph).toHaveAttribute('aria-label', 'Combined Icon');
+ expectFillColor(glyph, 'purple');
+ });
+
+ test('applies size enum with className and role', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('mock-glyph');
+
+ expectSize(glyph, '20');
+ expect(glyph).toHaveClass('accessible-class');
+ expect(glyph).toHaveAttribute('role', 'presentation');
+ expect(glyph).toHaveAttribute('aria-hidden', 'true');
+ });
+ });
+
+ describe('different glyph names', () => {
+ test('handles PascalCase glyph names correctly', () => {
+ const GlyphComponent = createGlyphComponent(
+ 'MyCustomGlyph',
+ MockSVGRGlyph,
+ );
+ expect(GlyphComponent.displayName).toBe('MyCustomGlyph');
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('aria-label', 'My Custom Glyph Icon');
+ });
+
+ test('handles single word glyph names correctly', () => {
+ const GlyphComponent = createGlyphComponent('Edit', MockSVGRGlyph);
+ expect(GlyphComponent.displayName).toBe('Edit');
+ render();
+ const glyph = screen.getByTestId('mock-glyph');
+ expect(glyph).toHaveAttribute('aria-label', 'Edit Icon');
+ });
+ });
+});
diff --git a/packages/icon/src/createIconComponent.spec.tsx b/packages/icon/src/createIconComponent.spec.tsx
new file mode 100644
index 0000000000..15a775cf79
--- /dev/null
+++ b/packages/icon/src/createIconComponent.spec.tsx
@@ -0,0 +1,446 @@
+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,
+ createMockSVGRComponent,
+ CustomSVGRGlyph,
+ expectFillColor,
+ expectSize,
+} 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 = createMockSVGRComponent('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');
+ expectSize(glyph, '32');
+ });
+
+ test('applies Size enum to custom SVGs', () => {
+ render();
+ const glyph = screen.getByTestId('raw-svg-glyph');
+ expectSize(glyph, '20');
+ });
+ });
+
+ describe('title prop with generated glyphs', () => {
+ // Use real generated glyphs to test title functionality
+ const IconComponent = createIconComponent(generatedGlyphs);
+
+ test('renders a title element when title prop is provided', () => {
+ render();
+ const glyph = screen.getByRole('img');
+ const titleElement = glyph.querySelector('title');
+ expect(titleElement).toBeInTheDocument();
+ expect(titleElement?.textContent).toBe('Edit Icon Title');
+ });
+
+ test('sets aria-labelledby to title element ID when title is provided', () => {
+ render();
+ const glyph = screen.getByRole('img');
+ const titleElement = glyph.querySelector('title');
+ const ariaLabelledBy = glyph.getAttribute('aria-labelledby');
+ expect(ariaLabelledBy).toBe(titleElement?.id);
+ });
+
+ test('combines title ID with aria-labelledby when both are provided', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByRole('img');
+ const titleElement = glyph.querySelector('title');
+ const ariaLabelledBy = glyph.getAttribute('aria-labelledby');
+ expect(ariaLabelledBy).toContain('external-label');
+ expect(ariaLabelledBy).toBe(`${titleElement?.id} external-label`);
+ });
+
+ test('does not render title when title prop is not provided', () => {
+ render();
+ const glyph = screen.getByRole('img');
+ const titleElement = glyph.querySelector('title');
+ expect(titleElement).not.toBeInTheDocument();
+ });
+ });
+
+ describe('title prop with custom SVGR glyphs', () => {
+ const CustomGlyphWithTitle = createMockSVGRComponent(
+ 'custom-glyph-with-title',
+ );
+
+ const customGlyphsWithTitle = {
+ CustomWithTitle: createGlyphComponent(
+ 'CustomWithTitle',
+ CustomGlyphWithTitle,
+ ),
+ };
+
+ const IconComponent = createIconComponent(customGlyphsWithTitle);
+
+ test('passes title through createGlyphComponent which sets aria-labelledby', () => {
+ render();
+ const glyph = screen.getByTestId('custom-glyph-with-title');
+ // createGlyphComponent sets aria-labelledby when title is provided
+ const ariaLabelledBy = glyph.getAttribute('aria-labelledby');
+ expect(ariaLabelledBy).not.toBeNull();
+ expect(ariaLabelledBy).toContain('icon-title');
+ });
+
+ test('sets aria-label when no title or aria-labelledby provided', () => {
+ render();
+ const glyph = screen.getByTestId('custom-glyph-with-title');
+ // Default aria-label is generated from glyph name
+ expect(glyph).toHaveAttribute('aria-label', 'Custom With Title 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');
+ });
+
+ test('applies multiple classNames to glyph SVG element', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveClass('class-one');
+ expect(glyph).toHaveClass('class-two');
+ });
+
+ test('applies className alongside fill style', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveClass('custom-class');
+ // fill applies a CSS class for the color style
+ const classList = Array.from(glyph.classList);
+ expect(classList.length).toBeGreaterThan(1);
+ });
+ });
+
+ describe('className prop with generated glyphs', () => {
+ const IconComponent = createIconComponent(generatedGlyphs);
+
+ test('applies className to generated glyph SVG element', () => {
+ render();
+ const glyph = screen.getByRole('img');
+ expect(glyph).toHaveClass('generated-glyph-class');
+ });
+
+ test('applies multiple classNames to generated glyph', () => {
+ render();
+ const glyph = screen.getByRole('img');
+ expect(glyph).toHaveClass('class-one');
+ expect(glyph).toHaveClass('class-two');
+ });
+ });
+
+ describe('className prop with custom SVGs', () => {
+ const RawSVGGlyph = createMockSVGRComponent('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');
+ expectSize(glyph, '24');
+ });
+
+ test('applies Size.Small correctly', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expectSize(glyph, '14');
+ });
+
+ test('applies Size.Default correctly', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expectSize(glyph, '16');
+ });
+
+ test('applies Size.Large correctly', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expectSize(glyph, '20');
+ });
+
+ test('applies Size.XLarge correctly', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expectSize(glyph, '24');
+ });
+
+ test('uses default size when size prop is not provided', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expectSize(glyph, '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', () => {
+ render();
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expectFillColor(glyph, 'red');
+ });
+
+ test('applies fill alongside className', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByTestId('custom-svgr-glyph');
+ expect(glyph).toHaveClass('custom-class');
+ expectFillColor(glyph, 'blue');
+ });
+ });
+
+ 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');
+ expectFillColor(glyph, 'purple');
+ });
+ });
+
+ describe('combined props with generated glyphs', () => {
+ const IconComponent = createIconComponent(generatedGlyphs);
+
+ test('applies all props correctly together', () => {
+ render(
+ ,
+ );
+ const glyph = screen.getByRole('img');
+
+ expectSize(glyph, '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);
+
+ expectFillColor(glyph, 'green');
+ });
+ });
+
+ 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');
+
+ expectSize(glyph, '32');
+ expect(glyph).toHaveClass('combined-custom-class');
+ expectFillColor(glyph, 'orange');
+ });
+
+ 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..c189d5d78c 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,17 @@ type GlyphObject = Record;
export function createIconComponent(
glyphs: G,
) {
+ const allGlyphsAreComponents = Object.values(glyphs).every(isComponentGlyph);
+ const glyphDict = allGlyphsAreComponents
+ ? glyphs
+ : mapValues(glyphs, (val, key) => {
+ 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..c73c50febe
--- /dev/null
+++ b/packages/icon/src/testUtils.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+
+import { SVGR } from './types';
+
+/**
+ * Creates a mock SVGR component for testing purposes
+ * @param testId - The data-testid to apply to the SVG element
+ */
+export const createMockSVGRComponent = (testId: string): SVGR.Component => {
+ const MockComponent: SVGR.Component = ({ children, ...props }) => (
+
+ );
+ return MockComponent;
+};
+
+// Pre-built mock components for common test scenarios
+export const MockSVGRGlyph = createMockSVGRComponent('mock-glyph');
+export const MockSVGRGlyphWithChildren = createMockSVGRComponent(
+ 'mock-glyph-with-children',
+);
+export const CustomSVGRGlyph = createMockSVGRComponent('custom-svgr-glyph');
+export const AnotherCustomGlyph = createMockSVGRComponent(
+ 'another-custom-glyph',
+);
+
+/**
+ * Size enum values and their expected pixel values for testing
+ */
+export const sizeTestCases = [
+ { size: 'small', enumValue: 'Small', expected: '14' },
+ { size: 'default', enumValue: 'Default', expected: '16' },
+ { size: 'large', enumValue: 'Large', expected: '20' },
+ { size: 'xlarge', enumValue: 'XLarge', expected: '24' },
+] as const;
+
+/**
+ * Asserts that an element has the expected height and width attributes
+ */
+export const expectSize = (element: HTMLElement, size: string) => {
+ expect(element).toHaveAttribute('height', size);
+ expect(element).toHaveAttribute('width', size);
+};
+
+/**
+ * Asserts that an element has the expected fill color applied via CSS
+ */
+export const expectFillColor = (element: HTMLElement, color: string) => {
+ const computedStyle = window.getComputedStyle(element);
+ expect(computedStyle.color).toBe(color);
+};