From dc3e8c1e87d71add38e57df8a0bb1aceb3971643 Mon Sep 17 00:00:00 2001 From: Johannes Korch <153843178+johannkor@users.noreply.github.com> Date: Thu, 28 Dec 2023 16:36:24 +0100 Subject: [PATCH 1/3] refactor: fix debounce function typings --- src/utils/debounce.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts index 45f02826..4a9002ad 100644 --- a/src/utils/debounce.ts +++ b/src/utils/debounce.ts @@ -5,10 +5,14 @@ * @param { number } wait Time to wait before execut the function * @param { boolean } immediate Param to define if the function will be executed immediately */ -const debounce = (func: (...args: any[]) => void, wait?: number, immediate?: boolean) => { +const debounce = ( + func: (...args: A) => void, + wait?: number, + immediate?: boolean, +) => { let timeout: NodeJS.Timeout | null = null - return function debounced(this: typeof func, ...args: any[]) { + return function debounced(this: T, ...args: A): void { const later = () => { timeout = null if (!immediate) { From 54bc0cb0e236189e6cf062748137520351acba08 Mon Sep 17 00:00:00 2001 From: Johannes Korch <153843178+johannkor@users.noreply.github.com> Date: Thu, 28 Dec 2023 16:49:58 +0100 Subject: [PATCH 2/3] fix: debounce prevented tooltip from closing There was an issue where the tooltip would stay open after the cursor had already left the anchor element. The issue would arise if: - there are two tooltip anchors A and B (same tooltip ID) - mouse goes over A to show the tooltip, user waits for tooltip to show - mouse quickly (within 50ms) leaves A, goes over B, then leaves B. The tooltip component applies debouncing on the show and hide events in case e.g. an anchor is simultaneously focused and mouseovered. If the second leave event occurred while that debounce was still active, it was never processed. This commit fixes the issue by making the show and hide debounce functions reset each other. For example, if the hide debounce has an active timeout, calling the debounced show function will now reset the timeout of the hide debounce. This solution still leaves the hide/show debounces effective against double-show/double-hide. --- src/components/Tooltip/Tooltip.tsx | 18 ++++++++++++++++-- src/utils/debounce.ts | 12 +++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index d42f5441..e4f78c67 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -372,8 +372,22 @@ const Tooltip = ({ // debounce handler to prevent call twice when // mouse enter and focus events being triggered toggether - const debouncedHandleShowTooltip = debounce(handleShowTooltip, 50, true) - const debouncedHandleHideTooltip = debounce(handleHideTooltip, 50, true) + const internalDebouncedHandleShowTooltip = debounce(handleShowTooltip, 50, true) + const internalDebouncedHandleHideTooltip = debounce(handleHideTooltip, 50, true) + // If either of the functions is called while the other is still debounced, + // reset the timeout. Otherwise if there is a sub-50ms (leave A, enter B, leave B) + // sequence of events, the tooltip will stay open because the hide debounce + // from leave A prevented the leave B event from calling it, leaving the + // tooltip visible. + const debouncedHandleShowTooltip = (e?: Event) => { + internalDebouncedHandleHideTooltip.reset() + internalDebouncedHandleShowTooltip(e) + } + const debouncedHandleHideTooltip = () => { + internalDebouncedHandleShowTooltip.reset() + internalDebouncedHandleHideTooltip() + } + const updateTooltipPosition = useCallback(() => { const actualPosition = imperativeOptions?.position ?? position if (actualPosition) { diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts index 4a9002ad..bdd0b4e2 100644 --- a/src/utils/debounce.ts +++ b/src/utils/debounce.ts @@ -12,7 +12,7 @@ const debounce = ( ) => { let timeout: NodeJS.Timeout | null = null - return function debounced(this: T, ...args: A): void { + const debounced = function debounced(this: T, ...args: A): void { const later = () => { timeout = null if (!immediate) { @@ -36,6 +36,16 @@ const debounce = ( timeout = setTimeout(later, wait) } } + + debounced.reset = () => { + if (!timeout) { + return + } + clearTimeout(timeout) + timeout = null + } + + return debounced } export default debounce From 235503e098cf607b90fb59712ed7b485ad1459a0 Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Tue, 2 Jan 2024 14:06:16 -0300 Subject: [PATCH 3/3] refactor: use same name as lodash for debounce cancel --- src/components/Tooltip/Tooltip.tsx | 4 ++-- src/utils/debounce.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index e4f78c67..4b6a9a06 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -380,11 +380,11 @@ const Tooltip = ({ // from leave A prevented the leave B event from calling it, leaving the // tooltip visible. const debouncedHandleShowTooltip = (e?: Event) => { - internalDebouncedHandleHideTooltip.reset() + internalDebouncedHandleHideTooltip.cancel() internalDebouncedHandleShowTooltip(e) } const debouncedHandleHideTooltip = () => { - internalDebouncedHandleShowTooltip.reset() + internalDebouncedHandleShowTooltip.cancel() internalDebouncedHandleHideTooltip() } diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts index bdd0b4e2..36d7deb4 100644 --- a/src/utils/debounce.ts +++ b/src/utils/debounce.ts @@ -37,7 +37,7 @@ const debounce = ( } } - debounced.reset = () => { + debounced.cancel = () => { if (!timeout) { return }