ariakit-react-utils
Version:
Ariakit React utils
396 lines (371 loc) • 11.2 kB
JavaScript
;
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;