Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/ui-avatar/src/Avatar/__new-tests__/Avatar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { runAxeCheck } from '@instructure/ui-axe-check'
import '@testing-library/jest-dom'
import Avatar from '../index'
import { IconGroupLine } from '@instructure/ui-icons'
import { View } from '@instructure/ui-view'

describe('<Avatar />', () => {
describe('for a11y', () => {
Expand Down Expand Up @@ -72,12 +73,17 @@ describe('<Avatar />', () => {
expect(getComputedStyle(initials).color).toBe('rgb(43, 122, 188)')
})

it('should return the underlying component', async () => {
it('refs should return the underlying component', async () => {
const elementRef = vi.fn()
const ref: React.Ref<View> = { current: null }
const { container } = render(
<Avatar name="Avatar Name" elementRef={elementRef} />
<>
<Avatar id="av1" name="Avatar Name" elementRef={elementRef} />
<Avatar id="av2" name="Avatar Name2" ref={ref} />
</>
)
expect(elementRef).toHaveBeenCalledWith(container.firstChild)
expect(ref.current!.props.id).toBe('av2')
expect(elementRef).toHaveBeenCalledWith(container.querySelector('#av1'))
})
})

Expand Down
225 changes: 118 additions & 107 deletions packages/ui-avatar/src/Avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@
*/

import { useStyle } from '@instructure/emotion'
import { useState, SyntheticEvent, useEffect } from 'react'
import {
useState,
SyntheticEvent,
useEffect,
forwardRef,
ForwardedRef
} from 'react'

import { View } from '@instructure/ui-view'
import { callRenderProp, passthroughProps } from '@instructure/ui-react-utils'
Expand All @@ -37,125 +43,130 @@ import generateComponentTheme from './theme'
category: components
---
**/
const Avatar = forwardRef(
(
{
size = 'medium',
color = 'default',
hasInverseColor = false,
showBorder = 'auto',
shape = 'circle',
display = 'inline-block',
onImageLoaded = (_event: SyntheticEvent) => {},
src,
name,
renderIcon,
alt,
as,
margin,
themeOverride,
elementRef,
...rest
}: AvatarProps,
ref: ForwardedRef<View>
) => {
const [loaded, setLoaded] = useState(false)

const Avatar = ({
size = 'medium',
color = 'default',
hasInverseColor = false,
showBorder = 'auto',
shape = 'circle',
display = 'inline-block',
onImageLoaded = (_event: SyntheticEvent) => {},
src,
name,
renderIcon,
alt,
as,
margin,
themeOverride,
elementRef,
...rest
}: AvatarProps) => {
const [loaded, setLoaded] = useState(false)
const styles = useStyle({
generateStyle,
generateComponentTheme,
params: {
loaded,
size,
color,
hasInverseColor,
shape,
src,
showBorder,
themeOverride
},
componentId: 'Avatar',
displayName: 'Avatar'
})

const styles = useStyle({
generateStyle,
generateComponentTheme,
params: {
loaded,
size,
color,
hasInverseColor,
shape,
src,
showBorder,
themeOverride
},
componentId: 'Avatar',
displayName: 'Avatar'
})
useEffect(() => {
// in case the image is unset in an update, show icons/initials again
if (loaded && !src) {
setLoaded(false)
}
}, [loaded, src])

useEffect(() => {
// in case the image is unset in an update, show icons/initials again
if (loaded && !src) {
setLoaded(false)
}
}, [loaded, src])
const makeInitialsFromName = () => {
if (!name || typeof name !== 'string') {
return
}
const currentName = name.trim()
if (currentName.length === 0) {
return
}

const makeInitialsFromName = () => {
if (!name || typeof name !== 'string') {
return
if (currentName.match(/\s+/)) {
const names = currentName.split(/\s+/)
return (names[0][0] + names[names.length - 1][0]).toUpperCase()
} else {
return currentName[0].toUpperCase()
}
}
const currentName = name.trim()
if (currentName.length === 0) {
return

const handleImageLoaded = (event: SyntheticEvent) => {
setLoaded(true)
onImageLoaded(event)
}

if (currentName.match(/\s+/)) {
const names = currentName.split(/\s+/)
return (names[0][0] + names[names.length - 1][0]).toUpperCase()
} else {
return currentName[0].toUpperCase()
const renderInitials = () => {
return (
<span css={styles?.initials} aria-hidden="true">
{makeInitialsFromName()}
</span>
)
}
}

const handleImageLoaded = (event: SyntheticEvent) => {
setLoaded(true)
onImageLoaded(event)
}
const renderContent = () => {
if (!renderIcon) {
return renderInitials()
}

const renderInitials = () => {
return (
<span css={styles?.initials} aria-hidden="true">
{makeInitialsFromName()}
</span>
)
}

const renderContent = () => {
if (!renderIcon) {
return renderInitials()
return <span css={styles?.iconSVG}>{callRenderProp(renderIcon)}</span>
}

return <span css={styles?.iconSVG}>{callRenderProp(renderIcon)}</span>
return (
<View
{...passthroughProps({
size,
color,
hasInverseColor,
showBorder,
shape,
display,
src,
name,
renderIcon,
alt,
as,
margin,
...rest
})}
ref={ref}
aria-label={alt ? alt : undefined}
role={alt ? 'img' : undefined}
as={as}
elementRef={elementRef}
margin={margin}
css={styles?.avatar}
display={display}
>
<img // This is visually hidden and is here for loading purposes only
src={src}
css={styles?.loadImage}
alt={alt}
onLoad={handleImageLoaded}
aria-hidden="true"
/>
{!loaded && renderContent()}
</View>
)
}

return (
<View
{...passthroughProps({
size,
color,
hasInverseColor,
showBorder,
shape,
display,
src,
name,
renderIcon,
alt,
as,
margin,
...rest
})}
aria-label={alt ? alt : undefined}
role={alt ? 'img' : undefined}
as={as}
elementRef={elementRef}
margin={margin}
css={styles?.avatar}
display={display}
>
<img // This is visually hidden and is here for loading purposes only
src={src}
css={styles?.loadImage}
alt={alt}
onLoad={handleImageLoaded}
aria-hidden="true"
/>
{!loaded && renderContent()}
</View>
)
}
)

export default Avatar
export { Avatar }
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import '@testing-library/jest-dom'
import { IconHeartLine } from '@instructure/ui-icons'

import { DateInput2 } from '../index'
import { TextInput } from '@instructure/ui-text-input'

const LABEL_TEXT = 'Choose a date'

Expand Down Expand Up @@ -124,6 +125,28 @@ describe('<DateInput2 />', () => {
expect(calendarLabel).toBeInTheDocument()
})

it('refs should return the underlying component', async () => {
const inputRef = vi.fn()
const ref: React.Ref<TextInput> = { current: null }
const { container } = render(
<DateInput2
id="dateInput2"
inputRef={inputRef}
ref={ref}
renderLabel={LABEL_TEXT}
screenReaderLabels={{
calendarIcon: 'Calendar',
nextMonthButton: 'Next month',
prevMonthButton: 'Previous month'
}}
/>
)
const dateInput = container.querySelector('input')
expect(inputRef).toHaveBeenCalledWith(dateInput)
expect(ref.current!.props.id).toBe('dateInput2')
expect(dateInput).toBeInTheDocument()
})

it('should render a custom calendar icon with screen reader label', async () => {
const iconLabel = 'Calendar icon Label'
const { container } = render(
Expand Down
Loading
Loading