diff --git a/src/components/ColorInput/ColorInput.css b/src/components/ColorInput/ColorInput.css index 6c570a0..82861d3 100644 --- a/src/components/ColorInput/ColorInput.css +++ b/src/components/ColorInput/ColorInput.css @@ -20,6 +20,7 @@ -moz-user-select: none; -webkit-user-select: none; pointer-events: none; + text-transform: uppercase; } input.color-input { diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 0000000..096ee94 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,16 @@ +import { DependencyList, useEffect } from "react"; +import useTimeoutFn from "./useTimeoutFn"; + +export type UseDebounceReturn = [() => boolean | null, () => void]; + +export default function useDebounce( + fn: Function, + ms: number = 0, + deps: DependencyList = [] +): UseDebounceReturn { + const [isReady, cancel, reset] = useTimeoutFn(fn, ms); + + useEffect(reset, deps); + + return [isReady, cancel]; +} diff --git a/src/hooks/useThrottle.ts b/src/hooks/useThrottle.ts new file mode 100644 index 0000000..8b953ab --- /dev/null +++ b/src/hooks/useThrottle.ts @@ -0,0 +1,37 @@ +import { useEffect, useRef, useState } from "react"; +import useUnmount from "./useUnmount"; + +const useThrottle = (value: T, ms: number = 200) => { + const [state, setState] = useState(value); + const timeout = useRef>(); + const nextValue = useRef(null) as any; + const hasNextValue = useRef(0) as any; + + useEffect(() => { + if (!timeout.current) { + setState(value); + const timeoutCallback = () => { + if (hasNextValue.current) { + hasNextValue.current = false; + setState(nextValue.current); + timeout.current = setTimeout(timeoutCallback, ms); + } else { + timeout.current = undefined; + } + }; + timeout.current = setTimeout(timeoutCallback, ms); + } else { + nextValue.current = value; + hasNextValue.current = true; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + useUnmount(() => { + timeout.current && clearTimeout(timeout.current); + }); + + return state; +}; + +export default useThrottle; diff --git a/src/hooks/useTimeoutFn.ts b/src/hooks/useTimeoutFn.ts new file mode 100644 index 0000000..f2f617a --- /dev/null +++ b/src/hooks/useTimeoutFn.ts @@ -0,0 +1,44 @@ +import { useCallback, useEffect, useRef } from "react"; + +export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void]; + +export default function useTimeoutFn( + fn: Function, + ms: number = 0 +): UseTimeoutFnReturn { + const ready = useRef(false); + const timeout = useRef>(); + const callback = useRef(fn); + + const isReady = useCallback(() => ready.current, []); + + const set = useCallback(() => { + ready.current = false; + timeout.current && clearTimeout(timeout.current); + + timeout.current = setTimeout(() => { + ready.current = true; + callback.current(); + }, ms); + }, [ms]); + + const clear = useCallback(() => { + ready.current = null; + timeout.current && clearTimeout(timeout.current); + }, []); + + // update ref when function changes + useEffect(() => { + callback.current = fn; + }, [fn]); + + // set on mount, clear on unmount + useEffect(() => { + set(); + + return clear; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ms]); + + return [isReady, clear, set]; +} diff --git a/src/hooks/useUnmount.ts b/src/hooks/useUnmount.ts new file mode 100644 index 0000000..3f2b436 --- /dev/null +++ b/src/hooks/useUnmount.ts @@ -0,0 +1,12 @@ +import { useRef, useEffect } from "react"; + +const useUnmount = (fn: () => any): void => { + const fnRef = useRef(fn); + + // update the ref each render so if it change the newest callback will be invoked + fnRef.current = fn; + + useEffect(() => () => fnRef.current(), []); +}; + +export default useUnmount;