-
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
Open
adamrasheed
wants to merge
11
commits into
main
Choose a base branch
from
ar/LG-5766-icon-fix
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+820
−2
Open
Changes from 9 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
3fc8669
[LG-5766] fix: Icon fill support
adamrasheed d30a4d6
cleanup test
adamrasheed 88a510f
more cleanup
adamrasheed 24253c6
added changeset
adamrasheed bdb66a3
updated createIconComponent, added tests, added story
adamrasheed 996ee97
updated createIconComponent, added tests
adamrasheed 89a9bf2
Merge branch 'main' into ar/LG-5766-icon-fix
adamrasheed d5b9ade
updated story
adamrasheed b7843ad
updated createIconComponent, cleaned up story
adamrasheed 01bab78
Merge branch 'main' into ar/LG-5766-icon-fix
adamrasheed f5b344a
refined tests
adamrasheed File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@leafygreen-ui/icon': patch | ||
| --- | ||
|
|
||
| Fixed an issue where icons generated through createIconComponent were not passing in fill value correctly | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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(<Icon glyph="Edit" fill="red" />); | ||||||
adamrasheed marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| const svg = container.querySelector('svg'); | ||||||
| expect(svg).toBeTruthy(); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| // 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'); | ||||||
|
|
||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(<GlyphComponent />); | ||
| 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(<GlyphComponent data-custom="custom-value" />); | ||
| 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(<GlyphComponent size={24} />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expectSize(glyph, '24'); | ||
adamrasheed marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| test('applies Size.Small correctly (14px)', () => { | ||
| render(<GlyphComponent size={Size.Small} />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expectSize(glyph, '14'); | ||
| }); | ||
|
|
||
| test('applies Size.Default correctly (16px)', () => { | ||
| render(<GlyphComponent size={Size.Default} />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expectSize(glyph, '16'); | ||
| }); | ||
|
|
||
| test('applies Size.Large correctly (20px)', () => { | ||
| render(<GlyphComponent size={Size.Large} />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expectSize(glyph, '20'); | ||
| }); | ||
|
|
||
| test('applies Size.XLarge correctly (24px)', () => { | ||
| render(<GlyphComponent size={Size.XLarge} />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expectSize(glyph, '24'); | ||
| }); | ||
|
|
||
| test('uses Size.Default (16px) when size prop is not provided', () => { | ||
| render(<GlyphComponent />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expectSize(glyph, '16'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('fill prop', () => { | ||
| const GlyphComponent = createGlyphComponent('TestGlyph', MockSVGRGlyph); | ||
|
|
||
| test('applies fill as CSS color', () => { | ||
| render(<GlyphComponent fill="red" />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expectFillColor(glyph, 'red'); | ||
adamrasheed marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| 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(); | ||
| }); | ||
adamrasheed marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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'); | ||
| }); | ||
adamrasheed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| describe('className prop', () => { | ||
| const GlyphComponent = createGlyphComponent('TestGlyph', MockSVGRGlyph); | ||
|
|
||
| test('applies className to the SVG element', () => { | ||
| render(<GlyphComponent className="my-custom-class" />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expect(glyph).toHaveClass('my-custom-class'); | ||
| }); | ||
|
|
||
| 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'); | ||
| }); | ||
adamrasheed marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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); | ||
| }); | ||
adamrasheed marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| describe('role prop', () => { | ||
| const GlyphComponent = createGlyphComponent('TestGlyph', MockSVGRGlyph); | ||
|
|
||
| test('applies role="img" by default', () => { | ||
| render(<GlyphComponent />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expect(glyph).toHaveAttribute('role', 'img'); | ||
| }); | ||
|
|
||
| test('applies role="presentation" when specified', () => { | ||
| render(<GlyphComponent role="presentation" />); | ||
| 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(<GlyphComponent role="invalid" />); | ||
|
|
||
| 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(<GlyphComponent />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expect(glyph).toHaveAttribute('aria-label', 'Test Glyph Icon'); | ||
| }); | ||
|
|
||
| test('applies custom aria-label when provided', () => { | ||
| render(<GlyphComponent aria-label="My Custom Label" />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expect(glyph).toHaveAttribute('aria-label', 'My Custom Label'); | ||
| }); | ||
|
|
||
| test('applies aria-labelledby when provided', () => { | ||
| render(<GlyphComponent aria-labelledby="my-label-id" />); | ||
| const glyph = screen.getByTestId('mock-glyph'); | ||
| expect(glyph).toHaveAttribute('aria-labelledby', 'my-label-id'); | ||
| }); | ||
|
|
||
| test('sets aria-labelledby when title is provided', () => { | ||
| render(<GlyphComponent title="Test Title" />); | ||
| 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', () => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we also check for the |
||
| render( | ||
| <GlyphComponent title="Test Title" aria-labelledby="external-label" />, | ||
| ); | ||
| 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(<GlyphComponent role="presentation" />); | ||
| 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(<GlyphComponent />); | ||
| 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( | ||
| <GlyphComponent | ||
| size={32} | ||
| className="combined-class" | ||
| fill="purple" | ||
| aria-label="Combined Icon" | ||
| />, | ||
| ); | ||
| 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( | ||
| <GlyphComponent | ||
| size={Size.Large} | ||
| className="accessible-class" | ||
| role="presentation" | ||
| />, | ||
| ); | ||
| 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(<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'); | ||
| }); | ||
| }); | ||
adamrasheed marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.