diff --git a/app/src/components/PrepareModules/index.js b/app/src/components/PrepareModules/index.js index 1cb21058090a..b00bbcecf06b 100644 --- a/app/src/components/PrepareModules/index.js +++ b/app/src/components/PrepareModules/index.js @@ -1,8 +1,12 @@ // @flow import * as React from 'react' import { useDispatch } from 'react-redux' -import some from 'lodash/some' -import { PrimaryButton, AlertModal, Icon } from '@opentrons/components' +import { + useTimeout, + PrimaryButton, + AlertModal, + Icon, +} from '@opentrons/components' import { sendModuleCommand } from '../../robot-api' import DeckMap from '../DeckMap' @@ -21,6 +25,16 @@ function PrepareModules(props: Props) { const dispatch = useDispatch() const [isHandling, setIsHandling] = React.useState(false) + // NOTE: this is the smarter implementation of isHandling that + // relies on the TC reporting its 'in_between' status while the lid m + // motor is moving, which currently doesn't happen because of a FW limitation + // const isHandling = some( + // modules, + // mod => mod.name === 'thermocycler' && mod.data?.lid === 'in_between' + // ) + + useTimeout(() => setIsHandling(false), isHandling ? LID_OPEN_DELAY_MS : null) + const handleOpenLidClick = () => { modules .filter(mod => mod.name === 'thermocycler') @@ -32,7 +46,6 @@ function PrepareModules(props: Props) { ) ) setIsHandling(true) - setTimeout(() => setIsHandling(false), LID_OPEN_DELAY_MS) } return ( diff --git a/components/src/hooks/__tests__/useInterval.test.js b/components/src/hooks/__tests__/useInterval.test.js index 1715d35e677d..3a35d032fece 100644 --- a/components/src/hooks/__tests__/useInterval.test.js +++ b/components/src/hooks/__tests__/useInterval.test.js @@ -3,7 +3,7 @@ import * as React from 'react' import { mount } from 'enzyme' import { useInterval } from '..' -describe('usePrevious hook', () => { +describe('useInterval hook', () => { const callback = jest.fn() beforeEach(() => { diff --git a/components/src/hooks/__tests__/useTimeout.test.js b/components/src/hooks/__tests__/useTimeout.test.js new file mode 100644 index 000000000000..9cd7b3446f06 --- /dev/null +++ b/components/src/hooks/__tests__/useTimeout.test.js @@ -0,0 +1,51 @@ +// @flow +import * as React from 'react' +import { mount } from 'enzyme' +import { useTimeout } from '..' + +describe('useTimeouthook', () => { + const callback = jest.fn() + + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.resetAllMocks() + jest.clearAllTimers() + jest.useRealTimers() + }) + + const TestUseTimeout = (props: { delay: number | null }) => { + useTimeout(callback, props.delay) + return + } + + test('delay `null` results in no calls', () => { + mount() + jest.runTimersToTime(10000) + + expect(callback).toHaveBeenCalledTimes(0) + }) + + test('delay sets a timeout', () => { + mount() + jest.runTimersToTime(3) + + expect(callback).toHaveBeenCalledTimes(1) + }) + + test('re-rendering with delay={null} clears the interval', () => { + const wrapper = mount() + + jest.runTimersToTime(2) + wrapper.setProps({ delay: null }) + + expect(callback).toHaveBeenCalledTimes(0) + + wrapper.setProps({ delay: 4 }) + jest.runTimersToTime(6) + + expect(callback).toHaveBeenCalledTimes(1) + }) +}) diff --git a/components/src/hooks/index.js b/components/src/hooks/index.js index 2f30b306f10b..278db5c0dae3 100644 --- a/components/src/hooks/index.js +++ b/components/src/hooks/index.js @@ -3,3 +3,4 @@ export * from './usePrevious' export * from './useInterval' +export * from './useTimeout' diff --git a/components/src/hooks/useTimeout.js b/components/src/hooks/useTimeout.js new file mode 100644 index 000000000000..dff188e13f0b --- /dev/null +++ b/components/src/hooks/useTimeout.js @@ -0,0 +1,30 @@ +// @flow +import { useEffect, useRef } from 'react' + +/** + * React hook to call a function on an interval; copied from: + * https://overreacted.io/making-setinterval-declarative-with-react-hooks/ + * + * @template T (type of the input value) + * @param {() => mixed} callback (function to call after timeout) + * @param {number | null} delay (timeout delay, or null to disable the timeout) + * @returns {void} + */ +export function useTimeout(callback: () => mixed, delay: number | null): void { + const savedCallback = useRef() + + // remember the latest callback + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + // set up the interval + useEffect(() => { + const currentCallback = () => + savedCallback.current && savedCallback.current() + if (delay !== null) { + const id = setTimeout(currentCallback, delay) + return () => clearTimeout(id) + } + }, [delay]) +}