Skip to content

Commit e010ea4

Browse files
HerrTopimatyasf
authored andcommitted
feat(ui-spinner,ui-avatar): refactor spinner
1 parent 620b421 commit e010ea4

File tree

8 files changed

+232
-458
lines changed

8 files changed

+232
-458
lines changed

docs/guides/upgrade-guide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ The new icons automatically sync with theme changes, support all InstUI color to
4141
- Setting `readonly` does not set the low level `<input>` to disabled, but to `readonly`. This also means that the input is still focusable when `readonly`
4242
- its DOM structure has been significantly simplified
4343

44+
### Spinner
45+
46+
- `as` prop has been removed, `Spinner` will always render as a `<div>` element.
47+
- `elementRef` prop has been removed, use the `ref` prop instead.
48+
4449
## Codemods
4550

4651
To ease the upgrade, we provide codemods that will automate most of the changes. Pay close attention to its output, it cannot refactor complex code! The codemod scripts can be run via the following commands:

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

Lines changed: 81 additions & 166 deletions
Large diffs are not rendered by default.

packages/ui-spinner/src/Spinner/README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ The `size` prop allows you to select from `x-small`, `small`, `medium` and `larg
1111
---
1212
type: example
1313
---
14-
<div>
15-
<Spinner renderTitle="Loading" size="x-small"/>
16-
<Spinner renderTitle="Loading" size="small" margin="0 0 0 medium" />
17-
<Spinner renderTitle="Loading" margin="0 0 0 medium" />
18-
<Spinner renderTitle="Loading" size="large" margin="0 0 0 medium" />
14+
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
15+
<Spinner renderTitle="Loading" size="x-small" />
16+
<Spinner renderTitle="Loading" size="small" />
17+
<Spinner renderTitle="Loading" />
18+
<Spinner renderTitle="Loading" size="large" />
1919
</div>
2020
```
2121

@@ -28,8 +28,8 @@ dark backgrounds.
2828
---
2929
type: example
3030
---
31-
<View background="primary-inverse" as="div">
32-
<Spinner renderTitle="Loading" variant="inverse" />
31+
<View background="primary-inverse" as="div" margin="spacing.spaceXs" padding="spacing.spaceLg">
32+
<Spinner renderTitle="Loading" variant="inverse" margin="spacing.spaceXs" />
3333
</View>
3434
```
3535

@@ -41,11 +41,11 @@ The `delay` prop allows you to delay the rendering of the spinner a desired time
4141
---
4242
type: example
4343
---
44-
<div>
44+
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
4545
<Spinner renderTitle="Loading" size="x-small" delay={1000} />
46-
<Spinner renderTitle="Loading" size="small" margin="0 0 0 medium" delay={2000} />
47-
<Spinner renderTitle="Loading" margin="0 0 0 medium" delay={3000} />
48-
<Spinner renderTitle="Loading" size="large" margin="0 0 0 medium" delay={4000} />
46+
<Spinner renderTitle="Loading" size="small" delay={2000} />
47+
<Spinner renderTitle="Loading" delay={3000} />
48+
<Spinner renderTitle="Loading" size="large" delay={4000} />
4949
</div>
5050
```
5151

