Skip to content

Commit

Permalink
Merge pull request #2 from konstantin-lukas/useEvent
Browse files Browse the repository at this point in the history
Use event
  • Loading branch information
konstantin-lukas authored Sep 1, 2024
2 parents 5d51f88 + ff45d70 commit a330a82
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 27 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ communicates intention.
- <b>useToggle:</b> Provides a simple boolean toggle that does not persist between page reloads.
- <b>useIntersectionObserver:</b> Provides a wrapper around the IntersectionObserver API, allowing you to test if an
element is currently visible on screen.
- <b>useEvent:</b> Encapsulates the code needed to correctly listen to events in React, including event listener
cleanup.

## Installation
Anzol is available on the NPM registry. To install it, just run:
Expand All @@ -43,8 +45,6 @@ suggestions.
user clicks anywhere outside the given element.
- <b>useLocalStorage:</b> Provides access to local storage, with the additional option to update all usages of this hook
when local storage changes.
- <b>useEvent:</b> Encapsulates the code needed to correctly listen to events in React, including event listener
cleanup.
- <b>usePreferredScheme:</b> Listens for changes in the user's preferred scheme and returns it.
- <b>useDarkMode:</b> Similar to usePreferredScheme but allows setting the user scheme manually and automatically
updates it when the preferred scheme changes. Uses local storage to save the chosen scheme across reloads.
Expand Down
4 changes: 2 additions & 2 deletions examples/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import DemoUseIntersectionObserver from "./components/demoUseIntersectionObserver";
import DemoUseEvent from "./components/demoUseEvent";

const App: React.FC = () => {
return (
<DemoUseIntersectionObserver/>
<DemoUseEvent/>
);
};

Expand Down
31 changes: 31 additions & 0 deletions examples/components/demoUseEvent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, {useEffect} from "react";
import { useEvent } from "../../src";

const DemoUseEvent = () => {
const clickTarget = useEvent<HTMLDivElement>("click", (e) => {
const t = (e.target as HTMLDivElement);
t.style.backgroundColor = t.style.backgroundColor === "red" ? "green" : "red";
});

const windowTarget = useEvent("scroll", _ => console.log("scroll"));
useEffect(() => {
windowTarget(document);
}, [windowTarget]);

return (
<div
ref={clickTarget}
style={{
padding: 50,
backgroundColor: "green",
textAlign: "center",
display: "inline-block",
height: "150vh",
}}
>
Click me :)
</div>
);
};

export default DemoUseEvent;
3 changes: 2 additions & 1 deletion examples/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import React from 'react';
import App from './App';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
// @ts-ignore
const root = createRoot(document.getElementById("root"));
root.render(<App />);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "anzol",
"version": "2.1.12",
"version": "2.2.0",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
Expand Down
61 changes: 61 additions & 0 deletions src/hooks/useEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect, useState } from "react";

/**
* Provides a wrapper around the EventListener API. Use the return value to define the event target.
* @param type - {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#type See type on mdn web docs}
* @param listener - {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#listener See listener on mdn web docs}
* @param options - {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options See options on mdn web docs}
* @param options.catpure - {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#capture See capture on mdn web docs}
* @param options.once - {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#once See once on mdn web docs}
* @param options.passive - {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#passive See passive on mdn web docs}
* @param options.signal - {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#signal See signal on mdn web docs}
* @template T - The type of the event target.
*
* @return {@link https://react.dev/reference/react/useState SetStateAction} - A function which is used to assign the event target for convenience. The returned
* value can be used like a ref when the target is an HTMLElement. If the target is the {@link window} or
* {@link document}, you can use it like a regular {@link https://react.dev/reference/react/useState useState} setter.
*
*
* @example
* ```tsx
* const DemoUseEvent = () => {
* const clickTarget = useEvent<HTMLDivElement>("click", (e) => {
* const t = (e.target as HTMLDivElement);
* t.style.backgroundColor = t.style.backgroundColor === "red" ? "green" : "red";
* });
*
* const windowTarget = useEvent("scroll", _ => console.log("scroll"));
* useEffect(() => windowTarget(document), [windowTarget]);
*
* return (
* <div
* ref={clickTarget}
* style={{ height: "150vh" }}
* >
* Click me :)
* </div>
* );
* };
* ```
*/
function useEvent<T extends EventTarget>(
type: string,
listener: EventListener,
options?: {
capture?: boolean,
once?: boolean,
passive?: boolean,
signal?: AbortSignal,
},
) {
const [target, setTarget] = useState<T | null>(null);
useEffect(() => {
if (target) {
target.addEventListener(type, listener, options);
return () => target.removeEventListener(type, listener, options);
}
}, [type, listener, target, options]);
return setTarget;
}

