Skip to content

Commit 6a6ba49

Browse files
committed
fix(ui-date-input,ui-avatar): add ref support to functional components
This is needed for things that try to access the ref prop, like our Transition package INSTUI-4520
1 parent cb1b2ae commit 6a6ba49

File tree

4 files changed

+362
-304
lines changed

4 files changed

+362
-304
lines changed

packages/ui-avatar/src/Avatar/__new-tests__/Avatar.test.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ 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 { View } from '@instructure/ui-view'
3233

3334
describe('<Avatar />', () => {
3435
describe('for a11y', () => {
@@ -72,12 +73,17 @@ describe('<Avatar />', () => {
7273
expect(getComputedStyle(initials).color).toBe('rgb(43, 122, 188)')
7374
})
7475

75-
it('should return the underlying component', async () => {
76+
it('refs should return the underlying component', async () => {
7677
const elementRef = vi.fn()
78+
const ref: React.Ref<View> = { current: null }
7779
const { container } = render(
78-
<Avatar name="Avatar Name" elementRef={elementRef} />
80+
<>
81+
<Avatar id="av1" name="Avatar Name" elementRef={elementRef} />
82+
<Avatar id="av2" name="Avatar Name2" ref={ref} />
83+
</>
7984
)
80-
expect(elementRef).toHaveBeenCalledWith(container.firstChild)
85+
expect(ref.current!.props.id).toBe('av2')
86+
expect(elementRef).toHaveBeenCalledWith(container.querySelector('#av1'))
8187
})
8288
})
8389

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

Lines changed: 118 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@
2323
*/
2424

2525
import { useStyle } from '@instructure/emotion'
26-
import { useState, SyntheticEvent, useEffect } from 'react'
26+
import {
27+
useState,
28+
SyntheticEvent,
29+
useEffect,
30+
forwardRef,
31+
ForwardedRef
32+
} from 'react'
2733

2834
import { View } from '@instructure/ui-view'
2935
import { callRenderProp, passthroughProps } from '@instructure/ui-react-utils'
@@ -37,125 +43,130 @@ import generateComponentTheme from './theme'
3743
category: components
3844
---
3945
**/
46+
const Avatar = forwardRef(
47+
(
48+
{
49+
size = 'medium',
50+
color = 'default',
51+
hasInverseColor = false,
52+
showBorder = 'auto',
53+
shape = 'circle',
54+
display = 'inline-block',
55+
onImageLoaded = (_event: SyntheticEvent) => {},
56+
src,
57+
name,
58+
renderIcon,
59+
alt,
60+
as,
61+
margin,
62+
themeOverride,
63+
elementRef,
64+
...rest
65+
}: AvatarProps,
66+
ref: ForwardedRef<View>
67+
) => {
68+
const [loaded, setLoaded] = useState(false)
4069

41-
const Avatar = ({
42-
size = 'medium',
43-
color = 'default',
44-
hasInverseColor = false,
45-
showBorder = 'auto',
46-
shape = 'circle',
47-
display = 'inline-block',
48-
onImageLoaded = (_event: SyntheticEvent) => {},
49-
src,
50-
name,
51-
renderIcon,
52-
alt,
53-
as,
54-
margin,
55-
themeOverride,
56-
elementRef,
57-
...rest
58-
}: AvatarProps) => {
59-
const [loaded, setLoaded] = useState(false)
70+
const styles = useStyle({
71+
generateStyle,
72+
generateComponentTheme,
73+
params: {
74+
loaded,
75+
size,
76+
color,
77+
hasInverseColor,
78+
shape,
79+
src,
80+
showBorder,
81+
themeOverride
82+
},
83+
componentId: 'Avatar',
84+
displayName: 'Avatar'
85+
})
6086

61-
const styles = useStyle({
62-
generateStyle,
63-
generateComponentTheme,
64-
params: {
65-
loaded,
66-
size,
67-
color,
68-
hasInverseColor,
69-
shape,
70-
src,
71-
showBorder,
72-
themeOverride
73-
},
74-
componentId: 'Avatar',
75-
displayName: 'Avatar'
76-
})
87+
useEffect(() => {
88+
// in case the image is unset in an update, show icons/initials again
89+
if (loaded && !src) {
90+
setLoaded(false)
91+
}
92+
}, [loaded, src])
7793

78-
useEffect(() => {
79-
// in case the image is unset in an update, show icons/initials again
80-
if (loaded && !src) {
81-
setLoaded(false)
82-
}
83-
}, [loaded, src])
94+
const makeInitialsFromName = () => {
95+
if (!name || typeof name !== 'string') {
96+
return
97+
}
98+
const currentName = name.trim()
99+
if (currentName.length === 0) {
100+
return
101+
}
84102

85-
const makeInitialsFromName = () => {
86-
if (!name || typeof name !== 'string') {
87-
return
103+
if (currentName.match(/\s+/)) {
104+
const names = currentName.split(/\s+/)
105+
return (names[0][0] + names[names.length - 1][0]).toUpperCase()
106+
} else {
107+
return currentName[0].toUpperCase()
108+
}
88109
}
89-
const currentName = name.trim()
90-
if (currentName.length === 0) {
91-
return
110+
111+
const handleImageLoaded = (event: SyntheticEvent) => {
112+
setLoaded(true)
113+
onImageLoaded(event)
92114
}
93115

94-
if (currentName.match(/\s+/)) {
95-
const names = currentName.split(/\s+/)
96-
return (names[0][0] + names[names.length - 1][0]).toUpperCase()
97-
} else {
98-
return currentName[0].toUpperCase()
116+
const renderInitials = () => {
117+
return (
118+
<span css={styles?.initials} aria-hidden="true">
119+
{makeInitialsFromName()}
120+
</span>
121+
)
99122
}
100-
}
101123

102-
const handleImageLoaded = (event: SyntheticEvent) => {
103-
setLoaded(true)
104-
onImageLoaded(event)
105-
}
124+
const renderContent = () => {
125+
if (!renderIcon) {
126+
return renderInitials()
127+
}
106128

107-
const renderInitials = () => {
108-
return (
109-
<span css={styles?.initials} aria-hidden="true">
110-
{makeInitialsFromName()}
111-
</span>
112-
)
113-
}
114-
115-
const renderContent = () => {
116-
if (!renderIcon) {
117-
return renderInitials()
129+
return <span css={styles?.iconSVG}>{callRenderProp(renderIcon)}</span>
118130
}
119131

120-
return <span css={styles?.iconSVG}>{callRenderProp(renderIcon)}</span>
132+
return (
133+
<View
134+
{...passthroughProps({
135+
size,
136+
color,
137+
hasInverseColor,
138+
showBorder,
139+
shape,
140+
display,
141+
src,
142+
name,
143+
renderIcon,
144+
alt,
145+
as,
146+
margin,
147+
...rest
148+
})}
149+
ref={ref}
150+
aria-label={alt ? alt : undefined}
151+
role={alt ? 'img' : undefined}
152+
as={as}
153+
elementRef={elementRef}
154+
margin={margin}
155+
css={styles?.avatar}
156+
display={display}
157+
>
158+
<img // This is visually hidden and is here for loading purposes only
159+
src={src}
160+
css={styles?.loadImage}
161+
alt={alt}
162+
onLoad={handleImageLoaded}
163+
aria-hidden="true"
164+
/>
165+
{!loaded && renderContent()}
166+
</View>
167+
)
121168
}
122-
123-
return (
124-
<View
125-
{...passthroughProps({
126-
size,
127-
color,
128-
hasInverseColor,
129-
showBorder,
130-
shape,
131-
display,
132-
src,
133-
name,
134-
renderIcon,
135-
alt,
136-
as,
137-
margin,
138-
...rest
139-
})}
140-
aria-label={alt ? alt : undefined}
141-
role={alt ? 'img' : undefined}
142-
as={as}
143-
elementRef={elementRef}
144-
margin={margin}
145-
css={styles?.avatar}
146-
display={display}
147-
>
148-
<img // This is visually hidden and is here for loading purposes only
149-
src={src}
150-
css={styles?.loadImage}
151-
alt={alt}
152-
onLoad={handleImageLoaded}
153-
aria-hidden="true"
154-
/>
155-
{!loaded && renderContent()}
156-
</View>
157-
)
158-
}
169+
)
159170

160171
export default Avatar
161172
export { Avatar }

packages/ui-date-input/src/DateInput2/__new-tests__/DateInput2.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import '@testing-library/jest-dom'
2929
import { IconHeartLine } from '@instructure/ui-icons'
3030

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

3334
const LABEL_TEXT = 'Choose a date'
3435

@@ -124,6 +125,28 @@ describe('<DateInput2 />', () => {
124125
expect(calendarLabel).toBeInTheDocument()
125126
})
126127

128+
it('refs should return the underlying component', async () => {
129+
const inputRef = vi.fn()
130+
const ref: React.Ref<TextInput> = { current: null }
131+
const { container } = render(
132+
<DateInput2
133+
id="dateInput2"
134+
inputRef={inputRef}
135+
ref={ref}
136+
renderLabel={LABEL_TEXT}
137+
screenReaderLabels={{
138+
calendarIcon: 'Calendar',
139+
nextMonthButton: 'Next month',
140+
prevMonthButton: 'Previous month'
141+
}}
142+
/>
143+
)
144+
const dateInput = container.querySelector('input')
145+
expect(inputRef).toHaveBeenCalledWith(dateInput)
146+
expect(ref.current!.props.id).toBe('dateInput2')
147+
expect(dateInput).toBeInTheDocument()
148+
})
149+
127150
it('should render a custom calendar icon with screen reader label', async () => {
128151
const iconLabel = 'Calendar icon Label'
129152
const { container } = render(

0 commit comments

Comments
 (0)