Skip to content

fix: make in-app survey text follow Dynamic Type [ENG-3427] - #62

Merged
itsjavi merged 8 commits into
mainfrom
claude/sweet-tesla-wprd0x
Oct 14, 2026
Merged

itsjavi merged 8 commits into
mainfrom
claude/sweet-tesla-wprd0x

Conversation

@itsjavi

@itsjavi itsjavi commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Ref ENG-3427

What & why

Was: in-app surveys on iOS rendered at the default text size whatever the respondent chose under Larger Text. WKWebView never applies Dynamic Type to web content.

Now: the survey's text follows Dynamic Type both ways, up to 200% at the accessibility sizes, on iPhone and iPad. The default size is unchanged.

Android needs no change: its WebView already scales page text by the system font scale unless the app calls setTextZoom, which the Android SDK never does (AwSettings.updateFontScaleLocked, aw_settings.cc → SetTextZoomFactor).

Where to look

  • WebView/SurveyTextScale.swift — Dynamic Type → -webkit-text-size-adjust, and the 200% cap
  • WebView/FormbricksViewModel.swift — where the value enters the page, read from the presenter's traits
  • WebView/SurveyWebView.swift — mobile content mode, so full-size iPads honour the value
Design points worth checking

Why -webkit-text-size-adjust. WebKit multiplies it into every computed font size (updateFontForTextSizeAdjust), and em lengths resolve against the adjusted size (resolveEm). The renderer compiles its spacing to em, so on iOS padding and gaps grow with the text while pixel-sized parts keep theirs. Android's text zoom grows only the text. WKWebView.pageZoom was the alternative: it scales everything, shrinks the layout viewport to ~190 CSS px at 2×, and would need the card rect for touch passthrough rescaled.

The 200% cap. It is Android's largest font scale and WCAG 1.4.4's resize target. Every question type checked fits a 402 pt iPhone card at 2× (screenshots); the matrix scrolls sideways. iOS goes to ~3× (AX5), so the largest accessibility sizes get 200%. It's a product call — one constant, maximumFactor.

Scale source. UIFontMetrics(forTextStyle: .body) — Apple's API for scaling custom font sizes — so xxxLarge gives 131%, slightly under the system body text's 135%.

Whose setting. The category comes from the trait source PresentSurveyManager already passes for the appearance: the host's key window (no overlay) or the presenting view controller (overlay). A host that overrides Dynamic Type there with traitOverrides caps the survey too. A SwiftUI-only .dynamicTypeSize(...) cap never reaches UIKit traits, so the SDK can't see it; such apps need to set the same cap in traitOverrides. Without a trait source the page keeps the default size.

Mobile content mode. Full-size iPads load WKWebView content in desktop-class mode, where WebKit ignores the percentage. The survey WebView asks for .mobile. At the default size the iPad render is pixel-identical to main; the user agent now says iPad instead of Mac.

Default size is untouched. At "Large" the template emits auto, the property's initial value, not 100%. Pixel-identical to main on iPhone and iPad.

Read once, at presentation. A Dynamic Type change while a survey is open applies to the next survey.

The renderer must not set it. It doesn't today: its only rule is #fbjs html { -webkit-text-size-adjust: 100% }, which matches nothing. If that selector were ever "fixed" to html, it would override this.

Coverage

Behaviour Level
Default size leaves the page as before (auto) unit (mutation): testTheDefaultSizeLeavesThePageUntouched, return "\(percent)%" unconditionally at SurveyTextScale.swift:43
Text follows the setting, smaller and larger unit (mutation): testTheTextFollowsTheSettingInBothDirections, return 1 at SurveyTextScale.swift:36
Accessibility sizes stop at 200% unit (mutation): testTheAccessibilitySizesStopAtTheCap, drop the min at SurveyTextScale.swift:36
The presenter's setting reaches the page's root element; no presenter gives auto unit (mutation): testTheSurveyPageCarriesThePresentersSetting, read UIApplication.preferredContentSizeCategory instead at FormbricksViewModel.swift:26 (red)
The survey WebView asks for mobile content unit (mutation): testTheSurveyWebViewAsksForMobileContent, drop the line at SurveyWebView.swift:33 (red)
Real WKWebView: text follows Dynamic Type, default size unchanged manual: iPhone 17 Pro and iPad Pro 13" simulators, iOS 26.5, renderer built from packages/surveys; computed styles read in the page, 0 pixels differ at Large (screenshots)
Long headline, matrix, ranking, NPS at 200%: card scrolls, Finish reachable manual: iPhone 17 Pro simulator (screenshots)
A host window's traitOverrides caps the survey manual: system at AX5, host window at Large → auto (screenshot)
Android text follows font scale up to 200% manual: Chromium source reading + emulation; no device

