[utils] Improve platform detection#4920
Conversation
commit: |
Bundle size
PerformanceTotal duration: 1,086.81 ms -104.56 ms(-8.8%) | Renders: 50 (+0) | Paint: 1,652.45 ms -169.88 ms(-9.3%) No significant changes — details Check out the code infra dashboard for more information about this PR. |
✅ Deploy Preview for base-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for base-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
| const [fallbackToCodeSandbox, setFallbackToCodeSandbox] = React.useState(false); | ||
| React.useEffect(() => { | ||
| if (isSafari || isEdge) { | ||
| if (platform.engine.webkit) { |
There was a problem hiding this comment.
Check if Edge works with stackblitz.
| import { FloatingUIOpenChangeDetails } from '../../internals/types'; | ||
|
|
||
| const isMacSafari = isMac && isSafari; | ||
| const isMacSafari = platform.os.mac && platform.engine.webkit; |
There was a problem hiding this comment.
Flag name could potentially be inaccurate.
|
Most use of |
|
I think we need both concerns:
But yes feature detection should be the default, and we should be very precise about what we're detecting, a flag like I've added more
|
|
My main concern is the bundle size increase here, especially for a change not critical to the lib. I've been trying to reduce the overall size of the library but size increases keep returning over time and I'd rather not implement a noticeable increase for just a tidy-up. Moving from individual booleans to one exported A better shape would be a namespace export that remains treeshakable. The grouped object reads nicely, but it turns the utility into an all-or-nothing import, which feels too expensive for this kind of helper. |
|
It's not just a tidy-up, I've seen some issues across our repositories where a flag/check would produce incorrect results for specific environments, it's also a correctness change.
Note that this module (like all
So you'd suggest breaking the object into its direct fields, which would be exported directly? I also considered that, although I felt like readability was a bit less optimal. Also, |
|
I've added a few commits to improve bundle-size, one of them being avoiding the From a quick search, it seems that bundlers can tree-shake import * as platform from '@base-ui/utils/platform'
// ...
if (platform.os.ios) { /* ... */ } |
|
+1 in favor of using |
|
@romgrk you can check |
|
I have applied the suggestions as well as went a bit overboard with bundle-size micro-optimization by avoiding duplicate inline strings and abusing regexes. The combined result is that PR now decreases bundle-size. |
| /** iPhone, iPad (including iPadOS 13+ reporting as macOS), iPod. */ | ||
| export const ios = | ||
| /^i(os$|p)/.test(lowerPlatform) || (lowerPlatform === 'macintel' && maxTouchPoints > 1); |
There was a problem hiding this comment.
The second check ((lowerPlatform === 'macintel' && maxTouchPoints > 1) will become incorrect as Apple plans to ship a mac latop with a touchscreen. This needs either strenghtening or abandoning.
If iOS wants to pretend to be a macOS, we should probably respect that. iPads can be used as a laptop with keyboard & mouse plugged.
If we were using ios as a proxy for "primary pointer is touch", we probably need to adjust those sites to use pointer. Also, pointer needs refinement because we'd need a flag with a semantic "primary pointer is touch", right now we only have "there is a pointer with touch capability" (includes laptops with touchscreen).
Thoughts on dropping the second check?
Summary
The previous
detectBrowser.tsexported a flat list of booleans (isWebKit,isFirefox,isMac,isIOS,isAndroid,isJSDOM) that conflated OS, rendering engine, and environment into one undifferentiated namespace. Several call sites had picked the wrong flag because the names suggest a single coherent category that doesn't actually exist:isFirefoxwas a brand, not an engine. Firefox-on-iOS is WebKit (its UA marker isFxiOS/), so checks like "Firefox text-direction bug" need to target the Gecko engine specifically, not the Firefox brand.isIOSwas being used both as a true OS check (iOS-only software keyboard behavior) and as a "touch is primary input" proxy — those are different things and call for different signals.isWebKitwas overloaded. Some sites meant "WebKit rendering bug" (CSS, layout), others meant "VoiceOver accessibility quirk" (Apple platform + WebKit). Conflating them made one or the other site wrong on platforms like WebKitGTK.The rule going forward: a quirk patch should name the thing it's actually patching — an OS behavior, an engine bug, an input modality, or an accessibility-tree quirk. Never "this browser".
Changes
Old (
@base-ui/utils/detectBrowser) — flat booleans:New (
@base-ui/utils/platform) — single grouped object:Migration
isWebKit(engine bug)platform.engine.webkitisWebKit(VoiceOver workaround)platform.screenReader.voiceOverisFirefoxplatform.engine.geckoisMacplatform.os.macisIOSplatform.os.iosisAndroidplatform.os.androidisJSDOM(source code)platform.env.jsdomisJSDOM(tests)import { isJSDOM } from '#test-utils'Notable specifics
FocusGuardpreviously gated itsrole="button"workaround onisWebKit, which was over-broad — on WebKitGTK + Orca the workaround was setting a meaningless role on a hidden span. Now gated onplatform.screenReader.voiceOver, which is(os.mac || os.ios) && engine.webkit— the only place where VoiceOver's NSAccessibility virtual-cursor path is active.useDismiss.tswas importing a function-formisWebKit()from@floating-ui/utils/domwhile the rest of the codebase used the boolean fromdetectBrowser. Unified onplatform.engine.webkit.geckoandblinkare both anchored to!webkit, so Firefox-on-iOS (FxiOS/, WebKit) and Chrome-on-iOS (CriOS/, WebKit) classify correctly as WebKit-not-Gecko/Blink regardless of future UA-string drift.pointer.hoverandpointer.touchare orthogonal.hoveris a modality check (derived from(hover: none));touchis a capability check (Touch/TouchEventconstructor presence). A hybrid laptop with a touchscreen readshover: true, touch: true; a phone readshover: false, touch: true; a desktop readshover: true, touch: false. Usehoverto choose UX (hover-vs-tap),touchto decide whether to bind touch handlers.falsewhennavigatoris undefined, exceptpointer.hover, which defaults totrue(assume desktop).isJSDOMnow also matches HappyDOM, matching the equivalent helper in@mui/x-internals/platform.Breaking change
@base-ui/utils/detectBrowseris removed. Consumers must import from@base-ui/utils/platformand update field references per the migration table above.vi.mock('@base-ui/utils/platform', …)users must mock the nestedplatformobject shape rather than individual booleans.