Angular / Template / Material / 3rd-party Audit — non-Chromium Browser Compatibility¶
Temporary audit document — supports the planning report at
../non-chromium-browser-compatibility-analysis.md. Delete once findings are tracked in tickets.
Summary¶
- Total findings: 18
- Severity breakdown: critical 3 / major 8 / minor 7
- Browsers affected: Safari 14 / Firefox 4 / both 12
Findings¶
F1. Auth0 silent logout iframe — broken under Safari ITP and Firefox ETP¶
- File:
src/app/core/auth/auth.effects.ts:600-619 - Browsers: Safari, Firefox
- Severity: critical
- Symptom: Silent sign-out (
silentSignOut$) creates a hidden iframe pointing athttps://${auth0CustomDomain}/v2/logout?client_id=${clientId}to clear the IdP session without redirecting the user. In Safari with Intelligent Tracking Prevention (ITP) the third-party iframe cookie context is partitioned, so the request runs but the IdP session cookie isn't actually invalidated; the user appears signed out locally yet still has an active IdP session. Firefox Total Cookie Protection / ETP behaves similarly. - Cause: Hidden third-party iframe pattern + cross-site cookies; Safari ITP blocks cookies on third-party iframes; Firefox ETP partitions storage by top-level site.
- Fix: Use the Auth0 SDK's
logout({ federated: false, openUrl })and either (a) accept a top-level redirect for sign-out, or (b) move the logout endpoint to a same-site sub-domain (e.g.auth.syrf.org.ukshares an eTLD+1 with the app). Long-term: BFF mode (bff-auth.provider.ts) avoids the iframe entirely — push auth0-mode users onto BFF. - Effort: medium
F2. Auth0 cacheLocation: 'localstorage' + useRefreshTokens: true — Safari Private Mode storage failures¶
- File:
src/main.ts:404-406 - Browsers: Safari (Private Browsing in particular), Firefox (Private)
- Severity: major
- Symptom: In Safari Private Browsing,
localStoragequota is 0 in some configurations and writes throwQuotaExceededError. Auth0 SDK swallows this but the refresh token is never persisted, forcing a fresh login on every page load and every silent token request. - Cause:
@auth0/auth0-spa-jscacheLocation: 'localstorage'(default ismemory). Combined withuseRefreshTokens: trueanduseRefreshTokensFallback: true, the fallback (silent auth via iframe) is itself blocked by ITP — so when the localStorage cache fails, the fallback also fails, leading to repeated forced logins. - Fix: Switch to
cacheLocation: 'memory'and rely on refresh tokens being kept in the auth state; or feature-detect Private Mode and fall back gracefully. Better: prefer BFF auth provider where token storage is server-side. - Effort: low
F3. SignalR — no explicit transport list; auto-falls-back to LongPolling under Safari edge cases¶
- File:
src/app/core/services/signal-r/signal-r.service.ts:189-199 - Browsers: Safari, Firefox (cross-site WebSocket subtleties)
- Severity: major
- Symptom:
HubConnectionBuilder().withAutomaticReconnect().withUrl(url, opts).build()does not pintransport: HttpTransportType.WebSockets. When the WebSocket upgrade fails (Safari ITP partitioning of cross-site cookies during the negotiate request, corporate proxies, or older Safari versions), SignalR silently falls back to ServerSentEvents and then LongPolling. LongPolling for SignalR is heavy and tends to break behind some Safari configurations because POSTs interleaved with the negotiate cookie can be cached differently. In BFF mode (cookie auth) Safari may strip the session cookie on the WS upgrade because the upgrade is a cross-site GET. - Cause: SignalR auto-negotiation + browser-specific cookie / WS handling.
accessTokenFactoryputs the bearer in?access_token=…(because browsers don't allowAuthorizationheaders on WebSocket); fine for Auth0 mode but exposes token in URL-logging contexts. - Fix: Explicitly pass
{ transport: HttpTransportType.WebSockets | HttpTransportType.LongPolling, skipNegotiation: false }, log transport actually used (hubConnection.connectionId+ a one-time event), and emit a Sentry breadcrumb on transport fallback so we can detect Safari-only fallback rates. Verify SignalR cookie path is same-site or use a sub-domain. - Effort: low
F4. Hidden file input pattern with programmatic click() — iOS Safari quirk¶
- File:
src/app/stage/stage-admin/review-data-upload/review-data-upload.component.html:84-93andsrc/app/stage/stage-admin/review-data-upload/search-decision-upload/search-decision-upload.component.html:108-120 - Browsers: Safari (iOS 14+ tightened user-gesture requirements), Firefox
- Severity: major
- Symptom: "Browse File" button calls
csvInput.click()on an<input type="file" fxHide>that is rendered with FlexLayout'sfxHide(CSSdisplay: none). iOS Safari has historically refused to open the file picker for hidden file inputs (at minimum needsvisibility: hidden+position: absolute+opacity: 0rather thandisplay: none). Some older Firefox versions also won't dispatch the picker on adisplay: noneinput. - Cause:
fxHidefrom@ngbracket/ngx-layoutappliesdisplay: none !important. Apple's WebKit refuses to dispatch native file pickers from elements that are not in the rendering tree. - Fix: Use a CSS sr-only / visually-hidden pattern instead of
fxHide:position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;. Or use a<label for="…">-wrapped button so the click is a native label click (most reliable across browsers). - Effort: trivial
F5. File input accept attribute mixes MIME types and dotted extensions inconsistently¶
- File:
src/app/project/project-overview/create-search/create-search.component.ts:336-340,src/app/project/project-overview/update-studies/update-studies.component.ts:288-289 - Browsers: Safari, Firefox
- Severity: minor
- Symptom:
allowedExtensions = 'text/plain, text/csv, text/tab-separated-values, .tsv, .csv'and'Application/xml'(capital A). Safari historically ignored unknown MIME entries and only honoured dotted extensions; Firefox accepts both but is case-sensitive on the MIME portion. Capital-A'Application/xml'is technically invalid per RFC 7231 (MIME types are case-insensitive in practice but Firefox file-picker filtering has been observed to mismatch). Result: Safari users may see "All files" instead of the filtered picker; Firefox may show empty filter. - Cause: Inconsistent
acceptvalue generation. - Fix: Use lowercase MIME types and prefer dotted extensions (
.csv,.tsv,.txt,.xml). Material/Angular has no validation here; just normalise the strings. - Effort: trivial
F6. Material datepicker uses date-fns adapter with navigator.language — locale parsing inconsistency¶
- File:
src/app/info/contact-us/contact-us.component.ts:286-292,src/app/project/project-admin/risk-of-bias-job-table/risk-of-bias-job-table.component.ts:47-51,src/app/core/dynamic-locale.ts:121-210 - Browsers: Safari, Firefox
- Severity: major
- Symptom:
MAT_DATE_LOCALEset tonavigator.language. Safari returns territory-less codes (eninstead ofen-GB) more often than Chromium and the territory-less dynamic-locale loader (dynamic-locale.ts:65) falls through to a default; users in en-GB get US date order (MM/dd/yyyy) parsing in the input field, leading to silent off-by-month/day date submissions. - Cause:
navigator.languagediffers by browser; Safari especially has been known to omit the region. The customDynamicLocaleDateAdapterfalls through toenUSif the locale code isn't found. - Fix: Detect region from
navigator.languages[0]then fall back toIntl.DateTimeFormat().resolvedOptions().locale. Add a debug log that warns when the adapter falls back toenUS. Long-term, prefer ISO format input (yyyy-MM-dd) for raw submissions. - Effort: low
F7. image-cropper (ngx-image-cropper@6.3.3) — Safari iOS image EXIF/orientation handling¶
- File:
src/app/manage/profile/image-cropper-dialog/image-cropper-dialog.component.html:1-12 - Browsers: Safari (especially iOS)
- Severity: major
- Symptom: Profile-picture cropper. iOS Safari's
<img>/Canvas pipeline may render images sideways when EXIF orientation is non-default (front-camera selfies).ngx-image-cropper@6added animageQualityand limited EXIF support but historic versions of WebKit ignoredimage-orientation: from-imageand the canvas didn't apply EXIF. - Cause: Safari/WebKit's historical refusal to honour EXIF in canvas drawImage.
ngx-image-cropperversions <6.4 had bugs around orientation when reading via FileReader on iOS. - Fix: Bump to latest
ngx-image-cropper(>=7.x) which auto-handles EXIF viacreateImageBitmap({ imageOrientation: 'from-image' }), and add CSSimage-orientation: from-imageon the<image-cropper>host. Test with iPhone selfies in QA. - Effort: low
F8. Handsontable — clipboard / contextMenu interactions on Safari¶
- File:
src/app/shared/form-controls/timepoint-spreadsheet/timepoint-spreadsheet.component.ts:132-147,src/app/shared/annotation/annotation-form/annotation-experiment-question/outcome-table-info/outcome-table-info.component.ts:13-28(handsontable@^14.4.0) - Browsers: Safari, Firefox
- Severity: major
- Symptom:
copyPaste: trueenables Handsontable's clipboard subsystem. Handsontable historically (and through 14.x) has had cross-browser issues: - Safari refuses
document.execCommand('copy')outside of a synchronous user-gesture stack — Handsontable's contextMenu copy/paste path is synchronous butexecCommandis deprecated; on Safari TP and recent macOS Safari, it fails silently. - Firefox blocks
navigator.clipboard.writeTextoutside secure contexts and outside user-gesture; pasting large grids may truncate. - Context menu (
contextMenu: { items: { row_above, row_below, remove_row } }) intercepts the OS-level menu and Safari users still get the OS menu on top in some configurations (a known WebKit bug withcontextmenuevent). - Cause:
handsontable@^14.4.0clipboard implementation; reliance on legacyexecCommand. - Fix: Upgrade to
handsontable@>=15which usesnavigator.clipboardAPI behind feature detection, and verify theoutcomeTableInfoComponentlazy-load (ensureHotBasicsRegistered) does not fire while page is hidden (Safari throttles background tabs more aggressively). Add a Sentry breadcrumb on copy/paste failure. - Effort: medium
F9. Highcharts ESM master import — module resolution on Safari (legacy)¶
- File:
src/app/stage/stage-overview/time-progress/time-progress.component.ts:10,current-progress-pie/current-progress-pie.component.ts:11,member-time-progress/member-time-progress.component.ts:6 - Browsers: Safari (versions <14)
- Severity: minor
- Symptom:
import Highcharts from 'highcharts/es-modules/masters/highcharts.src.js'— bundler-resolved fine, but the.src.jsmaster uses ES2018 features (private class fields in places). Safari <14 chokes on#privateField. Less an issue today (Safari 14+ is universal) but if you support older iPad/iOS in user base, this breaks rendering of progress charts. - Cause: Highcharts 11.x dropped IE/legacy Safari; ESM master assumes ES2020.
- Fix: Confirm browserslist excludes Safari <14; the build's ngBuild transpilation should target ES2020+ (verify in
tsconfig.build.json). If older Safari support is needed, switch tohighcharts/highcharts.js(UMD, broader compat). - Effort: trivial
F10. PDF.js worker URL via import.meta.url — Safari worker module support¶
- File:
src/app/pdf-tools/pdf-loader.service.ts:7-12 - Browsers: Safari (especially older iOS), Firefox (any version <=113 with module workers off)
- Severity: major
- Symptom:
pdf.worker.min.mjsis loaded as an ES module worker vianew URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url). ES module workers (new Worker(url, { type: 'module' })) only landed in Safari 15 and Firefox 114. Earlier versions throw "TypeError: Module Worker not supported" and the PDF preview fails to load entirely. - Cause: PDF.js 4.x ships only the
.mjsworker. Application code usesimport.meta.urlresolution. - Fix: Provide a non-module worker fallback (
pdf.worker.min.js) and detect support. Many projects ship a small wrapper that detects'serviceWorker' in navigatorplus module worker support and falls back. Confirm pdfjs-dist 4.10.38 ships a non-module build underlegacy/build/. - Effort: medium
F11. CDK Drag & Drop for question-tree designer — Safari touch/pointer event differences¶
- File:
src/app/project/project-admin/question-management/design/annotation-question-tree-drag-drop.feature.ts:140-310,design.component.html:57-200 - Browsers: Safari (iPad/touch), Firefox (mobile)
- Severity: major
- Symptom: Custom HTML5 drag-and-drop using native
(dragover),(dragleave),dataTransfer.dropEffect. iOS Safari does NOT fire native HTML5 drag events (only iPadOS 15+ on external pointer; touch never firesdragstart). The question-tree designer is fully broken on iPad without a Bluetooth mouse. Firefox has subtle differences withdragleavefiring more often than expected. - Cause: HTML5 drag API was never reimplemented for touch in iOS Safari. Application chose native DnD over CDK's
cdkDrag(which IS pointer-event based and works on touch). - Fix: Migrate the question-tree to
@angular/cdk/drag-drop(cdkDropList,cdkDrag) which uses Pointer Events under the hood and works cross-browser including touch. The codebase already uses CDK DnD elsewhere (input-array.component.ts:38,graph-selector.component.ts:41). - Effort: high
F12. <input type="search"> in mat-form-field — Safari forced UA styling¶
- File:
src/app/auth/dev-login/dev-login.component.html:35-44,src/app/admin/impersonation/impersonation.component.html:42-50,src/app/project-index/project-index.component.html:28-38 - Browsers: Safari
- Severity: minor
- Symptom: Safari's UA stylesheet for
<input type="search">enforces rounded corners, an internal "X" clear button, and the styled-search appearance even when the input is not in amat-form-field. The first two examples (dev-login, impersonation) are bare<input type="search">without a form field, so users see Safari's native "X" clear button alongside the custom one in the template — leading to double-X UX. - Cause: Safari's
-webkit-appearance: searchfieldcannot be fully reset withoutappearance: none+-webkit-appearance: none. - Fix: Add
appearance: none; -webkit-appearance: none;to.search-fieldSCSS, or change thetypetotext(no semantic loss for a non-form search box). - Effort: trivial
F13. scrollIntoView({ behavior: 'smooth' }) — Safari support partial¶
- File:
src/app/project/project-admin/project-options/project-options.component.ts:231-235 - Browsers: Safari (<15.4)
- Severity: minor
- Symptom:
behavior: 'smooth'was unsupported on Safari until 15.4 (March 2022). Older iPad/iOS users see an instant jump instead of smooth scroll. Not a functional break but a UX regression. - Cause: Safari implementation lag.
- Fix: Acceptable as graceful degradation (just snaps). If smooth is required on older Safari, polyfill with
smoothscroll-polyfillor a customrequestAnimationFrameease. - Effort: trivial
F14. @ngbracket/ngx-layout (fxLayout, fxHide, fxFlex) — old IE/Edge flex bugs leak into Safari¶
- File: Many — e.g.
src/app/shared/form-controls/timepoint-array/timepoint-array.component.html:7,17,26,49,55 - Browsers: Safari (subtle), Firefox (subtle)
- Severity: minor
- Symptom:
@ngbracket/ngx-layout@20.0.1is a community fork of the abandoned@angular/flex-layout. It applies inline styles via Angular directives. Safari and Firefox handleflex-basisdifferently than Chromium when the parent hasmin-width: auto(default) — flex children may overflow. Visible intimepoint-array.component.htmlwhere 3 flex children withclass="full-width"overlap or wrap unexpectedly on narrower viewports in Safari. - Cause: Flex-Layout-style libraries embed Chromium-tuned defaults; lack of
min-width: 0on flex children is a known cross-browser gotcha. - Fix: Add
min-width: 0to direct flex children; gradually migrate offngx-layoutto native CSS Grid / flex (Angular team officially deprecated@angular/flex-layout). - Effort: medium (across many components)
F15. Stray <script> reference in timepoint-array.component.html:1¶
- File:
src/app/shared/form-controls/timepoint-array/timepoint-array.component.html:1 - Browsers: all (but cross-browser parsing differences expose it)
- Severity: critical (security + correctness)
- Symptom:
<script src="../../../../../../../../../Desktop/main-es5.0d374fcad9b6d03574d7.js"></script>— an absolute-path-traversal<script>tag referencing a developer'sDesktop. In production builds this will 404; in browsers' error logging Safari/Firefox may surface a CSP violation or strict-mixed-content warning that doesn't appear in Chromium dev tools the same way. It's also a sign of a broken dev artefact in source control. - Cause: Accidental commit of a debugging script tag.
- Fix: Delete line 1 of
timepoint-array.component.html. (Out-of-scope for this audit but flagged because it surfaces differently per browser.) - Effort: trivial
F16. PWA display: standalone in site.webmanifest — Safari ignores¶
- File:
src/site.webmanifest:18 - Browsers: Safari
- Severity: minor
- Symptom: iOS Safari (until iOS 16.4) ignored Web Manifest entirely; even now it only honours a subset and prefers
apple-mobile-web-app-capable<meta>for "Add to Home Screen". Thedisplay: standaloneproperty is a no-op on iOS — users get a regular Safari tab when launching from home screen. - Cause: Apple's slow adoption of Web Manifest spec.
- Fix: Add
<meta name="apple-mobile-web-app-capable" content="yes">and<meta name="apple-mobile-web-app-status-bar-style" content="default">toindex.html. Provideapple-touch-iconlink tags (currently absent fromindex.html). - Effort: trivial
F17. localStorage for crash-recovery state — Safari Private Mode quota¶
- File:
src/app/app.component.ts:330,343,348,382,400,401,408,src/app/core/components/error-recovery-dialog/error-recovery-dialog.component.ts:28,32,src/app/core/auth/auth.effects.ts:640,699,721(impersonation) - Browsers: Safari (Private Browsing), Firefox (Private)
- Severity: major
- Symptom:
localStorage.setItem('app_recovered_from_crash', …)runs unconditionally; in Safari Private mode this throwsQuotaExceededError. Some calls are wrapped in try/catch but the recovery UI relies on the value being read back, so users in Private mode never see crash-recovery prompts and impersonation state silently doesn't persist. - Cause: Direct
localStorageaccess without availability detection. - Fix: Wrap in a
safeStorageutility that does feature-detect (try { localStorage.setItem('__t','1'); localStorage.removeItem('__t'); return true } catch { return false }) and gracefully no-ops when storage isn't writable. Already exists pattern atversion-info-dialog.component.ts:224(clipboard) — replicate. - Effort: low
F18. Auth0 authorizationParams.scope: 'openid profile email offline_access' — Safari 3rd-party cookie blockade on initial auth callback¶
- File:
src/main.ts:401 - Browsers: Safari, Firefox (ETP-strict)
- Severity: major
- Symptom: Auth0 Universal Login redirect-flow uses cookies on the auth0 domain. Safari ITP (iOS 13.4+) "redirect tracking" rules will purge cookies set by
auth0.comafter 7 days unless the user re-engages on that domain. Result: every ~7 days users get bounced to a fresh login. TheuseRefreshTokens: truemitigates ongoing API access, but the ID-token rotation flow uses iframes to re-establish session, and those iframes are subject to ITP partitioning (see F1, F2). - Cause: Apple's ITP & Safari partitioning of third-party storage; Auth0 default flow.
- Fix: Configure
auth0CustomDomainto be a sub-domain ofsyrf.org.uk(e.g.auth.syrf.org.uk) — this makes Auth0's cookies first-party and bypasses ITP/ETP. Audit infra to confirm the custom-domain CNAME is configured. Field nameauth0CustomDomaininapp-settings.interface.tssuggests this is intended; confirm staging/prod values. - Effort: low (config) / medium (infra)
Cross-cutting observations¶
-
Auth iframe pattern is fragile across non-Chromium browsers. F1, F2, F18 all stem from the same root cause: Safari ITP and Firefox ETP increasingly punish hidden iframes / third-party cookies. The BFF auth provider (
bff-auth.provider.ts) is the long-term answer; expedite migration off Auth0 SDK in browsers. -
Custom HTML5 drag-and-drop vs CDK DnD inconsistency. The codebase uses two drag systems: native HTML5 (
design.component) and CDK (input-array,graph-selector). CDK works on touch; native HTML5 does not. F11 is the highest-impact UX issue for Safari/iPad users. -
Hidden inputs via
display: none. Two file-upload components usefxHide(display:none) which iOS Safari historically refuses to dispatch native pickers from. Use sr-only patterns (F4). -
Storage failures are silently swallowed. Auth (
localStorage), crash-recovery, and impersonation all assumelocalStorageworks. Safari Private Browsing and aggressive ETP both break this; add a defensive wrapper (F17). -
Module Workers in PDF.js (F10). Worth verifying browser support telemetry — pdfjs-dist 4.10 dropped legacy worker. If you have any iOS 14 traffic, PDF preview is fully broken.
-
No central polyfills strategy.
polyfills.tsonly importszone.js. Verify the build'sbrowserslist(rootpackage.jsonor.browserslistrc) — there's no.browserslistrcvisible in the web service, so Angular CLI defaults apply (> 0.5%, last 2 versions, Firefox ESR, not dead). That actually does include Safari 14+ but you may unknowingly support older Safari versions where ESM workers (F10) and PWA standalone (F16) fail.