UNPKG

ariakit-react-utils

Version:

Ariakit React utils

396 lines (371 loc) 11.2 kB
'use strict'; var React = require('react'); var dom = require('ariakit-utils/dom'); var events = require('ariakit-utils/events'); var misc$1 = require('ariakit-utils/misc'); var misc = require('./misc.js'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React); // @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__namespace["use" + "Id"]; // @ts-ignore const useReactDeferredValue = React__namespace["use" + "DeferredValue"]; // @ts-ignore const useInsertionEffect = React__namespace["use" + "InsertionEffect"]; /** * `React.useLayoutEffect` that fallbacks to `React.useEffect` on server side. */ const useSafeLayoutEffect = dom.canUseDOM ? React.useLayoutEffect : React.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] = React.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 = React.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 = React.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] = React.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 = React.useRef(() => { throw new Error("Cannot call an event handler while rendering."); }); if (useInsertionEffect) { useInsertionEffect(() => { ref.current = callback; }); } else { ref.current = callback; } return React.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 React.useMemo(() => { if (!refs.some(Boolean)) return; return value => { refs.forEach(ref => { misc.setRef(ref, value); }); }; }, refs); } /** * Returns the ref element's ID. */ function useRefId(ref, deps) { const [id, setId] = React.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] = React.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] = React.useState(value); React.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] = React.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 = React.useRef(false); React.useEffect(() => { if (mounted.current) { return effect(); } mounted.current = true; }, deps); React.useEffect(() => () => { mounted.current = false; }, []); } /** * A `React.useLayoutEffect` that will not run on the first render. */ function useUpdateLayoutEffect(effect, deps) { const mounted = React.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] = React.useState(defaultState); const nextState = state !== undefined ? state : localState; const stateRef = useLiveRef(state); const setStateRef = useLiveRef(setState); const nextStateRef = useLiveRef(nextState); const setNextState = React.useCallback(prevValue => { const setStateProp = setStateRef.current; if (setStateProp) { if (isSetNextState(setStateProp)) { setStateProp(prevValue); } else { const nextValue = misc$1.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 React.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 = React.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] = React.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() { React.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. events.addGlobalEventListener("mousemove", setMouseMoving, true); // See https://github.com/ariakit/ariakit/issues/1137 events.addGlobalEventListener("mousedown", resetMouseMoving, true); events.addGlobalEventListener("mouseup", resetMouseMoving, true); events.addGlobalEventListener("keydown", resetMouseMoving, true); events.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; } exports.useBooleanEvent = useBooleanEvent; exports.useControlledState = useControlledState; exports.useDeferredValue = useDeferredValue; exports.useEvent = useEvent; exports.useForceUpdate = useForceUpdate; exports.useForkRef = useForkRef; exports.useId = useId; exports.useInitialValue = useInitialValue; exports.useIsMouseMoving = useIsMouseMoving; exports.useLazyValue = useLazyValue; exports.useLiveRef = useLiveRef; exports.usePortalRef = usePortalRef; exports.usePreviousValue = usePreviousValue; exports.useRefId = useRefId; exports.useSafeLayoutEffect = useSafeLayoutEffect; exports.useTagName = useTagName; exports.useUpdateEffect = useUpdateEffect; exports.useUpdateLayoutEffect = useUpdateLayoutEffect; exports.useWrapElement = useWrapElement;