ReviewOS

also looking at this

stacks/stx

fix(native): preserve modal presentation protocol properties

#2099
Merged chris wants to merge codex/native-modal-protocol-props into main
3 files +41 -2
packages/stx/src/native/prop-names.tsmodified+2-2
Changes to packages/stx/src/native/prop-names.ts
@@ -13,7 +13,7 @@ const NATIVE_PROP_NAMES = [
1313 'keyboardVerticalOffset', 'keyExtractor', 'listRole', 'maxLength', 'maximumDate', 'maximumTrackTintColor', 'maximumValue',
1414 'maxToRenderPerBatch', 'minimumDate', 'minimumTrackTintColor', 'minimumValue', 'minuteInterval', 'numberOfLines',
1515 'numColumns', 'onEndReachedThreshold', 'pagingEnabled', 'passwordRules', 'placeholderTextColor', 'pointerEvents',
16 'presentationStyle',
16 'presentationStyle', 'nonModal', 'dismissOnOutsideTap',
1717 'pressRetentionOffset', 'progressBackgroundColor', 'progressViewOffset', 'refreshControl', 'resizeMode',
1818 'returnKeyType', 'scrollEnabled', 'scrollTarget', 'secureTextEntry', 'selectedValue', 'selectionColor',
1919 'showsHorizontalScrollIndicator', 'showsVerticalScrollIndicator', 'statusBarTranslucent',
@@ -36,7 +36,7 @@ export const NATIVE_BOOLEAN_PROPS = new Set([
3636 'animating', 'autoCorrect', 'autoFocus', 'autoplay', 'autoPlay', 'bounces', 'controls', 'disabled',
3737 'editable', 'hidesWhenStopped', 'horizontal', 'loop', 'multiline', 'muted', 'pagingEnabled', 'paused',
3838 'refreshing', 'scrollEnabled', 'secureTextEntry', 'selectTextOnFocus', 'showsHorizontalScrollIndicator',
39 'showsVerticalScrollIndicator', 'transparent', 'visible',
39 'showsVerticalScrollIndicator', 'transparent', 'visible', 'nonModal', 'dismissOnOutsideTap',
4040 'fitToBounds', 'showsUserLocation', 'zoomEnabled', 'rotateEnabled', 'pitchEnabled', 'showsCompass', 'showsScale',
4141 'pictureInPicture',
4242])
packages/stx/test/native/html-to-ir.test.tsmodified+9-0
Changes to packages/stx/test/native/html-to-ir.test.ts
@@ -227,6 +227,15 @@ describe('events', () => {
227227 })
228228 })
229229
230 it.each(['nonModal dismissOnOutsideTap', 'non-modal dismiss-on-outside-tap'])('preserves native popup boolean protocol names: %s', async (attributes) => {
231 const { root } = await translateHtmlToIR(`<div data-native="Modal" ${attributes}></div>`)
232 expect(root.props).toMatchObject({ nonModal: true, dismissOnOutsideTap: true })
233 expect(root.props.nonmodal).toBeUndefined()
234 expect(root.props.dismissonoutsidetap).toBeUndefined()
235 const { root: blocking } = await translateHtmlToIR('<div data-native="Modal" nonModal="false" dismissOnOutsideTap="false"></div>')
236 expect(blocking.props).toMatchObject({ nonModal: false, dismissOnOutsideTap: false })
237 })
238
230239 it('decodes JSON object props forwarded by native components', async () => {
231240 const { root } = await translateHtmlToIR('<div data-native="ScrollView" contentContainerStyle="{&quot;paddingTop&quot;:12}"></div>')
232241
packages/stx/test/native/shared-overlays.test.tsmodified+30-0
Changes to packages/stx/test/native/shared-overlays.test.ts
@@ -5,6 +5,36 @@ import { compileSharedScreenBundle } from '../../src/native/compiler/bundle'
55import { compileNativeScreen, mountNativeScreen } from '../../src/native/testing'
66import { createTempDir } from '../../src/temp-dir'
77
8it.each([false, true])('keeps authored native popup presentation and dismisses only the requested layer (minify=%s)', async (minify) => {
9 const { code } = await compileNativeScreen(`<script client>
10const shown = state(true)
11const closeReason = state('')
12window.popupProbe = { shown, closeReason }
13</script>
14<Modal testID="authored-parent" :visible="true" animationType="none">
15 <Modal testID="authored-popup" :visible="shown" nonModal dismissOnOutsideTap animationType="none"
16 @request-close="closeReason.set($event.reason);shown.set(false)">
17 <Text>Choose a layer</Text>
18 </Modal>
19</Modal>`, { minify })
20 const phone = mountNativeScreen(code)
21 try {
22 await phone.settle()
23 const popup = phone.byTestID('authored-popup')!
24 expect(popup.type).toBe('Modal')
25 expect(popup.props).toMatchObject({ visible: true, nonModal: true, dismissOnOutsideTap: true })
26 expect(popup.props.nonmodal).toBeUndefined()
27 expect(popup.props.dismissonoutsidetap).toBeUndefined()
28 phone.press(popup, 'onRequestClose', { reason: 'backdrop' })
29 await phone.settle()
30 expect(phone.context.popupProbe.closeReason()).toBe('backdrop')
31 expect(phone.byTestID('authored-popup')!.props.visible).toBe(false)
32 expect(phone.byTestID('authored-parent')!.props.visible).toBe(true)
33 expect(phone.mutationErrors).toEqual([])
34 }
35 finally { phone.unmount() }
36}, 30_000)
37
838it.each([false, true])('presents capability-gated non-modal layers without stealing focus (minify=%s)', async (minify) => {
939 const { code } = await compileNativeScreen(`<script client>
1040const outer = useRef('outer')