Skip to content

fix(CDS-2427): size LinearGradient svg to measured pixels under Fabric - #810

Open
adrienzheng-cb wants to merge 2 commits into
masterfrom
adrien/cds-2427-lineargradient-fabric-sizing
Open

fix(CDS-2427): size LinearGradient svg to measured pixels under Fabric#810
adrienzheng-cb wants to merge 2 commits into
masterfrom
adrien/cds-2427-lineargradient-fabric-sizing

Conversation

@adrienzheng-cb

@adrienzheng-cb adrienzheng-cb commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What changed? Why?

After the CMR 8 upgrade (Expo SDK 56 / React Native 0.85 / new Yoga), cds-mobile's LinearGradient mis-sizes on Fabric — it expands to full screen instead of hugging its children.

The gradient is painted by an inset-only absoluteFillObject View wrapping an <Svg width="100%" height="100%">. A size-less absolute view with a percentage-sized SVG child is exactly what the new Yoga/react-native-svg-on-Fabric combination mishandles, so the wrapper blows up to the full screen.

The fix measures the wrapper via onLayout and hands the inner container, Svg, and Rect explicit pixel dimensions (seeded from numeric style width/height when present so there's no first-frame flash), instead of relying on absoluteFillObject + "100%".

Root cause (required for bugfixes)

StyleSheet.absoluteFillObject is inset-only (no width/height). Under RN 0.81's Yoga this still resolves to the parent's box, but the Yoga/react-native-svg behavior on RN 0.85 (Expo 56) sizes the percentage SVG against an unconstrained box, expanding the gradient to fill the screen. Giving the container/SVG concrete measured pixels restores correct sizing.

Validation

Manually reproduced and verified on a local Expo SDK 56 / React Native 0.85 build of apps/expo-app (temporary dependency bump, not part of this PR): on the LinearGradient story the gradient expands/collapses before the fix and hugs its children after it (see before/after below).

Note this is not reproducible on apps/expo-app in its committed state (Expo 54 / RN 0.81.5) — the new-Yoga behavior only manifests on 0.85. The fix also mirrors the exact approach already validated in production on CMR v8 / RN 0.85 by the Retail team in consumer/react-native#71155, where it currently ships as a downstream @cbhq/cds-mobile Yarn patch on the compiled LinearGradient.js. This PR upstreams that patch to source verbatim (same useState/useCallback/onLayout + pixel-sizing), so that downstream patch can be dropped once Retail bumps to a cds-mobile release containing it.

UI changes

Captured on the local Expo 56 / RN 0.85 build described above.

Before After
Before — LinearGradient expands to full screen on RN 0.85 After — LinearGradient hugs its children

Testing

How has it been tested?

  • Unit tests
  • Manual - iOS (Emulator / Device)
  • Manual - Android (Emulator / Device)

Testing instructions

Added regression tests (packages/mobile/src/gradients/__tests__/LinearGradient.test.tsx) asserting the SVG is sized from numeric style dims up front and to measured pixels after onLayout (never "100%"). mobile:test (gradient suite), mobile:typecheck, and the formatter are all clean.

Manual: on a temporary local Expo 56 / RN 0.85 build, open the LinearGradient story — the gradient bands hug their text with the fix, and expand/collapse without it. (The native Yoga expansion can only be exercised on RN 0.85; jsdom unit tests encode the fix's intent.)

Change management

type=routine
risk=low
impact=sev4

automerge=false

Fixes CDS-2427.

Made with Cursor

@linear

linear Bot commented Jul 27, 2026

Copy link
Copy Markdown

CDS-2427

@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 1
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1
CODEOWNERS 🟡 See below

🟡 CODEOWNERS

Code Owner Status Calculation
ui-systems-eng-team 🟡 0/1
Denominator calculation
Additional CODEOWNERS Requirement
Show calculation
Sum 0
0
From CODEOWNERS 1
Sum 1

@github-actions

Copy link
Copy Markdown
Contributor

@cb-heimdall

Copy link
Copy Markdown
Collaborator

Review Error for dh-cb @ 2026-07-27 18:51:11 UTC
User failed mfa authentication, either user does not exist or public email is not set on your github profile. \ see go/mfa-help

@cb-heimdall

Copy link
Copy Markdown
Collaborator

Review Error for dh-cb @ 2026-07-28 14:51:16 UTC
User failed mfa authentication, either user does not exist or public email is not set on your github profile. \ see go/mfa-help

After the CMR 8 upgrade (Expo 56 / RN 0.85 / new Yoga), the LinearGradient
wrapper — a size-less `absoluteFillObject` View wrapping an `Svg` with
`width/height="100%"` — expands to full screen instead of hugging its
children.

Measure the wrapper via `onLayout` and hand the inner container/`Svg`/`Rect`
explicit pixel dimensions (seeded from numeric `style` dims when present).
This mirrors the fix the Retail team validated in production on CMR v8 / RN
0.85 (consumer/react-native#71155, applied there as a downstream cds-mobile
yarn patch), and lets that patch be dropped once this ships.

Co-authored-by: Cursor <cursoragent@cursor.com>
@adrienzheng-cb
adrienzheng-cb force-pushed the adrien/cds-2427-lineargradient-fabric-sizing branch from 599c574 to b2c9368 Compare July 28, 2026 19:56

@cb-ekuersch cb-ekuersch left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

left a few comments - let's help the customer out and take care of that patch form them after upgrading!


type Coordinate = { x: number; y: number };

type PixelSize = { width: number; height: number } | null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: can we not use a type derived from the LayoutChangeEvent type?

}: LinearGradientProps) {
const [pixelSize, setPixelSize] = useState<PixelSize>(() => {
const flat = StyleSheet.flatten(style);
return flat && typeof flat.width === 'number' && typeof flat.height === 'number'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this initial state stores the computed w/h or the style properties passed in? What happens when no w/h is set in the style object on initial render?

<Svg height="100%" width="100%">
<View
key="GrandientSvgContainer"
style={{ position: 'absolute', top: 0, left: 0, width: svgWidth, height: svgHeight }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

non-memoized inline style object

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants