Skip to content

Non-Chromium Browser (Safari, Firefox) UI Compatibility Report

Scope: src/services/web/ (Angular 21 SPA). The audit covered four dimensions in parallel; full per-dimension findings live in audits/. This document is the synthesised cross-cutting report.

Sub-audits (load-bearing detail):

Executive Summary

The web app's non-Chromium breakage has one root cause and three independent fault families.

The root cause is the build configuration: there is no .browserslistrc anywhere in the repo, tsconfig.target is ES2022, and polyfills.ts contains a single import 'zone.js' line with a comment that still claims "Safari ≥ 10" support. In combination this means:

  • The Angular CLI default browserslist applies (last 2 Safari major / last 1 Firefox / Firefox ESR), so anyone on Safari 16 or earlier is already off the supported matrix without anyone explicitly choosing that.
  • TypeScript does not down-emit beyond its target; ES2022 syntax (#private fields, top-level await, Object.hasOwn, Array.prototype.at, Error.cause) is shipped verbatim and throws SyntaxError/TypeError on Safari ≤ 15.
  • No runtime shims exist for the API surface that the source actually uses (structuredClone, requestIdleCallback, the legacy IntersectionObserver/ResizeObserver floor).

Fault family 1 — Safari ITP / Firefox ETP storage and cookie partitioning. Auth0's silent-logout iframe, refresh-token cache in localStorage, third-party-cookie-dependent token rotation, and the 7-day Safari redirect-tracking purge all degrade. Effects: forced re-logins every ~7 days on Safari, sign-out doesn't actually invalidate the IdP session, repeated QuotaExceededError in Safari Private Browsing.

Fault family 2 — touch / iPad parity. The question-management designer reimplements drag-and-drop using native HTML5 drag events. iOS Safari never fires dragstart from touch, so the designer is fully non-functional on iPad without a Bluetooth mouse. Other parts of the codebase already use @angular/cdk/drag-drop (which is pointer-event based and works on touch); standardising on CDK is the only viable fix.

Fault family 3 — CSS prefix and feature-gate gaps. backdrop-filter is unprefixed, so the modal cover doesn't blur on any Safari version. input:has(+ mat-checkbox) { display: none } fails-open on Safari < 15.4 and Firefox < 121, surfacing duplicate native checkboxes. Several 100vh declarations cause action buttons to disappear behind iOS Safari's collapsing toolbar. A handful of CSS Transforms Level-2 properties (rotate:, translate:) are used without the shorthand fallback.

There are also two shipping bugs that aren't strictly Chromium-specific but surface visibly per browser:

  • study-table.effects.ts:74 calls msSaveOrOpenBlob without a typeof guard — a TypeError on every non-IE browser the moment that path executes.
  • timepoint-array.component.html:1 ships a stray <script src="../../../../../../../../../Desktop/main-es5.0d374fcad9b6d03574d7.js"> tag — a developer's accidental commit. Surfaces as a noisy CSP/mixed-content warning per browser; harmless but inflammatory.

Fault family 4 — metric-sensitive layout that Chromium happens to render narrowly enough to fit. Added in response to a user-reported "dates overlap and spill into release titles in the What's New section on Safari and Firefox" bug. The home-page release timeline at whats-new.component.scss:222 uses a fixed-rem grid track (auto 7.5rem 1fr auto) with white-space: nowrap + font-variant-numeric: tabular-nums + font-feature-settings: 'ss01' 1. The 'ss01' stylistic-set is a Roboto Flex feature that the regular Roboto loaded by index.html doesn't implement; Chromium silently ignores it, while Safari and Firefox shift glyph metrics by ~1–2 px per character — enough over an 18-character date string to overflow the 7.5 rem (120 px) track. The fix is to move the date column to max-content and the title column to minmax(0, 1fr) and to drop the 'ss01' request that does nothing in Roboto. See LAYOUT.F1. A sweep of the rest of the codebase for the same anti-pattern (fixed-rem grid track + nowrap + tabular-nums) returned zero further occurrences — the bug is isolated to one component.

Total: 63 findings — 8 critical, 28 major, 27 minor. Browser exposure: Safari 55 findings, Firefox 35, both 27.

Findings Master List

Findings are tagged <dimension>.F<n> (e.g. CSS.F1) — see the linked sub-audit for full detail.

Critical (8)

ID Title File Browsers Effort
BUILD.F1 No .browserslistrc — implicit narrow default repo-wide Safari ≤ 15, Firefox ESR tail trivial
BUILD.F2 tsconfig target: ES2022 ships unparseable syntax tsconfig.json:41 Safari ≤ 15.3 low
JS.F1 msSaveOrOpenBlob invoked without typeof guard — TypeError study-table.effects.ts:74-90 all non-IE trivial
JS.F2 dragstart never calls dataTransfer.setData() annotation-question-tree-drag-drop.feature.ts:509-527 Firefox trivial
CSS.F1 backdrop-filter missing -webkit- prefix on .cover app.component.scss:12 Safari (all), Firefox < 103 trivial
NG.F1 Auth0 silent-logout iframe broken under ITP / ETP auth.effects.ts:600-619 Safari, Firefox medium
NG.F11 HTML5 drag-and-drop in question-tree designersuperseded by question-management v2 (#2461, #2572-#2575); v1 is feature-flagged off annotation-question-tree-drag-drop.feature.ts:140-310 Safari (iPad/touch) n/a
NG.F15 Stray <script src="../../../Desktop/main-es5..."> tag timepoint-array.component.html:1 all (CSP-noisy on FF/Safari) trivial

Major (27 — top contributors)

ID Title File Browsers Effort
BUILD.F3 polyfills.ts only imports zone.js polyfills.ts:1-58 Safari ≤ 15, Firefox ≤ 95 medium
BUILD.F4 Production optimization shorthand leaves inlineCritical: true (Beasties drops @supports) angular.json:60-62 Safari trivial
BUILD.F5 useDefineForClassFields: false is a fig leaf at ES2022 tsconfig.json:45 Safari 14, Firefox ≤ 89 low
BUILD.F6 user-agent-data-types global types signal Chromium-only API use tsconfig.json:28 Safari, Firefox medium
JS.F3 navigator.userAgentData fallback parser misorders Edge/Chrome client-environment.service.ts:31-46 Safari, Firefox low
JS.F4 / NG.F10 PDF.js v4 worker as .mjs (module worker) pdf-loader.service.ts:5-13 Safari < 16.4, Firefox < 114 medium
JS.F5 crypto.subtle.digest no secure-context guard s3-file.service.ts:237-245 all on non-HTTPS low
JS.F6 4 navigator.clipboard.writeText call sites without fallback or .catch() profile.component.ts:264, project-overview.component.ts:175, study-table.component.ts:732, systematic-searches.component.ts:156 Safari, Firefox medium
NG.F2 Auth0 cacheLocation: 'localstorage' — Safari Private Mode quota main.ts:404-406 Safari Private low
NG.F3 SignalR no explicit transport — silent fallback to LongPolling signal-r.service.ts:189-199 Safari, Firefox low
NG.F4 Hidden <input type="file" fxHide> + programmatic click() review-data-upload.component.html:84-93, search-decision-upload.component.html:108-120 Safari iOS trivial
NG.F6 MAT_DATE_LOCALE set to navigator.language (Safari often returns territory-less code) contact-us.component.ts:286-292, risk-of-bias-job-table.component.ts:47-51 Safari, Firefox low
NG.F7 ngx-image-cropper@6.3.3 mishandles iOS EXIF orientation image-cropper-dialog.component.html:1-12 Safari iOS low
NG.F8 handsontable@^14.4.0 clipboard via execCommand timepoint-spreadsheet.component.ts:132-147 Safari, Firefox medium
NG.F17 Unconditional localStorage writes for crash-recovery app.component.ts:330+, error-recovery-dialog.component.ts:28-32 Safari Private low
NG.F18 Auth0 custom-domain CNAME — confirm same eTLD+1 to bypass ITP main.ts:401 Safari, Firefox low (config)
CSS.F2 Bare rotate: / translate: properties (no shorthand fallback) question-node.component.scss:30, 3 others Safari < 14.1 trivial
CSS.F3 overflow-y: clip not in Safari < 16 email-templates.component.scss:16 Safari < 16 trivial
CSS.F4 Bare :has() selector hides input that user must use styles.scss:362 Safari < 15.4, Firefox < 121 low
CSS.F5 100vh should be dvh/svh on iOS stage-reconcile.component.scss:7, stage-assign.component.scss:2 Safari iOS trivial
LAYOUT.F1 What's New "Earlier releases" date overflows into title (user-reported) whats-new.component.scss:222,286-293 Safari, Firefox trivial

(11 more major findings in the sub-audits; not all reproduced here.)

Minor (26)

Cosmetic, defensive, or already-mitigated-by-modern-browsers items. Examples: dead -moz-/-ms- prefixes in legacy Bootstrap partials, position: -webkit-sticky next to position: sticky in one file but not others, <input type="search"> Safari forced styling. See sub-audits for the full list.

Cross-Cutting Patterns

  1. The build config is the largest single lever. BUILD.F1-F4 collectively dominate the impact: fixing them removes silent breakage on Safari ≤ 15 without touching a single source file.
  2. Auth flows that depend on iframes / third-party cookies are universally fragile in non-Chromium. NG.F1, NG.F2, NG.F18 are three views of the same problem. The bff-auth.provider.ts pathway is the long-term answer — push more environments onto BFF mode.
  3. Two parallel drag-and-drop systems exist. Native HTML5 (broken on touch) in the question-tree designer; CDK (works) elsewhere. Standardise on CDK.
  4. localStorage is used unconditionally throughout. Safari Private Mode quota and Firefox ETP both throw. A safeStorage wrapper would resolve this once.
  5. Clipboard handling is duplicated 8 ways. Only 2 of 8 sites have a fallback; consolidating into a ClipboardService resolves JS.F6 + JS.F10 + NG.F8's clipboard subset together.
  6. No @supports blocks anywhere. Adding feature gates around :has() and backdrop-filter would harden critical paths without blocking modern browsers.
  7. The codebase mixes pre-modern (-moz-keyframes, -ms-touch-action) and bleeding-edge (100svh, :has(), individual translate:) CSS. A pass that removes dead vendor prefixes (CSS.F16-F18) and adds the prefixes Safari still needs (CSS.F1) would make rendering predictable.

Remediation Roadmap

The roadmap is organised by independent change sets that can ship in separate PRs and unlock most of the user-visible improvement quickly.

Wave 1 — Build configuration sprint (~1-2 days, single PR)

Highest blast radius for least risk. All in src/services/web/.

  • Add .browserslistrc (suggested content in BUILD.F1).
  • Lower tsconfig target from ES2022 to ES2020; keep useDefineForClassFields: false.
  • Audit source for structuredClone, Array.prototype.at, Object.hasOwn, requestIdleCallback; add only the core-js polyfill imports needed.
  • Set optimization.styles.inlineCritical: false in production (angular.json).
  • Delete dead custom-webpack.config.js and rewrite the stale polyfills.ts header comment.
  • Decide on user-agent-data-types: either remove globally and import locally, or add an ESLint rule banning navigator.userAgentData outside client-environment.service.ts.

Verification: Build under the new config; diff bundle output; smoke-test on Safari 15 (BrowserStack).

Wave 2 — CSS quick wins (~half a day, single PR)

All trivial-effort CSS findings, batched. Ship together because they all touch styling.

  • CSS.F1 — Add -webkit-backdrop-filter and a colour fallback to .cover.
  • CSS.F2 — Replace bare rotate: / translate: with transform shorthand (4 files).
  • CSS.F3 — Replace overflow-y: clip with overflow-y: hidden in email templates.
  • CSS.F4 — Wrap input:has(+ mat-checkbox) in @supports selector(:has(*)) (and follow-up: refactor to apply a class directly).
  • CSS.F5100vh100dvh in stage-reconcile and stage-assign.
  • CSS.F6 — Add standard line-clamp next to -webkit-line-clamp (5 files; defensive).
  • CSS.F7 — Drop deprecated -webkit-overflow-scrolling: touch.
  • CSS.F11 — Add -webkit-clip-path next to clip-path: polygon(...) in banner.
  • CSS.F13 — Add appearance: textfield reset for <input type="number"> to remove Safari spinner artifacts.
  • LAYOUT.F1 — Replace grid-template-columns: auto 7.5rem 1fr auto with auto max-content minmax(0, 1fr) auto in whats-new.component.scss:222; drop the font-feature-settings: 'ss01' 1 line at :29 (regular Roboto doesn't implement that stylistic set, it only contributes metric drift); add ellipsis safety net to .whats-new__past-date.
  • LAYOUT.F2 — Add overflow-wrap: anywhere to .whats-new__release-label for narrow desktop viewports.

Wave 3 — Single-line correctness fixes (~half a day, single PR)

  • JS.F1 — Replace msSaveOrOpenBlob block with saveBlob() import or delete the dead branch.
  • JS.F2 — Add dataTransfer.setData('text/plain', draggedQuestionId) at start of question-tree dragStart.
  • JS.F8 — Drop the boolean from (window.location.reload as any)(true).
  • JS.F11 — Wrap ResizeObserver construction in typeof !== 'undefined' guard.
  • NG.F4 — Replace fxHide on hidden file inputs with sr-only positioning (or <label for> pattern).
  • NG.F5 — Normalise accept strings to lowercase MIME + dotted extensions.
  • NG.F12 — Reset Safari <input type="search"> styling (or change type to text).
  • NG.F13 — Note scrollIntoView({behavior:'smooth'}) Safari < 15.4 graceful degradation; no fix needed.
  • NG.F15 — Delete the stray <script src="../../Desktop/..."> line from timepoint-array.component.html:1.
  • NG.F16 — Add apple-mobile-web-app-* meta tags, theme-color, apple-touch-icon to index.html.

Wave 4 — Auth fragility sprint (~1 sprint, multi-PR)

These are tied together by Safari ITP / Firefox ETP root cause.

  • NG.F2 — Switch Auth0 cacheLocation to 'memory'; rely on refresh tokens.
  • NG.F18 — Confirm auth0CustomDomain resolves to a same-eTLD+1 host in staging and prod (likely auth.syrf.org.uk); fix infra if not.
  • NG.F1 — Replace silent-logout iframe with a top-level redirect via logout({ federated: false, openUrl }) OR move logout to the same-site sub-domain.
  • NG.F17 — Introduce a safeStorage wrapper used everywhere localStorage is accessed.
  • Long-term: prioritise BFF auth provider migration for browsers — see bff-auth.provider.ts already in the codebase.

Wave 5 — Touch / iPad parity — ~1 sprint~~ DROPPED

  • NG.F11 — Migrate question-tree designer from native HTML5 drag-and-drop to @angular/cdk/drag-drop. No longer required: question-management v1 (the only consumer of this code) is behind a feature flag and is being replaced by v2 in PRs #2461, #2572, #2573, #2574, #2575. Touch parity will be a property of the v2 implementation; no migration of v1 needed.

Wave 6 — Library bumps (~1-2 days, separate PRs)

  • JS.F4 / NG.F10 — Either downgrade pdfjs-dist to 3.x, switch to its legacy/build/pdf.worker.min.js, or document the Safari 16.4+ / Firefox 114+ floor.
  • NG.F7 — Bump ngx-image-cropper to ≥ 7.x for native EXIF handling.
  • NG.F8 — Bump Handsontable to ≥ 15 for navigator.clipboard API.

Wave 7 — Hardening / consolidation (ongoing)

  • Introduce a ClipboardService consolidating JS.F6, JS.F10, and Handsontable's clipboard fallback path.
  • Improve MAT_DATE_LOCALE resolution (NG.F6).
  • Add Sentry breadcrumb on SignalR transport fallback (NG.F3) so we can detect Safari-only LongPolling rates.
  • Self-host Font Awesome / migrate to Material Symbols (BUILD.F10).
  • Plan a separate epic to retire @ngbracket/ngx-layout (fxFlex, fxLayout) — CSS.F14 / NG.F14.

Prevention Recommendations

To stop new regressions landing:

  1. Commit the .browserslistrc. It is the single source of truth for autoprefixer, lightning-css, esbuild, and core-js. Reviewers can reason about compatibility from one file.
  2. Add a stylelint rule disallowing :has(), backdrop-filter, overflow: clip outside an @supports block. (Existing sites grandfathered in via // stylelint-disable until Wave 2 lands.)
  3. Add an ESLint rule banning navigator.userAgentData, msSaveOrOpenBlob, and Element.prototype.scrollIntoViewIfNeeded outside the one feature-detection module that's allowed to use them.
  4. Expand the Playwright E2E matrix beyond Chromium. The repo already runs Playwright in CI (e2e/). Add webkit and firefox projects to playwright.config.ts and run them on the smoke label so regressions show up before merge.
  5. Add a "Browser support" section to the web service README stating the explicit minimum versions and what's polyfilled. Future maintainers will know whether to add new polyfills or refactor.
  6. Add a recurring browser-tail review (every 6 months) — drop versions with < 0.5% real-world traffic from .browserslistrc, lift tsconfig target accordingly, and trim now-unneeded polyfills.

Status

  • PR opened with this planning doc
  • CSS audit complete — see audits/01-css-audit.md
  • JS / Web API audit complete — see audits/02-js-webapi-audit.md
  • Angular / template audit complete — see audits/03-angular-template-audit.md
  • Build / config audit complete — see audits/04-build-config-audit.md
  • Layout audit (user-reported What's New bug + sweep) — see audits/05-layout-audit.md
  • Findings synthesised into final report (this document)
  • Remediation roadmap drafted
  • Wave 2 (CSS quick wins) folded into this PR — see commit history
  • Wave 3 (single-line correctness fixes) folded into this PR — see commit history
  • Roadmap reviewed by team — await sign-off before scheduling Waves 1, 4–7
  • Tickets opened for Wave 1, 4-7

Waves shipped in this PR

Wave Findings shipped Verification
Wave 2 CSS.F1, F2, F3, F4, F5, F6, F7, F11, F13; LAYOUT.F1, F2 pnpm exec ng build passes; affected component tests pass
Wave 3 JS.F1, F2, F8; NG.F4, F5, F12, F15, F16 typecheck + build pass; affected component tests pass

Notes on what was deliberately not folded in

  • JS.F11 (ResizeObserver guard) skipped. The audit flagged this as minor — modern Safari/Firefox fine; ResizeObserver shipped in Safari 13.1 and Firefox 69, both well below the Safari 14 / iOS 14 baseline that Wave 1 will set. Once Wave 1 lands, this finding is fully resolved by the explicit baseline.
  • NG.F13 (scrollIntoView({behavior:'smooth'})) has no concrete fix — graceful degradation on Safari < 15.4 is acceptable. No code change needed.

Findings against question-management v1 — not blocking

The current question-management implementation (everything under src/services/web/src/app/project/project-admin/question-management/design/) is behind a feature flag and will never reach production. It is being replaced by question-management v2, in flight across PRs #2461, #2572, #2573, #2574, #2575. Compatibility findings against v1 code therefore do not need to drive any new work; v2 supersedes them.

Findings affected:

  • JS.F2dragstart setData fix in annotation-question-tree-drag-drop.feature.ts. Already shipped in this PR as a defensive one-liner; harmless if v1 is later deleted.
  • NG.F11 — full HTML5 → CDK DnD migration of the question-tree designer. Drop from the Wave 5 roadmap — v2 will use a different drag system entirely.
  • CSS.F2question-node.component.scss:30 (one of four files in this finding). Already shipped; the other three CSS.F2 sites (styles.scss, project-overview.component.scss, create-project-wizard.component.scss) are unrelated to v1 and remain valid.

Other findings in this report — including NG.F8 (Handsontable in shared/annotation/annotation-form/annotation-experiment-question/outcome-table-info/ and shared/form-controls/timepoint-spreadsheet/) — touch runtime annotation code shared by both v1 and v2, so they remain valid.