Rerun: xcodebuild test -scheme FormbricksSDK -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' -only-testing:FormbricksSDKTests/SurveyTextScaleTests

Screenshots — iOS 26.5 simulators, the SDK in a host app, the survey renderer built from packages/surveys

main vs this PR, same survey and host, only the SDK differs.

iPhone 17 Pro main This PR
Large (default) → auto Default-size survey Same survey, 0 pixels differ
xxxLarge → 131% Default-size text despite the larger setting Text at 131%
AX5 → 200%, choice Default-size text despite the largest setting Text at 200%, card scrolls
AX5, long headline Default-size long question Long question at 200% Scrolled to the end: options wrap, Finish reachable
AX5, matrix Default-size matrix Matrix at 200%, first column visible Scrolled sideways, a cell selected, labels off screen
AX5, ranking Default-size ranking Ranking at 200%
AX5, NPS Default-size NPS 0–10 row still fits at 200%
iPad Pro 13" main (desktop-class mode) This PR (mobile mode)
Large (default) Default-size survey on iPad Same survey, 0 pixels differ
AX5 → 200% Default-size text: WebKit ignores the percentage Text at 200%
System at AX5, host caps it This PR
Host window traitOverrides at Large → survey at default size Host and survey both at default size
SwiftUI .dynamicTypeSize(...DynamicTypeSize.large) only → not visible to UIKit, survey at 200% Host text capped, survey at 200%

Open gaps

  • Headline line spacing is doubled on iPhone at large sizes (renderer). WebKit scales em line-heights twice under a percentage text-size-adjust: once through the adjusted font, once by the factor. The renderer's leading-* compile to calc(var(--spacing) * N) with --spacing: .25em, so at 200% headlines get a 96 px line-height for 32 px text instead of 48 px, and buttons 64 px. Unitless and pixel line-heights scale correctly, and the iPad in mobile mode is unaffected. The fix is unitless leading-* in packages/surveys; follow-up, not in this PR.
  • The matrix scrolls sideways at 200%, and its row labels leave the view when it does. Usable, not comfortable; a sticky label column in the renderer would fix it.
  • The React Native and Flutter SDKs' iOS WebViews have the same gap; not in this PR.
  • No device run, and Android at 200% is still unchecked on a device.
  • CI: PassthroughPresentationTests' 3 s wait times out intermittently on main (comments below).

Breaking changes

  • This is a breaking change

No public API change. Surveys on full-size iPads now load in mobile content mode, so their user agent says iPad instead of Mac; the layout at the default size is unchanged.


Note

AI model used — claude-opus-5-5.

itsjavi and others added 2 commits October 6, 2026 18:04
WKWebView never applies Dynamic Type to web content, and the survey
renderer sizes its text in CSS pixels, so respondents with a larger text
size saw default-sized surveys. The template now sets
-webkit-text-size-adjust on the root element from the user's content
size category: WebKit multiplies it into every computed font size, as
Android's WebView already does with the system font scale.

The default size emits `auto` (the initial value), so the page renders
as before. Accessibility sizes are capped at 200%, Android's largest
font scale, which the survey layout is known to fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WebKit resolves em lengths against the adjusted font size and the
renderer compiles its spacing to em, so padding grows with the text on
iOS, unlike Android's text-only zoom. The 2x cap still fits every
question type on a phone-width card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 0c25bfd6-dd5d-4e3c-9fc2-44ce573c3895
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

itsjavi commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