export default useEvent;
6 changes: 3 additions & 3 deletions src/hooks/useFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ export interface FetchOptions {
* assertions on the returned data. Here's a list of what parseTypes result in which return types:
* - "html": {@link Document}
* - "xml", "svg": {@link XMLDocument}
* - "text": {@link string}
* - "text": string
* - "response": {@link Response}
* - "json" (default): {@link any} - When parsing JSON it is generally recommended to pass a custom type instead.
* - "json" (default): any - When parsing JSON it is generally recommended to pass a custom type instead.
*
* @param url - The resource to fetch
* @param {FetchOptions} options - Allows you to configure how useFetch makes and returns requests
* @return FetchResult - An object containing information about the result of a fetch request
* @return {@link FetchResult} - An object containing information about the result of a fetch request
*
* @example
* ```tsx
Expand Down
15 changes: 3 additions & 12 deletions src/hooks/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import { type RefObject, useEffect, useRef, useState } from "react";

export interface IntersectionObserverOptions {
/** An Element or Document object which is an ancestor of the intended target, whose bounding rectangle
* will be considered the viewport. Any part of the target not visible in the visible area of the root is not considered
* visible. Default is null. Setting the root to null, makes the hook observe intersections with the viewport. */
/** Read on {@link https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#root mdn web docs}. */
root?: Element | Document | null,
/** A string which specifies a set of offsets to add to the root's bounding_box when
* calculating intersections, effectively shrinking or growing the root for calculation purposes. The syntax is
* approximately the same as that for the CSS margin property; see The intersection root and root margin for more
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".*/
/** Read on {@link https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#rootmargin mdn web docs}. */
rootMargin?: string,

/** Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of
* intersection area to total bounding box area for the observed target. A value of 0.0 means that even a single visible
* pixel counts as the target being visible. 1.0 means that the entire target element is visible. See Thresholds for a
* more in-depth description of how thresholds are used. The default is a threshold of 0.0. */
/** Read on {@link https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#threshold mdn web docs}. */
threshold?: number | number[],
}

Expand Down
39 changes: 33 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
export { default as useFetch, FetchResult, FetchResultData, ParseType, FetchOptions } from "./hooks/useFetch";
export { default as useDefer } from "./hooks/useDefer";
export { default as useFirstRender } from "./hooks/useFirstRender";
export { default as useToggle } from "./hooks/useToggle";
export { default as useIntersectionObserver, IntersectionObserverOptions } from "./hooks/useIntersectionObserver";
export { default as useIntersectionObserverArray, IntersectionObserverArrayOptions } from "./hooks/useIntersectionObserverArray";
export {
default as useFetch,
type FetchResult,
type FetchResultData,
type ParseType,
type FetchOptions,
} from "./hooks/useFetch";

export {
default as useDefer,
} from "./hooks/useDefer";

export {
default as useFirstRender,
} from "./hooks/useFirstRender";

export {
default as useToggle,
} from "./hooks/useToggle";

export {
default as useIntersectionObserver,
type IntersectionObserverOptions,
} from "./hooks/useIntersectionObserver";

export {
default as useIntersectionObserverArray,
type IntersectionObserverArrayOptions,
} from "./hooks/useIntersectionObserverArray";

export {
default as useEvent,
} from "./hooks/useEvent";
60 changes: 60 additions & 0 deletions tests/useEvent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEvent } from "../src";
import { fireEvent, render } from "@testing-library/react";
import { screen } from "@testing-library/dom";
import React, { useEffect } from "react";

function ElementEventComponent({ event }: { event: string }) {
const target = useEvent<HTMLDivElement>(event, (e) => {
const t = (e.target as HTMLDivElement);
t.style.backgroundColor = t.style.backgroundColor === "green" ? "red" : "green";
});
return (
<div ref={target} data-testid="target-div">
Hello, world!
</div>
);
}

function WindowEventComponent({ event, spyFunction }: { event: string, spyFunction: () => void }) {
const target = useEvent(event, spyFunction);
useEffect(() => {
target(window);
}, [target]);
return <></>;
}

test("should register events on elements and deregister them when props change", () => {

const { rerender } = render(<ElementEventComponent event="mouseover"/>);
const target = screen.getByTestId("target-div");

expect(target.style.backgroundColor).not.toBe("red");
expect(target.style.backgroundColor).not.toBe("green");
fireEvent.mouseOver(target, { target });
expect(target.style.backgroundColor).toBe("green");

rerender(<ElementEventComponent event="click"/>);
fireEvent.mouseOver(target, { target });
expect(target.style.backgroundColor).toBe("green");
fireEvent.click(target, { target });
expect(target.style.backgroundColor).toBe("red");

});

test("should register events on the window and deregister them when props change", () => {

const spyFunction = jest.fn();
const { rerender } = render(<WindowEventComponent event="mouseover" spyFunction={ spyFunction }/>);
const target = window;

expect(spyFunction).toHaveBeenCalledTimes(0);
fireEvent.mouseOver(target, { target });
expect(spyFunction).toHaveBeenCalledTimes(1);

rerender(<WindowEventComponent event="click" spyFunction={ spyFunction }/>);
fireEvent.mouseOver(target, { target });
expect(spyFunction).toHaveBeenCalledTimes(1);
fireEvent.click(target, { target });
expect(spyFunction).toHaveBeenCalledTimes(2);

});

0 comments on commit a330a82

Please sign in to comment.