diff --git a/UNRELEASED.md b/UNRELEASED.md index dbf090d6376..7f799cb9334 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -47,5 +47,8 @@ - Changed `aria-labelledby` to always exist on `TextField` ([#2401](https://github.com/Shopify/polaris-react/pull/2401)) - Converted `ButtonGroup > Item` into a functional component ([#2441](https://github.com/Shopify/polaris-react/pull/2441)) - Refactored BulkActions to make use of `ButtonGroup` ([#2441](https://github.com/Shopify/polaris-react/pull/2441)) +- Migrated `Loading` to use hooks ([#2303](https://github.com/Shopify/polaris-react/pull/2303)) + +### Deprecations ### Deprecations diff --git a/src/components/Frame/components/Loading/Loading.tsx b/src/components/Frame/components/Loading/Loading.tsx index 736c33b7b0a..ad2ef642c46 100644 --- a/src/components/Frame/components/Loading/Loading.tsx +++ b/src/components/Frame/components/Loading/Loading.tsx @@ -1,78 +1,57 @@ -import React from 'react'; -import debounce from 'lodash/debounce'; +import React, {useEffect, useState} from 'react'; import styles from './Loading.scss'; -export interface LoadingProps {} - -interface State { - progress: number; - step: number; - animation: number | null; -} - const INITIAL_STEP = 10; const STUCK_THRESHOLD = 99; -export class Loading extends React.Component { - state: State = { - progress: 0, - step: INITIAL_STEP, - animation: null, - }; +export function Loading() { + const [progress, setProgress] = useState(0.1); - private ariaValuenow = debounce(() => { - const {progress} = this.state; - return Math.floor(progress / 10) * 10; - }, 15); + useEffect(() => { + let animation: number; + let step = INITIAL_STEP; + let currentProgress = 0; + let previousProgress = 0; - componentDidMount() { - this.increment(); - } + const increment = () => { + if (currentProgress >= STUCK_THRESHOLD) { + return; + } - componentWillUnmount() { - const {animation} = this.state; + currentProgress = Math.min(currentProgress + step, 100); + const nextProgress = Math.floor(currentProgress) / 100; - if (animation != null) { - cancelAnimationFrame(animation); - } - } + if (nextProgress !== previousProgress) { + setProgress(nextProgress); + previousProgress = nextProgress; + } - render() { - const {progress} = this.state; + step = INITIAL_STEP ** -(currentProgress / 25); - const customStyles = { - transform: `scaleX(${Math.floor(progress) / 100})`, + animation = requestAnimationFrame(increment); }; - const ariaValuenow = this.ariaValuenow(); + increment(); - return ( -
-
-
- ); - } - - private increment() { - const {progress, step} = this.state; - - if (progress >= STUCK_THRESHOLD) { - return; - } + return () => { + cancelAnimationFrame(animation); + }; + }, []); - const animation = requestAnimationFrame(() => this.increment()); + const customStyles = { + transform: `scaleX(${progress})`, + }; - this.setState({ - progress: Math.min(progress + step, 100), - step: INITIAL_STEP ** -(progress / 25), - animation, - }); - } + return ( +
+
+
+ ); } diff --git a/src/components/Frame/components/Loading/index.ts b/src/components/Frame/components/Loading/index.ts index 2d49cfe791f..b32e0f6f8fd 100644 --- a/src/components/Frame/components/Loading/index.ts +++ b/src/components/Frame/components/Loading/index.ts @@ -1 +1 @@ -export {Loading, LoadingProps} from './Loading'; +export {Loading} from './Loading'; diff --git a/src/components/Frame/components/Loading/tests/Loading.test.tsx b/src/components/Frame/components/Loading/tests/Loading.test.tsx index a1b76e458d6..2136b229a9e 100644 --- a/src/components/Frame/components/Loading/tests/Loading.test.tsx +++ b/src/components/Frame/components/Loading/tests/Loading.test.tsx @@ -1,17 +1,38 @@ import React from 'react'; import {mountWithAppProvider} from 'test-utilities/legacy'; +import {animationFrame} from '@shopify/jest-dom-mocks'; +import {act} from 'react-dom/test-utils'; import {Loading} from '../Loading'; describe('', () => { - const loading = mountWithAppProvider(); + beforeEach(() => { + animationFrame.mock(); + }); + + afterEach(() => { + animationFrame.restore(); + }); + + it('increases over time', () => { + const loading = mountWithAppProvider(); - it('mounts', () => { - expect(loading.exists()).toBe(true); + for (let i = 0; i <= 100; i++) { + act(() => animationFrame.runFrame()); + } + + loading.update(); + + expect(loading.find('.Level').prop('aria-valuenow')).toBe(26); }); - it('unmounts safely', () => { + it('cancels the animationFrame on unmount', () => { + const cancelAnimationFrameSpy = jest.spyOn(window, 'cancelAnimationFrame'); + const loading = mountWithAppProvider(); + expect(() => { loading.unmount(); }).not.toThrow(); + + expect(cancelAnimationFrameSpy).toHaveBeenCalledTimes(1); }); }); diff --git a/src/components/Frame/components/index.ts b/src/components/Frame/components/index.ts index 507163e4956..143ccf2ee07 100644 --- a/src/components/Frame/components/index.ts +++ b/src/components/Frame/components/index.ts @@ -6,7 +6,7 @@ export { export {ToastManager, ToastManagerProps} from './ToastManager'; -export {Loading, LoadingProps} from './Loading'; +export {Loading} from './Loading'; export {ContextualSaveBar} from './ContextualSaveBar';