/** * Minimal browser / environment detection used by the floating-ui-react port. * * Mirrors `@base-ui/utils/detectBrowser` — only the subset the port actually * consumes is inlined here so we don't have to vendor a separate dependency. * Each flag matches the React source so behavior is 1:1. */ interface NavigatorUAData { brands: Array<{ brand: string; version: string }>; mobile: boolean; platform: string; } const hasNavigator = typeof navigator !== "undefined"; function getNavigatorData(): { platform: string; maxTouchPoints: number } { if (!hasNavigator) { return { platform: "", maxTouchPoints: -1 }; } const uaData = (navigator as { userAgentData?: NavigatorUAData }) .userAgentData; if (uaData?.platform) { return { platform: uaData.platform, maxTouchPoints: navigator.maxTouchPoints, }; } return { platform: navigator.platform ?? "", maxTouchPoints: navigator.maxTouchPoints ?? -1, }; } function getUserAgent(): string { if (!hasNavigator) { return ""; } const uaData = (navigator as { userAgentData?: NavigatorUAData }) .userAgentData; if (uaData && Array.isArray(uaData.brands)) { return uaData.brands .map(({ brand, version }) => `${brand}/${version}`) .join(" "); } // `navigator` can exist with an absent `userAgent` on non-browser runtimes // (Node ≥21, Deno, edge workers expose a partial Navigator), so the // `hasNavigator` guard alone isn't enough — coalesce to avoid a // module-load crash on `userAgent.includes(...)` below. return navigator.userAgent ?? ""; } function getPlatform(): string { if (!hasNavigator) { return ""; } const uaData = (navigator as { userAgentData?: NavigatorUAData }) .userAgentData; if (uaData?.platform) { return uaData.platform; } return navigator.platform ?? ""; } const nav = getNavigatorData(); const platform = getPlatform(); const userAgent = getUserAgent(); export const isAndroid = (hasNavigator && /android/i.test(platform)) || /android/i.test(userAgent); export const isJSDOM = userAgent.includes("jsdom/"); /** * happy-dom sets a user-agent beginning with "Mozilla/5.0 (X11; Linux x64) ..." * and does NOT include "jsdom/". We also treat happy-dom as JSDOM-ish because * it shares the same limitations (no real focus-visible, no real layout). */ const isHappyDOM = userAgent.includes("HappyDOM"); /** * Umbrella flag: true in either jsdom or happy-dom. Code that guards on * test-env limitations (e.g. `matches(':focus-visible')` throwing) should * use this. */ export const isTestEnv = isJSDOM || isHappyDOM; export const isIOS = nav.platform === "MacIntel" && nav.maxTouchPoints > 1 ? true : /iP(hone|ad|od)|iOS/.test(nav.platform); export const isMac = hasNavigator && platform.toLowerCase().startsWith("mac") && !navigator.maxTouchPoints; /** * Safari detection — mirrors `@base-ui/utils/detectBrowser`'s `isSafari`. * Uses `navigator.vendor` which is "Apple Computer, Inc." in Safari and * empty / different in Chrome/Firefox/Edge. */ export const isSafari = hasNavigator && /apple/i.test((navigator as Navigator).vendor ?? ""); /** * WebKit engine detection — mirrors `@base-ui/utils/detectBrowser`'s * `isWebKit`. Matches Safari AND other WebKit-shell browsers (WKWebView, * standalone iOS PWAs) without matching Chromium-based browsers (which * keep "AppleWebKit" in their UA for historical compat). */ export const isWebKit = hasNavigator && /AppleWebKit/.test(userAgent) && !/Edg|Chrome|Chromium|Firefox/.test(userAgent);