Created
July 12, 2024 15:37
-
-
Save ktmud/6b6d8fe64ca642f917efc08fb02e1fdc to your computer and use it in GitHub Desktop.
Useful hooks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useState } from 'react'; | |
/** | |
* Load data from an async function and update a state. | |
*/ | |
export default function useAsyncState<TValue>(loader: Promise<TValue> | (() => Promise<TValue>)) { | |
const [value, setValue] = useState<TValue>(); | |
useEffect(() => { | |
const promise = typeof loader === 'function' ? loader() : loader; | |
promise.then(setValue); | |
}, [loader]); | |
return value; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { RefObject } from 'react'; | |
import React, { useLayoutEffect, useRef } from 'react'; | |
type ElementOrRef = HTMLElement | RefObject<HTMLElement>; | |
type CSSSelector = string; | |
type ElementSelector = CSSSelector | ElementOrRef | null; | |
/** | |
* Type guard for excluding `null` and `undefined` values. | |
*/ | |
function isNotNullOrUndefined<T>(value: T | null | undefined): value is T { | |
return value != null; | |
} | |
export interface UseFocusedElementsOptions { | |
ignoreElements?: ElementSelector[]; | |
default?: ElementSelector; | |
root?: ElementSelector; | |
checkClick?: boolean; | |
} | |
const getElements = (elem?: ElementSelector, root?: ElementSelector): HTMLElement[] => { | |
const rootElem = root ? getElements(root)[0] : document.body; | |
if (typeof elem === 'string') { | |
return Array.from((rootElem || document).querySelectorAll(elem)); | |
} | |
return [elem && 'current' in elem ? elem.current : elem].filter(isNotNullOrUndefined); | |
}; | |
/** | |
* Find which elements from a list of elements have themselves or a descendant | |
* in focus. | |
* | |
* @param trackElements - list of child element to observe. Must be memoized. | |
* @param options.default - default focused element on initial load. | |
* @param options.root - the root element your elements are in. Provide to make | |
* sure hook is triggered when your elements are first mounted. Otherwise | |
* defaults document.body. | |
* @param options.checkClick - whether to check click event, too. If your track | |
* element is not focusable, use `checkClick` to detect a user's intention | |
* for "want to focus". | |
* @returns an array of elements in the trackElements that have focus. | |
*/ | |
export default function useFocusedElements( | |
trackElements: ElementSelector[], | |
{ | |
ignoreElements, | |
root = document.body, | |
checkClick: needToCheckClick = true, | |
default: defaultFocused, | |
}: UseFocusedElementsOptions = {}, | |
): HTMLElement[] | null { | |
const [elementsInFocus, setElementsInFocus_] = React.useState<HTMLElement[] | null>(null); | |
const candidateElementsRef = useRef<HTMLElement[]>(); | |
// Update state (which triggers renrender) in a timeout to give other event | |
// handlers registered by child components a chance to propagate. | |
const setElementsInFocus: typeof setElementsInFocus_ = React.useCallback((...args) => { | |
setTimeout(() => { | |
setElementsInFocus_(...args); | |
}, 10); | |
}, []); | |
useLayoutEffect(() => { | |
const rootContainer = getElements(root)[0]; | |
if (!rootContainer || trackElements.length < 1) { | |
return; | |
} | |
// find track element that is in focus | |
const findActiveTrackElements = (childOrSelf: Element | null) => { | |
// Only find candidate elements on first click | |
// We can't pre-query the elements on mount because they may have not been | |
// rendered yet (think lazily loaded components). | |
if (childOrSelf && childOrSelf !== document.body && rootContainer.contains(childOrSelf)) { | |
if (!candidateElementsRef.current) { | |
candidateElementsRef.current = trackElements | |
.map((selector) => getElements(selector, root)) | |
.flat(); | |
} | |
return candidateElementsRef.current.filter((elem) => { | |
return elem === childOrSelf || elem.contains(childOrSelf); | |
}); | |
} | |
return null; | |
}; | |
// Ignore clicks on elements that are in `ignoreElements`. | |
// | |
// This is useful when you want to show something on click, but do not | |
// want to trigger a re-render when users click on the conditionally | |
// displayed content. | |
const isIgnored = (elem: HTMLElement) => | |
(ignoreElements || []) | |
.map((selector) => getElements(selector)) | |
.flat() | |
.some((ignoreElem) => ignoreElem === elem || ignoreElem.contains(elem)); | |
let focusChecked = false; | |
const handleFocus = () => { | |
const focusedElem = document.activeElement as HTMLElement; | |
if (isIgnored(focusedElem)) { | |
return; | |
} | |
setElementsInFocus(findActiveTrackElements(focusedElem)); | |
focusChecked = true; | |
setTimeout(() => { | |
focusChecked = false; | |
}); | |
}; | |
const handleClick = (event: MouseEvent) => { | |
const clickedElem = event.target as HTMLElement; | |
if (isIgnored(clickedElem)) { | |
return; | |
} | |
if (!focusChecked) { | |
// don't check again if focus event already triggered, this skips | |
// an unnecessary re-rendering | |
setElementsInFocus(findActiveTrackElements(clickedElem)); | |
} | |
}; | |
const focusedElements = findActiveTrackElements(document.activeElement); | |
if (focusedElements) { | |
setElementsInFocus(focusedElements); | |
} | |
// Reset candidate elements whenever the tracking options changes. | |
// This forces the first focus/click to re-find the elements. | |
candidateElementsRef.current = undefined; | |
// `useCapture=true` to make sure we get the event before other handlers | |
// since they may preventDefault() and stopPropagation(). | |
document.addEventListener('focus', handleFocus, true); | |
if (needToCheckClick) { | |
document.addEventListener('click', handleClick, true); | |
} | |
return () => { | |
document.removeEventListener('focus', handleFocus, true); | |
if (needToCheckClick) { | |
document.removeEventListener('click', handleClick, true); | |
} | |
}; | |
}, [defaultFocused, ignoreElements, needToCheckClick, root, setElementsInFocus, trackElements]); | |
return elementsInFocus; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment