-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- In some cases, the API disconnects and the reconnects almost immediately afterwards, causing the "Reconnecting..." modal to appear and be distracting - Rather than have the API lie about whether it's connected or not, UI debounces when it displays the modal, just blocking interaction with a transparent modal for the duration of the disconnection if it reconnects immediately - Also added logging so we can see when connected/disconnected, in case this is further observed - Tested using the steps in #1149 , and also manually setting the `debounceMs` to `5000` to be able to react to it --------- Co-authored-by: Matthew Runyon <mattrunyonstuff@gmail.com>
- Loading branch information
1 parent
f440eb9
commit 9358e41
Showing
8 changed files
with
272 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import React from 'react'; | ||
import { act, render, screen } from '@testing-library/react'; | ||
import DebouncedModal from './DebouncedModal'; | ||
import Modal from './Modal'; | ||
|
||
const mockChildText = 'Mock Child'; | ||
const children = ( | ||
<Modal> | ||
<div>{mockChildText}</div> | ||
</Modal> | ||
); | ||
const DEFAULT_DEBOUNCE_MS = 250; | ||
|
||
beforeAll(() => { | ||
jest.useFakeTimers(); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.useRealTimers(); | ||
}); | ||
|
||
describe('display modal after debounce', () => { | ||
it('should render the modal after the debounce time has passed', () => { | ||
const { rerender } = render( | ||
<DebouncedModal isOpen={false} debounceMs={DEFAULT_DEBOUNCE_MS}> | ||
{children} | ||
</DebouncedModal> | ||
); | ||
expect( | ||
screen.queryByTestId('debounced-modal-backdrop') | ||
).not.toBeInTheDocument(); | ||
expect(screen.queryByText(mockChildText)).not.toBeInTheDocument(); | ||
|
||
act(() => { | ||
rerender( | ||
<DebouncedModal isOpen debounceMs={DEFAULT_DEBOUNCE_MS}> | ||
{children} | ||
</DebouncedModal> | ||
); | ||
}); | ||
expect( | ||
screen.queryByTestId('debounced-modal-backdrop') | ||
).toBeInTheDocument(); | ||
expect(screen.queryByText(mockChildText)).not.toBeInTheDocument(); | ||
|
||
act(() => { | ||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_MS); | ||
}); | ||
expect( | ||
screen.queryByTestId('debounced-modal-backdrop') | ||
).toBeInTheDocument(); | ||
expect(screen.queryByText(mockChildText)).toBeInTheDocument(); | ||
}); | ||
|
||
it('should not block interaction if set to false', () => { | ||
const { rerender } = render( | ||
<DebouncedModal | ||
isOpen={false} | ||
blockInteraction={false} | ||
debounceMs={DEFAULT_DEBOUNCE_MS} | ||
> | ||
{children} | ||
</DebouncedModal> | ||
); | ||
expect( | ||
screen.queryByTestId('debounced-modal-backdrop') | ||
).not.toBeInTheDocument(); | ||
expect(screen.queryByText(mockChildText)).not.toBeInTheDocument(); | ||
|
||
act(() => { | ||
rerender( | ||
<DebouncedModal | ||
isOpen | ||
blockInteraction={false} | ||
debounceMs={DEFAULT_DEBOUNCE_MS} | ||
> | ||
{children} | ||
</DebouncedModal> | ||
); | ||
}); | ||
expect( | ||
screen.queryByTestId('debounced-modal-backdrop') | ||
).not.toBeInTheDocument(); | ||
expect(screen.queryByText(mockChildText)).not.toBeInTheDocument(); | ||
|
||
act(() => { | ||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_MS + 5); | ||
}); | ||
expect( | ||
screen.queryByTestId('debounced-modal-backdrop') | ||
).not.toBeInTheDocument(); | ||
expect(screen.queryByText(mockChildText)).toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import React from 'react'; | ||
import { useDebouncedValue } from '@deephaven/react-hooks'; | ||
|
||
export type DebouncedModalProps = { | ||
/** Whether to block interaction immediately */ | ||
blockInteraction?: boolean; | ||
|
||
/** Children to render after the alloted debounce time */ | ||
children: React.ReactElement; | ||
|
||
/** Time to debounce */ | ||
debounceMs: number; | ||
|
||
/** | ||
* Will render the `children` `debounceMs` after `isOpen` is set to `true. | ||
* Will stop rendering immediately after `isOpen` is set to `false`. | ||
*/ | ||
isOpen?: boolean; | ||
}; | ||
|
||
/** | ||
* Display a modal after a debounce time. Blocks the screen from interaction immediately, | ||
* but then waits the set debounce time before rendering the modal. | ||
*/ | ||
function DebouncedModal({ | ||
blockInteraction = true, | ||
children, | ||
debounceMs, | ||
isOpen = false, | ||
}: DebouncedModalProps) { | ||
const debouncedIsOpen = useDebouncedValue(isOpen, debounceMs); | ||
|
||
return ( | ||
<> | ||
{blockInteraction && isOpen && ( | ||
<div | ||
className="modal-backdrop" | ||
style={{ backgroundColor: 'transparent' }} | ||
data-testid="debounced-modal-backdrop" | ||
/> | ||
)} | ||
{React.cloneElement(children, { isOpen: isOpen && debouncedIsOpen })} | ||
</> | ||
); | ||
} | ||
|
||
export default DebouncedModal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { act, renderHook } from '@testing-library/react-hooks'; | ||
import useDebouncedValue from './useDebouncedValue'; | ||
|
||
const DEFAULT_DEBOUNCE_MS = 100; | ||
beforeEach(() => { | ||
jest.useFakeTimers(); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.useRealTimers(); | ||
}); | ||
|
||
it('should return the initial value', () => { | ||
const value = 'mock value'; | ||
const { result } = renderHook(() => | ||
useDebouncedValue(value, DEFAULT_DEBOUNCE_MS) | ||
); | ||
expect(result.current).toBe(value); | ||
}); | ||
|
||
it('should return the initial value after the debounce time has elapsed', () => { | ||
const value = 'mock value'; | ||
const { result, rerender } = renderHook(() => | ||
useDebouncedValue(value, DEFAULT_DEBOUNCE_MS) | ||
); | ||
expect(result.current).toBe(value); | ||
expect(result.all.length).toBe(1); | ||
rerender(); | ||
act(() => { | ||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_MS); | ||
}); | ||
expect(result.current).toBe(value); | ||
expect(result.all.length).toBe(2); | ||
}); | ||
|
||
it('should return the updated value after the debounce time has elapsed', () => { | ||
const value = 'mock value'; | ||
const newValue = 'mock new value'; | ||
const { result, rerender } = renderHook((val = value) => | ||
useDebouncedValue(val, DEFAULT_DEBOUNCE_MS) | ||
); | ||
expect(result.current).toBe(value); | ||
rerender(newValue); | ||
act(() => { | ||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_MS); | ||
}); | ||
expect(result.current).toBe(newValue); | ||
}); | ||
|
||
it('should not return an intermediate value if the debounce time has not elapsed', () => { | ||
const value = 'mock value'; | ||
const intermediateValue = 'mock intermediate value'; | ||
const newValue = 'mock new value'; | ||
const { result, rerender } = renderHook((val = value) => | ||
useDebouncedValue(val, DEFAULT_DEBOUNCE_MS) | ||
); | ||
expect(result.current).toBe(value); | ||
rerender(intermediateValue); | ||
act(() => { | ||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_MS - 5); | ||
}); | ||
expect(result.current).toBe(value); | ||
rerender(newValue); | ||
act(() => { | ||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_MS - 5); | ||
}); | ||
expect(result.current).toBe(value); | ||
act(() => { | ||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_MS); | ||
}); | ||
expect(result.current).toBe(newValue); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { useEffect, useState } from 'react'; | ||
|
||
/** | ||
* Debounces a value. | ||
* Returns the initial value immediately. | ||
* Returns the latest value after no changes have occurred for the debounce duration. | ||
* @param value Value to debounce | ||
* @param debounceMs Amount of time to debounce | ||
* @returns The debounced value | ||
*/ | ||
export function useDebouncedValue<T>(value: T, debounceMs: number) { | ||
const [debouncedValue, setDebouncedValue] = useState<T>(value); | ||
useEffect(() => { | ||
const timeoutId = setTimeout(() => { | ||
setDebouncedValue(value); | ||
}, debounceMs); | ||
return () => { | ||
clearTimeout(timeoutId); | ||
}; | ||
}, [value, debounceMs]); | ||
|
||
return debouncedValue; | ||
} | ||
|
||
export default useDebouncedValue; |