@@ -3,6 +3,7 @@ import * as React from 'react'
33import classNames from 'classnames'
44
55import { Box } from '../box'
6+ import { polymorphicComponent } from '../utils/polymorphism'
67
78import {
89 getAvailableImageSources ,
@@ -16,8 +17,8 @@ import {
1617
1718import styles from './avatar.module.css'
1819
19- import type { ComponentProps } from 'react'
2020import type { ObfuscatedClassName } from '../utils/common-types'
21+ import type { PolymorphicComponentProps } from '../utils/polymorphism'
2122import type { AvatarImage , AvatarShape , AvatarSize , ImageSources } from './utils'
2223
2324type AvatarStyle = React . CSSProperties & {
@@ -28,7 +29,7 @@ type AvatarStyle = React.CSSProperties & {
2829/**
2930 * Props for the `Avatar` component.
3031 */
31- type AvatarProps = ObfuscatedClassName & {
32+ type AvatarOwnProps = ObfuscatedClassName & {
3233 /**
3334 * The rendered avatar size, in CSS pixels.
3435 */
@@ -69,99 +70,116 @@ type AvatarProps = ObfuscatedClassName & {
6970 * Test identifier applied to the avatar root element.
7071 */
7172 'data-testid' ?: string
72- } & Omit < ComponentProps < 'div' > , 'className' | 'style' >
73-
74- const AvatarContent = React . forwardRef < HTMLDivElement , AvatarProps > ( function AvatarContent (
75- {
76- size,
77- shape = 'circle' ,
78- name,
79- image,
80- alt,
81- exceptionallySetClassName,
82- 'data-testid' : testId ,
83- 'aria-hidden' : ariaHidden ,
84- 'aria-label' : ariaLabel ,
85- ...restProps
86- } ,
87- ref ,
88- ) {
89- const imageSources = getSources ( image , size )
90- const [ failedImageSources , setFailedImageSources ] = React . useState < string [ ] > ( [ ] )
91- const availableImageSources = getAvailableImageSources ( imageSources , failedImageSources )
92- const normalizedName = normalizeAvatarName ( name )
93- const initials = availableImageSources ? '' : getInitials ( name )
9473
95- const hasInitials = initials !== ''
96- const label = ariaLabel ?? alt ?? normalizedName
97- const isDecorative = ariaHidden || label === ''
98- const metaColorIndex = hasInitials ? getAvatarMetaColorIndex ( name ) : undefined
74+ /**
75+ * Avatar owns its root sizing styles. Use `exceptionallySetClassName` for the styling escape
76+ * hatch.
77+ */
78+ style ?: never
79+ }
9980
100- return (
101- < Box
102- ref = { ref }
103- className = { classNames (
104- styles . avatar ,
105- styles [ `shape-${ shape } ` ] ,
106- metaColorIndex !== undefined && styles [ `meta-color-${ metaColorIndex } ` ] ,
107- ! availableImageSources && ! hasInitials && styles . empty ,
108- exceptionallySetClassName ,
109- ) }
110- style = { getAvatarStyle ( size ) }
111- data-testid = { testId }
112- aria-hidden = { isDecorative || undefined }
113- display = "inlineFlex"
114- alignItems = "center"
115- justifyContent = "center"
116- flexShrink = { 0 }
117- overflow = "hidden"
118- textAlign = "center"
119- { ...restProps }
120- >
121- { availableImageSources ? (
122- < img
123- className = { styles . image }
124- src = { availableImageSources . src }
125- srcSet = { availableImageSources . srcSet }
126- sizes = { availableImageSources . sizes }
127- alt = { label ?? '' }
128- onError = { ( event ) => {
129- const failedSource = getFailedImageSource (
130- availableImageSources ,
131- event . currentTarget ,
132- )
133-
134- setFailedImageSources ( ( currentFailedSources ) =>
135- currentFailedSources . includes ( failedSource )
136- ? currentFailedSources
137- : [ ...currentFailedSources , failedSource ] ,
138- )
139- } }
140- />
141- ) : hasInitials ? (
142- < div
143- className = { styles . initials }
144- role = { label ? 'img' : undefined }
145- aria-label = { label }
146- >
147- { initials }
148- </ div >
149- ) : null }
150- </ Box >
151- )
152- } )
81+ type AvatarProps < ComponentType extends React . ElementType = 'div' > = PolymorphicComponentProps <
82+ ComponentType ,
83+ AvatarOwnProps ,
84+ 'omitClassName'
85+ >
86+
87+ const AvatarContent = polymorphicComponent < 'div' , AvatarOwnProps , 'omitClassName' > (
88+ function AvatarContent (
89+ {
90+ as,
91+ size,
92+ shape = 'circle' ,
93+ name,
94+ image,
95+ alt,
96+ exceptionallySetClassName,
97+ 'data-testid' : testId ,
98+ 'aria-hidden' : ariaHidden ,
99+ 'aria-label' : ariaLabel ,
100+ ...restProps
101+ } ,
102+ ref ,
103+ ) {
104+ const imageSources = getSources ( image , size )
105+ const [ failedImageSources , setFailedImageSources ] = React . useState < string [ ] > ( [ ] )
106+ const availableImageSources = getAvailableImageSources ( imageSources , failedImageSources )
107+ const normalizedName = normalizeAvatarName ( name )
108+ const initials = availableImageSources ? '' : getInitials ( name )
109+
110+ const hasInitials = initials !== ''
111+ const label = ariaLabel ?? alt ?? normalizedName
112+ const isDecorative = ariaHidden || label === ''
113+ const metaColorIndex = hasInitials ? getAvatarMetaColorIndex ( name ) : undefined
114+
115+ return (
116+ < Box
117+ as = { as }
118+ ref = { ref }
119+ className = { classNames (
120+ styles . avatar ,
121+ styles [ `shape-${ shape } ` ] ,
122+ metaColorIndex !== undefined && styles [ `meta-color-${ metaColorIndex } ` ] ,
123+ ! availableImageSources && ! hasInitials && styles . empty ,
124+ exceptionallySetClassName ,
125+ ) }
126+ style = { getAvatarStyle ( size ) }
127+ data-testid = { testId }
128+ aria-hidden = { isDecorative || undefined }
129+ display = "inlineFlex"
130+ alignItems = "center"
131+ justifyContent = "center"
132+ flexShrink = { 0 }
133+ overflow = "hidden"
134+ textAlign = "center"
135+ { ...restProps }
136+ >
137+ { availableImageSources ? (
138+ < img
139+ className = { styles . image }
140+ src = { availableImageSources . src }
141+ srcSet = { availableImageSources . srcSet }
142+ sizes = { availableImageSources . sizes }
143+ alt = { label ?? '' }
144+ onError = { ( event ) => {
145+ const failedSource = getFailedImageSource (
146+ availableImageSources ,
147+ event . currentTarget ,
148+ )
149+
150+ setFailedImageSources ( ( currentFailedSources ) =>
151+ currentFailedSources . includes ( failedSource )
152+ ? currentFailedSources
153+ : [ ...currentFailedSources , failedSource ] ,
154+ )
155+ } }
156+ />
157+ ) : hasInitials ? (
158+ < div
159+ className = { styles . initials }
160+ role = { label ? 'img' : undefined }
161+ aria-label = { label }
162+ >
163+ { initials }
164+ </ div >
165+ ) : null }
166+ </ Box >
167+ )
168+ } ,
169+ )
153170
154171/**
155172 * Displays an avatar from an image URL, a source map keyed by intrinsic
156173 * image width, or initials derived from the provided name (with a background
157174 * color).
158175 */
159- const Avatar = React . forwardRef < HTMLDivElement , AvatarProps > ( function Avatar (
160- { image, ...restProps } ,
176+ const Avatar = polymorphicComponent < 'div' , AvatarOwnProps , 'omitClassName' > ( function Avatar (
177+ { as , image, ...restProps } ,
161178 ref ,
162179) {
163180 return (
164181 < AvatarContent
182+ as = { as }
165183 ref = { ref }
166184 // Allows `AvatarContent` to remount when the image map changes,
167185 // which resets error states
0 commit comments