Skip to content

feat(react-native): let surveys cap how far their text scales, per text role - #4605

Draft
safaiyeh wants to merge 1 commit into
PostHog:mainfrom
safaiyeh:feat/rn-survey-max-font-size-multiplier
Draft

feat(react-native): let surveys cap how far their text scales, per text role#4605
safaiyeh wants to merge 1 commit into
PostHog:mainfrom
safaiyeh:feat/rn-survey-max-font-size-multiplier

Conversation

@safaiyeh

Copy link
Copy Markdown

Problem

React Native surveys render every Text and TextInput with no maxFontSizeMultiplier, so survey copy scales without a ceiling under the OS text-size setting.

At the largest accessibility sizes an 18pt question headline renders roughly one word per line on a small phone, with the ? alone on its own line. Nothing clips and the card still scrolls — but in an app that caps its own text, the survey becomes the one uncapped thing on the screen.

A host app cannot fix this from the outside. React Native only inherits maxFontSizeMultiplier through nested Text, and these are siblings inside Views, so the value has to come in through the survey's own API.

What this adds

appearance.maxFontSizeMultiplier — one number for the whole survey, or an object keyed by text role:

<Questions appearance={{ ...appearance, maxFontSizeMultiplier: 1.6 }}  />

<Questions
  appearance={{
    ...appearance,
    maxFontSizeMultiplier: { question: 1.5, description: 1.8, ratingNumber: 1.2 },
  }}
  
/>

Why per role, and not one number

One ceiling cannot serve every kind of text a survey draws. question is a headline that can wrap freely; ratingNumber sits inside a fixed-width button and has nowhere to grow; button sits in a row that has to stay one line. An app that already caps its own text by role wants to match those ceilings here rather than flatten them to a single value.

The nine roles cover all 16 text nodes the survey renders:

role nodes
question question headline
description question, intro-screen and thank-you descriptions
header intro-screen and thank-you headers
choice choice labels
input open-text answer, open-choice field
button submit / next label
ratingLabel lower- and upper-bound labels
ratingNumber the numeral inside a rating button
validationHint hint under an open-text answer

Because appearance is already threaded to every survey component, this needed no new prop plumbing.

Nothing changes unless it is set

  • An unset appearance passes undefined, which is React Native's "no ceiling" — every existing survey renders exactly as it does today.
  • A role omitted from the object is likewise uncapped.
  • 0 is passed through rather than swallowed. In React Native that is a documented value meaning no maximum, not an absent one, so the resolver tests typeof … === 'number' instead of truthiness.

Two supporting type fixes the option needs

Both are places where a React Native-only appearance field could not reach the component that reads it:

  • PostHogSurveyProvider's defaultSurveyAppearance was typed as the shared SurveyAppearance, so RN-only fields could not be passed — even though the provider already merges that object into a SurveyAppearanceTheme. Now Partial<SurveyAppearanceTheme>.
  • getQuestionComponent's intermediate props type narrowed appearance back to the shared SurveyAppearance, dropping every RN-only field; the as any on the call below hid it. The question components themselves already declare SurveyAppearanceTheme.

Tests

New test/surveyMaxFontSizeMultiplier.spec.tsx — 9 tests covering the resolver (unset, flat number, per-role, omitted role, 0) and the wiring (no ceiling when unconfigured; headline and description getting different ceilings; the button capped independently).

One line in test/surveys-autoScroll.spec.tsx: its react-native shim renders Text as a div and spreads unknown props, so it now strips this native-only prop alongside the others it already strips.

Checks

Ran from the repo root, per packages/react-native/CONTRIBUTING.md:

  • pnpm --filter=posthog-react-native lint — clean
  • pnpm --filter=posthog-react-native test612 passed, 42 suites
  • pnpm --filter=posthog-react-native build — clean

Changeset included (minor).

Deliberately left out

allowFontScaling. It would be one more field, but it turns OS text scaling off entirely rather than bounding it, which is the opposite of what this PR is for — happy to add it if you'd rather the surface be complete.

…xt role

