diff --git a/packages/ui-spinner/package.json b/packages/ui-spinner/package.json index 374b9a7032..688c7c1830 100644 --- a/packages/ui-spinner/package.json +++ b/packages/ui-spinner/package.json @@ -39,7 +39,9 @@ "@instructure/ui-babel-preset": "8.45.0", "@instructure/ui-test-locator": "8.45.0", "@instructure/ui-test-utils": "8.45.0", - "@instructure/ui-themes": "8.45.0" + "@instructure/ui-themes": "8.45.0", + "@testing-library/react": "^14.0.0", + "@testing-library/jest-dom": "^5.17.0" }, "peerDependencies": { "react": ">=16.8 <=18" diff --git a/packages/ui-spinner/src/Spinner/README.md b/packages/ui-spinner/src/Spinner/README.md index bffab7ec6e..94849a7fee 100644 --- a/packages/ui-spinner/src/Spinner/README.md +++ b/packages/ui-spinner/src/Spinner/README.md @@ -12,7 +12,7 @@ The `size` prop allows you to select from `x-small`, `small`, `medium` and `larg example: true ---
- + @@ -33,6 +33,22 @@ example: true ``` +### Delay rendering + +The `delay` prop allows you to delay the rendering of the spinner a desired time to prevent flickering in cases of very fast load times. + +```js +--- +example: true +--- +
+ + + + +
+``` + ### Screen reader support The `renderTitle` prop is read to screen readers. diff --git a/packages/ui-spinner/src/Spinner/__new-tests__/Spinner.test.tsx b/packages/ui-spinner/src/Spinner/__new-tests__/Spinner.test.tsx new file mode 100644 index 0000000000..3fcde37c88 --- /dev/null +++ b/packages/ui-spinner/src/Spinner/__new-tests__/Spinner.test.tsx @@ -0,0 +1,52 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React from 'react' +import { render, waitFor } from '@testing-library/react' + +import '@testing-library/jest-dom/extend-expect' +import Spinner from '../index' + +describe('', () => { + describe('with the delay prop', () => { + it('should delay rendering', async () => { + const { container } = render() + + const spinnerElements = container.querySelectorAll('[data-cid="Spinner"]') + + expect(spinnerElements.length).toBe(0) + + await waitFor( + () => { + const spinnerElements = container.querySelectorAll( + '[data-cid="Spinner"]' + ) + expect(spinnerElements.length).toBe(1) + }, + { + timeout: 400 + } + ) + }) + }) +}) diff --git a/packages/ui-spinner/src/Spinner/index.tsx b/packages/ui-spinner/src/Spinner/index.tsx index 13f216e058..67814927b2 100644 --- a/packages/ui-spinner/src/Spinner/index.tsx +++ b/packages/ui-spinner/src/Spinner/index.tsx @@ -37,7 +37,7 @@ import { withStyle, jsx } from '@instructure/emotion' import generateStyle from './styles' import generateComponentTheme from './theme' -import type { SpinnerProps } from './props' +import type { SpinnerProps, SpinnerState } from './props' import { allowedProps, propTypes } from './props' /** @@ -49,7 +49,7 @@ category: components @withDeterministicId() @withStyle(generateStyle, generateComponentTheme) @testable() -class Spinner extends Component { +class Spinner extends Component { static readonly componentId = 'Spinner' static allowedProps = allowedProps static propTypes = propTypes @@ -61,6 +61,7 @@ class Spinner extends Component { ref: Element | null = null private readonly titleId?: string + private delayTimeout?: NodeJS.Timeout handleRef = (el: Element | null) => { const { elementRef } = this.props @@ -76,16 +77,31 @@ class Spinner extends Component { super(props) this.titleId = props.deterministicId!() + + this.state = { + shouldRender: !props.delay + } } componentDidMount() { this.props.makeStyles?.() + const { delay } = this.props + + if (delay) { + this.delayTimeout = setTimeout(() => { + this.setState({ shouldRender: true }) + }, delay) + } } componentDidUpdate() { this.props.makeStyles?.() } + componentWillUnmount() { + clearTimeout(this.delayTimeout) + } + radius() { switch (this.props.size) { case 'x-small': @@ -99,7 +115,7 @@ class Spinner extends Component { } } - render() { + renderSpinner() { const passthroughProps = View.omitViewProps( omitProps(this.props, Spinner.allowedProps), Spinner @@ -148,6 +164,10 @@ class Spinner extends Component { ) } + + render() { + return this.state.shouldRender ? this.renderSpinner() : null + } } export default Spinner diff --git a/packages/ui-spinner/src/Spinner/props.ts b/packages/ui-spinner/src/Spinner/props.ts index ff4ff729ee..277faa5168 100644 --- a/packages/ui-spinner/src/Spinner/props.ts +++ b/packages/ui-spinner/src/Spinner/props.ts @@ -41,17 +41,17 @@ import { Renderable } from '@instructure/shared-types' type SpinnerOwnProps = { /** - * Give the spinner a title to be read by screenreaders + * Render Spinner "as" another HTML element */ - renderTitle?: Renderable + as?: AsElementType /** - * Different-sized spinners + * delay spinner rendering for a time (in ms). Used to prevent flickering in case of very fast load times */ - size?: 'x-small' | 'small' | 'medium' | 'large' + delay?: number /** - * Different color schemes for use with light or dark backgrounds + * provides a reference to the underlying html root element */ - variant?: 'default' | 'inverse' + elementRef?: (element: Element | null) => void /** * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`, * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via @@ -59,10 +59,17 @@ type SpinnerOwnProps = { */ margin?: Spacing /** - * provides a reference to the underlying html root element + * Give the spinner a title to be read by screenreaders */ - elementRef?: (element: Element | null) => void - as?: AsElementType + renderTitle?: Renderable + /** + * Different-sized spinners + */ + size?: 'x-small' | 'small' | 'medium' | 'large' + /** + * Different color schemes for use with light or dark backgrounds + */ + variant?: 'default' | 'inverse' } type PropKeys = keyof SpinnerOwnProps @@ -74,11 +81,16 @@ type SpinnerProps = SpinnerOwnProps & OtherHTMLAttributes & WithDeterministicIdProps +type SpinnerState = { + shouldRender: boolean +} + type SpinnerStyle = ComponentStyle< 'spinner' | 'circle' | 'circleTrack' | 'circleSpin' > const propTypes: PropValidators = { + delay: PropTypes.number, renderTitle: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), size: PropTypes.oneOf(['x-small', 'small', 'medium', 'large']), variant: PropTypes.oneOf(['default', 'inverse']), @@ -88,6 +100,7 @@ const propTypes: PropValidators = { } const allowedProps: AllowedPropKeys = [ + 'delay', 'renderTitle', 'size', 'variant', @@ -96,5 +109,5 @@ const allowedProps: AllowedPropKeys = [ 'as' ] -export type { SpinnerProps, SpinnerStyle } +export type { SpinnerProps, SpinnerState, SpinnerStyle } export { propTypes, allowedProps }