mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 18:25:30 +00:00
173 lines
4.5 KiB
TypeScript
173 lines
4.5 KiB
TypeScript
import {
|
|
useState,
|
|
useEffect,
|
|
useCallback,
|
|
useLayoutEffect,
|
|
useRef,
|
|
} from "react";
|
|
import { useDispatch, useSelector } from "react-redux";
|
|
import * as Utils from "./utils";
|
|
import { Action, Thunk, AppState } from "./types";
|
|
|
|
interface Size {
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export function useUnmountedRef(): { current: boolean } {
|
|
const unmountedRef = useRef(false);
|
|
useEffect(() => {
|
|
return () => {
|
|
unmountedRef.current = true;
|
|
};
|
|
}, []);
|
|
return unmountedRef;
|
|
}
|
|
|
|
export function usePromiseValueOrNull<T>(propValue: Promise<T>): T | null {
|
|
const [value, setValue] = useState<T | null>(null);
|
|
useEffect(() => {
|
|
let disposed = false;
|
|
propValue.then(resolvedValue => {
|
|
if (disposed) {
|
|
return;
|
|
}
|
|
setValue(resolvedValue);
|
|
});
|
|
|
|
return () => {
|
|
disposed = true;
|
|
};
|
|
}, [propValue]);
|
|
|
|
return value;
|
|
}
|
|
|
|
export function useScreenSize() {
|
|
const [size] = useState<Size>(Utils.getScreenSize());
|
|
// TODO: We could subscribe to screen size changes.
|
|
return size;
|
|
}
|
|
|
|
export function useWindowSize() {
|
|
const [size, setSize] = useState<Size>(Utils.getWindowSize());
|
|
const handler = useCallback(
|
|
Utils.throttle(() => {
|
|
setSize(Utils.getWindowSize());
|
|
}, 100) as () => void,
|
|
[]
|
|
);
|
|
useEffect(() => {
|
|
window.addEventListener("resize", handler);
|
|
return () => {
|
|
window.removeEventListener("resize", handler);
|
|
};
|
|
}, [handler]);
|
|
return size;
|
|
}
|
|
|
|
const cursorPositionRef = { current: { pageX: 0, pageY: 0 } };
|
|
window.document.addEventListener("mousemove", ({ pageX, pageY }) => {
|
|
cursorPositionRef.current = { pageX, pageY };
|
|
});
|
|
|
|
// We use a single global event listener because there is no way to get the
|
|
// mouse position aside from an event. Ideally we could create/clean up the
|
|
// event listener in the hook, but in the case where we want to check the cursor
|
|
// position on mount, that we wouldn't have had time to capture an event.
|
|
function useCursorPositionRef() {
|
|
return cursorPositionRef;
|
|
}
|
|
|
|
// CSS hover state is not respected if the cursor is already over the node when
|
|
// it is added to the DOM. This hook allows your component to know its hover
|
|
// state on mount without waiting for the mouse to move.
|
|
// https://stackoverflow.com/a/13259049/1263117
|
|
export function useIsHovered() {
|
|
const cursorRef = useCursorPositionRef();
|
|
const [hover, setHover] = useState(false);
|
|
const [node, setNode] = useState<HTMLElement | null>(null);
|
|
|
|
useLayoutEffect(() => {
|
|
if (node == null) {
|
|
setHover(false);
|
|
return;
|
|
}
|
|
const domRect = node.getBoundingClientRect();
|
|
const { pageX, pageY } = cursorRef.current;
|
|
setHover(
|
|
pageX >= domRect.left &&
|
|
pageX <= domRect.right &&
|
|
pageY >= domRect.top &&
|
|
pageY <= domRect.bottom
|
|
);
|
|
|
|
const enter = () => setHover(true);
|
|
const leave = () => setHover(false);
|
|
node.addEventListener("mouseenter", enter);
|
|
node.addEventListener("mouseleave", leave);
|
|
|
|
return () => {
|
|
node.removeEventListener("mouseenter", enter);
|
|
node.removeEventListener("mouseleave", leave);
|
|
};
|
|
}, [node, cursorRef]);
|
|
|
|
return { ref: setNode, hover };
|
|
}
|
|
|
|
export function useOnClickAway(
|
|
ref: Element | null,
|
|
callback: null | (() => void)
|
|
) {
|
|
useEffect(() => {
|
|
if (ref == null || callback == null) {
|
|
return;
|
|
}
|
|
|
|
const handleClickOut = (ee: MouseEvent) => {
|
|
const clickOutTarget = ee.target;
|
|
if (!(clickOutTarget instanceof Element)) {
|
|
// TypeScript doesn't realize this will always be true
|
|
return;
|
|
}
|
|
if (ref.contains(clickOutTarget)) {
|
|
return;
|
|
}
|
|
// If the click is _not_ inside the menu.
|
|
callback();
|
|
window.document.removeEventListener("click", handleClickOut, {
|
|
capture: true,
|
|
});
|
|
};
|
|
|
|
window.document.addEventListener("click", handleClickOut, {
|
|
capture: true,
|
|
});
|
|
|
|
return () => {
|
|
window.document.removeEventListener("click", handleClickOut, {
|
|
capture: true,
|
|
});
|
|
};
|
|
}, [ref, callback]);
|
|
}
|
|
|
|
// TODO: Return useSelector directly and apply the type without wrapping
|
|
export function useTypedSelector<T>(selector: (state: AppState) => T): T {
|
|
return useSelector(selector);
|
|
}
|
|
|
|
export function useActionCreator<T extends (...args: any[]) => Action | Thunk>(
|
|
actionCreator: T
|
|
): (...funcArgs: Parameters<T>) => void {
|
|
const dispatch = useDispatch();
|
|
return useCallback((...args) => dispatch(actionCreator(...args)), [
|
|
dispatch,
|
|
actionCreator,
|
|
]);
|
|
}
|
|
|
|
export function useTypedDispatch(): (action: Action | Thunk) => void {
|
|
return useDispatch();
|
|
}
|