build-and-analyze has two failures on dc81277:

  • SurveyTextScaleTests.testTheTextFollowsTheSettingInBothDirections — this PR's, fixed in the next push. The test pinned xxxLarge at 135%, the system body-size ratio (23/17). UIFontMetrics scales a custom 17 pt size to 131% there. The test now asserts the relationship (above 1×, below the cap) instead of a guessed table value.

  • PassthroughPresentationTests.testOverlaySurveyIsPresentedOnTheTopMostViewController — not this PR's. It fails identically on main at b782cb0 (run 37435222335) and in fix: let host-app touches through when the survey has no overlay [ENG-1818] #57's merge-queue run. Both logs show the main thread blocked for 9–20 s right after WebKit's Networking process launches, which happens when the first WKWebView is created on a cold simulator. The predicate is true by then, but wait(for:timeout: 3.0) has already expired on wall-clock time. No fix exists yet. Proposed patch, outside this PR's scope:

    -        wait(for: [presented], timeout: 3.0)
    +        // The first WKWebView in the test process cold-starts WebKit's helper processes, which can
    +        // block the main thread for many seconds on CI simulators.
    +        wait(for: [presented], timeout: 30.0)

    (Tests/FormbricksSDKTests/PassthroughPresentationTests.swift:129). The CI run triggered by the next push will show whether it passes this time.


Generated by Claude Code

itsjavi and others added 2 commits October 6, 2026 18:26
UIFontMetrics scales a custom 17 pt size to 131% at xxxLarge, not the
135% the system body-size table gives, so the exact-value assertion
failed on CI. The test now checks that the largest standard size grows
the text without reaching the cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependabot's group bump (#61) moved it to v8.2.2, which verifies the
scanner's GPG signature against keyservers this harden-runner macOS
runner cannot reach, so the SonarCloud step fails on every run. The
comment above the pin already explains why it is held at v6.0.0. This
restores that pin; it is a no-op once main carries the same revert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

itsjavi commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

build-and-analyze on b8d1f34: all 144 tests pass, including the presentation test that timed out last run. The job then fails in SonarCloud Scan: Failed to import SonarSource public key from all keyservers.

Not this PR's: #61 (dependabot, merged as b782cb0) moved sonarqube-scan-action from v6.0.0 to v8.2.2. The comment above that pin explains why it is held at v6.0.0: v8 verifies a GPG signature against keyservers this harden-runner macOS runner can't reach. So main fails the same step.

Ported the fix, the revert of that one line, in 9c767a1. It becomes a no-op once main carries the same revert. Dependabot will propose v8 again on its next weekly run unless .github/dependabot.yml gets an ignore for SonarSource/sonarqube-scan-action major updates. I've left that out to keep this PR's scope.


Generated by Claude Code

@itsjavi
itsjavi requested review from Dhruwang and pandeymangg and removed request for pandeymangg October 7, 2026 08:03
Comment thread .github/workflows/sonarqube.yml Resolved
itsjavi and others added 3 commits October 13, 2026 10:33
…rd0x

# Conflicts:
#	Sources/FormbricksSDK/WebView/FormbricksViewModel.swift
UIApplication.preferredContentSizeCategory ignores trait overrides, so a
host app that caps Dynamic Type on its window or view controller still
got a 200% survey. The view model now reads the category from the trait
source it already receives for the appearance (the key window, or the
presenting view controller), and without one the page keeps the default
size, so HTML-building tests no longer depend on the simulator setting.

The tests check the value alone instead of the whole CSS rule, and set
the SDK up only in the test that builds a page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Full-size iPads load WKWebView content in desktop-class mode, where
WebKit ignores -webkit-text-size-adjust, so the survey text stayed at the
default size there. The survey WebView now asks for mobile content. At
the default text size the iPad render is unchanged (pixel-identical on an
iPad Pro 13" simulator); the user agent now reports an iPad instead of a
Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@itsjavi
itsjavi enabled auto-merge October 13, 2026 13:53
@sonarqubecloud

Copy link
Copy Markdown

@mattinannt
mattinannt disabled auto-merge October 14, 2026 08:07
@mattinannt
mattinannt enabled auto-merge October 14, 2026 08:07
@itsjavi
itsjavi disabled auto-merge October 14, 2026 08:09
@itsjavi
itsjavi added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 1b3648b Oct 14, 2026
3 checks passed
@pandeymangg pandeymangg mentioned this pull request Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants