diff --git a/apps/bare-expo/e2e/expo-video/fullscreen-test.yaml b/apps/bare-expo/e2e/expo-video/fullscreen-test.yaml
index d2648c044db9eb..80c358c3f4ad19 100644
--- a/apps/bare-expo/e2e/expo-video/fullscreen-test.yaml
+++ b/apps/bare-expo/e2e/expo-video/fullscreen-test.yaml
@@ -4,29 +4,45 @@ jsEngine: graaljs
---
- openLink: bareexpo://components/video/fullscreen
-- tapOn: "Enter Fullscreen"
+- tapOn: 'Enter Fullscreen'
- runFlow:
- when:
- platform: Android
- commands:
- # Dismiss the android system fullscreen instruction popup
- - tapOn:
- text: "Got it"
- optional: true
- - assertVisible:
- id: "dev.expo.payments:id/exo_content_frame"
-- runFlow:
- when:
- platform: iOS
- commands:
- - assertVisible: "Video"
+ when:
+ platform: Android
+ commands:
+ # Dismiss the android system fullscreen instruction popup
+ - tapOn:
+ text: 'Got it'
+ optional: true
+ - assertVisible:
+ id: 'dev.expo.payments:id/exo_content_frame'
+
+# Once the fullscreen player is shown it covers the in-app button, so this confirms
+# we actually entered fullscreen on both platforms. (On iOS the native player title
+# is not asserted because its controls auto-hide and would flake on slow simulators.)
+- assertNotVisible: 'Enter Fullscreen'
-- assertNotVisible: "Enter Fullscreen"
-- tapOn:
- point: '${maestro.platform == "android" ? "94%,97%" : "11%,10%"}'
- delay: 400
- repeat: 2
+# Exit fullscreen. Android has a dedicated exit button in the bottom-right of the
+# custom fullscreen activity.
+- runFlow:
+ when:
+ platform: Android
+ commands:
+ - tapOn:
+ point: 94%,97%
+ delay: 400
+ repeat: 2
+# iOS uses the native AVPlayerViewController chrome, whose controls auto-hide. Tap the
+# center to reveal them, then tap the close button in the top-left exactly once: a
+# second tap would land on the screen's back button and navigate away from the test.
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - tapOn:
+ point: 50%,50%
+ - tapOn:
+ point: 11%,10%
-- assertVisible: "Enter Fullscreen"
-- assertVisible: "0 = onFullscreenEnter"
-- assertVisible: "1 = onFullscreenExit"
+- assertVisible: 'Enter Fullscreen'
+- assertVisible: '0 = onFullscreenEnter'
+- assertVisible: '1 = onFullscreenExit'
diff --git a/apps/bare-expo/e2e/expo-video/playback-test.yaml b/apps/bare-expo/e2e/expo-video/playback-test.yaml
index c780f57e19f321..a136277d873b1d 100644
--- a/apps/bare-expo/e2e/expo-video/playback-test.yaml
+++ b/apps/bare-expo/e2e/expo-video/playback-test.yaml
@@ -20,12 +20,22 @@ jsEngine: graaljs
- tapOn: Play
-- assertVisible: 'isPlaying = true'
+# Playback takes a moment to start after tapping Play; wait instead of
+# asserting instantly to avoid a startup race.
+- extendedWaitUntil:
+ visible: 'isPlaying = true'
+ timeout: 10000
- assertVisible: 'isAtStart = false'
- tapOn: Pause
- tapOn: Seek to 30s
- assertVisible: 'currentTime = 30'
+# The player reports currentTime = 30 before the paused surface repaints the
+# seeked frame. Wait for the screen to settle so the screenshot captures the
+# t=30s frame and not a stale/blank one. Safe only because the player is paused
+# here (a playing video never settles and would burn the full timeout).
+- waitForAnimationToEnd:
+ timeout: 5000
- runFlow:
file: ../_nested-flows/viewshot-comparison.yaml
env:
diff --git a/apps/bare-expo/scripts/lib/e2e-common.ts b/apps/bare-expo/scripts/lib/e2e-common.ts
index 796fa9dbdb4e59..ea67aff49f8656 100644
--- a/apps/bare-expo/scripts/lib/e2e-common.ts
+++ b/apps/bare-expo/scripts/lib/e2e-common.ts
@@ -63,10 +63,13 @@ jsEngine: graaljs
for (const testCase of testCases) {
contents.push(`\
- openLink: bareexpo://test-suite/run?tests=${testCase}
-# make sure we're running the right test
-- assertVisible:
- id: "test_suite_selection_query_text"
- text: "${testCase}"
+# make sure we're running the right test. Wait rather than assert instantly: after
+# clearState the app cold-starts, and the selection header may not have rendered yet.
+- extendedWaitUntil:
+ visible:
+ id: "test_suite_selection_query_text"
+ text: "${testCase}"
+ timeout: 30000
- extendedWaitUntil:
visible:
id: "test_suite_text_results"
diff --git a/apps/bare-expo/scripts/start-android-e2e-test.ts b/apps/bare-expo/scripts/start-android-e2e-test.ts
index 6292fe6fc90978..ab5df54befc5fb 100755
--- a/apps/bare-expo/scripts/start-android-e2e-test.ts
+++ b/apps/bare-expo/scripts/start-android-e2e-test.ts
@@ -120,7 +120,7 @@ async function testAsync(
},
});
console.timeEnd(TEST_DURATION_LABEL);
- } catch {
+ } catch (error: unknown) {
stopLogCollectionController.abort();
console.timeEnd(TEST_DURATION_LABEL);
console.warn(`\n⚠️ Maestro flow failed, because:\n\n`);
@@ -140,7 +140,7 @@ async function testAsync(
}
}
console.log('\n\n');
- throw new Error('e2e tests have failed.');
+ throw new Error('e2e tests have failed.', { cause: error });
} finally {
endGroup();
stopLogCollectionController.abort();
diff --git a/apps/bare-expo/scripts/start-ios-e2e-test.ts b/apps/bare-expo/scripts/start-ios-e2e-test.ts
index f5807252b0b756..b2c6f3bc9cf9b3 100755
--- a/apps/bare-expo/scripts/start-ios-e2e-test.ts
+++ b/apps/bare-expo/scripts/start-ios-e2e-test.ts
@@ -159,9 +159,8 @@ async function startSimulatorAsync(deviceId: string, timeout: number = 180_000)
);
const label = 'device startup duration';
console.time(label);
- const bootProc = spawnAsync('xcrun', ['simctl', 'bootstatus', deviceId, '-b'], {
- stdio: 'inherit',
- });
+ // Capture (don't inherit) stdio so the verbose boot/data-migration progress doesn't flood the logs.
+ const bootProc = spawnAsync('xcrun', ['simctl', 'bootstatus', deviceId, '-b']);
let timeoutHandle: NodeJS.Timeout | null = null;
const timeoutPromise = new Promise((_, reject) => {
@@ -239,7 +238,7 @@ async function testAsync(
},
});
console.timeEnd(TEST_DURATION_LABEL);
- } catch {
+ } catch (error: unknown) {
stopLogCollectionController.abort();
console.timeEnd(TEST_DURATION_LABEL);
console.warn(`\n⚠️ Maestro flow failed, because:\n\n`);
@@ -262,11 +261,8 @@ async function testAsync(
}
console.log('\n\n');
- throw new Error('e2e tests have failed.');
+ throw new Error('e2e tests have failed.', { cause: error });
}
- } catch (e: unknown) {
- console.error('Uncaught Error', e);
- throw e;
} finally {
endGroup();
stopLogCollectionController.abort();
diff --git a/apps/native-component-list/src/screens/UI/BottomSheetScreen.android.tsx b/apps/native-component-list/src/screens/UI/BottomSheetScreen.android.tsx
index f0f47fbdacca37..7d8456601a75a5 100644
--- a/apps/native-component-list/src/screens/UI/BottomSheetScreen.android.tsx
+++ b/apps/native-component-list/src/screens/UI/BottomSheetScreen.android.tsx
@@ -44,6 +44,7 @@ export default function BottomSheetScreen() {
// Configurable props
const [skipPartiallyExpanded, setSkipPartiallyExpanded] = React.useState(false);
+ const [initialFullyExpanded, setInitialFullyExpanded] = React.useState(false);
const [showDragHandle, setShowDragHandle] = React.useState(true);
const [useCustomDragHandle, setUseCustomDragHandle] = React.useState(false);
const [sheetGesturesEnabled, setSheetGesturesEnabled] = React.useState(true);
@@ -91,6 +92,15 @@ export default function BottomSheetScreen() {
+
+
+ Initial fully expanded
+
+
+ setShowSheet(false)}
skipPartiallyExpanded={skipPartiallyExpanded}
+ initialFullyExpanded={initialFullyExpanded}
showDragHandle={showDragHandle}
sheetGesturesEnabled={sheetGesturesEnabled}
properties={{
diff --git a/apps/test-suite/screens/SelectScreen.tsx b/apps/test-suite/screens/SelectScreen.tsx
index db494a3e29f308..645176f98e83df 100644
--- a/apps/test-suite/screens/SelectScreen.tsx
+++ b/apps/test-suite/screens/SelectScreen.tsx
@@ -87,6 +87,20 @@ export default function SelectScreen({ navigation }) {
({ url }: { url: string }) => {
url = url || '';
// TODO: Use Expo Linking library once parseURL is implemented for web
+
+ // Run a specific set of tests, e.g. bareexpo://test-suite/run?tests=basic,blur
+ // React Navigation linking also maps this URL to the run screen, but on a cold
+ // start the two race and this handler used to win by falling through to the
+ // selection list below. Handle the query explicitly so the deep link always runs.
+ const testsQueryMatch = url.match(/[?&]tests=([^&]+)/);
+
+ if (testsQueryMatch) {
+ const tests = getSelectedTestNames(decodeURIComponent(testsQueryMatch[1]));
+ const query = createQueryString(tests);
+ navigation.navigate(routeNames.run, { tests: query });
+ return;
+ }
+
if (url.includes(`/${routeNames.select}/`)) {
const selectedTests = url.split('/').pop();
if (selectedTests) {
diff --git a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/bottomsheet.mdx b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/bottomsheet.mdx
index fefd3b4614780a..4731fc1ef84153 100644
--- a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/bottomsheet.mdx
+++ b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/bottomsheet.mdx
@@ -111,6 +111,52 @@ export default function SkipPartiallyExpandedExample() {
}
```
+### Initial fully expanded state
+
+When `initialFullyExpanded` is `true`, the sheet opens directly in the fully expanded state on first composition while leaving the partial state reachable. Unlike `skipPartiallyExpanded`, the user can still drag down to the partial state. The `partialExpand()` method also continues to work.
+
+```tsx InitialFullyExpandedExample.tsx
+import { useRef, useState } from 'react';
+import { Host, ModalBottomSheet, Button, Column, Text } from '@expo/ui/jetpack-compose';
+import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose';
+import { paddingAll } from '@expo/ui/jetpack-compose/modifiers';
+
+export default function InitialFullyExpandedExample() {
+ const [visible, setVisible] = useState(false);
+ const sheetRef = useRef(null);
+
+ const hideSheet = async () => {
+ await sheetRef.current?.hide();
+ setVisible(false);
+ };
+
+ return (
+
+
+ {visible && (
+ setVisible(false)}
+ initialFullyExpanded>
+
+ This sheet opened fully expanded.
+ You can still drag it down to the partial state.
+
+
+
+
+ )}
+
+ );
+}
+```
+
### Custom colors
Use `containerColor`, `contentColor`, and `scrimColor` to customize the sheet's appearance.
diff --git a/docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/bottomsheet.mdx b/docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/bottomsheet.mdx
index fa0484b0138f25..ebb8134698547f 100644
--- a/docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/bottomsheet.mdx
+++ b/docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/bottomsheet.mdx
@@ -111,6 +111,52 @@ export default function SkipPartiallyExpandedExample() {
}
```
+### Initial fully expanded state
+
+When `initialFullyExpanded` is `true`, the sheet opens directly in the fully expanded state on first composition while leaving the partial state reachable. Unlike `skipPartiallyExpanded`, the user can still drag down to the partial state. The `partialExpand()` method also continues to work.
+
+```tsx InitialFullyExpandedExample.tsx
+import { useRef, useState } from 'react';
+import { Host, ModalBottomSheet, Button, Column, Text } from '@expo/ui/jetpack-compose';
+import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose';
+import { paddingAll } from '@expo/ui/jetpack-compose/modifiers';
+
+export default function InitialFullyExpandedExample() {
+ const [visible, setVisible] = useState(false);
+ const sheetRef = useRef(null);
+
+ const hideSheet = async () => {
+ await sheetRef.current?.hide();
+ setVisible(false);
+ };
+
+ return (
+
+
+ {visible && (
+ setVisible(false)}
+ initialFullyExpanded>
+
+ This sheet opened fully expanded.
+ You can still drag it down to the partial state.
+
+
+
+
+ )}
+
+ );
+}
+```
+
### Custom colors
Use `containerColor`, `contentColor`, and `scrimColor` to customize the sheet's appearance.
diff --git a/docs/public/static/data/unversioned/expo-ui/jetpack-compose/bottomsheet.json b/docs/public/static/data/unversioned/expo-ui/jetpack-compose/bottomsheet.json
index e787829240cd74..00b30997e801ed 100644
--- a/docs/public/static/data/unversioned/expo-ui/jetpack-compose/bottomsheet.json
+++ b/docs/public/static/data/unversioned/expo-ui/jetpack-compose/bottomsheet.json
@@ -1 +1 @@
-{"schemaVersion":"2.0","name":"expo-ui/jetpack-compose/bottomsheet","variant":"project","kind":1,"children":[{"name":"ModalBottomSheetProperties","variant":"declaration","kind":2097152,"children":[{"name":"shouldDismissOnBackPress","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the bottom sheet can be dismissed by pressing the back button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"shouldDismissOnClickOutside","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the bottom sheet can be dismissed by clicking outside (on the scrim)."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ModalBottomSheetProps","variant":"declaration","kind":2097152,"children":[{"name":"children","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The children of the "},{"kind":"code","text":"`ModalBottomSheet`"},{"kind":"text","text":" component.\nCan include a "},{"kind":"code","text":"`ModalBottomSheet.DragHandle`"},{"kind":"text","text":" slot for a custom drag handle."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"ReactNode","package":"@types/react","qualifiedName":"React.ReactNode"}},{"name":"containerColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The background color of the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"contentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The preferred color of the content inside the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Modifiers for the component."}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"onDismissRequest","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Callback function that is called when the user dismisses the bottom sheet\n(via swipe, back press, or tapping outside the scrim)."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"properties","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Properties for the modal window behavior."}]},"type":{"type":"reference","name":"ModalBottomSheetProperties","package":"@expo/ui"}},{"name":"ref","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Can be used to imperatively hide the bottom sheet with an animation."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.Ref"},"typeArguments":[{"type":"reference","name":"ModalBottomSheetRef","package":"@expo/ui"}],"name":"Ref","package":"@types/react","qualifiedName":"React.Ref"}},{"name":"scrimColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The color of the scrim overlay behind the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"sheetGesturesEnabled","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether gestures (swipe to dismiss) are enabled on the bottom sheet."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showDragHandle","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the default drag handle at the top of the bottom sheet.\nIgnored if a custom "},{"kind":"code","text":"`ModalBottomSheet.DragHandle`"},{"kind":"text","text":" slot is provided."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"skipPartiallyExpanded","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Immediately opens the bottom sheet in full screen."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ModalBottomSheetRef","variant":"declaration","kind":2097152,"children":[{"name":"expand","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically expands the bottom sheet to full height with an animation."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}},{"name":"hide","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically hides the bottom sheet with an animation.\nThe returned promise resolves after the dismiss animation completes."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}},{"name":"partialExpand","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically collapses the bottom sheet to partially expanded (~50%) state.\nOnly works when "},{"kind":"code","text":"`skipPartiallyExpanded`"},{"kind":"text","text":" is "},{"kind":"code","text":"`false`"},{"kind":"text","text":"."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}}]},{"name":"ModalBottomSheet","variant":"declaration","kind":32,"flags":{"isConst":true},"type":{"type":"query","queryType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/jetpack-compose/ModalBottomSheet/index.tsx","qualifiedName":"ModalBottomSheetComponent"},"name":"ModalBottomSheetComponent","package":"@expo/ui","preferValues":true}},"defaultValue":"ModalBottomSheetComponent"}],"packageName":"@expo/ui"}
\ No newline at end of file
+{"schemaVersion":"2.0","name":"expo-ui/jetpack-compose/bottomsheet","variant":"project","kind":1,"children":[{"name":"ModalBottomSheetProperties","variant":"declaration","kind":2097152,"children":[{"name":"shouldDismissOnBackPress","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the bottom sheet can be dismissed by pressing the back button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"shouldDismissOnClickOutside","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the bottom sheet can be dismissed by clicking outside (on the scrim)."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ModalBottomSheetProps","variant":"declaration","kind":2097152,"children":[{"name":"children","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The children of the "},{"kind":"code","text":"`ModalBottomSheet`"},{"kind":"text","text":" component.\nCan include a "},{"kind":"code","text":"`ModalBottomSheet.DragHandle`"},{"kind":"text","text":" slot for a custom drag handle."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"ReactNode","package":"@types/react","qualifiedName":"React.ReactNode"}},{"name":"containerColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The background color of the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"contentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The preferred color of the content inside the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"initialFullyExpanded","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Opens the sheet fully expanded on first composition. Ignored when "},{"kind":"code","text":"`skipPartiallyExpanded`"},{"kind":"text","text":" is "},{"kind":"code","text":"`true`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Modifiers for the component."}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"onDismissRequest","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Callback function that is called when the user dismisses the bottom sheet\n(via swipe, back press, or tapping outside the scrim)."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"properties","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Properties for the modal window behavior."}]},"type":{"type":"reference","name":"ModalBottomSheetProperties","package":"@expo/ui"}},{"name":"ref","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Can be used to imperatively hide the bottom sheet with an animation."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.Ref"},"typeArguments":[{"type":"reference","name":"ModalBottomSheetRef","package":"@expo/ui"}],"name":"Ref","package":"@types/react","qualifiedName":"React.Ref"}},{"name":"scrimColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The color of the scrim overlay behind the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"sheetGesturesEnabled","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether gestures (swipe to dismiss) are enabled on the bottom sheet."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showDragHandle","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the default drag handle at the top of the bottom sheet.\nIgnored if a custom "},{"kind":"code","text":"`ModalBottomSheet.DragHandle`"},{"kind":"text","text":" slot is provided."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"skipPartiallyExpanded","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Immediately opens the bottom sheet in full screen."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ModalBottomSheetRef","variant":"declaration","kind":2097152,"children":[{"name":"expand","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically expands the bottom sheet to full height with an animation."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}},{"name":"hide","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically hides the bottom sheet with an animation.\nThe returned promise resolves after the dismiss animation completes."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}},{"name":"partialExpand","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically collapses the bottom sheet to partially expanded (~50%) state.\nOnly works when "},{"kind":"code","text":"`skipPartiallyExpanded`"},{"kind":"text","text":" is "},{"kind":"code","text":"`false`"},{"kind":"text","text":"."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}}]},{"name":"ModalBottomSheet","variant":"declaration","kind":32,"flags":{"isConst":true},"type":{"type":"query","queryType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/jetpack-compose/ModalBottomSheet/index.tsx","qualifiedName":"ModalBottomSheetComponent"},"name":"ModalBottomSheetComponent","package":"@expo/ui","preferValues":true}},"defaultValue":"ModalBottomSheetComponent"}],"packageName":"@expo/ui"}
\ No newline at end of file
diff --git a/docs/public/static/data/unversioned/expo-ui/universal/button.json b/docs/public/static/data/unversioned/expo-ui/universal/button.json
index 891fd023cad2c2..f3f9673a24af2d 100644
--- a/docs/public/static/data/unversioned/expo-ui/universal/button.json
+++ b/docs/public/static/data/unversioned/expo-ui/universal/button.json
@@ -1 +1 @@
-{"schemaVersion":"2.0","name":"expo-ui/universal/button","variant":"project","kind":1,"children":[{"name":"ButtonProps","variant":"declaration","kind":256,"comment":{"summary":[{"kind":"text","text":"Props for the ["},{"kind":"code","text":"`Button`"},{"kind":"text","text":"](#button) component."}]},"children":[{"name":"children","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Custom content rendered inside the button. When provided, "},{"kind":"code","text":"`label`"},{"kind":"text","text":" is ignored."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"ReactNode","package":"@types/react","qualifiedName":"React.ReactNode"}},{"name":"disabled","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Whether the component is disabled. Disabled components do not respond to user interaction."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"hidden","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Whether the component is hidden."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"label","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Text label displayed inside the button. Ignored when "},{"kind":"code","text":"`children`"},{"kind":"text","text":" is provided."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Platform-specific modifier escape hatch. Pass an array of modifier configs\nfrom "},{"kind":"code","text":"`@expo/ui/swift-ui/modifiers`"},{"kind":"text","text":" or "},{"kind":"code","text":"`@expo/ui/jetpack-compose/modifiers`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"onAppear","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Called when the component appears on screen."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onDisappear","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Called when the component is removed from screen."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onPress","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Called when the button is pressed."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}},"overwrites":{"type":"reference","name":"UniversalBaseProps.onPress"}},{"name":"style","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Platform-agnostic style properties. These are translated to SwiftUI modifiers on iOS\nand Jetpack Compose modifiers on Android."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Pick"},"typeArguments":[{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheetTypes.d.ts","qualifiedName":"ViewStyle"},"name":"ViewStyle","package":"react-native"},{"type":"union","types":[{"type":"literal","value":"padding"},{"type":"literal","value":"paddingHorizontal"},{"type":"literal","value":"paddingVertical"},{"type":"literal","value":"paddingTop"},{"type":"literal","value":"paddingBottom"},{"type":"literal","value":"paddingLeft"},{"type":"literal","value":"paddingRight"},{"type":"literal","value":"backgroundColor"},{"type":"literal","value":"borderRadius"},{"type":"literal","value":"borderWidth"},{"type":"literal","value":"borderColor"},{"type":"literal","value":"opacity"},{"type":"literal","value":"width"},{"type":"literal","value":"height"}]}],"name":"Pick","package":"typescript"}},{"name":"testID","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Identifier used to locate the component in end-to-end tests."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"variant","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Visual variant of the button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'filled'"}]}]},"type":{"type":"reference","name":"ButtonVariant","package":"@expo/ui"}}]},{"name":"ButtonVariant","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Visual variant of a ["},{"kind":"code","text":"`Button`"},{"kind":"text","text":"](#button).\n\n- "},{"kind":"code","text":"`'filled'`"},{"kind":"text","text":" — solid background color (default).\n- "},{"kind":"code","text":"`'outlined'`"},{"kind":"text","text":" — transparent background with a border.\n- "},{"kind":"code","text":"`'text'`"},{"kind":"text","text":" — no background or border, text only."}]},"type":{"type":"union","types":[{"type":"literal","value":"filled"},{"type":"literal","value":"outlined"},{"type":"literal","value":"text"}]}},{"name":"Button","variant":"declaration","kind":64,"signatures":[{"name":"Button","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"A pressable button that supports multiple visual variants."}]},"parameters":[{"name":"__namedParameters","variant":"param","kind":32768,"type":{"type":"reference","name":"ButtonProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]}],"packageName":"@expo/ui"}
\ No newline at end of file
+{"schemaVersion":"2.0","name":"expo-ui/universal/button","variant":"project","kind":1,"children":[{"name":"ButtonProps","variant":"declaration","kind":256,"comment":{"summary":[{"kind":"text","text":"Props for the ["},{"kind":"code","text":"`Button`"},{"kind":"text","text":"](#button) component."}]},"children":[{"name":"children","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Custom content rendered inside the button. When provided, "},{"kind":"code","text":"`label`"},{"kind":"text","text":" is ignored."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"ReactNode","package":"@types/react","qualifiedName":"React.ReactNode"}},{"name":"disabled","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Whether the component is disabled. Disabled components do not respond to user interaction."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"hidden","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Whether the component is hidden."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"label","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Text label displayed inside the button. Ignored when "},{"kind":"code","text":"`children`"},{"kind":"text","text":" is provided."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Platform-specific modifier escape hatch. Pass an array of modifier configs\nfrom "},{"kind":"code","text":"`@expo/ui/swift-ui/modifiers`"},{"kind":"text","text":" or "},{"kind":"code","text":"`@expo/ui/jetpack-compose/modifiers`"},{"kind":"text","text":".\nA modifier supplied here replaces any modifier of the same type that the\ncomponent derives from "},{"kind":"code","text":"`style`"},{"kind":"text","text":" or other props."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"onAppear","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Called when the component appears on screen."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onDisappear","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Called when the component is removed from screen."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onPress","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Called when the button is pressed."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}},"overwrites":{"type":"reference","name":"UniversalBaseProps.onPress"}},{"name":"style","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Platform-agnostic style properties. These are translated to SwiftUI modifiers on iOS\nand Jetpack Compose modifiers on Android."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Pick"},"typeArguments":[{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheetTypes.d.ts","qualifiedName":"ViewStyle"},"name":"ViewStyle","package":"react-native"},{"type":"union","types":[{"type":"literal","value":"padding"},{"type":"literal","value":"paddingHorizontal"},{"type":"literal","value":"paddingVertical"},{"type":"literal","value":"paddingTop"},{"type":"literal","value":"paddingBottom"},{"type":"literal","value":"paddingLeft"},{"type":"literal","value":"paddingRight"},{"type":"literal","value":"backgroundColor"},{"type":"literal","value":"borderRadius"},{"type":"literal","value":"borderWidth"},{"type":"literal","value":"borderColor"},{"type":"literal","value":"opacity"},{"type":"literal","value":"width"},{"type":"literal","value":"height"}]}],"name":"Pick","package":"typescript"}},{"name":"testID","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Identifier used to locate the component in end-to-end tests."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"variant","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Visual variant of the button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'filled'"}]}]},"type":{"type":"reference","name":"ButtonVariant","package":"@expo/ui"}}]},{"name":"ButtonVariant","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Visual variant of a ["},{"kind":"code","text":"`Button`"},{"kind":"text","text":"](#button).\n\n- "},{"kind":"code","text":"`'filled'`"},{"kind":"text","text":" — solid background color (default).\n- "},{"kind":"code","text":"`'outlined'`"},{"kind":"text","text":" — transparent background with a border.\n- "},{"kind":"code","text":"`'text'`"},{"kind":"text","text":" — no background or border, text only."}]},"type":{"type":"union","types":[{"type":"literal","value":"filled"},{"type":"literal","value":"outlined"},{"type":"literal","value":"text"}]}},{"name":"Button","variant":"declaration","kind":64,"signatures":[{"name":"Button","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"A pressable button that supports multiple visual variants."}]},"parameters":[{"name":"__namedParameters","variant":"param","kind":32768,"type":{"type":"reference","name":"ButtonProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]}],"packageName":"@expo/ui"}
\ No newline at end of file
diff --git a/docs/public/static/data/v56.0.0/expo-ui/jetpack-compose/bottomsheet.json b/docs/public/static/data/v56.0.0/expo-ui/jetpack-compose/bottomsheet.json
index e787829240cd74..00b30997e801ed 100644
--- a/docs/public/static/data/v56.0.0/expo-ui/jetpack-compose/bottomsheet.json
+++ b/docs/public/static/data/v56.0.0/expo-ui/jetpack-compose/bottomsheet.json
@@ -1 +1 @@
-{"schemaVersion":"2.0","name":"expo-ui/jetpack-compose/bottomsheet","variant":"project","kind":1,"children":[{"name":"ModalBottomSheetProperties","variant":"declaration","kind":2097152,"children":[{"name":"shouldDismissOnBackPress","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the bottom sheet can be dismissed by pressing the back button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"shouldDismissOnClickOutside","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the bottom sheet can be dismissed by clicking outside (on the scrim)."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ModalBottomSheetProps","variant":"declaration","kind":2097152,"children":[{"name":"children","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The children of the "},{"kind":"code","text":"`ModalBottomSheet`"},{"kind":"text","text":" component.\nCan include a "},{"kind":"code","text":"`ModalBottomSheet.DragHandle`"},{"kind":"text","text":" slot for a custom drag handle."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"ReactNode","package":"@types/react","qualifiedName":"React.ReactNode"}},{"name":"containerColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The background color of the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"contentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The preferred color of the content inside the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Modifiers for the component."}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"onDismissRequest","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Callback function that is called when the user dismisses the bottom sheet\n(via swipe, back press, or tapping outside the scrim)."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"properties","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Properties for the modal window behavior."}]},"type":{"type":"reference","name":"ModalBottomSheetProperties","package":"@expo/ui"}},{"name":"ref","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Can be used to imperatively hide the bottom sheet with an animation."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.Ref"},"typeArguments":[{"type":"reference","name":"ModalBottomSheetRef","package":"@expo/ui"}],"name":"Ref","package":"@types/react","qualifiedName":"React.Ref"}},{"name":"scrimColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The color of the scrim overlay behind the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"sheetGesturesEnabled","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether gestures (swipe to dismiss) are enabled on the bottom sheet."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showDragHandle","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the default drag handle at the top of the bottom sheet.\nIgnored if a custom "},{"kind":"code","text":"`ModalBottomSheet.DragHandle`"},{"kind":"text","text":" slot is provided."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"skipPartiallyExpanded","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Immediately opens the bottom sheet in full screen."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ModalBottomSheetRef","variant":"declaration","kind":2097152,"children":[{"name":"expand","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically expands the bottom sheet to full height with an animation."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}},{"name":"hide","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically hides the bottom sheet with an animation.\nThe returned promise resolves after the dismiss animation completes."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}},{"name":"partialExpand","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically collapses the bottom sheet to partially expanded (~50%) state.\nOnly works when "},{"kind":"code","text":"`skipPartiallyExpanded`"},{"kind":"text","text":" is "},{"kind":"code","text":"`false`"},{"kind":"text","text":"."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}}]},{"name":"ModalBottomSheet","variant":"declaration","kind":32,"flags":{"isConst":true},"type":{"type":"query","queryType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/jetpack-compose/ModalBottomSheet/index.tsx","qualifiedName":"ModalBottomSheetComponent"},"name":"ModalBottomSheetComponent","package":"@expo/ui","preferValues":true}},"defaultValue":"ModalBottomSheetComponent"}],"packageName":"@expo/ui"}
\ No newline at end of file
+{"schemaVersion":"2.0","name":"expo-ui/jetpack-compose/bottomsheet","variant":"project","kind":1,"children":[{"name":"ModalBottomSheetProperties","variant":"declaration","kind":2097152,"children":[{"name":"shouldDismissOnBackPress","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the bottom sheet can be dismissed by pressing the back button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"shouldDismissOnClickOutside","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the bottom sheet can be dismissed by clicking outside (on the scrim)."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ModalBottomSheetProps","variant":"declaration","kind":2097152,"children":[{"name":"children","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The children of the "},{"kind":"code","text":"`ModalBottomSheet`"},{"kind":"text","text":" component.\nCan include a "},{"kind":"code","text":"`ModalBottomSheet.DragHandle`"},{"kind":"text","text":" slot for a custom drag handle."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"ReactNode","package":"@types/react","qualifiedName":"React.ReactNode"}},{"name":"containerColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The background color of the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"contentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The preferred color of the content inside the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"initialFullyExpanded","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Opens the sheet fully expanded on first composition. Ignored when "},{"kind":"code","text":"`skipPartiallyExpanded`"},{"kind":"text","text":" is "},{"kind":"code","text":"`true`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Modifiers for the component."}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"onDismissRequest","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Callback function that is called when the user dismisses the bottom sheet\n(via swipe, back press, or tapping outside the scrim)."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"properties","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Properties for the modal window behavior."}]},"type":{"type":"reference","name":"ModalBottomSheetProperties","package":"@expo/ui"}},{"name":"ref","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Can be used to imperatively hide the bottom sheet with an animation."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.Ref"},"typeArguments":[{"type":"reference","name":"ModalBottomSheetRef","package":"@expo/ui"}],"name":"Ref","package":"@types/react","qualifiedName":"React.Ref"}},{"name":"scrimColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The color of the scrim overlay behind the bottom sheet."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"sheetGesturesEnabled","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether gestures (swipe to dismiss) are enabled on the bottom sheet."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showDragHandle","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the default drag handle at the top of the bottom sheet.\nIgnored if a custom "},{"kind":"code","text":"`ModalBottomSheet.DragHandle`"},{"kind":"text","text":" slot is provided."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"skipPartiallyExpanded","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Immediately opens the bottom sheet in full screen."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ModalBottomSheetRef","variant":"declaration","kind":2097152,"children":[{"name":"expand","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically expands the bottom sheet to full height with an animation."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}},{"name":"hide","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically hides the bottom sheet with an animation.\nThe returned promise resolves after the dismiss animation completes."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}},{"name":"partialExpand","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Programmatically collapses the bottom sheet to partially expanded (~50%) state.\nOnly works when "},{"kind":"code","text":"`skipPartiallyExpanded`"},{"kind":"text","text":" is "},{"kind":"code","text":"`false`"},{"kind":"text","text":"."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}}}]},{"name":"ModalBottomSheet","variant":"declaration","kind":32,"flags":{"isConst":true},"type":{"type":"query","queryType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/jetpack-compose/ModalBottomSheet/index.tsx","qualifiedName":"ModalBottomSheetComponent"},"name":"ModalBottomSheetComponent","package":"@expo/ui","preferValues":true}},"defaultValue":"ModalBottomSheetComponent"}],"packageName":"@expo/ui"}
\ No newline at end of file
diff --git a/docs/public/static/data/v56.0.0/expo-ui/universal/button.json b/docs/public/static/data/v56.0.0/expo-ui/universal/button.json
index 891fd023cad2c2..f3f9673a24af2d 100644
--- a/docs/public/static/data/v56.0.0/expo-ui/universal/button.json
+++ b/docs/public/static/data/v56.0.0/expo-ui/universal/button.json
@@ -1 +1 @@
-{"schemaVersion":"2.0","name":"expo-ui/universal/button","variant":"project","kind":1,"children":[{"name":"ButtonProps","variant":"declaration","kind":256,"comment":{"summary":[{"kind":"text","text":"Props for the ["},{"kind":"code","text":"`Button`"},{"kind":"text","text":"](#button) component."}]},"children":[{"name":"children","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Custom content rendered inside the button. When provided, "},{"kind":"code","text":"`label`"},{"kind":"text","text":" is ignored."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"ReactNode","package":"@types/react","qualifiedName":"React.ReactNode"}},{"name":"disabled","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Whether the component is disabled. Disabled components do not respond to user interaction."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"hidden","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Whether the component is hidden."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"label","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Text label displayed inside the button. Ignored when "},{"kind":"code","text":"`children`"},{"kind":"text","text":" is provided."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Platform-specific modifier escape hatch. Pass an array of modifier configs\nfrom "},{"kind":"code","text":"`@expo/ui/swift-ui/modifiers`"},{"kind":"text","text":" or "},{"kind":"code","text":"`@expo/ui/jetpack-compose/modifiers`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"onAppear","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Called when the component appears on screen."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onDisappear","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Called when the component is removed from screen."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onPress","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Called when the button is pressed."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}},"overwrites":{"type":"reference","name":"UniversalBaseProps.onPress"}},{"name":"style","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Platform-agnostic style properties. These are translated to SwiftUI modifiers on iOS\nand Jetpack Compose modifiers on Android."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Pick"},"typeArguments":[{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheetTypes.d.ts","qualifiedName":"ViewStyle"},"name":"ViewStyle","package":"react-native"},{"type":"union","types":[{"type":"literal","value":"padding"},{"type":"literal","value":"paddingHorizontal"},{"type":"literal","value":"paddingVertical"},{"type":"literal","value":"paddingTop"},{"type":"literal","value":"paddingBottom"},{"type":"literal","value":"paddingLeft"},{"type":"literal","value":"paddingRight"},{"type":"literal","value":"backgroundColor"},{"type":"literal","value":"borderRadius"},{"type":"literal","value":"borderWidth"},{"type":"literal","value":"borderColor"},{"type":"literal","value":"opacity"},{"type":"literal","value":"width"},{"type":"literal","value":"height"}]}],"name":"Pick","package":"typescript"}},{"name":"testID","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Identifier used to locate the component in end-to-end tests."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"variant","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Visual variant of the button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'filled'"}]}]},"type":{"type":"reference","name":"ButtonVariant","package":"@expo/ui"}}]},{"name":"ButtonVariant","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Visual variant of a ["},{"kind":"code","text":"`Button`"},{"kind":"text","text":"](#button).\n\n- "},{"kind":"code","text":"`'filled'`"},{"kind":"text","text":" — solid background color (default).\n- "},{"kind":"code","text":"`'outlined'`"},{"kind":"text","text":" — transparent background with a border.\n- "},{"kind":"code","text":"`'text'`"},{"kind":"text","text":" — no background or border, text only."}]},"type":{"type":"union","types":[{"type":"literal","value":"filled"},{"type":"literal","value":"outlined"},{"type":"literal","value":"text"}]}},{"name":"Button","variant":"declaration","kind":64,"signatures":[{"name":"Button","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"A pressable button that supports multiple visual variants."}]},"parameters":[{"name":"__namedParameters","variant":"param","kind":32768,"type":{"type":"reference","name":"ButtonProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]}],"packageName":"@expo/ui"}
\ No newline at end of file
+{"schemaVersion":"2.0","name":"expo-ui/universal/button","variant":"project","kind":1,"children":[{"name":"ButtonProps","variant":"declaration","kind":256,"comment":{"summary":[{"kind":"text","text":"Props for the ["},{"kind":"code","text":"`Button`"},{"kind":"text","text":"](#button) component."}]},"children":[{"name":"children","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Custom content rendered inside the button. When provided, "},{"kind":"code","text":"`label`"},{"kind":"text","text":" is ignored."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"ReactNode","package":"@types/react","qualifiedName":"React.ReactNode"}},{"name":"disabled","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Whether the component is disabled. Disabled components do not respond to user interaction."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"hidden","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Whether the component is hidden."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"label","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Text label displayed inside the button. Ignored when "},{"kind":"code","text":"`children`"},{"kind":"text","text":" is provided."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Platform-specific modifier escape hatch. Pass an array of modifier configs\nfrom "},{"kind":"code","text":"`@expo/ui/swift-ui/modifiers`"},{"kind":"text","text":" or "},{"kind":"code","text":"`@expo/ui/jetpack-compose/modifiers`"},{"kind":"text","text":".\nA modifier supplied here replaces any modifier of the same type that the\ncomponent derives from "},{"kind":"code","text":"`style`"},{"kind":"text","text":" or other props."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"onAppear","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Called when the component appears on screen."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onDisappear","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Called when the component is removed from screen."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onPress","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Called when the button is pressed."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}},"overwrites":{"type":"reference","name":"UniversalBaseProps.onPress"}},{"name":"style","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Platform-agnostic style properties. These are translated to SwiftUI modifiers on iOS\nand Jetpack Compose modifiers on Android."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Pick"},"typeArguments":[{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheetTypes.d.ts","qualifiedName":"ViewStyle"},"name":"ViewStyle","package":"react-native"},{"type":"union","types":[{"type":"literal","value":"padding"},{"type":"literal","value":"paddingHorizontal"},{"type":"literal","value":"paddingVertical"},{"type":"literal","value":"paddingTop"},{"type":"literal","value":"paddingBottom"},{"type":"literal","value":"paddingLeft"},{"type":"literal","value":"paddingRight"},{"type":"literal","value":"backgroundColor"},{"type":"literal","value":"borderRadius"},{"type":"literal","value":"borderWidth"},{"type":"literal","value":"borderColor"},{"type":"literal","value":"opacity"},{"type":"literal","value":"width"},{"type":"literal","value":"height"}]}],"name":"Pick","package":"typescript"}},{"name":"testID","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Identifier used to locate the component in end-to-end tests."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"variant","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Visual variant of the button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'filled'"}]}]},"type":{"type":"reference","name":"ButtonVariant","package":"@expo/ui"}}]},{"name":"ButtonVariant","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Visual variant of a ["},{"kind":"code","text":"`Button`"},{"kind":"text","text":"](#button).\n\n- "},{"kind":"code","text":"`'filled'`"},{"kind":"text","text":" — solid background color (default).\n- "},{"kind":"code","text":"`'outlined'`"},{"kind":"text","text":" — transparent background with a border.\n- "},{"kind":"code","text":"`'text'`"},{"kind":"text","text":" — no background or border, text only."}]},"type":{"type":"union","types":[{"type":"literal","value":"filled"},{"type":"literal","value":"outlined"},{"type":"literal","value":"text"}]}},{"name":"Button","variant":"declaration","kind":64,"signatures":[{"name":"Button","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"A pressable button that supports multiple visual variants."}]},"parameters":[{"name":"__namedParameters","variant":"param","kind":32768,"type":{"type":"reference","name":"ButtonProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]}],"packageName":"@expo/ui"}
\ No newline at end of file
diff --git a/packages/expo-ui/CHANGELOG.md b/packages/expo-ui/CHANGELOG.md
index 209b5a8d81161a..6c48a49783e211 100644
--- a/packages/expo-ui/CHANGELOG.md
+++ b/packages/expo-ui/CHANGELOG.md
@@ -33,6 +33,9 @@
### 🐛 Bug fixes
+- [universal] Fix user-supplied `modifiers` having no effect when the component derives a modifier of the same type from its props, e.g. `buttonStyle` on `Button` always losing to the `variant` prop. ([#46815](https://github.com/expo/expo/pull/46815) by [@nishan](https://github.com/intergalacticspacehighway))
+- [android] Fix race between JS imperative `expand()` and Compose handler registration on `ModalBottomSheet`. Adds `initialFullyExpanded` prop to drive initial snap state natively. ([#46367](https://github.com/expo/expo/pull/46367) by [@duyanhv](https://github.com/duyanhv))
+- [iOS][android] Fix `community/bottom-sheet` blocking touches behind the sheet after it's dismissed. ([#46805](https://github.com/expo/expo/pull/46805) by [@nishan](https://github.com/intergalacticspacehighway))
- [android] Fix React Native touchables (e.g. `Pressable`) on `community/pager-view` pages not responding, or triggering the wrong page's handler, after navigating between pages. ([#46778](https://github.com/expo/expo/pull/46778) by [@nishan](https://github.com/intergalacticspacehighway))
- [iOS] Fix `font`, `dynamicTypeSize`, and `resizable` modifiers not applying to the SwiftUI `Image`. SF Symbols scale with Dynamic Type when a `font` modifier sets a `textStyle`. ([#46714](https://github.com/expo/expo/pull/46714) by [@ramonclaudio](https://github.com/ramonclaudio))
- [android] Fix React Native `ScrollView` nested scrolling inside `BottomSheet`. ([#46544](https://github.com/expo/expo/pull/46544) by [@nishan](https://github.com/intergalacticspacehighway))
diff --git a/packages/expo-ui/android/src/main/java/expo/modules/ui/ModalBottomSheetView.kt b/packages/expo-ui/android/src/main/java/expo/modules/ui/ModalBottomSheetView.kt
index 610a022b0ca25d..e800ec2adafaa7 100644
--- a/packages/expo-ui/android/src/main/java/expo/modules/ui/ModalBottomSheetView.kt
+++ b/packages/expo-ui/android/src/main/java/expo/modules/ui/ModalBottomSheetView.kt
@@ -10,6 +10,7 @@ import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
@@ -30,6 +31,7 @@ data class ModalBottomSheetPropertiesRecord(
@OptimizedComposeProps
data class ModalBottomSheetViewProps(
val skipPartiallyExpanded: Boolean = false,
+ val initialFullyExpanded: Boolean = false,
val containerColor: Color? = null,
val contentColor: Color? = null,
val scrimColor: Color? = null,
@@ -112,4 +114,16 @@ fun FunctionalComposableScope.ModalBottomSheetContent(
) {
Children(UIComposableScope(), filter = { !isSlotView(it) })
}
+
+ LaunchedEffect(Unit) {
+ if (props.initialFullyExpanded && !props.skipPartiallyExpanded) {
+ try {
+ sheetState.expand()
+ } catch (_: CancellationException) {
+ // Dismissal can cancel the expand animation.
+ } catch (_: Exception) {
+ // Expanded anchor may be unreachable; never crash the view.
+ }
+ }
+ }
}
diff --git a/packages/expo-ui/build/community/bottom-sheet/BottomSheet.android.d.ts.map b/packages/expo-ui/build/community/bottom-sheet/BottomSheet.android.d.ts.map
index da00921dfe520c..384baa0dc41373 100644
--- a/packages/expo-ui/build/community/bottom-sheet/BottomSheet.android.d.ts.map
+++ b/packages/expo-ui/build/community/bottom-sheet/BottomSheet.android.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"BottomSheet.android.d.ts","sourceRoot":"","sources":["../../../src/community/bottom-sheet/BottomSheet.android.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAMpE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAqC3C;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,2CAsKlD"}
\ No newline at end of file
+{"version":3,"file":"BottomSheet.android.d.ts","sourceRoot":"","sources":["../../../src/community/bottom-sheet/BottomSheet.android.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAMpE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAqC3C;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,2CA2JlD"}
\ No newline at end of file
diff --git a/packages/expo-ui/build/jetpack-compose/ModalBottomSheet/index.d.ts b/packages/expo-ui/build/jetpack-compose/ModalBottomSheet/index.d.ts
index 0530602700e789..48d45ae280db7f 100644
--- a/packages/expo-ui/build/jetpack-compose/ModalBottomSheet/index.d.ts
+++ b/packages/expo-ui/build/jetpack-compose/ModalBottomSheet/index.d.ts
@@ -49,6 +49,12 @@ export type ModalBottomSheetProps = {
* @default false
*/
skipPartiallyExpanded?: boolean;
+ /**
+ * Opens the sheet fully expanded on first composition. Ignored when `skipPartiallyExpanded` is `true`.
+ * @default false
+ * @platform android
+ */
+ initialFullyExpanded?: boolean;
/**
* The background color of the bottom sheet.
*/
diff --git a/packages/expo-ui/build/jetpack-compose/ModalBottomSheet/index.d.ts.map b/packages/expo-ui/build/jetpack-compose/ModalBottomSheet/index.d.ts.map
index 5d2d7aebc2948d..f929aa8fc7b642 100644
--- a/packages/expo-ui/build/jetpack-compose/ModalBottomSheet/index.d.ts.map
+++ b/packages/expo-ui/build/jetpack-compose/ModalBottomSheet/index.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/jetpack-compose/ModalBottomSheet/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAiB,MAAM,OAAO,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAUlD,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;OAGG;IACH,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;;OAEG;IACH,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B;;;OAGG;IACH,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;OAGG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC/B;;;OAGG;IACH,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;OAEG;IACH,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B;;OAEG;IACH,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B;;OAEG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;OAEG;IACH,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACxC;;OAEG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;CAC9B,CAAC;AAkCF;;GAEG;AACH,iBAAS,yBAAyB,CAAC,KAAK,EAAE,qBAAqB,2CAE9D;kBAFQ,yBAAyB;4BAPP;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE;;AAalD,eAAO,MAAM,gBAAgB,kCAA4B,CAAC"}
\ No newline at end of file
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/jetpack-compose/ModalBottomSheet/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAiB,MAAM,OAAO,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAUlD,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;OAGG;IACH,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;;OAEG;IACH,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B;;;OAGG;IACH,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;OAGG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC/B;;;OAGG;IACH,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;OAEG;IACH,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B;;OAEG;IACH,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B;;OAEG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;OAEG;IACH,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACxC;;OAEG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;CAC9B,CAAC;AAmCF;;GAEG;AACH,iBAAS,yBAAyB,CAAC,KAAK,EAAE,qBAAqB,2CAE9D;kBAFQ,yBAAyB;4BAPP;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE;;AAalD,eAAO,MAAM,gBAAgB,kCAA4B,CAAC"}
\ No newline at end of file
diff --git a/packages/expo-ui/build/universal/Button/index.ios.d.ts.map b/packages/expo-ui/build/universal/Button/index.ios.d.ts.map
index af44432002bdda..169e0f1a29319b 100644
--- a/packages/expo-ui/build/universal/Button/index.ios.d.ts.map
+++ b/packages/expo-ui/build/universal/Button/index.ios.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"index.ios.d.ts","sourceRoot":"","sources":["../../../src/universal/Button/index.ios.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,SAAS,CAAC;AAQ1D,wBAAgB,MAAM,CAAC,EACrB,QAAQ,EACR,KAAK,EACL,OAAO,EACP,OAAkB,EAClB,KAAK,EACL,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,MAAM,EACN,MAAM,EACN,SAAS,EAAE,cAAc,GAC1B,EAAE,WAAW,2CAoBb;AAED,cAAc,SAAS,CAAC"}
\ No newline at end of file
+{"version":3,"file":"index.ios.d.ts","sourceRoot":"","sources":["../../../src/universal/Button/index.ios.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,SAAS,CAAC;AAQ1D,wBAAgB,MAAM,CAAC,EACrB,QAAQ,EACR,KAAK,EACL,OAAO,EACP,OAAkB,EAClB,KAAK,EACL,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,MAAM,EACN,MAAM,EACN,SAAS,EAAE,cAAc,GAC1B,EAAE,WAAW,2CAwBb;AAED,cAAc,SAAS,CAAC"}
\ No newline at end of file
diff --git a/packages/expo-ui/build/universal/Text/index.ios.d.ts.map b/packages/expo-ui/build/universal/Text/index.ios.d.ts.map
index bd509edc25f3d9..7a56841e07a7da 100644
--- a/packages/expo-ui/build/universal/Text/index.ios.d.ts.map
+++ b/packages/expo-ui/build/universal/Text/index.ios.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"index.ios.d.ts","sourceRoot":"","sources":["../../../src/universal/Text/index.ios.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAuB,MAAM,SAAS,CAAC;AAyB9D,wBAAgB,IAAI,CAAC,EACnB,QAAQ,EACR,SAAS,EACT,aAAa,EACb,KAAK,EACL,OAAO,EACP,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,MAAM,EACN,MAAM,EACN,SAAS,EAAE,cAAc,GAC1B,EAAE,SAAS,2CA0DX;AAED,cAAc,SAAS,CAAC"}
\ No newline at end of file
+{"version":3,"file":"index.ios.d.ts","sourceRoot":"","sources":["../../../src/universal/Text/index.ios.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAuB,MAAM,SAAS,CAAC;AAyB9D,wBAAgB,IAAI,CAAC,EACnB,QAAQ,EACR,SAAS,EACT,aAAa,EACb,KAAK,EACL,OAAO,EACP,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,MAAM,EACN,MAAM,EACN,SAAS,EAAE,cAAc,GAC1B,EAAE,SAAS,2CA2DX;AAED,cAAc,SAAS,CAAC"}
\ No newline at end of file
diff --git a/packages/expo-ui/build/universal/modifierUtils.d.ts b/packages/expo-ui/build/universal/modifierUtils.d.ts
new file mode 100644
index 00000000000000..91603f025e8586
--- /dev/null
+++ b/packages/expo-ui/build/universal/modifierUtils.d.ts
@@ -0,0 +1,16 @@
+import type { ModifierConfig } from '../types';
+/**
+ * Drops derived modifiers that the user overrides through the `modifiers`
+ * escape hatch. A user-supplied modifier takes ownership of its `$type`, so
+ * the component skips the modifier of that type it would otherwise derive
+ * from `style`, `variant`, and similar props.
+ *
+ * Only pass style-derived modifiers as `derived`. Modifiers backing
+ * functional props (for example `onPress`, `onAppear`, `disabled`) must not
+ * go through this filter, or a user modifier of the same type would silently
+ * disable the prop.
+ */
+export declare function omitUserOverridden(derived: T[], userModifiers?: readonly ModifierConfig[]): T[];
+//# sourceMappingURL=modifierUtils.d.ts.map
\ No newline at end of file
diff --git a/packages/expo-ui/build/universal/modifierUtils.d.ts.map b/packages/expo-ui/build/universal/modifierUtils.d.ts.map
new file mode 100644
index 00000000000000..ab03bb27e819fb
--- /dev/null
+++ b/packages/expo-ui/build/universal/modifierUtils.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"modifierUtils.d.ts","sourceRoot":"","sources":["../../src/universal/modifierUtils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,aAAa,CAAC,EAAE,SAAS,cAAc,EAAE,GACxC,CAAC,EAAE,CAML"}
\ No newline at end of file
diff --git a/packages/expo-ui/build/universal/transformStyle.android.d.ts b/packages/expo-ui/build/universal/transformStyle.android.d.ts
index d3ffb6bcb77c2e..a33eded8e5184b 100644
--- a/packages/expo-ui/build/universal/transformStyle.android.d.ts
+++ b/packages/expo-ui/build/universal/transformStyle.android.d.ts
@@ -8,6 +8,9 @@ import type { UniversalBaseProps, UniversalStyle } from './types';
* box model where background includes the padding area and border is outermost:
* sizing → border → clip → background → padding → opacity
* → events → behavior → user escape-hatch
+ *
+ * Style-derived modifiers yield to user-supplied modifiers of the same
+ * `$type`, so the escape hatch can override anything derived from props.
*/
export declare function transformToModifiers(style: UniversalStyle | undefined, props: Pick, extraModifiers?: ModifierConfig[]): ModifierConfig[];
//# sourceMappingURL=transformStyle.android.d.ts.map
\ No newline at end of file
diff --git a/packages/expo-ui/build/universal/transformStyle.android.d.ts.map b/packages/expo-ui/build/universal/transformStyle.android.d.ts.map
index 7e3d42960693ac..968c360bffcb1c 100644
--- a/packages/expo-ui/build/universal/transformStyle.android.d.ts.map
+++ b/packages/expo-ui/build/universal/transformStyle.android.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"transformStyle.android.d.ts","sourceRoot":"","sources":["../../src/universal/transformStyle.android.ts"],"names":[],"mappings":"AAAA,OAAO,EAaL,KAAK,cAAc,EACpB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAElE;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,cAAc,GAAG,SAAS,EACjC,KAAK,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAC7E,cAAc,CAAC,EAAE,cAAc,EAAE,GAChC,cAAc,EAAE,CA8ElB"}
\ No newline at end of file
+{"version":3,"file":"transformStyle.android.d.ts","sourceRoot":"","sources":["../../src/universal/transformStyle.android.ts"],"names":[],"mappings":"AAAA,OAAO,EAaL,KAAK,cAAc,EACpB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAElE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,cAAc,GAAG,SAAS,EACjC,KAAK,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAC7E,cAAc,CAAC,EAAE,cAAc,EAAE,GAChC,cAAc,EAAE,CAkFlB"}
\ No newline at end of file
diff --git a/packages/expo-ui/build/universal/transformStyle.ios.d.ts b/packages/expo-ui/build/universal/transformStyle.ios.d.ts
index 0fff6ddc16e6fd..735efb8dc8b16f 100644
--- a/packages/expo-ui/build/universal/transformStyle.ios.d.ts
+++ b/packages/expo-ui/build/universal/transformStyle.ios.d.ts
@@ -9,6 +9,9 @@ import type { UniversalBaseProps, UniversalStyle } from './types';
* To match React Native's box model (background fills the full box):
* padding → sizing → background → clip → border → opacity
* → events → lifecycle → behavior → user escape-hatch
+ *
+ * Style-derived modifiers yield to user-supplied modifiers of the same
+ * `$type`, so the escape hatch can override anything derived from props.
*/
export declare function transformToModifiers(style: UniversalStyle | undefined, props: Pick, extraModifiers?: ModifierConfig[], options?: {
/** Alignment for the frame modifier (used by Column/Row). */
diff --git a/packages/expo-ui/build/universal/transformStyle.ios.d.ts.map b/packages/expo-ui/build/universal/transformStyle.ios.d.ts.map
index 363603bb35fc43..12fb64a24bd983 100644
--- a/packages/expo-ui/build/universal/transformStyle.ios.d.ts.map
+++ b/packages/expo-ui/build/universal/transformStyle.ios.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"transformStyle.ios.d.ts","sourceRoot":"","sources":["../../src/universal/transformStyle.ios.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,EAUL,KAAK,cAAc,EACpB,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAgBlE;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,cAAc,GAAG,SAAS,EACjC,KAAK,EAAE,IAAI,CACT,kBAAkB,EAClB,SAAS,GAAG,UAAU,GAAG,aAAa,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAC1E,EACD,cAAc,CAAC,EAAE,cAAc,EAAE,EACjC,OAAO,CAAC,EAAE;IACR,6DAA6D;IAC7D,cAAc,CAAC,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IAC1D,wDAAwD;IACxD,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC,GACA,cAAc,EAAE,CAwGlB"}
\ No newline at end of file
+{"version":3,"file":"transformStyle.ios.d.ts","sourceRoot":"","sources":["../../src/universal/transformStyle.ios.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,EAUL,KAAK,cAAc,EACpB,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAgBlE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,cAAc,GAAG,SAAS,EACjC,KAAK,EAAE,IAAI,CACT,kBAAkB,EAClB,SAAS,GAAG,UAAU,GAAG,aAAa,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAC1E,EACD,cAAc,CAAC,EAAE,cAAc,EAAE,EACjC,OAAO,CAAC,EAAE;IACR,6DAA6D;IAC7D,cAAc,CAAC,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IAC1D,wDAAwD;IACxD,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC,GACA,cAAc,EAAE,CA4GlB"}
\ No newline at end of file
diff --git a/packages/expo-ui/build/universal/types.d.ts b/packages/expo-ui/build/universal/types.d.ts
index 2b0f98aff61c55..fa05884333f3ab 100644
--- a/packages/expo-ui/build/universal/types.d.ts
+++ b/packages/expo-ui/build/universal/types.d.ts
@@ -22,6 +22,8 @@ export interface UniversalBaseProps {
/**
* Platform-specific modifier escape hatch. Pass an array of modifier configs
* from `@expo/ui/swift-ui/modifiers` or `@expo/ui/jetpack-compose/modifiers`.
+ * A modifier supplied here replaces any modifier of the same type that the
+ * component derives from `style` or other props.
* @platform android
* @platform ios
*/
diff --git a/packages/expo-ui/build/universal/types.d.ts.map b/packages/expo-ui/build/universal/types.d.ts.map
index daf2549e7c2118..0938ad3e9f9e75 100644
--- a/packages/expo-ui/build/universal/types.d.ts.map
+++ b/packages/expo-ui/build/universal/types.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/universal/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,SAAS,EACP,SAAS,GACT,mBAAmB,GACnB,iBAAiB,GACjB,YAAY,GACZ,eAAe,GACf,aAAa,GACb,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,aAAa,GACb,aAAa,GACb,SAAS,GACT,OAAO,GACP,QAAQ,CACX,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAGjC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC;IAEvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAI7B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAIrB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAIzB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC"}
\ No newline at end of file
+{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/universal/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,SAAS,EACP,SAAS,GACT,mBAAmB,GACnB,iBAAiB,GACjB,YAAY,GACZ,eAAe,GACf,aAAa,GACb,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,aAAa,GACb,aAAa,GACb,SAAS,GACT,OAAO,GACP,QAAQ,CACX,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAGjC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC;IAEvB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAI7B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAIrB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAIzB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC"}
\ No newline at end of file
diff --git a/packages/expo-ui/src/__mocks__/expo.ts b/packages/expo-ui/src/__mocks__/expo.ts
new file mode 100644
index 00000000000000..18cf08bd4ef138
--- /dev/null
+++ b/packages/expo-ui/src/__mocks__/expo.ts
@@ -0,0 +1,22 @@
+import { createElement } from 'react';
+import { View } from 'react-native';
+
+// Records a `(viewName, props)` call for every render of a view returned by
+// `requireNativeView`. Cleared automatically between tests (`clearMocks`).
+export const renderedNativeViews = jest.fn();
+
+export function findNativeViewProps(viewName: string) {
+ const call = renderedNativeViews.mock.calls.find(([name]) => name === viewName);
+ return call?.[1];
+}
+
+export const requireNativeModule = jest.fn(() => ({}));
+
+export const requireNativeView = jest.fn((moduleName: string, viewName: string) => {
+ return function MockNativeView(props: Record) {
+ renderedNativeViews(viewName, props);
+ // Swallow string children — the RN mock View can't render raw text.
+ const children = typeof props.children === 'string' ? undefined : props.children;
+ return createElement(View, { ...props, children });
+ };
+});
diff --git a/packages/expo-ui/src/community/bottom-sheet/BottomSheet.android.tsx b/packages/expo-ui/src/community/bottom-sheet/BottomSheet.android.tsx
index 0018c10813843d..7db63f5d991430 100644
--- a/packages/expo-ui/src/community/bottom-sheet/BottomSheet.android.tsx
+++ b/packages/expo-ui/src/community/bottom-sheet/BottomSheet.android.tsx
@@ -118,18 +118,6 @@ export function BottomSheet(props: BottomSheetProps) {
}
}, [clampIndex, indexProp, fireCloseCallbacks]);
- useEffect(() => {
- if (!isOpen) return;
-
- const targetIndex = pendingIndexRef.current ?? 0;
- if (hasMultipleSnapPoints && targetIndex === maxIndex) {
- sheetRef.current?.expand();
- } else if (hasMultipleSnapPoints) {
- sheetRef.current?.partialExpand();
- }
- pendingIndexRef.current = null;
- }, [hasMultipleSnapPoints, isOpen, maxIndex]);
-
const handleDismiss = useCallback(() => {
setIsOpen(false);
fireCloseCallbacks();
@@ -197,11 +185,12 @@ export function BottomSheet(props: BottomSheetProps) {
return (
-
+
-
+ {
onDismissRequest?.();
},
diff --git a/packages/expo-ui/src/universal/Button/__tests__/index.test.ios.tsx b/packages/expo-ui/src/universal/Button/__tests__/index.test.ios.tsx
new file mode 100644
index 00000000000000..967482129ab23f
--- /dev/null
+++ b/packages/expo-ui/src/universal/Button/__tests__/index.test.ios.tsx
@@ -0,0 +1,41 @@
+import { render } from '@testing-library/react-native';
+
+import { Button } from '..';
+import { findNativeViewProps } from '../../../__mocks__/expo';
+import { buttonStyle, controlSize } from '../../../swift-ui/modifiers';
+
+jest.mock('expo');
+
+function nativeButtonModifiers() {
+ return findNativeViewProps('Button')?.modifiers;
+}
+
+describe('Button', () => {
+ it('maps the variant prop to a buttonStyle modifier', () => {
+ render();
+ expect(nativeButtonModifiers()).toEqual([{ $type: 'buttonStyle', style: 'bordered' }]);
+ });
+
+ it('defaults to the filled variant', () => {
+ render();
+ expect(nativeButtonModifiers()).toEqual([{ $type: 'buttonStyle', style: 'borderedProminent' }]);
+ });
+
+ it('does not inject the variant buttonStyle when the user supplies a buttonStyle modifier', () => {
+ render();
+ expect(nativeButtonModifiers()).toEqual([{ $type: 'buttonStyle', style: 'glassProminent' }]);
+ });
+
+ it('prefers a user buttonStyle modifier over the variant prop', () => {
+ render();
+ expect(nativeButtonModifiers()).toEqual([{ $type: 'buttonStyle', style: 'automatic' }]);
+ });
+
+ it('keeps the variant buttonStyle when the user supplies unrelated modifiers', () => {
+ render();
+ expect(nativeButtonModifiers()).toEqual([
+ { $type: 'buttonStyle', style: 'plain' },
+ { $type: 'controlSize', size: 'large' },
+ ]);
+ });
+});
diff --git a/packages/expo-ui/src/universal/Button/index.ios.tsx b/packages/expo-ui/src/universal/Button/index.ios.tsx
index df916ee4fce9db..2b7f10a3c19b7c 100644
--- a/packages/expo-ui/src/universal/Button/index.ios.tsx
+++ b/packages/expo-ui/src/universal/Button/index.ios.tsx
@@ -2,6 +2,7 @@ import { Button as SwiftUIButton } from '@expo/ui/swift-ui';
import { buttonStyle } from '@expo/ui/swift-ui/modifiers';
import type { ModifierConfig } from '@expo/ui/swift-ui/modifiers';
+import { omitUserOverridden } from '../modifierUtils';
import { transformToModifiers } from '../transformStyle';
import type { ButtonProps, ButtonVariant } from './types';
@@ -24,7 +25,11 @@ export function Button({
testID,
modifiers: extraModifiers,
}: ButtonProps) {
- const buttonSpecificModifiers: ModifierConfig[] = [buttonStyle(variantButtonStyle[variant])];
+ // A user-supplied buttonStyle modifier replaces the variant's.
+ const buttonSpecificModifiers: ModifierConfig[] = omitUserOverridden(
+ [buttonStyle(variantButtonStyle[variant])],
+ extraModifiers
+ );
const universalModifiers = transformToModifiers(
style,
diff --git a/packages/expo-ui/src/universal/Text/__tests__/index.test.ios.tsx b/packages/expo-ui/src/universal/Text/__tests__/index.test.ios.tsx
new file mode 100644
index 00000000000000..8664da68217c85
--- /dev/null
+++ b/packages/expo-ui/src/universal/Text/__tests__/index.test.ios.tsx
@@ -0,0 +1,49 @@
+import { render } from '@testing-library/react-native';
+
+import { Text } from '..';
+import { findNativeViewProps } from '../../../__mocks__/expo';
+import { font, foregroundStyle, lineLimit, opacity } from '../../../swift-ui/modifiers';
+
+jest.mock('expo');
+
+function nativeTextModifiers() {
+ return findNativeViewProps('TextView')?.modifiers;
+}
+
+describe('Text', () => {
+ it('maps textStyle.color to a foregroundStyle modifier', () => {
+ render(hi);
+ expect(nativeTextModifiers()).toEqual([foregroundStyle('red')]);
+ });
+
+ it('does not inject textStyle-derived modifiers the user overrides', () => {
+ render(
+
+ hi
+
+ );
+ expect(nativeTextModifiers()).toEqual([foregroundStyle('blue')]);
+ });
+
+ it('prefers a user lineLimit modifier over numberOfLines', () => {
+ render(
+
+ hi
+
+ );
+ expect(nativeTextModifiers()).toEqual([lineLimit(5)]);
+ });
+
+ it('keeps derived modifiers the user does not override', () => {
+ render(
+
+ hi
+
+ );
+ expect(nativeTextModifiers()).toEqual([
+ font({ size: 20 }),
+ foregroundStyle('red'),
+ opacity(0.5),
+ ]);
+ });
+});
diff --git a/packages/expo-ui/src/universal/Text/index.ios.tsx b/packages/expo-ui/src/universal/Text/index.ios.tsx
index cc53362a44daef..6f9d7f8daa1b49 100644
--- a/packages/expo-ui/src/universal/Text/index.ios.tsx
+++ b/packages/expo-ui/src/universal/Text/index.ios.tsx
@@ -9,6 +9,7 @@ import {
} from '@expo/ui/swift-ui/modifiers';
import type { ModifierConfig } from '@expo/ui/swift-ui/modifiers';
+import { omitUserOverridden } from '../modifierUtils';
import { transformToModifiers } from '../transformStyle';
import type { TextProps, UniversalFontWeight } from './types';
@@ -98,7 +99,8 @@ export function Text({
extraModifiers
);
- const modifiers = [...textModifiers, ...universalModifiers];
+ // A user-supplied modifier replaces any text-derived modifier of the same type.
+ const modifiers = [...omitUserOverridden(textModifiers, extraModifiers), ...universalModifiers];
return (
diff --git a/packages/expo-ui/src/universal/__tests__/modifierUtils.test.ts b/packages/expo-ui/src/universal/__tests__/modifierUtils.test.ts
new file mode 100644
index 00000000000000..ec2a8da57cb97c
--- /dev/null
+++ b/packages/expo-ui/src/universal/__tests__/modifierUtils.test.ts
@@ -0,0 +1,28 @@
+import { omitUserOverridden } from '../modifierUtils';
+
+describe('omitUserOverridden', () => {
+ it('returns the derived modifiers as-is when the user supplies none', () => {
+ const derived = [{ $type: 'background', color: 'red' }];
+ expect(omitUserOverridden(derived, undefined)).toEqual(derived);
+ expect(omitUserOverridden(derived, [])).toEqual(derived);
+ });
+
+ it('drops derived modifiers whose $type the user supplied', () => {
+ const derived = [
+ { $type: 'background', color: 'red' },
+ { $type: 'padding', all: 8 },
+ ];
+ expect(omitUserOverridden(derived, [{ $type: 'padding', top: 4 }])).toEqual([
+ { $type: 'background', color: 'red' },
+ ]);
+ });
+
+ it('only drops derived modifiers, never user ones', () => {
+ const derived = [{ $type: 'padding', all: 8 }];
+ const user = [
+ { $type: 'padding', top: 4 },
+ { $type: 'padding', bottom: 2 },
+ ];
+ expect(omitUserOverridden(derived, user)).toEqual([]);
+ });
+});
diff --git a/packages/expo-ui/src/universal/__tests__/transformStyle.test.android.ts b/packages/expo-ui/src/universal/__tests__/transformStyle.test.android.ts
new file mode 100644
index 00000000000000..54450852c06b7e
--- /dev/null
+++ b/packages/expo-ui/src/universal/__tests__/transformStyle.test.android.ts
@@ -0,0 +1,30 @@
+import { alpha, background, clickable, paddingAll } from '../../jetpack-compose/modifiers';
+import { transformToModifiers } from '../transformStyle';
+
+describe('transformToModifiers (Android)', () => {
+ it('drops a style-derived modifier when the user supplies the same type', () => {
+ expect(transformToModifiers({ opacity: 0.5 }, {}, [alpha(0.8)])).toEqual([alpha(0.8)]);
+ });
+
+ it('keeps style-derived modifiers of types the user did not supply', () => {
+ expect(
+ transformToModifiers({ backgroundColor: 'red', padding: 8 }, {}, [paddingAll(4)])
+ ).toEqual([background('red'), paddingAll(4)]);
+ });
+
+ it('keeps the hidden alpha even when the user supplies an alpha modifier', () => {
+ expect(transformToModifiers(undefined, { hidden: true }, [alpha(0.8)])).toEqual([
+ alpha(0),
+ alpha(0.8),
+ ]);
+ });
+
+ it('keeps the onPress clickable when the user supplies their own clickable', () => {
+ const onPress = jest.fn();
+ const userClick = clickable(jest.fn());
+ expect(transformToModifiers(undefined, { onPress }, [userClick])).toEqual([
+ clickable(onPress),
+ userClick,
+ ]);
+ });
+});
diff --git a/packages/expo-ui/src/universal/__tests__/transformStyle.test.ios.ts b/packages/expo-ui/src/universal/__tests__/transformStyle.test.ios.ts
new file mode 100644
index 00000000000000..7372af6ba02d01
--- /dev/null
+++ b/packages/expo-ui/src/universal/__tests__/transformStyle.test.ios.ts
@@ -0,0 +1,40 @@
+import { background, disabled, font, onTapGesture, padding } from '../../swift-ui/modifiers';
+import { transformToModifiers } from '../transformStyle';
+
+describe('transformToModifiers (iOS)', () => {
+ it('drops a style-derived modifier when the user supplies the same type', () => {
+ expect(transformToModifiers({ backgroundColor: 'red' }, {}, [background('blue')])).toEqual([
+ background('blue'),
+ ]);
+ });
+
+ it('keeps style-derived modifiers of types the user did not supply', () => {
+ expect(
+ transformToModifiers({ backgroundColor: 'red', padding: 8 }, {}, [padding({ top: 4 })])
+ ).toEqual([background('red'), padding({ top: 4 })]);
+ });
+
+ it('drops textStyle-derived modifiers the user overrides', () => {
+ expect(
+ transformToModifiers(undefined, {}, [font({ textStyle: 'largeTitle' })], {
+ textStyle: { fontSize: 20 },
+ })
+ ).toEqual([font({ textStyle: 'largeTitle' })]);
+ });
+
+ it('keeps the onPress tap gesture when the user supplies their own onTapGesture', () => {
+ const onPress = jest.fn();
+ const userTap = onTapGesture(jest.fn());
+ expect(transformToModifiers(undefined, { onPress }, [userTap])).toEqual([
+ onTapGesture(onPress),
+ userTap,
+ ]);
+ });
+
+ it('keeps behavior modifiers when the user supplies the same type', () => {
+ expect(transformToModifiers(undefined, { disabled: true }, [disabled(false)])).toEqual([
+ disabled(true),
+ disabled(false),
+ ]);
+ });
+});
diff --git a/packages/expo-ui/src/universal/modifierUtils.ts b/packages/expo-ui/src/universal/modifierUtils.ts
new file mode 100644
index 00000000000000..f7fc7a177a8c97
--- /dev/null
+++ b/packages/expo-ui/src/universal/modifierUtils.ts
@@ -0,0 +1,23 @@
+import type { ModifierConfig } from '../types';
+
+/**
+ * Drops derived modifiers that the user overrides through the `modifiers`
+ * escape hatch. A user-supplied modifier takes ownership of its `$type`, so
+ * the component skips the modifier of that type it would otherwise derive
+ * from `style`, `variant`, and similar props.
+ *
+ * Only pass style-derived modifiers as `derived`. Modifiers backing
+ * functional props (for example `onPress`, `onAppear`, `disabled`) must not
+ * go through this filter, or a user modifier of the same type would silently
+ * disable the prop.
+ */
+export function omitUserOverridden(
+ derived: T[],
+ userModifiers?: readonly ModifierConfig[]
+): T[] {
+ if (!userModifiers?.length) {
+ return derived;
+ }
+ const userTypes = new Set(userModifiers.map((modifier) => modifier.$type));
+ return derived.filter((modifier) => !userTypes.has(modifier.$type));
+}
diff --git a/packages/expo-ui/src/universal/transformStyle.android.ts b/packages/expo-ui/src/universal/transformStyle.android.ts
index e5b92209828bf2..f73443a33e1013 100644
--- a/packages/expo-ui/src/universal/transformStyle.android.ts
+++ b/packages/expo-ui/src/universal/transformStyle.android.ts
@@ -14,6 +14,7 @@ import {
type ModifierConfig,
} from '@expo/ui/jetpack-compose/modifiers';
+import { omitUserOverridden } from './modifierUtils';
import type { UniversalBaseProps, UniversalStyle } from './types';
/**
@@ -24,13 +25,16 @@ import type { UniversalBaseProps, UniversalStyle } from './types';
* box model where background includes the padding area and border is outermost:
* sizing → border → clip → background → padding → opacity
* → events → behavior → user escape-hatch
+ *
+ * Style-derived modifiers yield to user-supplied modifiers of the same
+ * `$type`, so the escape hatch can override anything derived from props.
*/
export function transformToModifiers(
style: UniversalStyle | undefined,
props: Pick,
extraModifiers?: ModifierConfig[]
): ModifierConfig[] {
- const mods: ModifierConfig[] = [];
+ let mods: ModifierConfig[] = [];
if (style) {
// Sizing (outermost)
@@ -95,6 +99,10 @@ export function transformToModifiers(
}
}
+ // A user-supplied modifier replaces any style-derived modifier of the same
+ // type. The event and behavior modifiers below are never dropped.
+ mods = omitUserOverridden(mods, extraModifiers);
+
// Events — Compose uses clickable modifier
if (props.onPress) mods.push(clickable(props.onPress));
diff --git a/packages/expo-ui/src/universal/transformStyle.ios.ts b/packages/expo-ui/src/universal/transformStyle.ios.ts
index d5eceb54f448ee..11dfa66af717cf 100644
--- a/packages/expo-ui/src/universal/transformStyle.ios.ts
+++ b/packages/expo-ui/src/universal/transformStyle.ios.ts
@@ -19,6 +19,7 @@ import {
} from '@expo/ui/swift-ui/modifiers';
import type { UniversalTextStyle } from './Text/types';
+import { omitUserOverridden } from './modifierUtils';
import type { UniversalBaseProps, UniversalStyle } from './types';
const FONT_WEIGHT_MAP: Record[0]['weight']> = {
@@ -43,6 +44,9 @@ const FONT_WEIGHT_MAP: Record[0]['weight']> = {
* To match React Native's box model (background fills the full box):
* padding → sizing → background → clip → border → opacity
* → events → lifecycle → behavior → user escape-hatch
+ *
+ * Style-derived modifiers yield to user-supplied modifiers of the same
+ * `$type`, so the escape hatch can override anything derived from props.
*/
export function transformToModifiers(
style: UniversalStyle | undefined,
@@ -58,7 +62,7 @@ export function transformToModifiers(
textStyle?: UniversalTextStyle;
}
): ModifierConfig[] {
- const mods: ModifierConfig[] = [];
+ let mods: ModifierConfig[] = [];
// Text styling (innermost — applies to text content before container modifiers)
const textStyle = options?.textStyle;
@@ -146,6 +150,10 @@ export function transformToModifiers(
}
}
+ // A user-supplied modifier replaces any style-derived modifier of the same
+ // type. The event, lifecycle, and behavior modifiers below are never dropped.
+ mods = omitUserOverridden(mods, extraModifiers);
+
// Events
if (props.onPress) mods.push(onTapGesture(props.onPress));
diff --git a/packages/expo-ui/src/universal/types.ts b/packages/expo-ui/src/universal/types.ts
index 781a8a03bf72ee..446a1a00a2f0cb 100644
--- a/packages/expo-ui/src/universal/types.ts
+++ b/packages/expo-ui/src/universal/types.ts
@@ -44,6 +44,8 @@ export interface UniversalBaseProps {
/**
* Platform-specific modifier escape hatch. Pass an array of modifier configs
* from `@expo/ui/swift-ui/modifiers` or `@expo/ui/jetpack-compose/modifiers`.
+ * A modifier supplied here replaces any modifier of the same type that the
+ * component derives from `style` or other props.
* @platform android
* @platform ios
*/