-
Notifications
You must be signed in to change notification settings - Fork 72
[LG-5766] fix: Icon fill support #3377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: b7843ad The changes in this PR will be included in the next version bump. This PR includes changesets to release 74 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Size Change: +134 B (+0.01%) Total Size: 1.81 MB
ℹ️ View Unchanged
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR fixes icon fill support by explicitly passing the fill prop through the createIconComponent function to the underlying SVG components. The change ensures that the fill prop (which is converted to a CSS color property by the generated icon components) is properly threaded through the icon wrapper.
Key changes:
- Modified
createIconComponentto explicitly destructure and pass thefillprop to SVG components - Added test coverage to verify that the
fillprop correctly applies CSS color styling
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/icon/src/createIconComponent.tsx | Explicitly destructures fill from props and passes it to the SVG component |
| packages/icon/src/Icon.spec.tsx | Adds test to verify fill prop applies CSS color correctly |
|
Coverage after merging ar/LG-5766-icon-fix into main will be
Coverage Report for Changed Files
|
|||||||||||||||||||||||||
| test('`fill` prop applies CSS color correctly', () => { | ||
| const { container } = render(<Icon glyph="Edit" fill="red" />); | ||
| const svg = container.querySelector('svg'); | ||
| expect(svg).toBeTruthy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| expect(svg).toBeTruthy(); | |
| expect(svg).toBeInTheDocument(); |
| '@leafygreen-ui/icon': patch | ||
| --- | ||
|
|
||
| Fixed an issue where icons generated through createIconComponent were not passing in fill value correctly |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Fixed an issue where icons generated through createIconComponent were not passing in fill value correctly | |
| Fixed an issue where icons generated through `createIconComponent` were not passing in fill value correctly |
|
|
||
| return createGlyphComponent(key, val); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if val is not a valid svg element?
| export const MockSVGRGlyphWithChildren = createMockSVGRComponent( | ||
| 'mock-glyph-with-children', | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When would we need this? What is this testing?
| export const CustomSVGRGlyph = createMockSVGRComponent('custom-svgr-glyph'); | ||
| export const AnotherCustomGlyph = createMockSVGRComponent( | ||
| 'another-custom-glyph', | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to pre-define 3 separate test glyphs?
| test('applies numeric size to height and width', () => { | ||
| render(<GlyphComponent size={24} />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expectSize(glyph, '24'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO it's cleaner and less brittle to explicitly write the assertions
expect(glyph).toHaveAttribute('height', 24);
expect(glyph).toHaveAttribute('width', 24);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I'm surprised Jest/ESlint isn't complaining about a test without an explicit expect statement. Must've gotten rid of that rule
| /** | ||
| * 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is this used?
| }; | ||
|
|
||
| // Pre-built mock components for common test scenarios | ||
| export const MockSVGRGlyph = createMockSVGRComponent('mock-glyph'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically it's not a "mock" glyph, it's a test component
| test('applies fill as CSS color', () => { | ||
| render(<GlyphComponent fill="red" />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expectFillColor(glyph, 'red'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similarly, might be cleaner to just have the explicit assertion here.
Tests are one place where DRY can be more trouble than it's worth
| test('does not apply fill style when fill is not provided', () => { | ||
| render(<GlyphComponent />); | ||
| 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(); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this isn't testing what the title says
| test('applies fill alongside other props', () => { | ||
| render(<GlyphComponent fill="blue" className="custom-class" size={32} />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expect(glyph).toHaveClass('custom-class'); | ||
| expectSize(glyph, '32'); | ||
| expectFillColor(glyph, 'blue'); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure this is necessary
| test('applies multiple classNames to the SVG element', () => { | ||
| render(<GlyphComponent className="class-one class-two" />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expect(glyph).toHaveClass('class-one'); | ||
| expect(glyph).toHaveClass('class-two'); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not necessary
| test('applies className alongside fill style', () => { | ||
| render(<GlyphComponent className="custom-class" fill="green" />); | ||
| 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); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not necessary
| expect(ariaLabelledBy).toContain('icon-title'); | ||
| }); | ||
|
|
||
| test('combines title ID with aria-labelledby when both are provided', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we also check for the title element?
| describe('different glyph names', () => { | ||
| test('handles PascalCase glyph names correctly', () => { | ||
| const GlyphComponent = createGlyphComponent( | ||
| 'MyCustomGlyph', | ||
| MockSVGRGlyph, | ||
| ); | ||
| expect(GlyphComponent.displayName).toBe('MyCustomGlyph'); | ||
| render(<GlyphComponent />); | ||
| 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(<GlyphComponent />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expect(glyph).toHaveAttribute('aria-label', 'Edit Icon'); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not necessary
| const RawSVGGlyph = createMockSVGRComponent('raw-svg-glyph'); | ||
|
|
||
| const customSVGGlyphs = { | ||
| RawSVG: createGlyphComponent('RawSVG', RawSVGGlyph), | ||
| }; | ||
|
|
||
| const IconComponent = createIconComponent(customSVGGlyphs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're not testing the case where the SVG element is passed into createIconComponent directly
i.e.
const IconComponent = createIconComponent({ RawSVG: RawSVGGlyph });
| }); | ||
| }); | ||
|
|
||
| describe('title prop with custom SVGR glyphs', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if this is necessary
| test('applies multiple classNames to glyph SVG element', () => { | ||
| render( | ||
| <IconComponent glyph="CustomGlyph" className="class-one class-two" />, | ||
| ); | ||
| const glyph = screen.getByTestId('custom-svgr-glyph'); | ||
| expect(glyph).toHaveClass('class-one'); | ||
| expect(glyph).toHaveClass('class-two'); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not necessary
| test('applies className alongside fill style', () => { | ||
| render( | ||
| <IconComponent | ||
| glyph="CustomGlyph" | ||
| className="custom-class" | ||
| fill="red" | ||
| />, | ||
| ); | ||
| 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); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not necessary
| describe('className prop with generated glyphs', () => { | ||
| const IconComponent = createIconComponent(generatedGlyphs); | ||
|
|
||
| test('applies className to generated glyph SVG element', () => { | ||
| render(<IconComponent glyph="Edit" className="generated-glyph-class" />); | ||
| const glyph = screen.getByRole('img'); | ||
| expect(glyph).toHaveClass('generated-glyph-class'); | ||
| }); | ||
|
|
||
| test('applies multiple classNames to generated glyph', () => { | ||
| render(<IconComponent glyph="Edit" className="class-one class-two" />); | ||
| 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(<IconComponent glyph="RawSVG" className="my-custom-class" />); | ||
| const glyph = screen.getByTestId('raw-svg-for-class'); | ||
| expect(glyph).toHaveClass('my-custom-class'); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not necessary
| test('applies numeric size to glyph', () => { | ||
| render(<IconComponent glyph="CustomGlyph" size={24} />); | ||
| const glyph = screen.getByTestId('custom-svgr-glyph'); | ||
| expectSize(glyph, '24'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment about explicit expect
✍️ Proposed changes
Fixes a bug where the
fillprop was not being passed to the underlying SVG component when using icons created throughcreateIconComponent. Previously, thefillprop was being absorbed by the spread operator but not explicitly passed to theSVGComponent, causing the fill color to not apply correctly.🎟️ Jira ticket: LG-5766
✅ Checklist
pnpm changesetand documented my changes🧪 How to test changes
Iconcomponent and render it with afillprop:<Icon glyph="Edit" fill="red" />pnpm test --filter=@leafygreen-ui/iconto confirm the fixIcon->Customstory in Storybook