feat(Platform): allow DI override of userAgent #33751
Copy link
Copy link
Open
Labels
P4A relatively minor issue that is not relevant to core functionsA relatively minor issue that is not relevant to core functionsarea: cdk/platformfeatureLabel used to distinguish feature request from other issuesLabel used to distinguish feature request from other issues
Description
Feature Description
Right now it is hardcoded as global navigator.userAgent:
https://github.com/angular/components/blob/main/src/cdk/platform/platform.ts
However in SSR there's a way to get userAgent from the request and accessing global entities is bad practice. Would be great if there was a token for userAgent like we do here, or maybe at least inject(DOCUMENT).defaultView?.navigator?.userAgent way of accessing it, rather than just global navigator.userAgent
Use Case
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P4A relatively minor issue that is not relevant to core functionsA relatively minor issue that is not relevant to core functionsarea: cdk/platformfeatureLabel used to distinguish feature request from other issuesLabel used to distinguish feature request from other issues
Activity
lazerg commentedon Aug 31, 2026
Worth noting that every
navigator.userAgentread in there is short-circuited bythis.isBrowser, which comes fromPLATFORM_ID. On the server none of them execute and all the flags are alreadyfalse, so a userAgent token by itself wouldn't change anything under SSR. To get value out of it you'd also have to drop theisBrowsergate, and that's the risky half: the flags would start coming back true during server rendering, and everything that consumes them (tooltip,input,slider,paginated-tab-header,interactivity-checker,text-field/autosize,overlay-outside-click-dispatcher,layout/media-matcher) uses them for event wiring, focus handling and DOM measurement that only happens in the browser anyway. New hydration-mismatch surface, no change to the rendered markup.For the override itself,
Platformis a plain root-provided service, so replacing it wholesale already works today and is how the CDK's own tests do it:See focus-monitor.spec.ts. That gets you server-detected values without a new token.
The
inject(DOCUMENT).defaultView?.navigatorhalf seems like the more defensible ask on its own, sinceViewportRuler,FocusMonitorandHighContrastModeDetectoralready reach the window through_document.defaultView || windowrather than the global.waterplea commentedon Aug 31, 2026
isBrowser guard can be safely removed if the userAgent part was properly handled, for example it can be:
And then all the regex tests would return false. Accessing window and CSS can also be rewritten as
inject(DOCUMENT).defaultViewetc.lazerg commentedon Aug 31, 2026
platform-server's
DOCUMENTisn't a bare document.parseDocumentbuilds it withdomino.createWindow(html, url)and returnswindow.document, sodefaultViewis a live dominoWindow. It does carry anavigator(domino'sNavigatorID), but itsuserAgentis the empty string, and there's nochromeorCSSon that window, so under the default server setup the regexes would come out false the way you describe.Where it stops holding is the case the comment above
isBrowseris about, a document shimmed without you knowing. platform-server can run with DOM emulation off against a global jsdom or happy-dom, and both of those ship a default UA along the lines ofMozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/30.0.1.defaultView?.navigator?.userAgentpicks that up,WEBKITgoes true on the server, and everything downstream of it branches the wrong way during rendering. Same class of false positivePLATFORM_IDwas brought in for in #10659.One more wrinkle:
IOSuses!('MSStream' in window), andinthrows onundefined, so that one needs an actual guard, optional chaining won't cover it.Moving the global reads to
inject(DOCUMENT).defaultViewstill looks worth doing to me, it lines up with whatViewportRulerandFocusMonitoralready do. It just makes the reads safer rather than making the platform check redundant.