Skip to content

Commit e4f70ef

Browse files
committed
feat(ui-avatar): add lucide icons to Avatar
1 parent c1aba7a commit e4f70ef

File tree

7 files changed

+167
-4
lines changed

7 files changed

+167
-4
lines changed

packages/ui-avatar/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@instructure/ui-axe-check": "workspace:*",
3535
"@instructure/ui-babel-preset": "workspace:*",
3636
"@instructure/ui-color-utils": "workspace:*",
37+
"@instructure/ui-icons-lucide": "workspace:*",
3738
"@instructure/ui-themes": "workspace:*",
3839
"@testing-library/jest-dom": "^6.6.3",
3940
"@testing-library/react": "15.0.7",

packages/ui-avatar/src/Avatar/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,34 @@ readonly: true
142142
</View>
143143
```
144144

145+
### Using Lucide Icons
146+
147+
Avatar automatically sizes Lucide icons based on the Avatar's `size` prop. You don't need to manually set the icon size - the Avatar component handles this for you.
148+
149+
```js
150+
---
151+
type: example
152+
---
153+
<div>
154+
<View display="block" padding="small medium">
155+
<Avatar name="User Avatar" size="xx-small" renderIcon={UserInstUIIcon} />
156+
<Avatar name="User Avatar" size="x-small" renderIcon={UserInstUIIcon} />
157+
<Avatar name="User Avatar" size="small" renderIcon={UserInstUIIcon} />
158+
<Avatar name="User Avatar" size="medium" renderIcon={UserInstUIIcon} />
159+
<Avatar name="User Avatar" size="large" renderIcon={UserInstUIIcon} />
160+
<Avatar name="User Avatar" size="x-large" renderIcon={UserInstUIIcon} />
161+
<Avatar name="User Avatar" size="xx-large" renderIcon={UserInstUIIcon} />
162+
</View>
163+
<View display="block" padding="small medium">
164+
<Avatar name="Profile" size="small" color="accent2" renderIcon={CircleUserInstUIIcon} />
165+
<Avatar name="Group" size="medium" color="accent3" renderIcon={UsersInstUIIcon} />
166+
<Avatar name="Settings" size="large" color="accent4" renderIcon={SettingsInstUIIcon} />
167+
</View>
168+
</div>
169+
```
170+
171+
**Note:** When using Lucide icons with Avatar, do not specify the `size` prop on the icon itself. The Avatar component will automatically pass the appropriate size based on its own `size` prop.
172+
145173
### Size
146174

147175
The `size` prop allows you to select from `xx-small`, `x-small`, `small`, `medium` _(default)_, `large`, `x-large`, and `xx-large`. Each size has predefined dimensions and typography scales.

packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import { runAxeCheck } from '@instructure/ui-axe-check'
2929
import '@testing-library/jest-dom'
3030
import Avatar from '../index'
3131
import { IconGroupLine } from '@instructure/ui-icons'
32+
import {
33+
UserInstUIIcon,
34+
CircleUserInstUIIcon
35+
} from '@instructure/ui-icons-lucide'
3236

3337
describe('<Avatar />', () => {
3438
describe('for a11y', () => {
@@ -112,6 +116,113 @@ describe('<Avatar />', () => {
112116
const avatarSvg = container.querySelector('svg')
113117
expect(avatarSvg).toBeInTheDocument()
114118
})
119+
120+
it('should pass the correct size prop to icon based on Avatar size', async () => {
121+
const MockIcon = vi.fn((props: any) => (
122+
<svg data-testid="mock-icon" data-size={props.size}>
123+
<circle cx="25" cy="75" r="20" />
124+
</svg>
125+
))
126+
127+
const { container } = render(
128+
<Avatar name="avatar name" size="medium" renderIcon={MockIcon} />
129+
)
130+
131+
expect(MockIcon).toHaveBeenCalledWith(
132+
expect.objectContaining({ size: 'md' })
133+
)
134+
const icon = container.querySelector('[data-testid="mock-icon"]')
135+
expect(icon).toHaveAttribute('data-size', 'md')
136+
})
137+
138+
it('should map xx-small Avatar to lg icon size', async () => {
139+
const MockIcon = vi.fn(() => (
140+
<svg data-testid="mock-icon">
141+
<circle cx="25" cy="75" r="20" />
142+
</svg>
143+
))
144+
145+
render(
146+
<Avatar name="avatar name" size="xx-small" renderIcon={MockIcon} />
147+
)
148+
149+
expect(MockIcon).toHaveBeenCalledWith(
150+
expect.objectContaining({ size: 'xs' })
151+
)
152+
})
153+
154+
it('should map x-small Avatar to xl icon size', async () => {
155+
const MockIcon = vi.fn(() => (
156+
<svg data-testid="mock-icon">
157+
<circle cx="25" cy="75" r="20" />
158+
</svg>
159+
))
160+
161+
render(<Avatar name="avatar name" size="x-small" renderIcon={MockIcon} />)
162+
163+
expect(MockIcon).toHaveBeenCalledWith(
164+
expect.objectContaining({ size: 'xs' })
165+
)
166+
})
167+
168+
it('should work with icons that ignore the size prop (backwards compatibility)', async () => {
169+
const IconWithoutSize = () => (
170+
<svg data-testid="icon-without-size">
171+
<circle cx="25" cy="75" r="20" />
172+
</svg>
173+
)
174+
175+
const { container } = render(
176+
<Avatar name="avatar name" size="large" renderIcon={IconWithoutSize} />
177+
)
178+
179+
const icon = container.querySelector('[data-testid="icon-without-size"]')
180+
expect(icon).toBeInTheDocument()
181+
})
182+
183+
it('should display a Lucide icon with default size', async () => {
184+
const { container } = render(
185+
<Avatar name="avatar name" renderIcon={UserInstUIIcon} />
186+
)
187+
188+
const avatarSvg = container.querySelector('svg')
189+
expect(avatarSvg).toBeInTheDocument()
190+
})
191+
192+
it('should display a Lucide icon with medium Avatar size', async () => {
193+
const { container } = render(
194+
<Avatar
195+
name="avatar name"
196+
size="medium"
197+
renderIcon={CircleUserInstUIIcon}
198+
/>
199+
)
200+
201+
const avatarSvg = container.querySelector('svg')
202+
expect(avatarSvg).toBeInTheDocument()
203+
})
204+
205+
it('should display a Lucide icon with xx-small Avatar size', async () => {
206+
const { container } = render(
207+
<Avatar
208+
name="avatar name"
209+
size="xx-small"
210+
renderIcon={UserInstUIIcon}
211+
/>
212+
)
213+
214+
const avatarSvg = container.querySelector('svg')
215+
expect(avatarSvg).toBeInTheDocument()
216+
})
217+
218+
it('should display a Lucide icon with x-small Avatar size', async () => {
219+
const { container } = render(
220+
<Avatar name="avatar name" size="x-small" renderIcon={UserInstUIIcon} />
221+
)
222+
223+
const avatarSvg = container.querySelector('svg')
224+
expect(avatarSvg).toBeInTheDocument()
225+
})
115226
})
116227

117228
describe('when an image src url is provided', () => {

packages/ui-avatar/src/Avatar/index.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,17 @@ const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
121121
/>
122122
)
123123

124+
// Map Avatar sizes to Lucide icon semantic size tokens
125+
const avatarSizeToIconSize = {
126+
'xx-small': 'xs',
127+
'x-small': 'xs',
128+
small: 'sm',
129+
medium: 'md',
130+
large: 'lg',
131+
'x-large': 'xl',
132+
'xx-large': '2xl'
133+
} as const
134+
124135
const renderContent = () => {
125136
//image in avatar - prioritize image over icon
126137
if (src) {
@@ -133,9 +144,14 @@ const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
133144
}
134145

135146
//icon in avatar
136-
//TODO-REWORK make the icon inherit the size prop of the Avatar when the icons have it
137-
if (renderIcon) {
138-
return callRenderProp(renderIcon)
147+
if (
148+
renderIcon &&
149+
(renderIcon as React.ComponentType)?.displayName?.startsWith(
150+
'wrapLucideIcon'
151+
)
152+
) {
153+
const iconSize = avatarSizeToIconSize[size]
154+
return callRenderProp(renderIcon, { size: iconSize })
139155
}
140156

141157
//initials in avatar

packages/ui-avatar/src/Avatar/props.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,9 @@ type AvatarOwnProps = {
9797
elementRef?: (element: Element | null) => void
9898
/**
9999
* An icon, or function that returns an icon that gets displayed. If the `src` prop is provided, `src` will have priority.
100+
* When using Lucide icons, Avatar will automatically pass the appropriate size prop based on the Avatar's size.
100101
*/
101-
renderIcon?: Renderable
102+
renderIcon?: Renderable<{ size?: string | number }>
102103
}
103104

104105
export type AvatarState = {

packages/ui-avatar/tsconfig.build.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
},
3434
{
3535
"path": "../ui-icons/tsconfig.build.json"
36+
},
37+
{
38+
"path": "../ui-icons-lucide/tsconfig.build.json"
3639
}
3740
]
3841
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)