@@ -57,5 +57,7 @@ The `renderTitle` prop is read to screen readers.
5757
---
5858
type: example
5959
---
60-
<Spinner renderTitle={() => "Hello world"} />
60+
<div>
61+
<Spinner renderTitle={() => "Hello world"} margin="spacing.spaceXs" />
62+
</div>
6163
```

packages/ui-spinner/src/Spinner/__tests__/Spinner.test.tsx

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import { vi, expect } from 'vitest'
2828
import type { MockInstance } from 'vitest'
2929

3030
import '@testing-library/jest-dom'
31-
import { View } from '@instructure/ui-view'
3231
import Spinner from '../index'
3332
import type { SpinnerProps } from '../props'
3433

@@ -82,41 +81,6 @@ describe('<Spinner />', () => {
8281
expect(spinner).toHaveTextContent('I have translated Loading')
8382
})
8483

85-
describe('when passing down props to View', () => {
86-
const allowedProps: { [key: string]: any } = {
87-
margin: 'small',
88-
elementRef: () => {},
89-
as: 'div'
90-
}
91-
92-
View.allowedProps
93-
.filter((prop) => prop !== 'children')
94-
.forEach((prop) => {
95-
if (Object.keys(allowedProps).indexOf(prop) < 0) {
96-
it(`should NOT allow the '${prop}' prop`, async () => {
97-
const props = {
98-
[prop]: 'foo'
99-
}
100-
const expectedErrorMessage = `prop '${prop}' is not allowed.`
101-
102-
render(<Spinner renderTitle="Loading" {...props} />)
103-
104-
expect(consoleErrorMock).toHaveBeenCalledWith(
105-
expect.stringContaining(expectedErrorMessage),
106-
expect.any(String)
107-
)
108-
})
109-
} else {
110-
it(`should allow the '${prop}' prop`, async () => {
111-
const props = { [prop]: allowedProps[prop] }
112-
render(<Spinner renderTitle="Loading" {...props} />)
113-
114-
expect(consoleErrorMock).not.toHaveBeenCalled()
115-
})
116-
}
117-
})
118-
})
119-
12084
describe('with the delay prop', () => {
12185
it('should delay rendering', async () => {
12286
render(<Spinner renderTitle="Loading" delay={300} />)

packages/ui-spinner/src/Spinner/index.tsx

Lines changed: 61 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -22,150 +22,113 @@
2222
* SOFTWARE.
2323
*/
2424

25-
import { Component } from 'react'
25+
import { useState, useEffect, forwardRef } from 'react'
2626

27-
import { View } from '@instructure/ui-view'
27+
import { useStyle } from '@instructure/emotion'
2828
import {
2929
callRenderProp,
3030
omitProps,
31-
withDeterministicId
31+
useDeterministicId
3232
} from '@instructure/ui-react-utils'
3333
import { logError as error } from '@instructure/console'
3434

35-
import { withStyleRework as withStyle } from '@instructure/emotion'
36-
3735
import generateStyle from './styles'
38-
import generateComponentTheme from './theme'
39-
import type { SpinnerProps, SpinnerState } from './props'
36+
import type { SpinnerProps } from './props'
4037
import { allowedProps } from './props'
4138

4239
/**
4340
---
4441
category: components
4542
---
4643
**/
47-
@withDeterministicId()
48-
@withStyle(generateStyle, generateComponentTheme)
49-
class Spinner extends Component<SpinnerProps, SpinnerState> {
50-
static readonly componentId = 'Spinner'
51-
static allowedProps = allowedProps
52-
static defaultProps = {
53-
as: 'div',
54-
size: 'medium',
55-
variant: 'default'
56-
}
57-
58-
ref: Element | null = null
59-
private readonly titleId?: string
60-
private delayTimeout?: NodeJS.Timeout
61-
62-
handleRef = (el: Element | null) => {
63-
const { elementRef } = this.props
64-
65-
this.ref = el
66-
67-
if (typeof elementRef === 'function') {
68-
elementRef(el)
69-
}
70-
}
71-
72-
constructor(props: SpinnerProps) {
73-
super(props)
74-
75-
this.titleId = props.deterministicId!()
76-
77-
this.state = {
78-
shouldRender: !props.delay
79-
}
80-
}
81-
82-
componentDidMount() {
83-
this.props.makeStyles?.()
84-
const { delay } = this.props
85-
44+
const Spinner = forwardRef<HTMLDivElement, SpinnerProps>((props, ref) => {
45+
const {
46+
size = 'medium',
47+
variant = 'default',
48+
delay,
49+
renderTitle,
50+
margin,
51+
themeOverride
52+
} = props
53+
54+
const [shouldRender, setShouldRender] = useState(!delay)
55+
// Deterministic ID generation
56+
const [titleId, setTitleId] = useState<string | undefined>()
57+
const getId = useDeterministicId('Spinner')
58+
useEffect(() => {
59+
setTitleId(getId())
60+
}, [])
61+
62+
const styles = useStyle({
63+
generateStyle,
64+
themeOverride,
65+
params: {
66+
size,
67+
variant,
68+
margin
69+
},
70+
componentId: 'Spinner',
71+
displayName: 'Spinner'
72+
})
73+
74+
useEffect(() => {
8675
if (delay) {
87-
this.delayTimeout = setTimeout(() => {
88-
this.setState({ shouldRender: true })
76+
const delayTimeout = setTimeout(() => {
77+
setShouldRender(true)
8978
}, delay)
90-
}
91-
}
92-
93-
componentDidUpdate() {
94-
this.props.makeStyles?.()
95-
}
96-
97-
componentWillUnmount() {
98-
clearTimeout(this.delayTimeout)
99-
}
10079

101-
radius() {
102-
switch (this.props.size) {
103-
case 'x-small':
104-
return '0.5em'
105-
case 'small':
106-
return '1em'
107-
case 'large':
108-
return '2.25em'
109-
default:
110-
return '1.75em'
80+
return () => clearTimeout(delayTimeout)
11181
}
112-
}
113-
114-
renderSpinner() {
115-
const passthroughProps = View.omitViewProps(
116-
omitProps(this.props, Spinner.allowedProps),
117-
Spinner
118-
)
82+
return undefined
83+
}, [delay])
11984

120-
const hasTitle = this.props.renderTitle
85+
const renderSpinner = () => {
12186
error(
122-
!!hasTitle,
123-
'[Spinner] The renderTitle prop is necessary for screen reader support.'
87+
!!renderTitle,
88+
'[Spinner] The `renderTitle` prop is necessary for screen reader support.'
12489
)
12590

91+
const passthroughProps = omitProps(props, allowedProps)
92+
12693
return (
127-
<View
94+
<div
12895
{...passthroughProps}
129-
as={this.props.as}
130-
elementRef={this.handleRef}
131-
css={this.props.styles?.spinner}
132-
margin={this.props.margin}
96+
css={styles?.spinner}
97+
ref={ref}
13398
data-cid="Spinner"
13499
>
135100
<svg
136-
css={this.props.styles?.circle}
101+
css={styles?.circle}
137102
role="img"
138-
aria-labelledby={this.titleId}
103+
aria-labelledby={titleId}
139104
focusable="false"
140105
>
141-
<title id={this.titleId}>
142-
{callRenderProp(this.props.renderTitle)}
143-
</title>
106+
<title id={titleId}>{callRenderProp(renderTitle)}</title>
144107
<g role="presentation">
145-
{this.props.variant !== 'inverse' && (
108+
{variant !== 'inverse' && (
146109
<circle
147-
css={this.props.styles?.circleTrack}
110+
css={styles?.circleTrack}
148111
cx="50%"
149112
cy="50%"
150-
r={this.radius()}
113+
r={styles?.radius as string}
151114
/>
152115
)}
153116
<circle
154-
css={this.props.styles?.circleSpin}
117+
css={styles?.circleSpin}
155118
cx="50%"
156119
cy="50%"
157-
r={this.radius()}
120+
r={styles?.radius as string}
158121
/>
159122
</g>
160123
</svg>
161-
</View>
124+
</div>
162125
)
163126
}
164127

165-
render() {
166-
return this.state.shouldRender ? this.renderSpinner() : null
167-
}
168-
}
128+
return shouldRender ? renderSpinner() : null
129+
})
130+
131+
Spinner.displayName = 'Spinner'
169132

170133
export default Spinner
171134
export { Spinner }

0 commit comments

Comments
 (0)