also looking at this
fix(native): preserve explicit grid row tracks
#2092
5 files
+42
-4
| @@ -2,7 +2,7 @@ import type { STXStyle } from './ir' | ||
| 2 | 2 | |
| 3 | 3 | /** Every property `STXStyle` declares, for inline `style=""` translation. */ |
| 4 | 4 | const STYLE_KEYS = new Set([ |
| 5 | 'display', 'gridColumns', 'gridTemplateColumns', 'gridAutoRows', | |
| 5 | 'display', 'gridColumns', 'gridTemplateColumns', 'gridTemplateRows', 'gridAutoRows', | |
| 6 | 6 | 'flex', 'flexGrow', 'flexShrink', 'flexBasis', 'flexDirection', 'flexWrap', |
| 7 | 7 | 'justifyContent', 'alignItems', 'alignSelf', 'alignContent', 'position', 'top', 'right', |
| 8 | 8 | 'bottom', 'left', 'zIndex', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', |
| @@ -161,7 +161,7 @@ export function parseNativeStyle(input: unknown, report: (diagnostic: NativeStyl | ||
| 161 | 161 | translated = undefined |
| 162 | 162 | } |
| 163 | 163 | else if (property === 'shadowOffset' || property === 'transform') translated = structuredStyle(property, value) |
| 164 | else if (property === 'gridTemplateColumns') translated = finite(value) || typeof value === 'string' ? value : undefined | |
| 164 | else if (property === 'gridTemplateColumns' || property === 'gridTemplateRows') translated = finite(value) || typeof value === 'string' ? value : undefined | |
| 165 | 165 | // Font weights are strings in the IR, even when authored as object numbers. |
| 166 | 166 | else if (property === 'fontWeight' && finite(value)) translated = String(value) |
| 167 | 167 | else if (typeof value === 'string') translated = value |
| @@ -149,6 +149,8 @@ export interface STXStyle { | ||
| 149 | 149 | gridColumns?: number |
| 150 | 150 | /** CSS-shaped grid tracks understood by the maintained Craft hosts. */ |
| 151 | 151 | gridTemplateColumns?: string | number |
| 152 | /** Explicit equal rows within a definite native grid height. */ | |
| 153 | gridTemplateRows?: string | number | |
| 152 | 154 | /** Fixed native row height for grid layouts. */ |
| 153 | 155 | gridAutoRows?: number |
| 154 | 156 | flex?: number |
| @@ -407,7 +407,7 @@ describe('style vocabulary coverage', () => { | ||
| 407 | 407 | |
| 408 | 408 | /** |
| 409 | 409 | * Emitted by the compiler and absent from the iOS mirror, so dropped in |
| 410 | * silence. This list must only shrink. | |
| 410 | * silence. Non-grid gaps must only shrink; grid features target Craft. | |
| 411 | 411 | */ |
| 412 | 412 | const IOS_DROPS = [ |
| 413 | 413 | 'alignContent', 'aspectRatio', 'backgroundImage', 'borderBottomColor', |
| @@ -416,7 +416,7 @@ describe('style vocabulary coverage', () => { | ||
| 416 | 416 | 'columnGap', 'elevation', 'flexBasis', 'rowGap', 'transform', |
| 417 | 417 | // Craft's native host lays out `display: grid` with `gridColumns`; this |
| 418 | 418 | // older renderer has no grid (added with grid-cols-N, 2026-10). |
| 419 | 'gridAutoRows', 'gridColumns', 'gridTemplateColumns', | |
| 419 | 'gridAutoRows', 'gridColumns', 'gridTemplateColumns', 'gridTemplateRows', | |
| 420 | 420 | ] |
| 421 | 421 | |
| 422 | 422 | it('iOS drops exactly the properties on the known list, and no more', () => { |
| @@ -16,6 +16,21 @@ describe('shared native inline-style normalization', () => { | ||
| 16 | 16 | expect(parseNativeStyle({ fontWeight: 700 })).toEqual({ fontWeight: '700' }) |
| 17 | 17 | }) |
| 18 | 18 | |
| 19 | it('preserves explicit grid row tracks in object, serialized and CSS styles', async () => { | |
| 20 | for (const rows of ['repeat(2, 1fr)', '1fr 1fr', 2]) { | |
| 21 | const expected = { display: 'grid', gridTemplateRows: rows } | |
| 22 | for (const input of [expected, JSON.stringify(expected)]) { | |
| 23 | const diagnostics: NativeStyleDiagnostic[] = [] | |
| 24 | expect(parseNativeStyle(input, d => diagnostics.push(d))).toEqual(expected) | |
| 25 | expect(diagnostics).toEqual([]) | |
| 26 | } | |
| 27 | } | |
| 28 | const css = 'display:grid;grid-template-rows:repeat(2, 1fr)' | |
| 29 | expect(parseNativeStyle(css)).toEqual({ display: 'grid', gridTemplateRows: 'repeat(2, 1fr)' }) | |
| 30 | expect((await translateHtmlToIR(`<div style="${css}"><span>row</span></div>`)).root.style) | |
| 31 | .toEqual({ display: 'grid', gridTemplateRows: 'repeat(2, 1fr)' }) | |
| 32 | }) | |
| 33 | ||
| 19 | 34 | it('does not corrupt quoted font names or semicolons in CSS function values', () => { |
| 20 | 35 | const image = 'url("data:image/svg+xml;base64,abc")' |
| 21 | 36 | expect(parseNativeStyle(`background-image:${image};font-family:"A;B";padding:8px`)) |
| @@ -2,6 +2,27 @@ import { describe, expect, it } from 'bun:test' | ||
| 2 | 2 | import { compileNativeScreen, mountNativeScreen } from '../../src/native/testing' |
| 3 | 3 | |
| 4 | 4 | describe.each([false, true])('reactive native inline styles (minify=%s)', (minify) => { |
| 5 | it('delivers explicit grid rows through the compiled native bundle and clears changed tracks', async () => { | |
| 6 | const { code } = await compileNativeScreen(`<script client> | |
| 7 | const tracks = state({ gridTemplateRows: 'repeat(2, 1fr)' }) | |
| 8 | </script><View testID="grid" style="display:grid;height:84px" :style="tracks"> | |
| 9 | <View testID="static-grid" style={{"display":"grid","gridTemplateRows":"repeat(2, 1fr)","height":84}} /> | |
| 10 | <Button testID="change" @click="tracks.set({gridTemplateRows:3})">Change rows</Button> | |
| 11 | <Button testID="clear" @click="tracks.set({})">Clear rows</Button> | |
| 12 | </View>`, { minify }) | |
| 13 | const phone = mountNativeScreen(code, 'Screen') | |
| 14 | await phone.settle() | |
| 15 | expect(phone.byTestID('static-grid')!.style).toMatchObject({ display: 'grid', height: 84, gridTemplateRows: 'repeat(2, 1fr)' }) | |
| 16 | expect(phone.byTestID('grid')!.style).toMatchObject({ display: 'grid', height: 84, gridTemplateRows: 'repeat(2, 1fr)' }) | |
| 17 | phone.press(phone.byTestID('change')!) | |
| 18 | await phone.settle() | |
| 19 | expect(phone.byTestID('grid')!.style.gridTemplateRows).toBe(3) | |
| 20 | phone.press(phone.byTestID('clear')!) | |
| 21 | await phone.settle() | |
| 22 | expect(phone.byTestID('grid')!.style.gridTemplateRows).toBeNull() | |
| 23 | phone.unmount() | |
| 24 | }, 30_000) | |
| 25 | ||
| 5 | 26 | it('normalizes CSS and objects, clears previous values, and protects hidden nodes', async () => { |
| 6 | 27 | const { code } = await compileNativeScreen(`<script client> |
| 7 | 28 | const styles = state('padding-left:12px;font-size:1.5rem;width:50%;font-weight:700') |