Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rn-survey-max-font-size-multiplier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-react-native': minor
---

feat(react-native): let surveys cap how far their text scales with the OS text-size setting, per text role — `appearance.maxFontSizeMultiplier` takes one number for the whole survey or an object keyed by role (`question`, `description`, `header`, `choice`, `input`, `button`, `ratingLabel`, `ratingNumber`, `validationHint`). Unset, text scales without a ceiling exactly as before.
8 changes: 6 additions & 2 deletions packages/react-native/src/surveys/PostHogSurveyProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useSurveyStorage } from './useSurveyStorage'
import { useActivatedSurveys } from './useActivatedSurveys'
import { SurveyModal } from './components/SurveyModal'
import { defaultSurveyAppearance, getContrastingTextColor, SurveyAppearanceTheme } from './surveys-utils'
import { Survey, SurveyAppearance, SurveyType, type SurveyResponses } from '@posthog/core'
import { Survey, SurveyType, type SurveyResponses } from '@posthog/core'
import { usePostHog } from '../hooks/usePostHog'
import { useFeatureFlags } from '../hooks/useFeatureFlags'
import { PostHog } from '../posthog-rn'
Expand Down Expand Up @@ -73,8 +73,12 @@ export type PostHogSurveyProviderProps = {

/**
* The default appearance for surveys when not specified in PostHog.
*
* Accepts the React Native-only appearance fields as well (e.g.
* `maxFontSizeMultiplier`) — they are merged into the same theme object the
* 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.


/**
* If true, PosHog appearance will be ignored and defaultSurveyAppearance is always used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { Linking, Text, TouchableOpacity, View } from 'react-native'

import { createSafeStyleSheet } from '../safeStyleSheet'
import { SurveyAppearanceTheme } from '../surveys-utils'
import { getMaxFontSizeMultiplier, SurveyAppearanceTheme } from '../surveys-utils'

export function BottomSection({
text,
Expand Down Expand Up @@ -43,7 +43,12 @@ export function BottomSection({
}
}}
>
<Text style={[styles.buttonText, { color: appearance.submitButtonTextColor }]}>{text}</Text>
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'button')}
style={[styles.buttonText, { color: appearance.submitButtonTextColor }]}
>
{text}
</Text>
</TouchableOpacity>
</View>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createSafeStyleSheet } from '../safeStyleSheet'
import {
defaultDescriptionOpacity,
getContrastingTextColor,
getMaxFontSizeMultiplier,
shouldRenderDescription,
SurveyAppearanceTheme,
} from '../surveys-utils'
Expand Down Expand Up @@ -33,9 +34,19 @@ export function ConfirmationMessage({
return (
<View style={styleOverrides}>
<View style={styles.thankYouMessageContainer}>
<Text style={[styles.thankYouMessageHeader, { color: textColor }]}>{header}</Text>
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'header')}
style={[styles.thankYouMessageHeader, { color: textColor }]}
>
{header}
</Text>
{shouldRenderDescription(description, contentType) && (
<Text style={{ color: textColor, opacity: defaultDescriptionOpacity }}>{description}</Text>
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'description')}
style={{ color: textColor, opacity: defaultDescriptionOpacity }}
>
{description}
</Text>
)}
</View>
{isModal && (
Expand Down
17 changes: 15 additions & 2 deletions packages/react-native/src/surveys/components/IntroMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createSafeStyleSheet } from '../safeStyleSheet'
import {
defaultDescriptionOpacity,
getContrastingTextColor,
getMaxFontSizeMultiplier,
shouldRenderDescription,
SurveyAppearanceTheme,
} from '../surveys-utils'
Expand All @@ -29,9 +30,21 @@ export function IntroMessage({
return (
<View>
<View style={styles.introMessageContainer}>
{header ? <Text style={[styles.introMessageHeader, { color: textColor }]}>{header}</Text> : null}
{header ? (
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'header')}
style={[styles.introMessageHeader, { color: textColor }]}
>
{header}
</Text>
) : null}
{shouldRenderDescription(description, contentType) && (
<Text style={{ color: textColor, opacity: defaultDescriptionOpacity }}>{description}</Text>
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'description')}
style={{ color: textColor, opacity: defaultDescriptionOpacity }}
>
{description}
</Text>
)}
</View>
<BottomSection
Expand Down
13 changes: 11 additions & 2 deletions packages/react-native/src/surveys/components/QuestionHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createSafeStyleSheet } from '../safeStyleSheet'
import {
defaultDescriptionOpacity,
getContrastingTextColor,
getMaxFontSizeMultiplier,
shouldRenderDescription,
SurveyAppearanceTheme,
} from '../surveys-utils'
Expand All @@ -26,9 +27,17 @@ export function QuestionHeader({

return (
<View style={styles.container}>
<Text style={[styles.question, { color: textColor }]}>{question}</Text>
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'question')}
style={[styles.question, { color: textColor }]}
>
{question}
</Text>
{shouldRenderDescription(description, descriptionContentType) && (
<Text style={[styles.description, { color: textColor, opacity: defaultDescriptionOpacity }]}>
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'description')}
style={[styles.description, { color: textColor, opacity: defaultDescriptionOpacity }]}
>
{description}
</Text>
)}
Expand Down
15 changes: 13 additions & 2 deletions packages/react-native/src/surveys/components/QuestionTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import {
defaultRatingLabelOpacity,
getContrastingTextColor,
getMaxFontSizeMultiplier,
getDisplayOrderChoices,
SurveyAppearanceTheme,
} from '../surveys-utils'
Expand Down Expand Up @@ -121,6 +122,7 @@ export function OpenTextQuestion({
/>
<View style={styles.textInputContainer}>
<TextInput
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'input')}
style={[
styles.textInput,
{
Expand All @@ -144,6 +146,7 @@ export function OpenTextQuestion({
/>
{requirementsHint && (
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'validationHint')}
style={[
styles.validationHint,
{ color: appearance.textColor ?? getContrastingTextColor(appearance.backgroundColor) },
Expand Down Expand Up @@ -266,6 +269,7 @@ export function RatingQuestion({
</View>
<View style={styles.ratingText}>
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'ratingLabel')}
style={{
color: appearance.textColor ?? getContrastingTextColor(appearance.backgroundColor),
opacity: defaultRatingLabelOpacity,
Expand All @@ -274,6 +278,7 @@ export function RatingQuestion({
{question.lowerBoundLabel}
</Text>
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'ratingLabel')}
style={{
color: appearance.textColor ?? getContrastingTextColor(appearance.backgroundColor),
opacity: defaultRatingLabelOpacity,
Expand Down Expand Up @@ -315,7 +320,9 @@ export function RatingButton({
]}
onPress={() => setActiveNumber(num)}
>
<Text style={{ color: textColor }}>{num}</Text>
<Text maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'ratingNumber')} style={{ color: textColor }}>
{num}
</Text>
</TouchableOpacity>
)
}
Expand Down Expand Up @@ -392,14 +399,18 @@ export function MultipleChoiceQuestion({
}}
>
<View style={styles.choiceText}>
<Text style={{ flexGrow: 1, color: choiceTextColor }}>
<Text
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'choice')}
style={{ flexGrow: 1, color: choiceTextColor }}
>
{choice}
{isOpenChoice ? ':' : ''}
</Text>
<View style={styles.rightCheckArea}>{isSelected && <CheckSVG />}</View>
</View>
{isOpenChoice && (
<TextInput
maxFontSizeMultiplier={getMaxFontSizeMultiplier(appearance, 'input')}
style={styles.openEndedInput}
onChangeText={(userValue) => {
setOpenEndedInput(userValue)
Expand Down
14 changes: 5 additions & 9 deletions packages/react-native/src/surveys/components/Surveys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@ import React, { useMemo, useState } from 'react'
import { StyleProp, ViewStyle } from 'react-native'

import { getDisplayOrderQuestions, getNextSurveyStep, SurveyAppearanceTheme } from '../surveys-utils'
import {
Survey,
SurveyAppearance,
SurveyQuestion,
type SurveyResponses,
maybeAdd,
SurveyQuestionBranchingType,
} from '@posthog/core'
import { Survey, SurveyQuestion, type SurveyResponses, maybeAdd, SurveyQuestionBranchingType } from '@posthog/core'
import {
buildSurveyResponseProperties,
getSurveyInteractionProperty,
Expand Down Expand Up @@ -140,7 +133,10 @@ export function Questions({

type GetQuestionComponentProps = {
question: SurveyQuestion
appearance: SurveyAppearance
// The question components each declare `SurveyAppearanceTheme`; typing this
// intermediate as the shared `SurveyAppearance` dropped every React
// Native-only field, which the `as any` below then hid.
appearance: SurveyAppearanceTheme
styleOverrides?: StyleProp<ViewStyle>
onSubmit: (res: string | string[] | number | null) => void
}
Expand Down
73 changes: 73 additions & 0 deletions packages/react-native/src/surveys/surveys-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,85 @@ export const defaultDescriptionOpacity = 0.8
export const defaultRatingLabelOpacity = 0.7

// textColor and inputTextColor are optional overrides (auto-calculated if not provided)
/**
* The distinct kinds of text a survey renders, each at its own base size. They
* are separate because one ceiling cannot serve all of them: `question` is an
* 18pt headline that can wrap freely, while `ratingNumber` sits inside a
* fixed-width button and has nowhere to grow.
*/
export type SurveyTextRole =
/** The question headline. */
| 'question'
/** Body copy under a question, an intro screen, or the thank-you screen. */
| 'description'
/** The intro-screen and thank-you-screen headers. */
| 'header'
/** A choice label in a single- or multiple-choice question. */
| 'choice'
/** Text the user types - the open-text answer and the open-choice field. */
| 'input'
/** The submit / next button label. */
| 'button'
/** The lower- and upper-bound labels under a rating scale. */
| 'ratingLabel'
/** The numeral inside a rating button, which cannot grow past the button. */
| 'ratingNumber'
/** The validation hint under an open-text answer. */
| 'validationHint'

export type SurveyAppearanceTheme = Omit<
Required<SurveyAppearance>,
'widgetSelector' | 'widgetType' | 'widgetColor' | 'widgetLabel' | 'shuffleQuestions' | 'textColor' | 'inputTextColor'
> & {
textColor?: string
inputTextColor?: string
/**
* 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.

*
* Pass a number to cap every role at once, or an object to cap them
* separately: a survey headline can usually take more scaling than a numeral
* inside a rating button, and a host app that already caps its own text by
* role will want to match those ceilings here.
*
* ```ts
* maxFontSizeMultiplier: 1.6
* // or
* maxFontSizeMultiplier: { question: 1.5, description: 1.8, ratingNumber: 1.2 }
* ```
*
* Roles left out of the object are uncapped, as they are today.
*
* Leave it unset (the default) and survey text scales without a ceiling. That
* is what the OS asks for, but not always what a fixed-size survey card can
* hold - at the largest accessibility sizes an unbounded 18pt headline renders
* roughly one word per line.
*
* React Native only inherits this prop through nested `Text`, so a host app
* cannot apply it from the outside - it has to come in here.
*
* @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.

}

/**
* The ceiling for one role, from either form of `maxFontSizeMultiplier`.
*
* Returns `undefined` when nothing is configured for that role, which is also
* React Native's "no ceiling" - so an unset appearance renders exactly as it
* did before this option existed.
*/
export function getMaxFontSizeMultiplier(
appearance: Pick<SurveyAppearanceTheme, 'maxFontSizeMultiplier'>,
role: SurveyTextRole
): number | undefined {
const configured = appearance.maxFontSizeMultiplier
if (typeof configured === 'number') {
return configured
}
return configured?.[role]
}
export const defaultSurveyAppearance: SurveyAppearanceTheme = {
backgroundColor: defaultBackgroundColor,
Expand Down
Loading
Loading