From 9460e26c5f129973ed397850c8df653f6f86b3bb Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Mon, 18 Nov 2024 02:21:57 -0300 Subject: [PATCH 01/30] refactor: useEventlistener (with EventMap generic overrid) --- .../src/useEventListener/useEventListener.ts | 159 ++++++------------ 1 file changed, 52 insertions(+), 107 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 3c70701a..748e2020 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -1,121 +1,66 @@ -import { useEffect, useRef } from 'react' +import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect" +import type { RefObject } from "react" +import { useEffect, useRef } from "react" -import type { RefObject } from 'react' - -import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect' - -// MediaQueryList Event based useEventListener interface -function useEventListener( - eventName: K, - handler: (event: MediaQueryListEventMap[K]) => void, - element: RefObject, - options?: boolean | AddEventListenerOptions, -): void - -// Window Event based useEventListener interface -function useEventListener( - eventName: K, - handler: (event: WindowEventMap[K]) => void, - element?: undefined, - options?: boolean | AddEventListenerOptions, -): void +// use of CustomEventsMap at app global declaration +/** Element as string to Matching EventMap */ +type ElementToEventMap = { + Window: [Window, WindowEventMap] + HTMLElement: [HTMLElement, HTMLElementEventMap] + Document: [Document, DocumentEventMap] + MediaQueryList: [MediaQueryList, MediaQueryListEventMap] + RTCDataChannel: [RTCDataChannel, RTCDataChannelEventMap] + RTCPeerConnection: [RTCPeerConnection, RTCPeerConnectionEventMap] + SpeechSynthesis: [SpeechSynthesis, SpeechSynthesisEventMap] + SpeechSynthesisUtterance: [SpeechSynthesisUtterance, SpeechSynthesisUtteranceEventMap] +} -// Element Event based useEventListener interface -function useEventListener< - K extends keyof HTMLElementEventMap & keyof SVGElementEventMap, - T extends Element = K extends keyof HTMLElementEventMap - ? HTMLDivElement - : SVGElement, ->( - eventName: K, - handler: - | ((event: HTMLElementEventMap[K]) => void) - | ((event: SVGElementEventMap[K]) => void), - element: RefObject, - options?: boolean | AddEventListenerOptions, -): void +/** Return `T` if `M` undefined */ +type Fallback = M extends undefined ? T : M -// Document Event based useEventListener interface -function useEventListener( - eventName: K, - handler: (event: DocumentEventMap[K]) => void, - element: RefObject, - options?: boolean | AddEventListenerOptions, -): void +/** Return `EventMap` type of matching element ref (from config argument) + * Intersected with `CustomEventsMap` (from global declaration) */ +type EventMapOf = { + [K in keyof ElementToEventMap]: E extends ElementToEventMap[K][0] ? ElementToEventMap[K][1] & CustomEventsMap : never +}[keyof ElementToEventMap] -/** - * Custom hook that attaches event listeners to DOM elements, the window, or media query lists. - * @template KW - The type of event for window events. - * @template KH - The type of event for HTML or SVG element events. - * @template KM - The type of event for media query list events. - * @template T - The type of the DOM element (default is `HTMLElement`). - * @param {KW | KH | KM} eventName - The name of the event to listen for. - * @param {(event: WindowEventMap[KW] | HTMLElementEventMap[KH] | SVGElementEventMap[KH] | MediaQueryListEventMap[KM] | Event) => void} handler - The event handler function. - * @param {RefObject} [element] - The DOM element or media query list to attach the event listener to (optional). - * @param {boolean | AddEventListenerOptions} [options] - An options object that specifies characteristics about the event listener (optional). - * @public - * @see [Documentation](https://usehooks-ts.com/react-hook/use-event-listener) - * @example - * ```tsx - * // Example 1: Attach a window event listener - * useEventListener('resize', handleResize); - * ``` - * @example - * ```tsx - * // Example 2: Attach a document event listener with options - * const elementRef = useRef(document); - * useEventListener('click', handleClick, elementRef, { capture: true }); - * ``` - * @example - * ```tsx - * // Example 3: Attach an element event listener - * const buttonRef = useRef(null); - * useEventListener('click', handleButtonClick, buttonRef); - * ``` - */ function useEventListener< - KW extends keyof WindowEventMap, - KH extends keyof HTMLElementEventMap & keyof SVGElementEventMap, - KM extends keyof MediaQueryListEventMap, - T extends HTMLElement | SVGAElement | MediaQueryList = HTMLElement, + /** Custom Event Map (optional generic)*/ + M extends Record | undefined = undefined, + /** Element Type of Optional refObject (defaults to Window) */ + E extends ElementToEventMap[keyof ElementToEventMap][0] = Window, + /** eventName Key of type custom EventMap if present */ + K extends keyof Fallback> = keyof Fallback>, >( - eventName: KW | KH | KM, - handler: ( - event: - | WindowEventMap[KW] - | HTMLElementEventMap[KH] - | SVGElementEventMap[KH] - | MediaQueryListEventMap[KM] - | Event, - ) => void, - element?: RefObject, - options?: boolean | AddEventListenerOptions, + eventName: K & string, + handler: (event: Fallback>[K]) => void, + config: { + /** Litening Target (defaults to window) */ + ref?: RefObject + options?: boolean | AddEventListenerOptions + } = {}, ) { - // Create a ref that stores handler - const savedHandler = useRef(handler) - - useIsomorphicLayoutEffect(() => { - savedHandler.current = handler - }, [handler]) - - useEffect(() => { - // Define the listening target - const targetElement: T | Window = element?.current ?? window + // Create a ref that stores handler + const savedHandler = useRef(handler) + useIsomorphicLayoutEffect(() => { + savedHandler.current = handler + }, [handler]) - if (!(targetElement && targetElement.addEventListener)) return + useEffect(() => { + // Define the listening target + const targetElement: E | Window = config.ref?.current ?? window + if (!targetElement) return - // Create event listener that calls handler function stored in ref - const listener: typeof handler = event => { - savedHandler.current(event) - } + // Create event listener that calls handler function stored in ref + const eventListener: EventListener = (event) => savedHandler.current(event as Parameters[0]) - targetElement.addEventListener(eventName, listener, options) + targetElement.addEventListener(eventName, eventListener, config.options) - // Remove event listener on cleanup - return () => { - targetElement.removeEventListener(eventName, listener, options) - } - }, [eventName, element, options]) + // Remove event listener on cleanup + return () => { + targetElement.removeEventListener(eventName, eventListener) + } + }, [eventName, config]) } export { useEventListener } From 845d04f2e3198cdfc0e67d1790b9a55d3200a12a Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Mon, 18 Nov 2024 02:33:31 -0300 Subject: [PATCH 02/30] doc: updated tsDoc, match new generics & config parameter --- .../src/useEventListener/useEventListener.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 748e2020..cfdeb16a 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -24,6 +24,35 @@ type EventMapOf = { [K in keyof ElementToEventMap]: E extends ElementToEventMap[K][0] ? ElementToEventMap[K][1] & CustomEventsMap : never }[keyof ElementToEventMap] +/** + * Custom hook that attaches event listeners to DOM elements, the window, or media query lists. + * @template M - The type of custom Event Map (optional generic), overrides any other element to events mapping. + * @template E - The type of the DOM element (default is `Window`). + * @template K - The type of event name, Key of an EventMap (match for DOM element). + * @param {K} eventName - The name of the event to listen for. + * @param {(event: Fallback>[K]) => void} handler - The event handler function. + * @param {RefObject} config.ref - A reference that specifies the DOM element to attach the event listener to. + * @param {boolean | AddEventListenerOptions} config.options - Event listener Options. + * @public + * @see [Documentation](https://usehooks-ts.com/react-hook/use-event-listener) + * @example + * ```tsx + * // Example 1: Attach a window event listener + * useEventListener('resize', handleResize); + * ``` + * @example + * ```tsx + * // Example 2: Attach a document event listener with options + * const ref = useRef(document); + * useEventListener('click', handleClick, { ref, options: { capture: true } }); + * ``` + * @example + * ```tsx + * // Example 3: Attach an element event listener + * const ref = useRef(null); + * useEventListener('click', handleButtonClick, { ref }); + * ``` + */ function useEventListener< /** Custom Event Map (optional generic)*/ M extends Record | undefined = undefined, From 48b9a2daae1ab596ebdd4d670d72b65f29049df7 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Mon, 18 Nov 2024 14:43:11 -0300 Subject: [PATCH 03/30] chore: backwards compatibility for DocumentEventMap type declaration (intersect with DocumentEventMap for document listeners) --- packages/usehooks-ts/src/useEventListener/useEventListener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index cfdeb16a..68d9ea09 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -7,7 +7,7 @@ import { useEffect, useRef } from "react" type ElementToEventMap = { Window: [Window, WindowEventMap] HTMLElement: [HTMLElement, HTMLElementEventMap] - Document: [Document, DocumentEventMap] + Document: [Document, DocumentEventMap & DocumentEventMap] MediaQueryList: [MediaQueryList, MediaQueryListEventMap] RTCDataChannel: [RTCDataChannel, RTCDataChannelEventMap] RTCPeerConnection: [RTCPeerConnection, RTCPeerConnectionEventMap] From 2f458d0029a59445ee3bf178c7d9d356c2b2a428 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Mon, 18 Nov 2024 14:44:29 -0300 Subject: [PATCH 04/30] typo --- packages/usehooks-ts/src/useEventListener/useEventListener.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 68d9ea09..a3ee78af 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -1,6 +1,6 @@ -import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect" -import type { RefObject } from "react" import { useEffect, useRef } from "react" +import type { RefObject } from "react" +import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect" // use of CustomEventsMap at app global declaration /** Element as string to Matching EventMap */ From 29e8fef30813bd729ab3b17037ac138faab65ca2 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Mon, 18 Nov 2024 15:33:33 -0300 Subject: [PATCH 05/30] refactor: renamed CustomEventsMap to CustomEventMap + initialized declaration & (removed unnecessary document map intersection) --- .../src/useEventListener/useEventListener.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index a3ee78af..475b8a55 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -2,12 +2,17 @@ import { useEffect, useRef } from "react" import type { RefObject } from "react" import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect" -// use of CustomEventsMap at app global declaration +// Recommended usage: move CustomEventMap to global declaration +/** Extends EventMap declarations for all DOM Elements (intersection)*/ +interface CustomEventMap { + "your-custom-event": CustomEvent<{ isCustom: boolean }> +} + /** Element as string to Matching EventMap */ type ElementToEventMap = { Window: [Window, WindowEventMap] HTMLElement: [HTMLElement, HTMLElementEventMap] - Document: [Document, DocumentEventMap & DocumentEventMap] + Document: [Document, DocumentEventMap] MediaQueryList: [MediaQueryList, MediaQueryListEventMap] RTCDataChannel: [RTCDataChannel, RTCDataChannelEventMap] RTCPeerConnection: [RTCPeerConnection, RTCPeerConnectionEventMap] @@ -19,9 +24,9 @@ type ElementToEventMap = { type Fallback = M extends undefined ? T : M /** Return `EventMap` type of matching element ref (from config argument) - * Intersected with `CustomEventsMap` (from global declaration) */ + * Intersected with `CustomEventMap` (from global declaration) */ type EventMapOf = { - [K in keyof ElementToEventMap]: E extends ElementToEventMap[K][0] ? ElementToEventMap[K][1] & CustomEventsMap : never + [K in keyof ElementToEventMap]: E extends ElementToEventMap[K][0] ? ElementToEventMap[K][1] & CustomEventMap : never }[keyof ElementToEventMap] /** From 419d60cfa2c5165e48bca851cb07bee13bdaf701 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Mon, 18 Nov 2024 19:26:53 -0300 Subject: [PATCH 06/30] chore: reformat prettier & updated config argument on listener hook call within other hooks --- .../src/useEventListener/useEventListener.ts | 16 ++++-- packages/usehooks-ts/src/useHover/useHover.ts | 30 +++++----- .../useOnClickOutside/useOnClickOutside.ts | 57 ++++++++----------- 3 files changed, 48 insertions(+), 55 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 475b8a55..5fa6a4a1 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -20,14 +20,18 @@ type ElementToEventMap = { SpeechSynthesisUtterance: [SpeechSynthesisUtterance, SpeechSynthesisUtteranceEventMap] } -/** Return `T` if `M` undefined */ -type Fallback = M extends undefined ? T : M +/** Return `T` if `M` undefined or never */ +type Fallback = [M] extends [undefined | never] ? T : M /** Return `EventMap` type of matching element ref (from config argument) - * Intersected with `CustomEventMap` (from global declaration) */ -type EventMapOf = { - [K in keyof ElementToEventMap]: E extends ElementToEventMap[K][0] ? ElementToEventMap[K][1] & CustomEventMap : never -}[keyof ElementToEventMap] + * Intersected with `CustomEventMap` (from global declaration) + * Fallback to HTMLElement (if generic never or undefined) */ +type EventMapOf = Fallback< + { + [K in keyof ElementToEventMap]: E extends ElementToEventMap[K][0] ? ElementToEventMap[K][1] & CustomEventMap : never + }[keyof ElementToEventMap], + HTMLElement +> /** * Custom hook that attaches event listeners to DOM elements, the window, or media query lists. diff --git a/packages/usehooks-ts/src/useHover/useHover.ts b/packages/usehooks-ts/src/useHover/useHover.ts index d9a99894..a121f8a0 100644 --- a/packages/usehooks-ts/src/useHover/useHover.ts +++ b/packages/usehooks-ts/src/useHover/useHover.ts @@ -1,8 +1,8 @@ -import { useState } from 'react' +import { useState } from "react" -import type { RefObject } from 'react' +import type { Ref, RefObject } from "react" -import { useEventListener } from '../useEventListener' +import { useEventListener } from "../useEventListener" /** * Custom hook that tracks whether a DOM element is being hovered over. @@ -18,20 +18,18 @@ import { useEventListener } from '../useEventListener' * // Access the isHovered variable to determine if the button is being hovered over. * ``` */ -export function useHover( - elementRef: RefObject, -): boolean { - const [value, setValue] = useState(false) +export function useHover(elementRef: RefObject): boolean { + const [value, setValue] = useState(false) - const handleMouseEnter = () => { - setValue(true) - } - const handleMouseLeave = () => { - setValue(false) - } + const handleMouseEnter = () => { + setValue(true) + } + const handleMouseLeave = () => { + setValue(false) + } - useEventListener('mouseenter', handleMouseEnter, elementRef) - useEventListener('mouseleave', handleMouseLeave, elementRef) + useEventListener("mouseenter", handleMouseEnter, { ref: elementRef as RefObject }) + useEventListener("mouseleave", handleMouseLeave, { ref: elementRef as RefObject }) - return value + return value } diff --git a/packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.ts b/packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.ts index a0372d57..b88af000 100644 --- a/packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.ts +++ b/packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.ts @@ -1,15 +1,9 @@ -import type { RefObject } from 'react' +import type { RefObject } from "react" -import { useEventListener } from '../useEventListener' +import { useEventListener } from "../useEventListener" /** Supported event types. */ -type EventType = - | 'mousedown' - | 'mouseup' - | 'touchstart' - | 'touchend' - | 'focusin' - | 'focusout' +type EventType = "mousedown" | "mouseup" | "touchstart" | "touchend" | "focusin" | "focusout" /** * Custom hook that handles clicks outside a specified element. @@ -30,32 +24,29 @@ type EventType = * ``` */ export function useOnClickOutside( - ref: RefObject | RefObject[], - handler: (event: MouseEvent | TouchEvent | FocusEvent) => void, - eventType: EventType = 'mousedown', - eventListenerOptions: AddEventListenerOptions = {}, + ref: RefObject | RefObject[], + handler: (event: MouseEvent | TouchEvent | FocusEvent) => void, + eventType: EventType = "mousedown", + eventListenerOptions: AddEventListenerOptions = {}, ): void { - useEventListener( - eventType, - event => { - const target = event.target as Node + useEventListener( + eventType, + (event) => { + const target = event.target as Node - // Do nothing if the target is not connected element with document - if (!target || !target.isConnected) { - return - } + // Do nothing if the target is not connected element with document + if (!target || !target.isConnected) { + return + } - const isOutside = Array.isArray(ref) - ? ref - .filter(r => Boolean(r.current)) - .every(r => r.current && !r.current.contains(target)) - : ref.current && !ref.current.contains(target) + const isOutside = Array.isArray(ref) + ? ref.filter((r) => Boolean(r.current)).every((r) => r.current && !r.current.contains(target)) + : ref.current && !ref.current.contains(target) - if (isOutside) { - handler(event) - } - }, - undefined, - eventListenerOptions, - ) + if (isOutside) { + handler(event) + } + }, + { options: eventListenerOptions }, + ) } From b4d6f8a776b45b468ece713193d5c8f402ac8c7b Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Wed, 20 Nov 2024 00:43:00 -0300 Subject: [PATCH 07/30] refactor: prev listener parameter name --- .../usehooks-ts/src/useEventListener/useEventListener.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 5fa6a4a1..2767b006 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -90,13 +90,13 @@ function useEventListener< if (!targetElement) return // Create event listener that calls handler function stored in ref - const eventListener: EventListener = (event) => savedHandler.current(event as Parameters[0]) + const listener: EventListener = (event) => savedHandler.current(event as Parameters[0]) - targetElement.addEventListener(eventName, eventListener, config.options) + targetElement.addEventListener(eventName, listener, config.options) // Remove event listener on cleanup return () => { - targetElement.removeEventListener(eventName, eventListener) + targetElement.removeEventListener(eventName, listener) } }, [eventName, config]) } From d6240fda28b7fbadf44804c8a36e8491f71cc1e7 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Wed, 20 Nov 2024 00:43:13 -0300 Subject: [PATCH 08/30] fix: listener tests --- .../useEventListener/useEventListener.test.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts index 51add33c..8bc81ca6 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts @@ -52,6 +52,7 @@ describe('useEventListener()', () => { useEventListener(eventName, handler) }) + expect(windowAddEventListenerSpy).toHaveBeenCalledTimes(1) expect(windowAddEventListenerSpy).toHaveBeenCalledWith( eventName, expect.any(Function), @@ -60,10 +61,10 @@ describe('useEventListener()', () => { unmount() + expect(windowRemoveEventListenerSpy).toHaveBeenCalledTimes(1) expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, - expect.any(Function), - options, + expect.any(Function) ) }) @@ -73,7 +74,7 @@ describe('useEventListener()', () => { const options = undefined const { unmount } = renderHook(() => { - useEventListener(eventName, handler, ref, options) + useEventListener(eventName, handler, {ref, options}) }) expect(refAddEventListenerSpy).toHaveBeenCalledTimes(1) @@ -85,20 +86,20 @@ describe('useEventListener()', () => { unmount() + expect(refRemoveEventListenerSpy).toHaveBeenCalledTimes(1) expect(refRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, - expect.any(Function), - options, + expect.any(Function) ) }) it('should bind/unbind the event listener to the document when document is provided', () => { const eventName = 'test-event' - const handler = vitest.fn() + const listener = vitest.fn() const options = undefined const { unmount } = renderHook(() => { - useEventListener(eventName, handler, docRef, options) + useEventListener(eventName, listener, {ref: docRef, options}) }) expect(docAddEventListenerSpy).toHaveBeenCalledTimes(1) @@ -110,11 +111,12 @@ describe('useEventListener()', () => { unmount() + expect(docRemoveEventListenerSpy).toHaveBeenCalledTimes(1) expect(docRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, - expect.any(Function), - options, + expect.any(Function) ) + }) it('should pass the options to the event listener', () => { @@ -127,7 +129,7 @@ describe('useEventListener()', () => { } renderHook(() => { - useEventListener(eventName, handler, undefined, options) + useEventListener(eventName, handler, {options}) }) expect(windowAddEventListenerSpy).toHaveBeenCalledWith( @@ -142,7 +144,7 @@ describe('useEventListener()', () => { const handler = vitest.fn() renderHook(() => { - useEventListener(eventName, handler, ref) + useEventListener(eventName, handler, {ref}) }) fireEvent.click(ref.current) @@ -155,10 +157,10 @@ describe('useEventListener()', () => { const keydownHandler = vitest.fn() renderHook(() => { - useEventListener('click', clickHandler, ref) + useEventListener('click', clickHandler, {ref}) }) renderHook(() => { - useEventListener('keydown', keydownHandler, ref) + useEventListener('keydown', keydownHandler, {ref}) }) fireEvent.click(ref.current) From 16149c3499a63752ce9243f8ada6089d9e5aecd5 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Wed, 20 Nov 2024 01:27:26 -0300 Subject: [PATCH 09/30] add: support for straight Element or RefObject refactor: renamed config ref to element --- .../src/useEventListener/useEventListener.test.ts | 10 +++++----- .../src/useEventListener/useEventListener.ts | 14 +++++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts index 8bc81ca6..71b4b611 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts @@ -74,7 +74,7 @@ describe('useEventListener()', () => { const options = undefined const { unmount } = renderHook(() => { - useEventListener(eventName, handler, {ref, options}) + useEventListener(eventName, handler, {element: ref, options}) }) expect(refAddEventListenerSpy).toHaveBeenCalledTimes(1) @@ -99,7 +99,7 @@ describe('useEventListener()', () => { const options = undefined const { unmount } = renderHook(() => { - useEventListener(eventName, listener, {ref: docRef, options}) + useEventListener(eventName, listener, {element: docRef, options}) }) expect(docAddEventListenerSpy).toHaveBeenCalledTimes(1) @@ -144,7 +144,7 @@ describe('useEventListener()', () => { const handler = vitest.fn() renderHook(() => { - useEventListener(eventName, handler, {ref}) + useEventListener(eventName, handler, {element: ref}) }) fireEvent.click(ref.current) @@ -157,10 +157,10 @@ describe('useEventListener()', () => { const keydownHandler = vitest.fn() renderHook(() => { - useEventListener('click', clickHandler, {ref}) + useEventListener('click', clickHandler, {element: ref}) }) renderHook(() => { - useEventListener('keydown', keydownHandler, {ref}) + useEventListener('keydown', keydownHandler, {element: ref}) }) fireEvent.click(ref.current) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 2767b006..f066337d 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from "react" -import type { RefObject } from "react" +import type { MutableRefObject, Ref, RefObject } from "react" import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect" // Recommended usage: move CustomEventMap to global declaration @@ -33,6 +33,8 @@ type EventMapOf = Fallback< HTMLElement > +type validElements = ElementToEventMap[keyof ElementToEventMap][0] + /** * Custom hook that attaches event listeners to DOM elements, the window, or media query lists. * @template M - The type of custom Event Map (optional generic), overrides any other element to events mapping. @@ -40,7 +42,7 @@ type EventMapOf = Fallback< * @template K - The type of event name, Key of an EventMap (match for DOM element). * @param {K} eventName - The name of the event to listen for. * @param {(event: Fallback>[K]) => void} handler - The event handler function. - * @param {RefObject} config.ref - A reference that specifies the DOM element to attach the event listener to. + * @param {RefObject} config.element - A reference that specifies the DOM element to attach the event listener to. * @param {boolean | AddEventListenerOptions} config.options - Event listener Options. * @public * @see [Documentation](https://usehooks-ts.com/react-hook/use-event-listener) @@ -73,8 +75,9 @@ function useEventListener< eventName: K & string, handler: (event: Fallback>[K]) => void, config: { - /** Litening Target (defaults to window) */ - ref?: RefObject + /** Litening Target (defaults to window) (supports, ref or plain Element) */ + element?: RefObject | E + /** eventListener Options */ options?: boolean | AddEventListenerOptions } = {}, ) { @@ -86,7 +89,8 @@ function useEventListener< useEffect(() => { // Define the listening target - const targetElement: E | Window = config.ref?.current ?? window + const targetElement: E | Window = config.element ? ("current" in config.element ? (config.element.current ?? window) : config.element ) : window + if (!targetElement) return // Create event listener that calls handler function stored in ref From 226f473256beccc4f13f9df82874c5518ca8a92f Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Wed, 20 Nov 2024 01:48:12 -0300 Subject: [PATCH 10/30] doc: updated demo and .md for listener hook --- .../useEventListener.demo.tsx | 16 ++++- .../src/useEventListener/useEventListener.md | 64 ++++++++++++++++--- .../src/useEventListener/useEventListener.ts | 12 ++-- packages/usehooks-ts/src/useHover/useHover.ts | 4 +- 4 files changed, 77 insertions(+), 19 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx index 9727e455..a13fc35d 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx @@ -6,6 +6,8 @@ export default function Component() { // Define button ref const buttonRef = useRef(null) const documentRef = useRef(document) + const speechSynthesis = new SpeechSynthesis() + const speechSynthesisRef = useRef(speechSynthesis) const onScroll = (event: Event) => { console.log('window scrolled!', event) @@ -15,6 +17,10 @@ export default function Component() { console.log('button clicked!', event) } + const onEventTarget = (event: Event) => { + console.log('speechSynthesis!', event) + } + const onVisibilityChange = (event: Event) => { console.log('doc visibility changed!', { isVisible: !document.hidden, @@ -26,10 +32,16 @@ export default function Component() { useEventListener('scroll', onScroll) // example with document based event - useEventListener('visibilitychange', onVisibilityChange, documentRef) + useEventListener('visibilitychange', onVisibilityChange, {element: documentRef}) // example with element based event - useEventListener('click', onClick, buttonRef) + useEventListener('click', onClick, {element: buttonRef}) + + // example with EventTarget element based event + useEventListener('voiceschanged', onEventTarget, {element: speechSynthesisRef}) + + // Example without ref + useEventListener('voiceschanged', onEventTarget, {element: speechSynthesis}) return (
diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.md b/packages/usehooks-ts/src/useEventListener/useEventListener.md index 8535ea36..bafb184b 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.md +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.md @@ -1,21 +1,67 @@ -Use EventListener with simplicity by React Hook. +## Use EventListener with simplicity by React Hook. -Supports `Window`, `Element` and `Document` and custom events with almost the same parameters as the native [`addEventListener` options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#syntax). See examples below. +Supports `Window`, `Element`, `Document`, `EventTarget` Based and custom events with almost the same parameters as the native [`addEventListener` options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#syntax). See examples below. -If you want to use your CustomEvent using Typescript, you have to declare the event type. -Find which kind of Event you want to extends: +If you want to use your CustomEvent using Typescript follow one of three options: -- `MediaQueryListEventMap` -- `WindowEventMap` -- `HTMLElementEventMap` -- `DocumentEventMap` +## 1. Globally Declared CustomEventMap, intersects all DOM Elements EventMaps + +```ts +// globals.d.ts +declare global { + /** Extends EventMap declarations for all DOM Elements (intersection)*/ + interface CustomEventMap { + 'my-custom-event': CustomEvent<{ isCustom: boolean }> + order: { orderId: number; name: string } + delivery: { itemCount: number } + } +} + +// page.tsx + +useEventListener('delivery', ({ itemCount }) => { + console.log('count', itemCount) +}) + +useEventListener('my-custom-event', event => { + console.log('boolean:', event.detail.isCustom) +}) +``` -Then declare your custom event: +## 2. Use EventMap generic override: + +```typescript +type OrderEventMap = { + order: { orderId: number; name: string } + delivery: { itemCount: number } +} +useEventListener('order', ({ orderId }) => { + console.log('id', orderId) +}) +``` + +## 3. Declare the event type as an intersection of the specific Element EventMap: ```ts +// global.d.ts declare global { + /** Intersects EventMap declarations for DOM Document Element (default DOM target for hook listener) */ interface DocumentEventMap { 'my-custom-event': CustomEvent<{ exampleArg: string }> } } ``` + +Available EventMap at useEventListener.ts + +- `WindowEventMap` +- `HTMLElementEventMap` +- `DocumentEventMap` + +Event Targets: + +- `MediaQueryListEventMap` +- `RTCDataChannelEventMap` +- `RTCPeerConnectionEventMap` +- `SpeechSynthesisEventMap` +- `SpeechSynthesisUtteranceEventMap` diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index f066337d..29ce6ada 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -33,8 +33,6 @@ type EventMapOf = Fallback< HTMLElement > -type validElements = ElementToEventMap[keyof ElementToEventMap][0] - /** * Custom hook that attaches event listeners to DOM elements, the window, or media query lists. * @template M - The type of custom Event Map (optional generic), overrides any other element to events mapping. @@ -54,14 +52,14 @@ type validElements = ElementToEventMap[keyof ElementToEventMap][0] * @example * ```tsx * // Example 2: Attach a document event listener with options - * const ref = useRef(document); - * useEventListener('click', handleClick, { ref, options: { capture: true } }); + * const element = useRef(document); + * useEventListener('click', handleClick, { element, options: { capture: true } }); * ``` * @example * ```tsx * // Example 3: Attach an element event listener - * const ref = useRef(null); - * useEventListener('click', handleButtonClick, { ref }); + * const element = useRef(null); + * useEventListener('click', handleButtonClick, { element }); * ``` */ function useEventListener< @@ -106,3 +104,5 @@ function useEventListener< } export { useEventListener } + +useEventListener("your-custom-event", (event) => {console.log(event.detail.isCustom)}) diff --git a/packages/usehooks-ts/src/useHover/useHover.ts b/packages/usehooks-ts/src/useHover/useHover.ts index a121f8a0..f5820ad4 100644 --- a/packages/usehooks-ts/src/useHover/useHover.ts +++ b/packages/usehooks-ts/src/useHover/useHover.ts @@ -28,8 +28,8 @@ export function useHover(elementRef: RefObj setValue(false) } - useEventListener("mouseenter", handleMouseEnter, { ref: elementRef as RefObject }) - useEventListener("mouseleave", handleMouseLeave, { ref: elementRef as RefObject }) + useEventListener("mouseenter", handleMouseEnter, { element: elementRef as RefObject }) + useEventListener("mouseleave", handleMouseLeave, { element: elementRef as RefObject }) return value } From 056d53ae6e73bc6255617ebd1b4479fa1a00347a Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Wed, 20 Nov 2024 01:49:06 -0300 Subject: [PATCH 11/30] chore: cleanup unnecessary --- packages/usehooks-ts/src/useEventListener/useEventListener.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 29ce6ada..fb4f68ad 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -104,5 +104,3 @@ function useEventListener< } export { useEventListener } - -useEventListener("your-custom-event", (event) => {console.log(event.detail.isCustom)}) From f6d57361cfb23ddc6cc08e57fc516bcfa657d2b5 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Wed, 20 Nov 2024 01:50:59 -0300 Subject: [PATCH 12/30] chore: unnecessary imports --- packages/usehooks-ts/src/useEventListener/useEventListener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index fb4f68ad..13a5befa 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from "react" -import type { MutableRefObject, Ref, RefObject } from "react" +import type { RefObject } from "react" import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect" // Recommended usage: move CustomEventMap to global declaration From eabc7063f397ddb4b133e59b967776f4a2049349 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Thu, 21 Nov 2024 13:26:40 -0300 Subject: [PATCH 13/30] doc: Window is default hook element --- packages/usehooks-ts/src/useEventListener/useEventListener.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.md b/packages/usehooks-ts/src/useEventListener/useEventListener.md index bafb184b..9491b0dc 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.md +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.md @@ -45,8 +45,8 @@ useEventListener('order', ({ orderId }) => { ```ts // global.d.ts declare global { - /** Intersects EventMap declarations for DOM Document Element (default DOM target for hook listener) */ - interface DocumentEventMap { + /** Intersects EventMap declarations for DOM Window Element (default DOM target for hook listener) */ + interface WindowEventMap { 'my-custom-event': CustomEvent<{ exampleArg: string }> } } From 5026898a6ce46dfd462ffe28eda3cd9656e4de22 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Thu, 21 Nov 2024 14:13:03 -0300 Subject: [PATCH 14/30] chore: prettier reformat --- .../useEventListener.demo.tsx | 12 +- .../useEventListener/useEventListener.test.ts | 19 ++-- .../src/useEventListener/useEventListener.ts | 104 ++++++++++-------- packages/usehooks-ts/src/useHover/useHover.ts | 34 +++--- .../useOnClickOutside/useOnClickOutside.ts | 56 ++++++---- 5 files changed, 126 insertions(+), 99 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx index a13fc35d..53231d5a 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx @@ -32,16 +32,20 @@ export default function Component() { useEventListener('scroll', onScroll) // example with document based event - useEventListener('visibilitychange', onVisibilityChange, {element: documentRef}) + useEventListener('visibilitychange', onVisibilityChange, { + element: documentRef, + }) // example with element based event - useEventListener('click', onClick, {element: buttonRef}) + useEventListener('click', onClick, { element: buttonRef }) // example with EventTarget element based event - useEventListener('voiceschanged', onEventTarget, {element: speechSynthesisRef}) + useEventListener('voiceschanged', onEventTarget, { + element: speechSynthesisRef, + }) // Example without ref - useEventListener('voiceschanged', onEventTarget, {element: speechSynthesis}) + useEventListener('voiceschanged', onEventTarget, { element: speechSynthesis }) return (
diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts index 71b4b611..7a47472c 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts @@ -64,7 +64,7 @@ describe('useEventListener()', () => { expect(windowRemoveEventListenerSpy).toHaveBeenCalledTimes(1) expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, - expect.any(Function) + expect.any(Function), ) }) @@ -74,7 +74,7 @@ describe('useEventListener()', () => { const options = undefined const { unmount } = renderHook(() => { - useEventListener(eventName, handler, {element: ref, options}) + useEventListener(eventName, handler, { element: ref, options }) }) expect(refAddEventListenerSpy).toHaveBeenCalledTimes(1) @@ -89,7 +89,7 @@ describe('useEventListener()', () => { expect(refRemoveEventListenerSpy).toHaveBeenCalledTimes(1) expect(refRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, - expect.any(Function) + expect.any(Function), ) }) @@ -99,7 +99,7 @@ describe('useEventListener()', () => { const options = undefined const { unmount } = renderHook(() => { - useEventListener(eventName, listener, {element: docRef, options}) + useEventListener(eventName, listener, { element: docRef, options }) }) expect(docAddEventListenerSpy).toHaveBeenCalledTimes(1) @@ -114,9 +114,8 @@ describe('useEventListener()', () => { expect(docRemoveEventListenerSpy).toHaveBeenCalledTimes(1) expect(docRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, - expect.any(Function) + expect.any(Function), ) - }) it('should pass the options to the event listener', () => { @@ -129,7 +128,7 @@ describe('useEventListener()', () => { } renderHook(() => { - useEventListener(eventName, handler, {options}) + useEventListener(eventName, handler, { options }) }) expect(windowAddEventListenerSpy).toHaveBeenCalledWith( @@ -144,7 +143,7 @@ describe('useEventListener()', () => { const handler = vitest.fn() renderHook(() => { - useEventListener(eventName, handler, {element: ref}) + useEventListener(eventName, handler, { element: ref }) }) fireEvent.click(ref.current) @@ -157,10 +156,10 @@ describe('useEventListener()', () => { const keydownHandler = vitest.fn() renderHook(() => { - useEventListener('click', clickHandler, {element: ref}) + useEventListener('click', clickHandler, { element: ref }) }) renderHook(() => { - useEventListener('keydown', keydownHandler, {element: ref}) + useEventListener('keydown', keydownHandler, { element: ref }) }) fireEvent.click(ref.current) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 13a5befa..0e090071 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -1,23 +1,26 @@ -import { useEffect, useRef } from "react" -import type { RefObject } from "react" -import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect" +import { useEffect, useRef } from 'react' +import type { RefObject } from 'react' +import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect' // Recommended usage: move CustomEventMap to global declaration /** Extends EventMap declarations for all DOM Elements (intersection)*/ interface CustomEventMap { - "your-custom-event": CustomEvent<{ isCustom: boolean }> + 'your-custom-event': CustomEvent<{ isCustom: boolean }> } /** Element as string to Matching EventMap */ type ElementToEventMap = { - Window: [Window, WindowEventMap] - HTMLElement: [HTMLElement, HTMLElementEventMap] - Document: [Document, DocumentEventMap] - MediaQueryList: [MediaQueryList, MediaQueryListEventMap] - RTCDataChannel: [RTCDataChannel, RTCDataChannelEventMap] - RTCPeerConnection: [RTCPeerConnection, RTCPeerConnectionEventMap] - SpeechSynthesis: [SpeechSynthesis, SpeechSynthesisEventMap] - SpeechSynthesisUtterance: [SpeechSynthesisUtterance, SpeechSynthesisUtteranceEventMap] + Window: [Window, WindowEventMap] + HTMLElement: [HTMLElement, HTMLElementEventMap] + Document: [Document, DocumentEventMap] + MediaQueryList: [MediaQueryList, MediaQueryListEventMap] + RTCDataChannel: [RTCDataChannel, RTCDataChannelEventMap] + RTCPeerConnection: [RTCPeerConnection, RTCPeerConnectionEventMap] + SpeechSynthesis: [SpeechSynthesis, SpeechSynthesisEventMap] + SpeechSynthesisUtterance: [ + SpeechSynthesisUtterance, + SpeechSynthesisUtteranceEventMap, + ] } /** Return `T` if `M` undefined or never */ @@ -27,10 +30,12 @@ type Fallback = [M] extends [undefined | never] ? T : M * Intersected with `CustomEventMap` (from global declaration) * Fallback to HTMLElement (if generic never or undefined) */ type EventMapOf = Fallback< - { - [K in keyof ElementToEventMap]: E extends ElementToEventMap[K][0] ? ElementToEventMap[K][1] & CustomEventMap : never - }[keyof ElementToEventMap], - HTMLElement + { + [K in keyof ElementToEventMap]: E extends ElementToEventMap[K][0] + ? ElementToEventMap[K][1] & CustomEventMap + : never + }[keyof ElementToEventMap], + HTMLElement > /** @@ -63,44 +68,49 @@ type EventMapOf = Fallback< * ``` */ function useEventListener< - /** Custom Event Map (optional generic)*/ - M extends Record | undefined = undefined, - /** Element Type of Optional refObject (defaults to Window) */ - E extends ElementToEventMap[keyof ElementToEventMap][0] = Window, - /** eventName Key of type custom EventMap if present */ - K extends keyof Fallback> = keyof Fallback>, + /** Custom Event Map (optional generic)*/ + M extends Record | undefined = undefined, + /** Element Type of Optional refObject (defaults to Window) */ + E extends ElementToEventMap[keyof ElementToEventMap][0] = Window, + /** eventName Key of type custom EventMap if present */ + K extends keyof Fallback> = keyof Fallback>, >( - eventName: K & string, - handler: (event: Fallback>[K]) => void, - config: { - /** Litening Target (defaults to window) (supports, ref or plain Element) */ - element?: RefObject | E - /** eventListener Options */ - options?: boolean | AddEventListenerOptions - } = {}, + eventName: K & string, + handler: (event: Fallback>[K]) => void, + config: { + /** Litening Target (defaults to window) (supports, ref or plain Element) */ + element?: RefObject | E + /** eventListener Options */ + options?: boolean | AddEventListenerOptions + } = {}, ) { - // Create a ref that stores handler - const savedHandler = useRef(handler) - useIsomorphicLayoutEffect(() => { - savedHandler.current = handler - }, [handler]) + // Create a ref that stores handler + const savedHandler = useRef(handler) + useIsomorphicLayoutEffect(() => { + savedHandler.current = handler + }, [handler]) - useEffect(() => { - // Define the listening target - const targetElement: E | Window = config.element ? ("current" in config.element ? (config.element.current ?? window) : config.element ) : window + useEffect(() => { + // Define the listening target + const targetElement: E | Window = config.element + ? 'current' in config.element + ? config.element.current ?? window + : config.element + : window - if (!targetElement) return + if (!targetElement) return - // Create event listener that calls handler function stored in ref - const listener: EventListener = (event) => savedHandler.current(event as Parameters[0]) + // Create event listener that calls handler function stored in ref + const listener: EventListener = event => + savedHandler.current(event as Parameters[0]) - targetElement.addEventListener(eventName, listener, config.options) + targetElement.addEventListener(eventName, listener, config.options) - // Remove event listener on cleanup - return () => { - targetElement.removeEventListener(eventName, listener) - } - }, [eventName, config]) + // Remove event listener on cleanup + return () => { + targetElement.removeEventListener(eventName, listener, config.options) + } + }, [eventName, config]) } export { useEventListener } diff --git a/packages/usehooks-ts/src/useHover/useHover.ts b/packages/usehooks-ts/src/useHover/useHover.ts index f5820ad4..9528391d 100644 --- a/packages/usehooks-ts/src/useHover/useHover.ts +++ b/packages/usehooks-ts/src/useHover/useHover.ts @@ -1,8 +1,8 @@ -import { useState } from "react" +import { useState } from 'react' -import type { Ref, RefObject } from "react" +import type { Ref, RefObject } from 'react' -import { useEventListener } from "../useEventListener" +import { useEventListener } from '../useEventListener' /** * Custom hook that tracks whether a DOM element is being hovered over. @@ -18,18 +18,24 @@ import { useEventListener } from "../useEventListener" * // Access the isHovered variable to determine if the button is being hovered over. * ``` */ -export function useHover(elementRef: RefObject): boolean { - const [value, setValue] = useState(false) +export function useHover( + elementRef: RefObject, +): boolean { + const [value, setValue] = useState(false) - const handleMouseEnter = () => { - setValue(true) - } - const handleMouseLeave = () => { - setValue(false) - } + const handleMouseEnter = () => { + setValue(true) + } + const handleMouseLeave = () => { + setValue(false) + } - useEventListener("mouseenter", handleMouseEnter, { element: elementRef as RefObject }) - useEventListener("mouseleave", handleMouseLeave, { element: elementRef as RefObject }) + useEventListener('mouseenter', handleMouseEnter, { + element: elementRef as RefObject, + }) + useEventListener('mouseleave', handleMouseLeave, { + element: elementRef as RefObject, + }) - return value + return value } diff --git a/packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.ts b/packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.ts index b88af000..0cccaebe 100644 --- a/packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.ts +++ b/packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.ts @@ -1,9 +1,15 @@ -import type { RefObject } from "react" +import type { RefObject } from 'react' -import { useEventListener } from "../useEventListener" +import { useEventListener } from '../useEventListener' /** Supported event types. */ -type EventType = "mousedown" | "mouseup" | "touchstart" | "touchend" | "focusin" | "focusout" +type EventType = + | 'mousedown' + | 'mouseup' + | 'touchstart' + | 'touchend' + | 'focusin' + | 'focusout' /** * Custom hook that handles clicks outside a specified element. @@ -24,29 +30,31 @@ type EventType = "mousedown" | "mouseup" | "touchstart" | "touchend" | "focusin" * ``` */ export function useOnClickOutside( - ref: RefObject | RefObject[], - handler: (event: MouseEvent | TouchEvent | FocusEvent) => void, - eventType: EventType = "mousedown", - eventListenerOptions: AddEventListenerOptions = {}, + ref: RefObject | RefObject[], + handler: (event: MouseEvent | TouchEvent | FocusEvent) => void, + eventType: EventType = 'mousedown', + eventListenerOptions: AddEventListenerOptions = {}, ): void { - useEventListener( - eventType, - (event) => { - const target = event.target as Node + useEventListener( + eventType, + event => { + const target = event.target as Node - // Do nothing if the target is not connected element with document - if (!target || !target.isConnected) { - return - } + // Do nothing if the target is not connected element with document + if (!target || !target.isConnected) { + return + } - const isOutside = Array.isArray(ref) - ? ref.filter((r) => Boolean(r.current)).every((r) => r.current && !r.current.contains(target)) - : ref.current && !ref.current.contains(target) + const isOutside = Array.isArray(ref) + ? ref + .filter(r => Boolean(r.current)) + .every(r => r.current && !r.current.contains(target)) + : ref.current && !ref.current.contains(target) - if (isOutside) { - handler(event) - } - }, - { options: eventListenerOptions }, - ) + if (isOutside) { + handler(event) + } + }, + { options: eventListenerOptions }, + ) } From c77ba5ff9bd96b6a1100964fdb952faef5d9517f Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Thu, 21 Nov 2024 14:53:39 -0300 Subject: [PATCH 15/30] fix: options at removeEventListener --- .../src/useEventListener/useEventListener.test.ts | 7 +++++-- .../usehooks-ts/src/useEventListener/useEventListener.ts | 6 +----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts index 7a47472c..0109a8f2 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts @@ -65,6 +65,7 @@ describe('useEventListener()', () => { expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, expect.any(Function), + options, ) }) @@ -90,16 +91,17 @@ describe('useEventListener()', () => { expect(refRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, expect.any(Function), + options, ) }) it('should bind/unbind the event listener to the document when document is provided', () => { const eventName = 'test-event' - const listener = vitest.fn() + const handler = vitest.fn() const options = undefined const { unmount } = renderHook(() => { - useEventListener(eventName, listener, { element: docRef, options }) + useEventListener(eventName, handler, { element: docRef, options }) }) expect(docAddEventListenerSpy).toHaveBeenCalledTimes(1) @@ -115,6 +117,7 @@ describe('useEventListener()', () => { expect(docRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, expect.any(Function), + options, ) }) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 0e090071..3e7dec22 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -92,11 +92,7 @@ function useEventListener< useEffect(() => { // Define the listening target - const targetElement: E | Window = config.element - ? 'current' in config.element - ? config.element.current ?? window - : config.element - : window + const targetElement: E | Window = config.element ? 'current' in config.element ? config.element.current ?? window : config.element : window if (!targetElement) return From 28b29ee53c2dffe82e532a105a13bc848a09072a Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Thu, 21 Nov 2024 14:57:29 -0300 Subject: [PATCH 16/30] refactor: better format & git blame --- packages/usehooks-ts/src/useHover/useHover.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/usehooks-ts/src/useHover/useHover.ts b/packages/usehooks-ts/src/useHover/useHover.ts index 9528391d..2e562113 100644 --- a/packages/usehooks-ts/src/useHover/useHover.ts +++ b/packages/usehooks-ts/src/useHover/useHover.ts @@ -30,12 +30,8 @@ export function useHover( setValue(false) } - useEventListener('mouseenter', handleMouseEnter, { - element: elementRef as RefObject, - }) - useEventListener('mouseleave', handleMouseLeave, { - element: elementRef as RefObject, - }) + useEventListener('mouseenter', handleMouseEnter, { element: elementRef as RefObject }) + useEventListener('mouseleave', handleMouseLeave, { element: elementRef as RefObject }) return value } From be628b17fec9aeaade6a2a8594ce274c54fe7b78 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Thu, 21 Nov 2024 14:59:32 -0300 Subject: [PATCH 17/30] chore: unnecessary import --- packages/usehooks-ts/src/useHover/useHover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/usehooks-ts/src/useHover/useHover.ts b/packages/usehooks-ts/src/useHover/useHover.ts index 2e562113..4aed2662 100644 --- a/packages/usehooks-ts/src/useHover/useHover.ts +++ b/packages/usehooks-ts/src/useHover/useHover.ts @@ -1,6 +1,6 @@ import { useState } from 'react' -import type { Ref, RefObject } from 'react' +import type { RefObject } from 'react' import { useEventListener } from '../useEventListener' From 09b9c7e8144a830929b014323aed76e0687a1346 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Thu, 21 Nov 2024 15:00:49 -0300 Subject: [PATCH 18/30] chore: prop check & cleaner blame --- packages/usehooks-ts/src/useEventListener/useEventListener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 3e7dec22..dda44aef 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -94,7 +94,7 @@ function useEventListener< // Define the listening target const targetElement: E | Window = config.element ? 'current' in config.element ? config.element.current ?? window : config.element : window - if (!targetElement) return + if (!(targetElement && targetElement.addEventListener)) return // Create event listener that calls handler function stored in ref const listener: EventListener = event => From 62add405783b17d9ac61fa675a60d52e6d810436 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Thu, 21 Nov 2024 15:05:12 -0300 Subject: [PATCH 19/30] typo --- packages/usehooks-ts/src/useEventListener/useEventListener.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index dda44aef..ebc43062 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -86,6 +86,7 @@ function useEventListener< ) { // Create a ref that stores handler const savedHandler = useRef(handler) + useIsomorphicLayoutEffect(() => { savedHandler.current = handler }, [handler]) From 6f54d09937eb500f577c2f2423ff13ecb45644de Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Thu, 21 Nov 2024 15:07:46 -0300 Subject: [PATCH 20/30] typo --- packages/usehooks-ts/src/useEventListener/useEventListener.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index ebc43062..afc5395f 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -98,8 +98,7 @@ function useEventListener< if (!(targetElement && targetElement.addEventListener)) return // Create event listener that calls handler function stored in ref - const listener: EventListener = event => - savedHandler.current(event as Parameters[0]) + const listener: EventListener = event => savedHandler.current(event as Parameters[0]) targetElement.addEventListener(eventName, listener, config.options) From bb6974e86ce0ba5f270714df02cf1f62a4190399 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Thu, 21 Nov 2024 15:23:13 -0300 Subject: [PATCH 21/30] chore: cleanup --- .../src/useEventListener/useEventListener.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index afc5395f..af74018f 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -17,10 +17,7 @@ type ElementToEventMap = { RTCDataChannel: [RTCDataChannel, RTCDataChannelEventMap] RTCPeerConnection: [RTCPeerConnection, RTCPeerConnectionEventMap] SpeechSynthesis: [SpeechSynthesis, SpeechSynthesisEventMap] - SpeechSynthesisUtterance: [ - SpeechSynthesisUtterance, - SpeechSynthesisUtteranceEventMap, - ] + SpeechSynthesisUtterance: [SpeechSynthesisUtterance, SpeechSynthesisUtteranceEventMap,] } /** Return `T` if `M` undefined or never */ @@ -28,15 +25,10 @@ type Fallback = [M] extends [undefined | never] ? T : M /** Return `EventMap` type of matching element ref (from config argument) * Intersected with `CustomEventMap` (from global declaration) - * Fallback to HTMLElement (if generic never or undefined) */ -type EventMapOf = Fallback< - { - [K in keyof ElementToEventMap]: E extends ElementToEventMap[K][0] - ? ElementToEventMap[K][1] & CustomEventMap - : never - }[keyof ElementToEventMap], - HTMLElement -> + * If Element not in map default Fallback to HTMLElement */ +type EventMapOf = Fallback<{ + [K in keyof ElementToEventMap]: E extends ElementToEventMap[K][0] ? ElementToEventMap[K][1] & CustomEventMap : never + }[keyof ElementToEventMap], HTMLElement> /** * Custom hook that attaches event listeners to DOM elements, the window, or media query lists. From fa3c3eeecd33df67858a158c96fcf247a2273e67 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Thu, 21 Nov 2024 17:23:55 -0300 Subject: [PATCH 22/30] chore: cleanup --- .../useEventListener.demo.tsx | 37 ++++--------------- .../useEventListener/useEventListener.test.ts | 3 -- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx index 53231d5a..15a43b1d 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx @@ -5,47 +5,24 @@ import { useEventListener } from './useEventListener' export default function Component() { // Define button ref const buttonRef = useRef(null) - const documentRef = useRef(document) + const documentRef = useRef(document) const speechSynthesis = new SpeechSynthesis() - const speechSynthesisRef = useRef(speechSynthesis) - - const onScroll = (event: Event) => { - console.log('window scrolled!', event) - } - - const onClick = (event: Event) => { - console.log('button clicked!', event) - } - - const onEventTarget = (event: Event) => { - console.log('speechSynthesis!', event) - } - - const onVisibilityChange = (event: Event) => { - console.log('doc visibility changed!', { - isVisible: !document.hidden, - event, - }) - } + const speechSynthesisRef = useRef(speechSynthesis) // example with window based event - useEventListener('scroll', onScroll) + useEventListener('scroll', ev => console.log('scrolled!', ev)) // example with document based event - useEventListener('visibilitychange', onVisibilityChange, { - element: documentRef, - }) + useEventListener('visibilitychange', ev => console.log('visibility!', ev), {element: documentRef }) // example with element based event - useEventListener('click', onClick, { element: buttonRef }) + useEventListener('click', ev => console.log('clicked!', ev.pageX), { element: buttonRef }) // example with EventTarget element based event - useEventListener('voiceschanged', onEventTarget, { - element: speechSynthesisRef, - }) + useEventListener('voiceschanged', ev => console.log('speech!', ev), { element: speechSynthesisRef }) // Example without ref - useEventListener('voiceschanged', onEventTarget, { element: speechSynthesis }) + useEventListener('voiceschanged', ev => console.log('speech!', ev), { element: speechSynthesis }) return (
diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts index 0109a8f2..7ee4da9b 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts @@ -61,7 +61,6 @@ describe('useEventListener()', () => { unmount() - expect(windowRemoveEventListenerSpy).toHaveBeenCalledTimes(1) expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, expect.any(Function), @@ -87,7 +86,6 @@ describe('useEventListener()', () => { unmount() - expect(refRemoveEventListenerSpy).toHaveBeenCalledTimes(1) expect(refRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, expect.any(Function), @@ -113,7 +111,6 @@ describe('useEventListener()', () => { unmount() - expect(docRemoveEventListenerSpy).toHaveBeenCalledTimes(1) expect(docRemoveEventListenerSpy).toHaveBeenCalledWith( eventName, expect.any(Function), From e00ca6f73acb8c5a920ff36f793fc099fb86b71e Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Fri, 22 Nov 2024 00:42:25 -0300 Subject: [PATCH 23/30] chore: improved pr diff --- .../useEventListener.demo.tsx | 23 +++++++++++++++---- .../useEventListener/useEventListener.test.ts | 1 - .../src/useEventListener/useEventListener.ts | 2 ++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx index 15a43b1d..960f106f 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx @@ -5,18 +5,33 @@ import { useEventListener } from './useEventListener' export default function Component() { // Define button ref const buttonRef = useRef(null) - const documentRef = useRef(document) + const documentRef = useRef(document) const speechSynthesis = new SpeechSynthesis() const speechSynthesisRef = useRef(speechSynthesis) + const onScroll = (event: Event) => { + console.log('window scrolled!', event) + } + + const onClick = (event: Event) => { + console.log('button clicked!', event) + } + + const onVisibilityChange = (event: Event) => { + console.log('doc visibility changed!', { + isVisible: !document.hidden, + event, + }) + } + // example with window based event - useEventListener('scroll', ev => console.log('scrolled!', ev)) + useEventListener('scroll', onScroll) // example with document based event - useEventListener('visibilitychange', ev => console.log('visibility!', ev), {element: documentRef }) + useEventListener('visibilitychange', onVisibilityChange, {element: documentRef }) // example with element based event - useEventListener('click', ev => console.log('clicked!', ev.pageX), { element: buttonRef }) + useEventListener('click', onClick, { element: buttonRef }) // example with EventTarget element based event useEventListener('voiceschanged', ev => console.log('speech!', ev), { element: speechSynthesisRef }) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts index 7ee4da9b..c49ff5ef 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts @@ -52,7 +52,6 @@ describe('useEventListener()', () => { useEventListener(eventName, handler) }) - expect(windowAddEventListenerSpy).toHaveBeenCalledTimes(1) expect(windowAddEventListenerSpy).toHaveBeenCalledWith( eventName, expect.any(Function), diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index af74018f..50393282 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -1,5 +1,7 @@ import { useEffect, useRef } from 'react' + import type { RefObject } from 'react' + import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect' // Recommended usage: move CustomEventMap to global declaration From fb1d7033642c1cbe76ad958dedc08ed2d2088524 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Fri, 22 Nov 2024 00:44:55 -0300 Subject: [PATCH 24/30] doc: demo shorter demo --- .../src/useEventListener/useEventListener.demo.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx index 960f106f..49b068dc 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx @@ -5,9 +5,7 @@ import { useEventListener } from './useEventListener' export default function Component() { // Define button ref const buttonRef = useRef(null) - const documentRef = useRef(document) - const speechSynthesis = new SpeechSynthesis() - const speechSynthesisRef = useRef(speechSynthesis) + const speechSynthesisRef = useRef(new SpeechSynthesis()) const onScroll = (event: Event) => { console.log('window scrolled!', event) @@ -28,7 +26,7 @@ export default function Component() { useEventListener('scroll', onScroll) // example with document based event - useEventListener('visibilitychange', onVisibilityChange, {element: documentRef }) + useEventListener('visibilitychange', onVisibilityChange, {element: document }) // or useRef(document) // example with element based event useEventListener('click', onClick, { element: buttonRef }) @@ -36,9 +34,6 @@ export default function Component() { // example with EventTarget element based event useEventListener('voiceschanged', ev => console.log('speech!', ev), { element: speechSynthesisRef }) - // Example without ref - useEventListener('voiceschanged', ev => console.log('speech!', ev), { element: speechSynthesis }) - return (
From 5c29d2e49c59939b61d47a2ae2e503b02f1e7ac7 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Fri, 22 Nov 2024 00:48:04 -0300 Subject: [PATCH 25/30] typo --- .../src/useEventListener/useEventListener.demo.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx index 49b068dc..f9861024 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx @@ -25,8 +25,8 @@ export default function Component() { // example with window based event useEventListener('scroll', onScroll) - // example with document based event - useEventListener('visibilitychange', onVisibilityChange, {element: document }) // or useRef(document) + // example with document based event (element prop supports ref or Element) + useEventListener('visibilitychange', onVisibilityChange, {element: document }) // example with element based event useEventListener('click', onClick, { element: buttonRef }) From 49732cdf84d58f5e0dce0acd91af01176c7015ca Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Fri, 22 Nov 2024 00:49:20 -0300 Subject: [PATCH 26/30] doc: unnecessary explanation --- .../usehooks-ts/src/useEventListener/useEventListener.demo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx index f9861024..64d07be1 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx @@ -25,7 +25,7 @@ export default function Component() { // example with window based event useEventListener('scroll', onScroll) - // example with document based event (element prop supports ref or Element) + // example with document based event useEventListener('visibilitychange', onVisibilityChange, {element: document }) // example with element based event From fdfde9b0169d5a1e2871c522cd19b9dba5b96c1c Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Fri, 22 Nov 2024 00:50:19 -0300 Subject: [PATCH 27/30] typo --- .../src/useEventListener/useEventListener.demo.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx index 64d07be1..f051b433 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx @@ -25,15 +25,15 @@ export default function Component() { // example with window based event useEventListener('scroll', onScroll) + // example with EventTarget element based event + useEventListener('voiceschanged', ev => console.log('speech!', ev), { element: speechSynthesisRef }) + // example with document based event useEventListener('visibilitychange', onVisibilityChange, {element: document }) // example with element based event useEventListener('click', onClick, { element: buttonRef }) - // example with EventTarget element based event - useEventListener('voiceschanged', ev => console.log('speech!', ev), { element: speechSynthesisRef }) - return (
From 19a5671a3d99ccacaf20f30404055e2af7a710db Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Fri, 22 Nov 2024 14:21:10 -0300 Subject: [PATCH 28/30] add: accept any element (only if eventmap generic) + fix: eventmap generic as interface --- .../src/useEventListener/useEventListener.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 50393282..6d82d528 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -19,11 +19,14 @@ type ElementToEventMap = { RTCDataChannel: [RTCDataChannel, RTCDataChannelEventMap] RTCPeerConnection: [RTCPeerConnection, RTCPeerConnectionEventMap] SpeechSynthesis: [SpeechSynthesis, SpeechSynthesisEventMap] - SpeechSynthesisUtterance: [SpeechSynthesisUtterance, SpeechSynthesisUtteranceEventMap,] + SpeechSynthesisUtterance: [SpeechSynthesisUtterance, SpeechSynthesisUtteranceEventMap] } +/** If A exists Return B else C */ +type ifGen = [A] extends [undefined | never] ? C : B; + /** Return `T` if `M` undefined or never */ -type Fallback = [M] extends [undefined | never] ? T : M +type Fallback = ifGen /** Return `EventMap` type of matching element ref (from config argument) * Intersected with `CustomEventMap` (from global declaration) @@ -63,9 +66,10 @@ type EventMapOf = Fallback<{ */ function useEventListener< /** Custom Event Map (optional generic)*/ - M extends Record | undefined = undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + M extends Record | undefined = undefined, /** Element Type of Optional refObject (defaults to Window) */ - E extends ElementToEventMap[keyof ElementToEventMap][0] = Window, + E extends ifGen = ifGen, /** eventName Key of type custom EventMap if present */ K extends keyof Fallback> = keyof Fallback>, >( From 99e36e26547820ffa4ee9de22089aa5fa541f3f4 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Fri, 22 Nov 2024 14:31:25 -0300 Subject: [PATCH 29/30] add: tests for EventTarget from PR #585 --- .../useEventListener/useEventListener.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts index c49ff5ef..69f0240f 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts @@ -19,6 +19,11 @@ declare global { interface DocumentEventMap { 'test-event': CustomEvent } + + /** Since TestTarget not at ElementsToEventMap, we need to use an EventMap generic override */ + interface TestTargetEventMap { + 'boundary': SpeechSynthesisEvent + } } const windowAddEventListenerSpy = vitest.spyOn(window, 'addEventListener') @@ -38,6 +43,14 @@ const docRemoveEventListenerSpy = vitest.spyOn( 'removeEventListener', ) +class TestTarget extends EventTarget {} +const testTarget = new TestTarget() +const targetAddEventListenerSpy = vitest.spyOn(testTarget, 'addEventListener') +const targetRemoveEventListenerSpy = vitest.spyOn( + testTarget, + 'removeEventListener', +) + describe('useEventListener()', () => { afterEach(() => { vitest.clearAllMocks() @@ -92,6 +105,31 @@ describe('useEventListener()', () => { ) }) + it('should bind/unbind the event listener to the EventTarget when EventTarget is provided', () => { + const eventName = 'boundary' + const handler = vitest.fn() + const options = undefined + + const { unmount } = renderHook(() => { + useEventListener("boundary", handler, { element: testTarget, options }) + }) + + expect(targetAddEventListenerSpy).toHaveBeenCalledTimes(1) + expect(targetAddEventListenerSpy).toHaveBeenCalledWith( + eventName, + expect.any(Function), + options, + ) + + unmount() + + expect(targetRemoveEventListenerSpy).toHaveBeenCalledWith( + eventName, + expect.any(Function), + options, + ) + }) + it('should bind/unbind the event listener to the document when document is provided', () => { const eventName = 'test-event' const handler = vitest.fn() From 5987504ac175b3e3bcc392a00b1d1ce1cb3b4a47 Mon Sep 17 00:00:00 2001 From: JimGitFE Date: Fri, 22 Nov 2024 14:44:56 -0300 Subject: [PATCH 30/30] add: more EventTargets & AudioNodes (template for more EventMap additions..) --- .../src/useEventListener/useEventListener.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.ts index 6d82d528..67e9f347 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.ts @@ -15,13 +15,31 @@ type ElementToEventMap = { Window: [Window, WindowEventMap] HTMLElement: [HTMLElement, HTMLElementEventMap] Document: [Document, DocumentEventMap] + // + // EventTargets + TextTrack: [TextTrack, TextTrackEventMap] + BaseAudioContext: [BaseAudioContext, BaseAudioContextEventMap] + BroadcastChannel: [BroadcastChannel, BroadcastChannelEventMap] + FileReader: [FileReader, FileReaderEventMap] + HTMLMediaElement: [HTMLMediaElement, HTMLMediaElementEventMap] MediaQueryList: [MediaQueryList, MediaQueryListEventMap] + Notification: [Notification, NotificationEventMap] + // + // ... add more elements here + // RTCDataChannel: [RTCDataChannel, RTCDataChannelEventMap] RTCPeerConnection: [RTCPeerConnection, RTCPeerConnectionEventMap] SpeechSynthesis: [SpeechSynthesis, SpeechSynthesisEventMap] SpeechSynthesisUtterance: [SpeechSynthesisUtterance, SpeechSynthesisUtteranceEventMap] + WebSocket: [WebSocket, WebSocketEventMap] + XMLHttpRequest: [XMLHttpRequest, XMLHttpRequestEventMap] + // + // Audio Nodes + AudioScheduledSourceNode: [AudioScheduledSourceNode, AudioScheduledSourceNodeEventMap] + AudioWorkletNode: [AudioWorkletNode, AudioWorkletNodeEventMap] + // + // ... add more elements here } - /** If A exists Return B else C */ type ifGen = [A] extends [undefined | never] ? C : B;