From 802937635a3c879db803432f2377975063f369cf Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 17:21:54 +0000 Subject: [PATCH 01/17] chore: replace findKey --- src/SpatialNavigation.ts | 2 +- src/utils.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/utils.ts diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index b06c8a7..39087b8 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -2,7 +2,6 @@ import { DebouncedFunc } from 'lodash'; import debounce from 'lodash/debounce'; import difference from 'lodash/difference'; import filter from 'lodash/filter'; -import findKey from 'lodash/findKey'; import first from 'lodash/first'; import forEach from 'lodash/forEach'; import forOwn from 'lodash/forOwn'; @@ -11,6 +10,7 @@ import throttle from 'lodash/throttle'; import VisualDebugger from './VisualDebugger'; import WritingDirection from './WritingDirection'; import measureLayout, { getBoundingClientRect } from './measureLayout'; +import { findKey } from './utils'; const DIRECTION_LEFT = 'left'; const DIRECTION_RIGHT = 'right'; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..fb7786f --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,12 @@ +/* eslint-disable import/prefer-default-export */ + +interface FindKeyPredicate { + (value: T, key: string, obj: { [key: string]: T }): boolean; +} + +const findKey = ( + obj: { [key: string]: T }, + predicate: FindKeyPredicate = (o) => !!o +): string | undefined => Object.keys(obj).find((key) => predicate(obj[key], key, obj)); + +export { findKey } From b0829df6540b5ab00764c2fb891a5fe43fe94c47 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 17:24:42 +0000 Subject: [PATCH 02/17] chore: replace filter --- src/SpatialNavigation.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index 39087b8..d5d6634 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -1,7 +1,6 @@ import { DebouncedFunc } from 'lodash'; import debounce from 'lodash/debounce'; import difference from 'lodash/difference'; -import filter from 'lodash/filter'; import first from 'lodash/first'; import forEach from 'lodash/forEach'; import forOwn from 'lodash/forOwn'; @@ -1019,7 +1018,7 @@ class SpatialNavigationService { /** * Get only the siblings with the coords on the way of our moving direction */ - const siblings = filter(this.focusableComponents, (component) => { + const siblings = Object.values(this.focusableComponents).filter((component) => { if ( component.parentFocusKey === parentFocusKey && component.focusable @@ -1149,8 +1148,7 @@ class SpatialNavigationService { * A component closest to the top left viewport corner (0,0) is returned. */ getForcedFocusKey(): string | undefined { - const forceFocusableComponents = filter( - this.focusableComponents, + const forceFocusableComponents = Object.values(this.focusableComponents).filter( (component) => component.focusable && component.forceFocus ); @@ -1193,10 +1191,8 @@ class SpatialNavigationService { return targetFocusKey; } - const children = filter( - this.focusableComponents, - (component) => - component.parentFocusKey === targetFocusKey && component.focusable + const children = Object.values(this.focusableComponents).filter((component) => + component.parentFocusKey === targetFocusKey && component.focusable ); if (children.length > 0) { From 9a8cbcb733ed49a36e2ade07a3f08c51c78080e2 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 17:25:32 +0000 Subject: [PATCH 03/17] chore: replace first --- src/SpatialNavigation.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index d5d6634..b5b1a72 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -1,7 +1,6 @@ import { DebouncedFunc } from 'lodash'; import debounce from 'lodash/debounce'; import difference from 'lodash/difference'; -import first from 'lodash/first'; import forEach from 'lodash/forEach'; import forOwn from 'lodash/forOwn'; import sortBy from 'lodash/sortBy'; @@ -154,13 +153,13 @@ const getChildClosestToOrigin = ( const comparator = writingDirection === WritingDirection.LTR ? ({ layout }: FocusableComponent) => - Math.abs(layout.left) + Math.abs(layout.top) + Math.abs(layout.left) + Math.abs(layout.top) : ({ layout }: FocusableComponent) => - Math.abs(window.innerWidth - layout.right) + Math.abs(layout.top); + Math.abs(window.innerWidth - layout.right) + Math.abs(layout.top); const childrenClosestToOrigin = sortBy(children, comparator); - return first(childrenClosestToOrigin); + return childrenClosestToOrigin[0] }; /** @@ -1083,7 +1082,7 @@ class SpatialNavigationService { focusKey ); - const nextComponent = first(sortedSiblings); + const nextComponent = sortedSiblings[0]; this.log( 'smartNavigate', @@ -1173,7 +1172,7 @@ class SpatialNavigationService { ROOT_FOCUS_KEY ); - return first(sortedForceFocusableComponents)?.focusKey; + return sortedForceFocusableComponents[0]?.focusKey; } /** From 54d9b20333bd20dafd360efafe3b220e92594d32 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 17:26:36 +0000 Subject: [PATCH 04/17] chore: replace forOwn --- src/SpatialNavigation.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index b5b1a72..010020b 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -2,7 +2,6 @@ import { DebouncedFunc } from 'lodash'; import debounce from 'lodash/debounce'; import difference from 'lodash/difference'; import forEach from 'lodash/forEach'; -import forOwn from 'lodash/forOwn'; import sortBy from 'lodash/sortBy'; import throttle from 'lodash/throttle'; import VisualDebugger from './VisualDebugger'; @@ -693,7 +692,8 @@ class SpatialNavigationService { const draw = () => { requestAnimationFrame(() => { this.visualDebugger.clearLayouts(); - forOwn(this.focusableComponents, (component, focusKey) => { + Object.keys(this.focusableComponents).forEach((focusKey) => { + const component = this.focusableComponents[focusKey]; this.visualDebugger.drawLayout( component.layout, focusKey, @@ -975,7 +975,7 @@ class SpatialNavigationService { this.log('smartNavigate', 'this.focusKey', this.focusKey); if (!fromParentFocusKey) { - forOwn(this.focusableComponents, (component) => { + Object.values(this.focusableComponents).forEach((component) => { // eslint-disable-next-line no-param-reassign component.layoutUpdated = false; }); @@ -1634,7 +1634,7 @@ class SpatialNavigationService { return; } - forOwn(this.focusableComponents, (component, focusKey) => { + Object.keys(this.focusableComponents).forEach((focusKey) => { this.updateLayout(focusKey); }); } From efc0133dc6cf5c0a3da4326c761af99c619e913e Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 17:27:13 +0000 Subject: [PATCH 05/17] chore: replace forEach --- src/SpatialNavigation.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index 010020b..13240d2 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -1,7 +1,6 @@ import { DebouncedFunc } from 'lodash'; import debounce from 'lodash/debounce'; import difference from 'lodash/difference'; -import forEach from 'lodash/forEach'; import sortBy from 'lodash/sortBy'; import throttle from 'lodash/throttle'; import VisualDebugger from './VisualDebugger'; @@ -1499,7 +1498,7 @@ class SpatialNavigationService { this.parentsHavingFocusedChild ); - forEach(parentsToRemoveFlag, (parentFocusKey) => { + parentsToRemoveFlag.forEach((parentFocusKey) => { const parentComponent = this.focusableComponents[parentFocusKey]; if (parentComponent && parentComponent.trackChildren) { @@ -1508,7 +1507,7 @@ class SpatialNavigationService { this.onIntermediateNodeBecameBlurred(parentFocusKey, focusDetails); }); - forEach(parentsToAddFlag, (parentFocusKey) => { + parentsToAddFlag.forEach((parentFocusKey) => { const parentComponent = this.focusableComponents[parentFocusKey]; if (parentComponent && parentComponent.trackChildren) { From 74a8e2649a113e164138e0a5ce304b6ee6a22bf3 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 17:32:43 +0000 Subject: [PATCH 06/17] chore: replace difference --- src/SpatialNavigation.ts | 21 ++++++++++----------- src/utils.ts | 11 ++++++++++- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index 13240d2..4f4c1c1 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -1,12 +1,11 @@ import { DebouncedFunc } from 'lodash'; import debounce from 'lodash/debounce'; -import difference from 'lodash/difference'; import sortBy from 'lodash/sortBy'; import throttle from 'lodash/throttle'; import VisualDebugger from './VisualDebugger'; import WritingDirection from './WritingDirection'; import measureLayout, { getBoundingClientRect } from './measureLayout'; -import { findKey } from './utils'; +import { difference, findKey } from './utils'; const DIRECTION_LEFT = 'left'; const DIRECTION_RIGHT = 'right'; @@ -174,6 +173,7 @@ const normalizeKeyMap = (keyMap: BackwardsCompatibleKeyMap) => { return newKeyMap; }; + class SpatialNavigationService { private focusableComponents: { [index: string]: FocusableComponent }; @@ -268,22 +268,22 @@ class SpatialNavigationService { const itemStart = isVertical ? layout.top : writingDirection === WritingDirection.LTR - ? layout.left - : layout.right; + ? layout.left + : layout.right; const itemEnd = isVertical ? layout.bottom : writingDirection === WritingDirection.LTR - ? layout.right - : layout.left; + ? layout.right + : layout.left; return isIncremental ? isSibling ? itemStart : itemEnd : isSibling - ? itemEnd - : itemStart; + ? itemEnd + : itemStart; } /** @@ -401,7 +401,7 @@ class SpatialNavigationService { const intersectionLength = Math.max( 0, Math.min(refCoordinateB, siblingCoordinateB) - - Math.max(refCoordinateA, siblingCoordinateA) + Math.max(refCoordinateA, siblingCoordinateA) ); return intersectionLength >= thresholdDistance; @@ -1125,8 +1125,7 @@ class SpatialNavigationService { // eslint-disable-next-line no-console console.log( `%c${functionName}%c${debugString}`, - `background: ${ - DEBUG_FN_COLORS[this.logIndex % DEBUG_FN_COLORS.length] + `background: ${DEBUG_FN_COLORS[this.logIndex % DEBUG_FN_COLORS.length] }; color: black; padding: 1px 5px;`, 'background: #333; color: #BADA55; padding: 1px 5px;', ...rest diff --git a/src/utils.ts b/src/utils.ts index fb7786f..9bf7879 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,4 +9,13 @@ const findKey = ( predicate: FindKeyPredicate = (o) => !!o ): string | undefined => Object.keys(obj).find((key) => predicate(obj[key], key, obj)); -export { findKey } +/** + * This function only works for string or numbers since Set checks for reference equality, + * meaning objects wouldn't work. + */ +const difference = (array: T[], ...values: T[][]): T[] => { + const exclusionSet = new Set(values.flat()); + return array.filter(item => !exclusionSet.has(item)); +} + +export { findKey, difference } From 20fe16c347d5d672b0bd6e8f57e9f760cfba57e4 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 17:50:16 +0000 Subject: [PATCH 07/17] chore: replace sortBy --- src/SpatialNavigation.ts | 106 ++++++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 35 deletions(-) diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index 4f4c1c1..0826f4c 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -1,6 +1,5 @@ import { DebouncedFunc } from 'lodash'; import debounce from 'lodash/debounce'; -import sortBy from 'lodash/sortBy'; import throttle from 'lodash/throttle'; import VisualDebugger from './VisualDebugger'; import WritingDirection from './WritingDirection'; @@ -147,14 +146,15 @@ const getChildClosestToOrigin = ( children: FocusableComponent[], writingDirection: WritingDirection ) => { - const comparator = - writingDirection === WritingDirection.LTR - ? ({ layout }: FocusableComponent) => - Math.abs(layout.left) + Math.abs(layout.top) - : ({ layout }: FocusableComponent) => - Math.abs(window.innerWidth - layout.right) + Math.abs(layout.top); + const comparator = writingDirection === WritingDirection.LTR + ? (a: FocusableComponent, b: FocusableComponent) => + (Math.abs(a.layout.left) + Math.abs(a.layout.top)) - + (Math.abs(b.layout.left) + Math.abs(b.layout.top)) + : (a: FocusableComponent, b: FocusableComponent) => + (Math.abs(window.innerWidth - a.layout.right) + Math.abs(a.layout.top)) - + (Math.abs(window.innerWidth - b.layout.right) + Math.abs(b.layout.top)); - const childrenClosestToOrigin = sortBy(children, comparator); + const childrenClosestToOrigin = children.sort(comparator); return childrenClosestToOrigin[0] }; @@ -505,37 +505,68 @@ class SpatialNavigationService { currentLayout ); - return sortBy(siblings, (sibling) => { - const siblingCorners = SpatialNavigationService.getRefCorners( + return siblings.sort((a, b) => { + const siblingCornersA = SpatialNavigationService.getRefCorners( direction, true, - sibling.layout + a.layout + ); + const siblingCornersB = SpatialNavigationService.getRefCorners( + direction, + true, + b.layout ); - const isAdjacentSlice = SpatialNavigationService.isAdjacentSlice( + const isAdjacentSliceA = SpatialNavigationService.isAdjacentSlice( refCorners, - siblingCorners, + siblingCornersA, + isVerticalDirection + ); + const isAdjacentSliceB = SpatialNavigationService.isAdjacentSlice( + refCorners, + siblingCornersB, isVerticalDirection ); - const primaryAxisFunction = isAdjacentSlice + const primaryAxisFunctionA = isAdjacentSliceA + ? SpatialNavigationService.getPrimaryAxisDistance + : SpatialNavigationService.getSecondaryAxisDistance; + const primaryAxisFunctionB = isAdjacentSliceB ? SpatialNavigationService.getPrimaryAxisDistance : SpatialNavigationService.getSecondaryAxisDistance; - const secondaryAxisFunction = isAdjacentSlice + const secondaryAxisFunctionA = isAdjacentSliceA + ? SpatialNavigationService.getSecondaryAxisDistance + : SpatialNavigationService.getPrimaryAxisDistance; + const secondaryAxisFunctionB = isAdjacentSliceB ? SpatialNavigationService.getSecondaryAxisDistance : SpatialNavigationService.getPrimaryAxisDistance; - const primaryAxisDistance = primaryAxisFunction( + const primaryAxisDistanceA = primaryAxisFunctionA( refCorners, - siblingCorners, + siblingCornersA, isVerticalDirection, this.distanceCalculationMethod, this.customDistanceCalculationFunction ); - const secondaryAxisDistance = secondaryAxisFunction( + const primaryAxisDistanceB = primaryAxisFunctionB( refCorners, - siblingCorners, + siblingCornersB, + isVerticalDirection, + this.distanceCalculationMethod, + this.customDistanceCalculationFunction + ); + + const secondaryAxisDistanceA = secondaryAxisFunctionA( + refCorners, + siblingCornersA, + isVerticalDirection, + this.distanceCalculationMethod, + this.customDistanceCalculationFunction + ); + const secondaryAxisDistanceB = secondaryAxisFunctionB( + refCorners, + siblingCornersB, isVerticalDirection, this.distanceCalculationMethod, this.customDistanceCalculationFunction @@ -544,46 +575,51 @@ class SpatialNavigationService { /** * The higher this value is, the less prioritised the candidate is */ - const totalDistancePoints = - primaryAxisDistance * MAIN_COORDINATE_WEIGHT + secondaryAxisDistance; + const totalDistancePointsA = + primaryAxisDistanceA * MAIN_COORDINATE_WEIGHT + secondaryAxisDistanceA; + const totalDistancePointsB = + primaryAxisDistanceB * MAIN_COORDINATE_WEIGHT + secondaryAxisDistanceB; /** * + 1 here is in case of distance is zero, but we still want to apply Adjacent priority weight */ - const priority = - (totalDistancePoints + 1) / - (isAdjacentSlice ? ADJACENT_SLICE_WEIGHT : DIAGONAL_SLICE_WEIGHT); + const priorityA = + (totalDistancePointsA + 1) / + (isAdjacentSliceA ? ADJACENT_SLICE_WEIGHT : DIAGONAL_SLICE_WEIGHT); + const priorityB = + (totalDistancePointsB + 1) / + (isAdjacentSliceB ? ADJACENT_SLICE_WEIGHT : DIAGONAL_SLICE_WEIGHT); this.log( 'smartNavigate', - `distance (primary, secondary, total weighted) for ${sibling.focusKey} relative to ${focusKey} is`, - primaryAxisDistance, - secondaryAxisDistance, - totalDistancePoints + `distance (primary, secondary, total weighted) for ${a.focusKey} relative to ${focusKey} is`, + primaryAxisDistanceA, + secondaryAxisDistanceA, + totalDistancePointsA ); this.log( 'smartNavigate', - `priority for ${sibling.focusKey} relative to ${focusKey} is`, - priority + `priority for ${a.focusKey} relative to ${focusKey} is`, + priorityA ); if (this.visualDebugger) { this.visualDebugger.drawPoint( - siblingCorners.a.x, - siblingCorners.a.y, + siblingCornersA.a.x, + siblingCornersA.a.y, 'yellow', 6 ); this.visualDebugger.drawPoint( - siblingCorners.b.x, - siblingCorners.b.y, + siblingCornersA.b.x, + siblingCornersA.b.y, 'yellow', 6 ); } - return priority; + return priorityA - priorityB; }); } From bc8cce95cc7b4b4432f444bb8df90b66531a3a97 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 18:05:30 +0000 Subject: [PATCH 08/17] chore: replace debounce --- src/SpatialNavigation.ts | 4 +--- src/utils.ts | 45 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index 0826f4c..afd474a 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -1,10 +1,8 @@ -import { DebouncedFunc } from 'lodash'; -import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle'; import VisualDebugger from './VisualDebugger'; import WritingDirection from './WritingDirection'; import measureLayout, { getBoundingClientRect } from './measureLayout'; -import { difference, findKey } from './utils'; +import { debounce, DebouncedFunc, difference, findKey } from './utils'; const DIRECTION_LEFT = 'left'; const DIRECTION_RIGHT = 'right'; diff --git a/src/utils.ts b/src/utils.ts index 9bf7879..36bd33f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -18,4 +18,47 @@ const difference = (array: T[], ...values: T[][]): T[ return array.filter(item => !exclusionSet.has(item)); } -export { findKey, difference } +type DebouncedFunc void> = { + (...args: Parameters): void; + cancel: () => void; +}; + +function debounce void>( + func: T, + wait: number, + { leading = false, trailing = true }: { leading?: boolean; trailing?: boolean } +): DebouncedFunc { + let timeoutId: ReturnType | null = null; + let lastArgs: Parameters | null = null; + + const invokeFunc = (args: Parameters) => { + func(...args); + }; + + const debounced = (...args: Parameters) => { + lastArgs = args; + + if (leading && !timeoutId) { + invokeFunc(args); // Call immediately on the first call + } + + if (timeoutId) clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + if (trailing && lastArgs) { + invokeFunc(lastArgs); // Call on trailing edge + } + timeoutId = null; + }, wait); + }; + + // Add the cancel method to clear any pending timeout + debounced.cancel = () => { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = null; + }; + + return debounced; +} + +export { findKey, difference, debounce, DebouncedFunc } From 44489d5498d58ff1142876639f6d16c32552fd39 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 18:05:53 +0000 Subject: [PATCH 09/17] chore: replace throttle --- src/SpatialNavigation.ts | 3 +-- src/utils.ts | 49 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index afd474a..ce96214 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -1,8 +1,7 @@ -import throttle from 'lodash/throttle'; import VisualDebugger from './VisualDebugger'; import WritingDirection from './WritingDirection'; import measureLayout, { getBoundingClientRect } from './measureLayout'; -import { debounce, DebouncedFunc, difference, findKey } from './utils'; +import { debounce, DebouncedFunc, difference, findKey, throttle } from './utils'; const DIRECTION_LEFT = 'left'; const DIRECTION_RIGHT = 'right'; diff --git a/src/utils.ts b/src/utils.ts index 36bd33f..db0cdcd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -61,4 +61,51 @@ function debounce void>( return debounced; } -export { findKey, difference, debounce, DebouncedFunc } +type ThrottleFunc void> = { + (...args: Parameters): void; + cancel: () => void; +}; + +const throttle = void>( + func: T, + wait: number, + { leading = true, trailing = true }: { leading?: boolean; trailing?: boolean } = {} +): ThrottleFunc => { + let timeoutId: ReturnType | null = null; + let lastExecuted = 0; + let lastArgs: Parameters | null = null; + + const invokeFunc = (args: Parameters) => { + func(...args); + lastExecuted = Date.now(); + }; + + const throttled: ThrottleFunc = (...args: Parameters) => { + const now = Date.now(); + const remainingTime = wait - (now - lastExecuted); + + if (remainingTime <= 0) { + if (leading) { + invokeFunc(args); // Call immediately if enough time has passed + } + } else if (!timeoutId && trailing) { + timeoutId = setTimeout(() => { + if (lastArgs) { + invokeFunc(lastArgs); // Call the function on trailing edge + } + timeoutId = null; + }, remainingTime); + } + + lastArgs = args; + }; + + throttled.cancel = () => { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = null; + }; + + return throttled; +}; + +export { findKey, difference, debounce, DebouncedFunc, throttle } From 052cdf4ce1129a4b38e32e38ff6bb7936fc5f1cb Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 18:19:43 +0000 Subject: [PATCH 10/17] chore: remove lodash --- package-lock.json | 9 ++++----- package.json | 3 --- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6fd45db..f9e54a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "@noriginmedia/norigin-spatial-navigation", "version": "2.2.1", "license": "MIT", - "dependencies": { - "lodash": "^4.17.21" - }, "devDependencies": { "@types/jest": "^29.5.12", "@types/lodash": "^4.14.179", @@ -6641,7 +6638,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -14868,7 +14866,8 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "lodash.memoize": { "version": "4.1.2", diff --git a/package.json b/package.json index e2d4a9d..8d66829 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,6 @@ "peerDependencies": { "react": ">=16.8.0" }, - "dependencies": { - "lodash": "^4.17.21" - }, "devDependencies": { "@types/jest": "^29.5.12", "@types/lodash": "^4.14.179", From ccb855de6cbdbb688f9f2c14e81ec037347b160d Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 18:20:57 +0000 Subject: [PATCH 11/17] chore: replace noop and randomid --- src/useFocusable.ts | 3 +-- src/utils.ts | 31 ++++++++++++++++++++----------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/useFocusable.ts b/src/useFocusable.ts index 9a7ba35..a6edc5e 100644 --- a/src/useFocusable.ts +++ b/src/useFocusable.ts @@ -6,8 +6,6 @@ import { useEffect, useState } from 'react'; -import noop from 'lodash/noop'; -import uniqueId from 'lodash/uniqueId'; import { SpatialNavigation, FocusableComponentLayout, @@ -16,6 +14,7 @@ import { Direction } from './SpatialNavigation'; import { useFocusContext } from './useFocusContext'; +import { noop, uniqueId } from './utils'; export type EnterPressHandler

= ( props: P, diff --git a/src/utils.ts b/src/utils.ts index db0cdcd..2fba67c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -13,21 +13,30 @@ const findKey = ( * This function only works for string or numbers since Set checks for reference equality, * meaning objects wouldn't work. */ -const difference = (array: T[], ...values: T[][]): T[] => { +const difference = (array: T[], ...values: T[][]): T[] => { const exclusionSet = new Set(values.flat()); return array.filter(item => !exclusionSet.has(item)); } +const noop: VoidFunction = () => null; + +let counter = 0; + +const uniqueId = (prefix: string = ''): string => { + counter += 1; + return `${prefix}${counter}`; +}; + type DebouncedFunc void> = { (...args: Parameters): void; cancel: () => void; }; -function debounce void>( +const debounce = void>( func: T, wait: number, { leading = false, trailing = true }: { leading?: boolean; trailing?: boolean } -): DebouncedFunc { +): DebouncedFunc => { let timeoutId: ReturnType | null = null; let lastArgs: Parameters | null = null; @@ -89,13 +98,13 @@ const throttle = void>( invokeFunc(args); // Call immediately if enough time has passed } } else if (!timeoutId && trailing) { - timeoutId = setTimeout(() => { - if (lastArgs) { - invokeFunc(lastArgs); // Call the function on trailing edge - } - timeoutId = null; - }, remainingTime); - } + timeoutId = setTimeout(() => { + if (lastArgs) { + invokeFunc(lastArgs); // Call the function on trailing edge + } + timeoutId = null; + }, remainingTime); + } lastArgs = args; }; @@ -108,4 +117,4 @@ const throttle = void>( return throttled; }; -export { findKey, difference, debounce, DebouncedFunc, throttle } +export { findKey, difference, debounce, DebouncedFunc, throttle, noop, uniqueId } From e24c8451ec599d1bfd7c13c83b967137ca860197 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 18:23:01 +0000 Subject: [PATCH 12/17] chore: remove shuffle --- src/App.tsx | 2 +- src/utils.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f2063e4..f543c42 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,6 @@ import React, { useCallback, useEffect, useState, useRef } from 'react'; import ReactDOMClient from 'react-dom/client'; // eslint-disable-next-line import/no-extraneous-dependencies import styled, { createGlobalStyle } from 'styled-components'; -import shuffle from 'lodash/shuffle'; import { useFocusable, init, @@ -17,6 +16,7 @@ import { FocusableComponentLayout, KeyPressDetails } from './index'; +import { shuffle } from './utils'; const logo = require('../logo.png').default; diff --git a/src/utils.ts b/src/utils.ts index 2fba67c..002d83d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -27,6 +27,18 @@ const uniqueId = (prefix: string = ''): string => { return `${prefix}${counter}`; }; +const shuffle = (array: T[]): T[] => { + const shuffledArray = [...array]; + + // Fisher-Yates (Knuth) shuffle algorithm + for (let i = shuffledArray.length - 1; i > 0; i -=1) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]; // Swap elements + } + + return shuffledArray; +}; + type DebouncedFunc void> = { (...args: Parameters): void; cancel: () => void; @@ -117,4 +129,4 @@ const throttle = void>( return throttled; }; -export { findKey, difference, debounce, DebouncedFunc, throttle, noop, uniqueId } +export { findKey, difference, debounce, DebouncedFunc, throttle, noop, uniqueId, shuffle } From 70b0dd35f795fcc4edd3b20a71beafe2037faa78 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 18:43:30 +0000 Subject: [PATCH 13/17] fix: throttle cancel --- src/utils.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 002d83d..08091bb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,7 +31,7 @@ const shuffle = (array: T[]): T[] => { const shuffledArray = [...array]; // Fisher-Yates (Knuth) shuffle algorithm - for (let i = shuffledArray.length - 1; i > 0; i -=1) { + for (let i = shuffledArray.length - 1; i > 0; i -= 1) { const j = Math.floor(Math.random() * (i + 1)); [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]; // Swap elements } @@ -102,31 +102,34 @@ const throttle = void>( }; const throttled: ThrottleFunc = (...args: Parameters) => { + lastArgs = args; const now = Date.now(); - const remainingTime = wait - (now - lastExecuted); + const timeSinceLastCall = now - lastExecuted; + const remainingTime = wait - timeSinceLastCall; if (remainingTime <= 0) { if (leading) { - invokeFunc(args); // Call immediately if enough time has passed + invokeFunc(args); } - } else if (!timeoutId && trailing) { + } else if (trailing && !timeoutId) { timeoutId = setTimeout(() => { if (lastArgs) { - invokeFunc(lastArgs); // Call the function on trailing edge + invokeFunc(lastArgs); } timeoutId = null; }, remainingTime); } - - lastArgs = args; }; throttled.cancel = () => { - if (timeoutId) clearTimeout(timeoutId); - timeoutId = null; + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + lastExecuted = 0; // Reset lastExecuted time to ensure it can be triggered again }; return throttled; -}; +} export { findKey, difference, debounce, DebouncedFunc, throttle, noop, uniqueId, shuffle } From d0fc800c6185ca48b4bcf1b92ebff7e8494a1e35 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 18:51:25 +0000 Subject: [PATCH 14/17] chore: remove lodash from webpack --- webpack.config.prod.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.prod.js b/webpack.config.prod.js index 069fea3..f14afee 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -16,7 +16,7 @@ module.exports = { resolve: { extensions: ['.tsx', '.ts', '.js', '.jsx'] }, - externals: ['react', /^lodash(\/.+)?$/], + externals: ['react'], output: { filename: 'index.js', path: path.resolve(__dirname, 'dist'), From 6d1f77e2e871b3b9b97ed98adb2339fa23c3a2aa Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 19 Feb 2025 18:53:56 +0000 Subject: [PATCH 15/17] chore: add source maps --- webpack.config.prod.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack.config.prod.js b/webpack.config.prod.js index f14afee..1fde932 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -4,6 +4,7 @@ module.exports = { mode: 'production', entry: './src/index.ts', target: ['web', 'es5'], + devtool: 'source-map', module: { rules: [ { From 8965f8b9ba3a65a61d72d6fc1c6232cc52397acc Mon Sep 17 00:00:00 2001 From: christianEconify Date: Thu, 20 Feb 2025 15:35:38 +0000 Subject: [PATCH 16/17] chore: add tests and align utils to lodash Wrote an indentical set of tests, one that ran against lodash utils and one that ran against our own utils. Made changes to our own utils to ensure that the correct outcome was obtained --- package-lock.json | 1 + package.json | 1 + src/__tests__/utils.lodash.test.ts | 193 +++++++++++++++++++++++++++++ src/__tests__/utils.test.ts | 192 ++++++++++++++++++++++++++++ src/utils.ts | 5 +- 5 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/utils.lodash.test.ts create mode 100644 src/__tests__/utils.test.ts diff --git a/package-lock.json b/package-lock.json index b77c8a0..47eec24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "html-webpack-plugin": "^5.5.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "lodash": "^4.17.21", "pre-commit": "^1.2.2", "prettier": "^2.5.1", "react": "^18.2.0", diff --git a/package.json b/package.json index def1f07..abf6e76 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "html-webpack-plugin": "^5.5.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "lodash": "^4.17.21", "pre-commit": "^1.2.2", "prettier": "^2.5.1", "react": "^18.2.0", diff --git a/src/__tests__/utils.lodash.test.ts b/src/__tests__/utils.lodash.test.ts new file mode 100644 index 0000000..ae9c092 --- /dev/null +++ b/src/__tests__/utils.lodash.test.ts @@ -0,0 +1,193 @@ +import { debounce, difference, findKey, noop, shuffle, throttle, uniqueId } from "lodash"; + +describe('lodash utils', () => { + describe('findKey', () => { + it('should find the key based on the predicate', () => { + const obj = { a: 1, b: 2, c: 3 }; + const predicate = (value: number) => value === 2; + expect(findKey(obj, predicate)).toBe('b'); + }); + + it('should return undefined if no key matches the predicate', () => { + const obj = { a: 1, b: 2, c: 3 }; + const predicate = (value: number) => value === 4; + expect(findKey(obj, predicate)).toBeUndefined(); + }); + }); + + describe('difference', () => { + it('should return the difference between arrays', () => { + expect(difference([1, 2, 3], [2, 3])).toEqual([1]); + expect(difference(['a', 'b', 'c'], ['b'])).toEqual(['a', 'c']); + }); + + it('should return the original array if no values are excluded', () => { + expect(difference([1, 2, 3], [])).toEqual([1, 2, 3]); + }); + }); + + describe('debounce', () => { + jest.useFakeTimers(); + + it('should debounce a function', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100); + debouncedFunc(); + debouncedFunc(); + jest.advanceTimersByTime(50); + debouncedFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(1); + }); + + it('should cancel a debounced function', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100); + debouncedFunc(); + jest.advanceTimersByTime(50) + debouncedFunc.cancel(); + jest.advanceTimersByTime(100); + expect(func).not.toHaveBeenCalled(); + }); + + it('should debounce a function with leading option', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { leading: true }); + debouncedFunc(); + debouncedFunc(); + jest.advanceTimersByTime(50); + debouncedFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should debounce a function with trailing option', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { trailing: true }); + debouncedFunc(); + debouncedFunc(); + jest.advanceTimersByTime(50); + debouncedFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(1); + }); + + it('should debounce a function with both leading and trailing options', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { leading: true, trailing: true }); + debouncedFunc(); + debouncedFunc(); + jest.advanceTimersByTime(50); + debouncedFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should debounce a function with both leading and trailing options set to false', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { leading: false, trailing: false }); + debouncedFunc(); + debouncedFunc(); + jest.advanceTimersByTime(50); + debouncedFunc(); + jest.advanceTimersByTime(100); + expect(func).not.toHaveBeenCalled(); + }); + }); + + describe('throttle', () => { + jest.useFakeTimers(); + + it('should throttle a function', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100); + throttledFunc(); + throttledFunc(); + jest.advanceTimersByTime(50); + throttledFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should cancel a throttled function', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100); + throttledFunc(); + throttledFunc.cancel(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(1); + }); + + it('should throttle a function with leading option', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100, { leading: true }); + throttledFunc(); + throttledFunc(); + jest.advanceTimersByTime(50); + throttledFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should throttle a function with trailing option', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100, { trailing: true }); + throttledFunc(); + throttledFunc(); + jest.advanceTimersByTime(50); + throttledFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should throttle a function with both leading and trailing options', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100, { leading: true, trailing: true }); + throttledFunc(); + throttledFunc(); + jest.advanceTimersByTime(50); + throttledFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should throttle a function with both leading and trailing options set to false', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100, { leading: false, trailing: false }); + throttledFunc(); + throttledFunc(); + jest.advanceTimersByTime(50); + throttledFunc(); + jest.advanceTimersByTime(100); + expect(func).not.toHaveBeenCalled(); + }); + }); + + describe('noop', () => { + it('should do nothing', () => { + expect(noop()).toBeUndefined() + }); + }); + + describe('uniqueId', () => { + it('should generate unique ids with a prefix', () => { + expect(uniqueId('prefix_')).toBe('prefix_1'); + expect(uniqueId('prefix_')).toBe('prefix_2'); + }); + + it('should generate unique ids without a prefix', () => { + expect(uniqueId()).toBe('3'); + expect(uniqueId()).toBe('4'); + }); + }); + + describe('shuffle', () => { + it('should shuffle an array', () => { + const array = [1, 2, 3, 4, 5]; + const shuffledArray = shuffle(array); + expect(shuffledArray).not.toEqual(array); + expect(shuffledArray.sort()).toEqual(array.sort()); + }); + }); +}); + diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts new file mode 100644 index 0000000..1ca59e7 --- /dev/null +++ b/src/__tests__/utils.test.ts @@ -0,0 +1,192 @@ +import { findKey, difference, debounce, throttle, noop, uniqueId, shuffle } from '../utils'; + +describe('utils', () => { + describe('findKey', () => { + it('should find the key based on the predicate', () => { + const obj = { a: 1, b: 2, c: 3 }; + const predicate = (value: number) => value === 2; + expect(findKey(obj, predicate)).toBe('b'); + }); + + it('should return undefined if no key matches the predicate', () => { + const obj = { a: 1, b: 2, c: 3 }; + const predicate = (value: number) => value === 4; + expect(findKey(obj, predicate)).toBeUndefined(); + }); + }); + + describe('difference', () => { + it('should return the difference between arrays', () => { + expect(difference([1, 2, 3], [2, 3])).toEqual([1]); + expect(difference(['a', 'b', 'c'], ['b'])).toEqual(['a', 'c']); + }); + + it('should return the original array if no values are excluded', () => { + expect(difference([1, 2, 3], [])).toEqual([1, 2, 3]); + }); + }); + + describe('debounce', () => { + jest.useFakeTimers(); + + it('should debounce a function', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100); + debouncedFunc(); + debouncedFunc(); + jest.advanceTimersByTime(50); + debouncedFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(1); + }); + + it('should cancel a debounced function', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100); + debouncedFunc(); + jest.advanceTimersByTime(50) + debouncedFunc.cancel(); + jest.advanceTimersByTime(100); + expect(func).not.toHaveBeenCalled(); + }); + + it('should debounce a function with leading option', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { leading: true }); + debouncedFunc(); + debouncedFunc(); + jest.advanceTimersByTime(50); + debouncedFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should debounce a function with trailing option', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { trailing: true }); + debouncedFunc(); + debouncedFunc(); + jest.advanceTimersByTime(50); + debouncedFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(1); + }); + + it('should debounce a function with both leading and trailing options', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { leading: true, trailing: true }); + debouncedFunc(); + debouncedFunc(); + jest.advanceTimersByTime(50); + debouncedFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should debounce a function with both leading and trailing options set to false', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { leading: false, trailing: false }); + debouncedFunc(); + debouncedFunc(); + jest.advanceTimersByTime(50); + debouncedFunc(); + jest.advanceTimersByTime(100); + expect(func).not.toHaveBeenCalled(); + }); + }); + + describe('throttle', () => { + jest.useFakeTimers(); + + it('should throttle a function', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100); + throttledFunc(); + throttledFunc(); + jest.advanceTimersByTime(50); + throttledFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should cancel a throttled function', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100); + throttledFunc(); + throttledFunc.cancel(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(1); + }); + + it('should throttle a function with leading option', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100, { leading: true }); + throttledFunc(); + throttledFunc(); + jest.advanceTimersByTime(50); + throttledFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should throttle a function with trailing option', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100, { trailing: true }); + throttledFunc(); + throttledFunc(); + jest.advanceTimersByTime(50); + throttledFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should throttle a function with both leading and trailing options', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100, { leading: true, trailing: true }); + throttledFunc(); + throttledFunc(); + jest.advanceTimersByTime(50); + throttledFunc(); + jest.advanceTimersByTime(100); + expect(func).toHaveBeenCalledTimes(2); + }); + + it('should throttle a function with both leading and trailing options set to false', () => { + const func = jest.fn(); + const throttledFunc = throttle(func, 100, { leading: false, trailing: false }); + throttledFunc(); + throttledFunc(); + jest.advanceTimersByTime(50); + throttledFunc(); + jest.advanceTimersByTime(100); + expect(func).not.toHaveBeenCalled(); + }); + }); + + describe('noop', () => { + it('should do nothing', () => { + expect(noop()).toBeUndefined(); + }); + }); + + describe('uniqueId', () => { + it('should generate unique ids with a prefix', () => { + expect(uniqueId('prefix_')).toBe('prefix_1'); + expect(uniqueId('prefix_')).toBe('prefix_2'); + }); + + it('should generate unique ids without a prefix', () => { + expect(uniqueId()).toBe('3'); + expect(uniqueId()).toBe('4'); + }); + }); + + describe('shuffle', () => { + it('should shuffle an array', () => { + const array = [1, 2, 3, 4, 5]; + const shuffledArray = shuffle(array); + expect(shuffledArray).not.toEqual(array); + expect(shuffledArray.sort()).toEqual(array.sort()); + }); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index 08091bb..64a71d2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -18,7 +18,7 @@ const difference = (array: T[], ...values: T[][]): T[ return array.filter(item => !exclusionSet.has(item)); } -const noop: VoidFunction = () => null; +const noop: VoidFunction = () => {}; let counter = 0; @@ -47,7 +47,7 @@ type DebouncedFunc void> = { const debounce = void>( func: T, wait: number, - { leading = false, trailing = true }: { leading?: boolean; trailing?: boolean } + options?: { leading?: boolean; trailing?: boolean } ): DebouncedFunc => { let timeoutId: ReturnType | null = null; let lastArgs: Parameters | null = null; @@ -59,6 +59,7 @@ const debounce = void>( const debounced = (...args: Parameters) => { lastArgs = args; + const { leading = false, trailing = true} = options || {}; if (leading && !timeoutId) { invokeFunc(args); // Call immediately on the first call } From 8e920244c3b52d5462cb4166a9f870bae39f4776 Mon Sep 17 00:00:00 2001 From: christianEconify Date: Wed, 26 Feb 2025 10:18:13 +0000 Subject: [PATCH 17/17] chore: add test for multiple rapid calls --- src/__tests__/utils.lodash.test.ts | 28 ++++++++++++++++++++++++++++ src/__tests__/utils.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/__tests__/utils.lodash.test.ts b/src/__tests__/utils.lodash.test.ts index ae9c092..a4041ba 100644 --- a/src/__tests__/utils.lodash.test.ts +++ b/src/__tests__/utils.lodash.test.ts @@ -93,6 +93,34 @@ describe('lodash utils', () => { jest.advanceTimersByTime(100); expect(func).not.toHaveBeenCalled(); }); + + it('should debounce a function with leading option and reset timeout correctly', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { leading: true }); + debouncedFunc(); // Should call immediately + jest.advanceTimersByTime(10); + debouncedFunc(); // Should not call immediately, should reset timeout + jest.advanceTimersByTime(90); + expect(func).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(10); + expect(func).toHaveBeenCalledTimes(2); // Should call after wait period + }); + + it('should debounce a function with leading option and handle multiple rapid calls', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { leading: true }); + debouncedFunc(); // Should call immediately + jest.advanceTimersByTime(10); + debouncedFunc(); + debouncedFunc(); + debouncedFunc(); // Should not call immediately, should reset timeout + jest.advanceTimersByTime(10); + debouncedFunc(); // Should not call immediately, should reset timeout again + jest.advanceTimersByTime(80); + expect(func).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(20); + expect(func).toHaveBeenCalledTimes(2); // Should call after wait period + }); }); describe('throttle', () => { diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 1ca59e7..8eee5ad 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -93,6 +93,34 @@ describe('utils', () => { jest.advanceTimersByTime(100); expect(func).not.toHaveBeenCalled(); }); + + it('should debounce a function with leading option and reset timeout correctly', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { leading: true }); + debouncedFunc(); // Should call immediately + jest.advanceTimersByTime(10); + debouncedFunc(); // Should not call immediately, should reset timeout + jest.advanceTimersByTime(90); + expect(func).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(10); + expect(func).toHaveBeenCalledTimes(2); // Should call after wait period + }); + + it('should debounce a function with leading option and handle multiple rapid calls', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 100, { leading: true }); + debouncedFunc(); // Should call immediately + jest.advanceTimersByTime(10); + debouncedFunc(); + debouncedFunc(); + debouncedFunc(); // Should not call immediately, should reset timeout + jest.advanceTimersByTime(10); + debouncedFunc(); // Should not call immediately, should reset timeout again + jest.advanceTimersByTime(80); + expect(func).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(20); + expect(func).toHaveBeenCalledTimes(2); // Should call after wait period + }); }); describe('throttle', () => {