React Native surveys render every `Text` and `TextInput` with no
`maxFontSizeMultiplier`, so survey copy scales without a ceiling under the OS
text-size setting. At the largest accessibility sizes an 18pt question headline
renders roughly one word per line on a small phone, and a host app cannot fix it
from the outside: React Native only inherits `maxFontSizeMultiplier` through
nested `Text`, and these are siblings inside `View`s.

`appearance.maxFontSizeMultiplier` now carries a ceiling into the survey. It
takes either one number for the whole survey, or an object keyed by text role:

    maxFontSizeMultiplier: 1.6
    maxFontSizeMultiplier: { question: 1.5, description: 1.8, ratingNumber: 1.2 }

Per-role rather than one number, because one ceiling cannot serve every kind of
text a survey draws. `question` is a headline that can wrap freely; `ratingNumber`
sits inside a fixed-width button and has nowhere to grow; an app that already
caps its own text by role wants to match those ceilings here rather than flatten
them. The nine roles cover all sixteen text nodes the survey renders.

Nothing changes unless it is set: an unset appearance passes `undefined`, which
is React Native's "no ceiling", so every existing survey renders exactly as it
did. A role omitted from the object is likewise uncapped. `0` is passed through
rather than swallowed — in React Native that is a documented value meaning "no
maximum", not an absent one.

Two supporting changes the option needs to reach where it is read:

- `PostHogSurveyProvider`'s `defaultSurveyAppearance` was typed as the shared
  `SurveyAppearance`, so React Native-only appearance fields could not be passed
  even though the provider already merges them into a `SurveyAppearanceTheme`.
- `getQuestionComponent`'s intermediate props type narrowed `appearance` back to
  the shared type, dropping every React Native-only field; the `as any` on the
  call below hid it. The question components themselves already declare
  `SurveyAppearanceTheme`.

The auto-scroll spec's react-native shim renders `Text` as a `div` and spreads
unknown props, so it now strips this native-only prop alongside the others it
already strips.
@safaiyeh
safaiyeh requested a review from a team as a code owner August 22, 2026 23:42
@marandaneto
marandaneto requested a review from a team August 23, 2026 06:49
@marandaneto

Copy link
Copy Markdown
Member

thanks @safaiyeh
i'll defer to @PostHog/team-surveys since this is a SDK configuration only

The shared SurveyAppearance type and PostHog survey API do not include this field, so it cannot currently be configured from the PostHog survey editor/API

Moving to draft until we hear from @PostHog/team-surveys

@marandaneto
marandaneto marked this pull request as draft August 23, 2026 07:03

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline review findings from the validated review.

* survey components read, and PostHog never sends them down.
*/
defaultSurveyAppearance?: SurveyAppearance
defaultSurveyAppearance?: Partial<SurveyAppearanceTheme>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: Preserve previously accepted appearance keys

Changing this prop from SurveyAppearance to Partial<SurveyAppearanceTheme> drops widgetSelector, widgetType, widgetColor, widgetLabel, and shuffleQuestions from accepted object-literal keys, so existing TypeScript callers using them fail excess-property checks. Please extend the existing public appearance contract with the React Native-only field rather than narrowing it.

*
* @default undefined (no ceiling)
*/
maxFontSizeMultiplier?: number | Partial<Record<SurveyTextRole, number>>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: Expose the new option through a public appearance type

The package root still exports only SurveyAppearance from @posthog/core, which lacks this field, while neither SurveyAppearanceTheme nor the referenced SurveyTextRole is publicly exported. Consumers therefore cannot type reusable appearance configuration containing maxFontSizeMultiplier, and API Extractor reports forgotten exports. Please export a public React Native appearance type and its referenced role type, and use it for public props.

/**
* Caps how far survey text may grow under the OS text-size setting, as a
* multiple of its base size - React Native's `maxFontSizeMultiplier`, applied
* to every `Text` and `TextInput` the survey renders.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: Apply the survey-wide cap to fallback icon text

When react-native-svg or its native view managers are unavailable, icons.tsx renders glyph fallbacks with React Native Text, but those nodes never receive this cap. A numeric whole-survey limit therefore does not cover every Text, and the glyphs can scale and clip inside their fixed dimensions. Pass the relevant cap to those fallbacks or disable font scaling for icon-only glyphs.

@marandaneto
marandaneto requested a review from a team August 23, 2026 07:07
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