/** * General-purpose helpers for state, objects, strings, scheduling, and * assertions. * @module General utilities */ import type { AnyFunction, AnyObject, BivariantCallback, SetStateAction, } from "./types.ts"; /** * Empty function. */ export function noop(..._: any[]): any {} /** * Compares two objects. * @example * shallowEqual({ a: "a" }, {}); // false * shallowEqual({ a: "a" }, { b: "b" }); // false * shallowEqual({ a: "a" }, { a: "a" }); // true * shallowEqual({ a: "a" }, { a: "a", b: "b" }); // false */ export function shallowEqual(a?: AnyObject, b?: AnyObject) { if (a === b) return true; if (!a) return false; if (!b) return false; if (typeof a !== "object") return false; if (typeof b !== "object") return false; const aKeys = Object.keys(a); const bKeys = Object.keys(b); const { length } = aKeys; if (bKeys.length !== length) return false; for (const key of aKeys) { if (a[key] !== b[key]) { return false; } } return true; } /** * Receives a `setState` argument and calls it with `currentValue` if it's a * function. Otherwise return the argument as the new value. * @example * applyState((value) => value + 1, 1); // 2 * applyState(2, 1); // 2 */ export function applyState( argument: SetStateAction, currentValue: T | (() => T), ) { if (isUpdater(argument)) { const value = isLazyValue(currentValue) ? currentValue() : currentValue; return argument(value); } return argument; } function isUpdater( argument: SetStateAction, ): argument is BivariantCallback<(prevState: T) => T> { return typeof argument === "function"; } function isLazyValue(value: T | (() => T)): value is () => T { return typeof value === "function"; } /** * Checks whether `arg` is an object or not. * @returns {boolean} */ export function isObject(arg: any): arg is Record { return typeof arg === "object" && arg != null; } /** * Checks whether `arg` is empty or not. * @example * isEmpty([]); // true * isEmpty(["a"]); // false * isEmpty({}); // true * isEmpty({ a: "a" }); // false * isEmpty(); // true * isEmpty(null); // true * isEmpty(undefined); // true * isEmpty(""); // true */ export function isEmpty(arg: any): boolean { if (Array.isArray(arg)) return !arg.length; if (isObject(arg)) return !Object.keys(arg).length; if (arg == null) return true; if (arg === "") return true; return false; } /** * Checks whether `arg` is an integer or not. * @example * isInteger(1); // true * isInteger(1.5); // false * isInteger("1"); // true * isInteger("1.5"); // false */ export function isInteger(arg: any): boolean { if (typeof arg === "number") { return Math.floor(arg) === arg; } return String(Math.floor(Number(arg))) === arg; } /** * Checks whether `prop` is an own property of `obj` or not. */ export function hasOwnProperty( object: T, prop: keyof any, ): prop is keyof T { if (typeof Object.hasOwn === "function") { return Object.hasOwn(object, prop); } return Object.prototype.hasOwnProperty.call(object, prop); } /** * Receives functions as arguments and returns a new function that calls all. */ export function chain(...fns: T[]) { return (...args: T extends AnyFunction ? Parameters : never) => { for (const fn of fns) { if (typeof fn === "function") { fn(...args); } } }; } /** * Returns a string with the truthy values of `args` separated by space. */ export function cx(...args: Array) { return args.filter(Boolean).join(" ") || undefined; } /** * Removes diacritics from a string. */ export function normalizeString(str: string) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } /** * Omits specific keys from an object. * @example * omit({ a: "a", b: "b" }, ["a"]); // { b: "b" } */ export function omit( object: T, keys: ReadonlyArray | K[], ) { const result = { ...object } as Omit; for (const key of keys) { if (hasOwnProperty(result, key)) { delete result[key]; } } return result; } /** * Picks specific keys from an object. * @example * pick({ a: "a", b: "b" }, ["a"]); // { a: "a" } */ export function pick( object: T, paths: ReadonlyArray | K[], ) { const result = {} as Pick; for (const key of paths) { if (hasOwnProperty(object, key)) { result[key] = object[key]; } } return result; } /** * Returns the same argument. */ export function identity(value: T) { return value; } /** * Runs right before the next paint. */ export function beforePaint(cb: () => void = noop) { const raf = requestAnimationFrame(cb); return () => cancelAnimationFrame(raf); } /** * Runs after the next paint. */ export function afterPaint(cb: () => void = noop) { let raf = requestAnimationFrame(() => { raf = requestAnimationFrame(cb); }); return () => cancelAnimationFrame(raf); } const defaultWarnOnceKey = {}; const warningMessages = new WeakMap>(); /** * Logs a warning only once for each message and key. * @example * if (process.env.NODE_ENV !== "production") { * warnOnce("Warning"); * } */ export function warnOnce(message: string, key?: object) { const warningKey = key || defaultWarnOnceKey; let messages = warningMessages.get(warningKey); if (!messages) { messages = new Set(); warningMessages.set(warningKey, messages); } if (messages.has(message)) return; messages.add(message); console.warn(message); } /** * Asserts that a condition is true, otherwise throws an error. * @example * invariant( * condition, * process.env.NODE_ENV !== "production" && "Invariant failed" * ); */ export function invariant( condition: any, message?: string | boolean, ): asserts condition { if (condition) return; if (typeof message !== "string") throw new Error("Invariant failed"); throw new Error(message); } /** * Similar to `Object.keys` but returns a type-safe array of keys. */ export function getKeys(obj: T) { return Object.keys(obj) as Array; } /** * Checks whether a boolean event prop (e.g., hideOnInteractOutside) was * intentionally set to false, either with a boolean value or a callback that * returns false. */ export function isFalsyBooleanCallback( booleanOrCallback?: boolean | ((...args: T) => boolean), ...args: T ) { const result = typeof booleanOrCallback === "function" ? booleanOrCallback(...args) : booleanOrCallback; if (result == null) return false; return !result; } /** * Checks whether something is disabled or not based on its props. */ export function disabledFromProps(props: { disabled?: boolean; "aria-disabled"?: boolean | "true" | "false"; }) { return ( props.disabled || props["aria-disabled"] === true || props["aria-disabled"] === "true" ); } /** * Checks whether something is disabled or not based on its DOM attributes. */ export function disabledFromElement(element: Element) { if (element.getAttribute("aria-disabled") === "true") return true; if ("disabled" in element && element.disabled === true) return true; return false; } /** * Removes undefined values from an object. */ export function removeUndefinedValues(obj: T) { const result = {} as T; for (const key in obj) { if (obj[key] !== undefined) { result[key] = obj[key]; } } return result; } /** * Returns the first value that is not `undefined`. */ export function defaultValue( ...values: T ): DefaultValue; export function defaultValue(...values: unknown[]) { for (const value of values) { if (value !== undefined) return value; } return undefined; } type DefaultValue = T extends [ infer Head, ...infer Rest, ] ? Rest extends [] ? T[number] | Other : undefined extends Head ? DefaultValue> : Exclude : never;