UNPKG

ariakit-react-utils

Version:

Ariakit React utils

358 lines (336 loc) 10.1 kB
import * as React from 'react'; import { useLayoutEffect, useEffect, useState, useRef, useCallback, useMemo, useReducer } from 'react'; import { canUseDOM } from 'ariakit-utils/dom'; import { addGlobalEventListener } from 'ariakit-utils/events'; import { applyState } from 'ariakit-utils/misc'; import { setRef } from './misc.js'; // @ts-ignore Access React v18 hooks using string concatenation in order to // prevent Webpack from inferring that they are not present in React v17. For // example, React.useId will raise a compile time error when using React v17, // but React['use' + 'Id'] will not. const useReactId = React["use" + "Id"]; // @ts-ignore const useReactDeferredValue = React["use" + "DeferredValue"]; // @ts-ignore const useInsertionEffect = React["use" + "InsertionEffect"]; /** * `React.useLayoutEffect` that fallbacks to `React.useEffect` on server side. */ const useSafeLayoutEffect = canUseDOM ? useLayoutEffect : useEffect; /** * Returns a value that never changes even if the argument is updated. * @example * function Component({ prop }) { * const initialProp = useInitialValue(prop); * } */ function useInitialValue(value) { const [initialValue] = useState(value); return initialValue; } /** * Returns a value that is lazily initiated and never changes. * @example * function Component() { * const set = useLazyValue(() => new Set()); * } */ function useLazyValue(init) { const ref = useRef(); if (ref.current === undefined) { ref.current = init(); } return ref.current; } /** * Creates a `React.RefObject` that is constantly updated with the incoming * value. * @example * function Component({ prop }) { * const propRef = useLiveRef(prop); * } */ function useLiveRef(value) { const ref = useRef(value); useSafeLayoutEffect(() => { ref.current = value; }); return ref; } /** * Keeps the reference of the previous value to be used in the render phase. */ function usePreviousValue(value) { const [previousValue, setPreviousValue] = useState(value); if (value !== previousValue) { setPreviousValue(value); } return previousValue; } /** * Creates a stable callback function that has access to the latest state and * can be used within event handlers and effect callbacks. Throws when used in * the render phase. * @example * function Component(props) { * const onClick = useEvent(props.onClick); * React.useEffect(() => {}, [onClick]); * } */ function useEvent(callback) { const ref = useRef(() => { throw new Error("Cannot call an event handler while rendering."); }); if (useInsertionEffect) { useInsertionEffect(() => { ref.current = callback; }); } else { ref.current = callback; } return useCallback(function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return ref.current?.(...args); }, []); } /** * Merges React Refs into a single memoized function ref so you can pass it to * an element. * @example * const Component = React.forwardRef((props, ref) => { * const internalRef = React.useRef(); * return <div {...props} ref={useForkRef(internalRef, ref)} />; * }); */ function useForkRef() { for (var _len2 = arguments.length, refs = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { refs[_key2] = arguments[_key2]; } return useMemo(() => { if (!refs.some(Boolean)) return; return value => { refs.forEach(ref => { setRef(ref, value); }); }; }, refs); } /** * Returns the ref element's ID. */ function useRefId(ref, deps) { const [id, setId] = useState(undefined); useSafeLayoutEffect(() => { setId(ref?.current?.id); }, deps); return id; } /** * Generates a unique ID. Uses React's useId if available. */ function useId(defaultId) { if (useReactId) { const reactId = useReactId(); if (defaultId) return defaultId; return reactId; } const [id, setId] = useState(defaultId); useSafeLayoutEffect(() => { if (defaultId || id) return; const random = Math.random().toString(36).substr(2, 6); setId(`id-${random}`); }, [defaultId, id]); return defaultId || id; } /** * Uses React's useDeferredValue if available. */ function useDeferredValue(value) { if (useReactDeferredValue) { return useReactDeferredValue(value); } const [deferredValue, setDeferredValue] = useState(value); useEffect(() => { const raf = requestAnimationFrame(() => setDeferredValue(value)); return () => cancelAnimationFrame(raf); }, [value]); return deferredValue; } /** * Returns the tag name by parsing an element ref and the `as` prop. * @example * function Component(props) { * const ref = React.useRef(); * const tagName = useTagName(ref, "button"); // div * return <div ref={ref} {...props} />; * } */ function useTagName(ref, type) { const [tagName, setTagName] = useState(() => stringOrUndefined(type)); useSafeLayoutEffect(() => { setTagName(ref?.current?.tagName.toLowerCase() || stringOrUndefined(type)); }, [ref, type]); return tagName; } function stringOrUndefined(type) { if (typeof type === "string") { return type; } return; } /** * A `React.useEffect` that will not run on the first render. */ function useUpdateEffect(effect, deps) { const mounted = useRef(false); useEffect(() => { if (mounted.current) { return effect(); } mounted.current = true; }, deps); useEffect(() => () => { mounted.current = false; }, []); } /** * A `React.useLayoutEffect` that will not run on the first render. */ function useUpdateLayoutEffect(effect, deps) { const mounted = useRef(false); useSafeLayoutEffect(() => { if (mounted.current) { return effect(); } mounted.current = true; }, deps); useSafeLayoutEffect(() => () => { mounted.current = false; }, []); } /** * A custom version of `React.useState` that uses the `state` and `setState` * arguments. If they're not provided, it will use the internal state. */ function useControlledState(defaultState, state, setState) { const [localState, setLocalState] = useState(defaultState); const nextState = state !== undefined ? state : localState; const stateRef = useLiveRef(state); const setStateRef = useLiveRef(setState); const nextStateRef = useLiveRef(nextState); const setNextState = useCallback(prevValue => { const setStateProp = setStateRef.current; if (setStateProp) { if (isSetNextState(setStateProp)) { setStateProp(prevValue); } else { const nextValue = applyState(prevValue, nextStateRef.current); nextStateRef.current = nextValue; setStateProp(nextValue); } } if (stateRef.current === undefined) { setLocalState(prevValue); } }, []); defineSetNextState(setNextState); return [nextState, setNextState]; } const SET_NEXT_STATE = Symbol("setNextState"); function isSetNextState(arg) { return arg[SET_NEXT_STATE] === true; } function defineSetNextState(arg) { if (!isSetNextState(arg)) { Object.defineProperty(arg, SET_NEXT_STATE, { value: true }); } } /** * A React hook similar to `useState` and `useReducer`, but with the only * purpose of re-rendering the component. */ function useForceUpdate() { return useReducer(() => [], []); } /** * Returns an event callback similar to `useEvent`, but this also accepts a * boolean value, which will be turned into a function. */ function useBooleanEvent(booleanOrCallback) { return useEvent(typeof booleanOrCallback === "function" ? booleanOrCallback : () => booleanOrCallback); } /** * Returns props with an additional `wrapElement` prop. */ function useWrapElement(props, callback, deps) { if (deps === void 0) { deps = []; } const wrapElement = useCallback(element => { if (props.wrapElement) { element = props.wrapElement(element); } return callback(element); }, [...deps, props.wrapElement]); return { ...props, wrapElement }; } /** * Merges the portalRef prop and returns a `domReady` to be used in the * components that use Portal underneath. */ function usePortalRef(portalProp, portalRefProp) { if (portalProp === void 0) { portalProp = false; } const [portalNode, setPortalNode] = useState(null); const portalRef = useForkRef(setPortalNode, portalRefProp); const domReady = !portalProp || portalNode; return { portalRef, portalNode, domReady }; } /** * Returns a function that checks whether the mouse is moving. */ function useIsMouseMoving() { useEffect(() => { // We're not returning the event listener cleanup function here because we // may lose some events if this component is unmounted, but others are // still mounted. addGlobalEventListener("mousemove", setMouseMoving, true); // See https://github.com/ariakit/ariakit/issues/1137 addGlobalEventListener("mousedown", resetMouseMoving, true); addGlobalEventListener("mouseup", resetMouseMoving, true); addGlobalEventListener("keydown", resetMouseMoving, true); addGlobalEventListener("scroll", resetMouseMoving, true); }, []); const isMouseMoving = useEvent(() => mouseMoving); return isMouseMoving; } let mouseMoving = false; let previousScreenX = 0; let previousScreenY = 0; function hasMouseMovement(event) { const movementX = event.movementX || event.screenX - previousScreenX; const movementY = event.movementY || event.screenY - previousScreenY; previousScreenX = event.screenX; previousScreenY = event.screenY; return movementX || movementY || process.env.NODE_ENV === "test"; } function setMouseMoving(event) { if (!hasMouseMovement(event)) return; mouseMoving = true; } function resetMouseMoving() { mouseMoving = false; } export { useBooleanEvent, useControlledState, useDeferredValue, useEvent, useForceUpdate, useForkRef, useId, useInitialValue, useIsMouseMoving, useLazyValue, useLiveRef, usePortalRef, usePreviousValue, useRefId, useSafeLayoutEffect, useTagName, useUpdateEffect, useUpdateLayoutEffect, useWrapElement };