From 8d23e708cd96c6d0d432ad290a5e0968aa1b74e7 Mon Sep 17 00:00:00 2001 From: Winton DeShong Date: Mon, 20 Jul 2020 17:25:44 -0400 Subject: [PATCH 1/2] Added new hooks to exports --- src/enumerations/window-events.ts | 3 ++ src/hooks/make-cancellable.ts | 37 +++++++++++++++++++++ src/hooks/use-debounce.ts | 17 ++++++++++ src/hooks/use-onclick-outside.ts | 54 +++++++++++++++++++++++++++++++ src/hooks/use-page-errors.ts | 29 +++++++++++++++++ src/hooks/use-text-overflow.ts | 30 +++++++++++++++++ src/hooks/use-window.ts | 33 +++++++++++++++++++ src/index.ts | 6 ++++ 8 files changed, 209 insertions(+) create mode 100644 src/enumerations/window-events.ts create mode 100644 src/hooks/make-cancellable.ts create mode 100644 src/hooks/use-debounce.ts create mode 100644 src/hooks/use-onclick-outside.ts create mode 100644 src/hooks/use-page-errors.ts create mode 100644 src/hooks/use-text-overflow.ts create mode 100644 src/hooks/use-window.ts diff --git a/src/enumerations/window-events.ts b/src/enumerations/window-events.ts new file mode 100644 index 0000000..9b2dfee --- /dev/null +++ b/src/enumerations/window-events.ts @@ -0,0 +1,3 @@ +export enum WindowEvents { + Resize = "resize", +} diff --git a/src/hooks/make-cancellable.ts b/src/hooks/make-cancellable.ts new file mode 100644 index 0000000..bf61582 --- /dev/null +++ b/src/hooks/make-cancellable.ts @@ -0,0 +1,37 @@ +import { PromiseFactory } from "andculturecode-javascript-core"; + +// --------------------------------------------------------- +// #region Public Methods +// --------------------------------------------------------- + +/** + * Wrap the provided promise in a promise that intercepts cancellation requests + */ +const makeCancellable = (promise: Promise) => { + let isCanceled = false; + + const wrappedPromise = new Promise((resolve, reject) => + promise + .then((value: any) => + isCanceled ? PromiseFactory.pending() : resolve(value) + ) + .catch((error: any) => + isCanceled ? PromiseFactory.pending() : reject(error) + ) + ); + + return { + promise: wrappedPromise, + cancel() { + isCanceled = true; + }, + }; +}; + +// #endregion Public Methods + +// --------------------------------------------------------- +// Exports +// --------------------------------------------------------- + +export { makeCancellable }; diff --git a/src/hooks/use-debounce.ts b/src/hooks/use-debounce.ts new file mode 100644 index 0000000..bf96f74 --- /dev/null +++ b/src/hooks/use-debounce.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; + +/** + * Throttles the supplied value for a set amount of milliseconds + * @param value + * @param delay number of milliseconds to delay + */ +export function useDebounce(value: T, delay: number = 200) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(handler); + }, [value, delay]); + + return debouncedValue; +} diff --git a/src/hooks/use-onclick-outside.ts b/src/hooks/use-onclick-outside.ts new file mode 100644 index 0000000..91ec6fd --- /dev/null +++ b/src/hooks/use-onclick-outside.ts @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from "react"; + +/** + * Custom hook providing utility to take some action when a mouse event is fired outside of an element. + * @param ref + * @param handler + * @param deps + */ +export function useOnClickOutside( + ref: React.RefObject, + handler: () => void, + deps?: React.DependencyList | undefined +) { + // Ensure we only attach one event + const [hasEvent, setHasEvent] = useState(false); + + useEffect(() => { + const layout = document.getElementById("root"); + if (hasEvent || layout == null) { + return; + } + + const event = (e: Event) => { + handleClickOutside(e); + toggleEvent("remove", event); + }; + + const handleClickOutside = (event: Event) => { + if ( + ref?.current == null || + ref.current.contains(event.target as Node) + ) { + return true; + } + + handler(); + }; + + const toggleEvent = ( + action: "add" | "remove", + e: (e: Event) => void + ) => { + const fn = + action === "add" + ? layout.addEventListener + : layout.removeEventListener; + + fn("mousedown", e); + setHasEvent(action === "add"); + }; + + toggleEvent("add", event); + }, [deps, handler, hasEvent, ref]); +} diff --git a/src/hooks/use-page-errors.ts b/src/hooks/use-page-errors.ts new file mode 100644 index 0000000..84bde5c --- /dev/null +++ b/src/hooks/use-page-errors.ts @@ -0,0 +1,29 @@ +import { useState, useCallback } from "react"; +import { ResultRecord } from "andculturecode-javascript-core"; + +/** + * Hook to bundle common page error handling functionality + */ +export function usePageErrors() { + const [pageErrors, setPageErrors] = useState>([]); + + const handlePageLoadError = useCallback((result: any) => { + if (result instanceof ResultRecord) { + setPageErrors((e) => [...e, ...result.listErrorMessages()]); + return; + } + + setPageErrors((e) => [...e, result]); + }, []); + + const resetPageErrors = useCallback(() => { + setPageErrors([]); + }, []); + + return { + handlePageLoadError, + pageErrors, + resetPageErrors, + setPageErrors, + }; +} diff --git a/src/hooks/use-text-overflow.ts b/src/hooks/use-text-overflow.ts new file mode 100644 index 0000000..1b93bfe --- /dev/null +++ b/src/hooks/use-text-overflow.ts @@ -0,0 +1,30 @@ +import { RefObject, useCallback, useEffect, useState } from "react"; +import { useWindow } from "./use-window"; + +/** + * A custom hook for determining if elements overflow their containers. + * Useful for when you have text-overflow: ellipsis; and you want to + * detect whether the text is actually long enough to trigger the ellipsis + * to appear. + * @param ref + */ +export function useTextOverflow(ref: RefObject) { + const getIsOverflowed = useCallback((): boolean => { + if (ref.current == null) { + return false; + } + + return ref.current.offsetWidth < ref.current.scrollWidth; + }, [ref]); + + const { width, height } = useWindow(); + const [isOverflowed, setIsOverflowed] = useState(getIsOverflowed()); + + useEffect(() => setIsOverflowed(getIsOverflowed()), [ + getIsOverflowed, + width, + height, + ]); + + return isOverflowed; +} diff --git a/src/hooks/use-window.ts b/src/hooks/use-window.ts new file mode 100644 index 0000000..f264ff0 --- /dev/null +++ b/src/hooks/use-window.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from "react"; +import { WindowEvents } from "../enumerations/window-events"; + +/** + * Get window properties. + * Hook into window events and make properties more easily accessible to components. + */ +export function useWindow() { + const [height, setHeight] = useState(window.innerHeight); + const [width, setWidth] = useState(window.innerWidth); + + useEffect(() => { + const handleWindowResize = () => { + setWidth(window.innerWidth); + setHeight(window.innerHeight); + }; + + const toggleEvent = (action: "add" | "remove") => { + const fn = + action === "add" + ? window.addEventListener + : window.removeEventListener; + + fn(WindowEvents.Resize, handleWindowResize); + }; + + toggleEvent("add"); + + return () => toggleEvent("remove"); + }, []); + + return { width, height }; +} diff --git a/src/index.ts b/src/index.ts index 6b0c00d..6ec0429 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,8 +26,14 @@ export { Redirects, RedirectsProps } from "./components/routing/redirects"; // #region Hooks // ----------------------------------------------------------------------------------------- +export { makeCancellable } from "./hooks/make-cancellable"; export { useCancellablePromise } from "./hooks/use-cancellable-promise"; +export { useDebounce } from "./hooks/use-debounce"; export { useLocalization } from "./hooks/use-localization"; +export { useOnClickOutside } from "./hooks/use-onclick-outside"; +export { usePageErrors } from "./hooks/use-page-errors"; +export { useTextOverflow } from "./hooks/use-text-overflow"; +export { useWindow } from "./hooks/use-window"; // #endregion From 35a527a44950e8fcdbf88c007d1906befa8144d2 Mon Sep 17 00:00:00 2001 From: Winton DeShong Date: Mon, 20 Jul 2020 17:33:02 -0400 Subject: [PATCH 2/2] Added stubbed test files with related github issues --- src/hooks/use-debounce.test.tsx | 3 +++ src/hooks/use-onclick-outside.test.tsx | 3 +++ src/hooks/use-page-errors.test.tsx | 3 +++ src/hooks/use-text-overflow.test.tsx | 3 +++ src/hooks/use-window.test.tsx | 3 +++ 5 files changed, 15 insertions(+) create mode 100644 src/hooks/use-debounce.test.tsx create mode 100644 src/hooks/use-onclick-outside.test.tsx create mode 100644 src/hooks/use-page-errors.test.tsx create mode 100644 src/hooks/use-text-overflow.test.tsx create mode 100644 src/hooks/use-window.test.tsx diff --git a/src/hooks/use-debounce.test.tsx b/src/hooks/use-debounce.test.tsx new file mode 100644 index 0000000..2f1cc2e --- /dev/null +++ b/src/hooks/use-debounce.test.tsx @@ -0,0 +1,3 @@ +describe("useDebounce", () => { + test.skip("TODO - https://github.com/AndcultureCode/AndcultureCode.JavaScript.React/issues/20", () => {}); +}); diff --git a/src/hooks/use-onclick-outside.test.tsx b/src/hooks/use-onclick-outside.test.tsx new file mode 100644 index 0000000..27fd9e5 --- /dev/null +++ b/src/hooks/use-onclick-outside.test.tsx @@ -0,0 +1,3 @@ +describe("useOnclickOutside", () => { + test.skip("TODO - https://github.com/AndcultureCode/AndcultureCode.JavaScript.React/issues/21", () => {}); +}); diff --git a/src/hooks/use-page-errors.test.tsx b/src/hooks/use-page-errors.test.tsx new file mode 100644 index 0000000..511fdae --- /dev/null +++ b/src/hooks/use-page-errors.test.tsx @@ -0,0 +1,3 @@ +describe("usePageErrors", () => { + test.skip("TODO - https://github.com/AndcultureCode/AndcultureCode.JavaScript.React/issues/22", () => {}); +}); diff --git a/src/hooks/use-text-overflow.test.tsx b/src/hooks/use-text-overflow.test.tsx new file mode 100644 index 0000000..2516ffa --- /dev/null +++ b/src/hooks/use-text-overflow.test.tsx @@ -0,0 +1,3 @@ +describe("useTextOverflow", () => { + test.skip("TODO - https://github.com/AndcultureCode/AndcultureCode.JavaScript.React/issues/23", () => {}); +}); diff --git a/src/hooks/use-window.test.tsx b/src/hooks/use-window.test.tsx new file mode 100644 index 0000000..28605e3 --- /dev/null +++ b/src/hooks/use-window.test.tsx @@ -0,0 +1,3 @@ +describe("useWindow", () => { + test.skip("TODO - https://github.com/AndcultureCode/AndcultureCode.JavaScript.React/issues/24", () => {}); +});