expo-dev-menu 56.0.5 → 56.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.7 — 2026-05-13
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - Fix FAB safe-area bounds ([#45647](https://github.com/expo/expo/pull/45647) by [@Wenszel](https://github.com/Wenszel))
18
+
19
+ ## 56.0.6 — 2026-05-11
20
+
21
+ _This version does not introduce any user-facing changes._
22
+
13
23
  ## 56.0.5 — 2026-05-08
14
24
 
15
25
  ### 💡 Others
@@ -12,7 +12,7 @@ apply plugin: 'expo-module-gradle-plugin'
12
12
  apply plugin: 'org.jetbrains.kotlin.plugin.compose'
13
13
 
14
14
  group = 'host.exp.exponent'
15
- version = '56.0.5'
15
+ version = '56.0.7'
16
16
 
17
17
  def hasDevLauncher = findProject(":expo-dev-launcher") != null
18
18
  def configureInRelease = findProperty("expo.devmenu.configureInRelease") == "true"
@@ -29,7 +29,7 @@ android {
29
29
 
30
30
  defaultConfig {
31
31
  versionCode 10
32
- versionName '56.0.5'
32
+ versionName '56.0.7'
33
33
  }
34
34
 
35
35
  buildTypes {
@@ -4,8 +4,6 @@ import android.content.Context
4
4
  import android.content.SharedPreferences
5
5
  import androidx.compose.animation.core.Animatable
6
6
  import androidx.compose.animation.core.VectorConverter
7
- import androidx.compose.foundation.layout.WindowInsets
8
- import androidx.compose.foundation.layout.systemBars
9
7
  import androidx.compose.runtime.Composable
10
8
  import androidx.compose.runtime.LaunchedEffect
11
9
  import androidx.compose.runtime.getValue
@@ -15,7 +13,6 @@ import androidx.compose.runtime.remember
15
13
  import androidx.compose.runtime.setValue
16
14
  import androidx.compose.ui.geometry.Offset
17
15
  import androidx.compose.ui.platform.LocalContext
18
- import androidx.compose.ui.platform.LocalDensity
19
16
  import androidx.core.content.edit
20
17
  import kotlinx.coroutines.delay
21
18
 
@@ -25,17 +22,9 @@ private const val FAB_POSITION_X = "fabPositionX"
25
22
  private const val FAB_POSITION_Y = "fabPositionY"
26
23
  private const val FAB_POSITION_UNSET = -1f
27
24
 
28
- data class FabBounds(
29
- val screen: Offset,
30
- val safe: Offset,
31
- val safeMinY: Float,
32
- val drag: Offset,
33
- val halfFab: Offset
34
- )
35
-
36
25
  class FabState(
37
26
  initialOffset: Offset,
38
- val bounds: FabBounds,
27
+ var fabAreaBounds: Offset,
39
28
  val prefs: SharedPreferences
40
29
  ) {
41
30
  val animatedOffset = Animatable(initialOffset, Offset.VectorConverter)
@@ -51,12 +40,9 @@ class FabState(
51
40
  }
52
41
 
53
42
  fun savePosition(offset: Offset) {
54
- val safeWidth = bounds.safe.x
55
- val safeHeight = bounds.safe.y - bounds.safeMinY
56
-
57
43
  // Store position as 0–1 ratios within the safe area so it survives screen size changes
58
- val normalizedX = if (safeWidth > 0f) offset.x / safeWidth else 0f
59
- val normalizedY = if (safeHeight > 0f) (offset.y - bounds.safeMinY) / safeHeight else 0f
44
+ val normalizedX = if (fabAreaBounds.x > 0f) offset.x / fabAreaBounds.x else 0f
45
+ val normalizedY = if (fabAreaBounds.y > 0f) offset.y / fabAreaBounds.y else 0f
60
46
 
61
47
  prefs.edit {
62
48
  putFloat(FAB_POSITION_X, normalizedX)
@@ -66,40 +52,27 @@ class FabState(
66
52
  }
67
53
 
68
54
  @Composable
69
- fun rememberFabState(screenBounds: Offset, totalFabSizePx: Offset): FabState {
70
- val density = LocalDensity.current
71
- val systemBarInsets = WindowInsets.systemBars
72
- val safeInsetTop = with(density) { systemBarInsets.getTop(this).toFloat() }
73
- val safeInsetBottom = with(density) { systemBarInsets.getBottom(this).toFloat() }
74
- val halfFab = Offset(totalFabSizePx.x / 2f, totalFabSizePx.y / 2f)
75
-
76
- val fabBounds = FabBounds(
77
- screen = screenBounds,
78
- safe = Offset(screenBounds.x, screenBounds.y - safeInsetBottom),
79
- safeMinY = safeInsetTop,
80
- drag = Offset(screenBounds.x + halfFab.x, screenBounds.y + halfFab.y),
81
- halfFab = halfFab
82
- )
83
-
55
+ fun rememberFabState(fabAreaBounds: Offset): FabState {
84
56
  val context = LocalContext.current
85
57
  val prefs = remember { context.getSharedPreferences(FAB_PREFS, Context.MODE_PRIVATE) }
86
58
 
87
- val initialOffset = remember(fabBounds.safe, fabBounds.safeMinY) {
59
+ val initialOffset = remember {
88
60
  val savedX = prefs.getFloat(FAB_POSITION_X, FAB_POSITION_UNSET)
89
61
  val savedY = prefs.getFloat(FAB_POSITION_Y, FAB_POSITION_UNSET)
90
- val safeMaxX = maxOf(0f, fabBounds.safe.x)
91
- val safeMaxY = maxOf(fabBounds.safeMinY, fabBounds.safe.y)
92
62
  if (savedX != FAB_POSITION_UNSET && savedY != FAB_POSITION_UNSET) {
93
63
  Offset(
94
- x = (savedX * safeMaxX).coerceIn(0f, safeMaxX),
95
- y = (savedY * (safeMaxY - fabBounds.safeMinY) + fabBounds.safeMinY).coerceIn(fabBounds.safeMinY, safeMaxY)
64
+ x = (savedX * fabAreaBounds.x).coerceIn(0f, fabAreaBounds.x),
65
+ y = (savedY * fabAreaBounds.y).coerceIn(0f, fabAreaBounds.y)
96
66
  )
97
67
  } else {
98
- Offset(fabBounds.safe.x, fabBounds.safeMinY)
68
+ Offset(fabAreaBounds.x, 0f)
99
69
  }
100
70
  }
101
71
 
102
- val state = remember { FabState(initialOffset, fabBounds, prefs) }
72
+ val state = remember { FabState(initialOffset, fabAreaBounds, prefs) }
73
+ // We can't simply update the entire state when the bounds change,
74
+ // because it would result in resetting the interaction state (isPressed, isDragging).
75
+ state.fabAreaBounds = fabAreaBounds
103
76
 
104
77
  LaunchedEffect(state.lastInteractionTime) {
105
78
  state.isIdle = false
@@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
5
5
  import androidx.compose.runtime.mutableStateOf
6
6
  import androidx.compose.runtime.remember
7
7
  import androidx.compose.ui.geometry.Offset
8
+ import androidx.compose.ui.geometry.Rect
8
9
  import androidx.compose.ui.unit.IntOffset
9
10
  import expo.modules.devmenu.fab.ExpoVelocityTracker.PointF
10
11
  import kotlin.math.roundToInt
@@ -49,6 +50,15 @@ internal fun Offset.coerceIn(minX: Float = 0f, maxX: Float, minY: Float = 0f, ma
49
50
  )
50
51
  }
51
52
 
53
+ internal fun Offset.coerceIn(rect: Rect): Offset {
54
+ return this.coerceIn(
55
+ minX = rect.left,
56
+ maxX = rect.right,
57
+ minY = rect.top,
58
+ maxY = rect.bottom
59
+ )
60
+ }
61
+
52
62
  @Composable
53
63
  internal fun <T> rememberPrevious(current: T): T? {
54
64
  val previousRef = remember { mutableStateOf<T?>(null) }
@@ -14,12 +14,14 @@ import androidx.compose.foundation.layout.BoxWithConstraints
14
14
  import androidx.compose.foundation.layout.fillMaxSize
15
15
  import androidx.compose.foundation.layout.offset
16
16
  import androidx.compose.foundation.layout.padding
17
+ import androidx.compose.foundation.layout.safeDrawingPadding
17
18
  import androidx.compose.foundation.layout.size
18
19
  import androidx.compose.runtime.Composable
19
20
  import androidx.compose.runtime.LaunchedEffect
20
21
  import androidx.compose.runtime.remember
21
22
  import androidx.compose.ui.Modifier
22
23
  import androidx.compose.ui.geometry.Offset
24
+ import androidx.compose.ui.geometry.Rect
23
25
  import androidx.compose.ui.input.pointer.pointerInput
24
26
  import androidx.compose.ui.input.pointer.positionChange
25
27
  import androidx.compose.ui.platform.LocalDensity
@@ -48,7 +50,11 @@ fun MovableFloatingActionButton(
48
50
  margin: Dp = Margin,
49
51
  onPress: () -> Unit = {}
50
52
  ) {
51
- BoxWithConstraints(modifier = modifier.fillMaxSize()) {
53
+ BoxWithConstraints(
54
+ modifier = modifier
55
+ .safeDrawingPadding()
56
+ .fillMaxSize()
57
+ ) {
52
58
  val totalFabSize = DpSize(fabSize.width + margin * 2, fabSize.height + margin * 2)
53
59
  val totalFabSizePx = with(LocalDensity.current) {
54
60
  Offset(totalFabSize.width.toPx(), totalFabSize.height.toPx())
@@ -58,7 +64,16 @@ fun MovableFloatingActionButton(
58
64
  y = constraints.maxHeight - totalFabSizePx.y
59
65
  )
60
66
 
61
- val fab = rememberFabState(bounds, totalFabSizePx)
67
+ val halfFab = Offset(totalFabSizePx.x / 2f, totalFabSizePx.y / 2f)
68
+ val dragBounds = Rect(
69
+ left = -halfFab.x,
70
+ top = -halfFab.y,
71
+ right = bounds.x + halfFab.x,
72
+ bottom = bounds.y + halfFab.y
73
+ )
74
+
75
+ val fab = rememberFabState(bounds)
76
+
62
77
  val isFabDisplayable = state.showFab &&
63
78
  !state.isInPictureInPictureMode &&
64
79
  bounds.x >= 0f &&
@@ -70,7 +85,7 @@ fun MovableFloatingActionButton(
70
85
  LaunchedEffect(state.isOpen) {
71
86
  if (state.isOpen) {
72
87
  fab.restingOffset = fab.animatedOffset.value
73
- val isOnLeftSide = fab.animatedOffset.value.x < fab.bounds.safe.x / 2f
88
+ val isOnLeftSide = fab.animatedOffset.value.x < fab.fabAreaBounds.x / 2f
74
89
  val offScreenX = if (isOnLeftSide) -totalFabSizePx.x else constraints.maxWidth.toFloat()
75
90
  fab.animatedOffset.animateTo(
76
91
  targetValue = Offset(offScreenX, fab.animatedOffset.value.y),
@@ -97,14 +112,13 @@ fun MovableFloatingActionButton(
97
112
  previousBounds?.let {
98
113
  val oldX = fab.animatedOffset.value.x
99
114
  val oldY = fab.animatedOffset.value.y
100
- val newX = (oldX / previousBounds.x) * fab.bounds.safe.x
101
- val newY = (oldY / previousBounds.y) * fab.bounds.safe.y
115
+ val newX = (oldX / previousBounds.x) * fab.fabAreaBounds.x
116
+ val newY = (oldY / previousBounds.y) * fab.fabAreaBounds.y
102
117
  val newTarget = calculateTargetPosition(
103
118
  currentPosition = Offset(newX, newY),
104
119
  velocity = ExpoVelocityTracker.PointF(0f, 0f),
105
- bounds = fab.bounds.safe,
106
- totalFabWidth = totalFabSizePx.x,
107
- minY = fab.bounds.safeMinY
120
+ bounds = fab.fabAreaBounds,
121
+ totalFabWidth = totalFabSizePx.x
108
122
  )
109
123
 
110
124
  fab.animatedOffset.snapTo(newTarget)
@@ -140,7 +154,7 @@ fun MovableFloatingActionButton(
140
154
 
141
155
  drag(pointerId) { change ->
142
156
  dragOffset = (dragOffset + change.positionChange())
143
- .coerceIn(minX = -fab.bounds.halfFab.x, maxX = fab.bounds.drag.x, minY = -fab.bounds.halfFab.y, maxY = fab.bounds.drag.y)
157
+ .coerceIn(dragBounds)
144
158
  dragDistance += change.positionChange().getDistance()
145
159
  velocityTracker.registerPosition(dragOffset.x, dragOffset.y)
146
160
 
@@ -187,7 +201,12 @@ private fun CoroutineScope.handleRelease(
187
201
  totalFabSizePx: Offset
188
202
  ) {
189
203
  val velocity = velocityTracker.calculateVelocity()
190
- val newOffset = calculateTargetPosition(fab.animatedOffset.value, velocity, fab.bounds.safe, totalFabSizePx.x, fab.bounds.safeMinY)
204
+ val newOffset = calculateTargetPosition(
205
+ currentPosition = fab.animatedOffset.value,
206
+ velocity = velocity,
207
+ bounds = fab.fabAreaBounds,
208
+ totalFabWidth = totalFabSizePx.x
209
+ )
191
210
 
192
211
  velocityTracker.clear()
193
212
  launch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-dev-menu",
3
- "version": "56.0.5",
3
+ "version": "56.0.7",
4
4
  "description": "Expo/React Native module with the developer menu.",
5
5
  "main": "build/DevMenu.js",
6
6
  "types": "build/DevMenu.d.ts",
@@ -32,15 +32,15 @@
32
32
  "@types/node": "^22.14.0",
33
33
  "react": "19.2.3",
34
34
  "react-native": "0.85.3",
35
- "expo": "56.0.0-preview.7",
36
- "babel-preset-expo": "56.0.5",
37
- "expo-module-scripts": "56.0.2"
35
+ "babel-preset-expo": "56.0.7",
36
+ "expo-module-scripts": "56.0.2",
37
+ "expo": "56.0.0-preview.10"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "expo": "*",
41
41
  "react-native": "*"
42
42
  },
43
- "gitHead": "a30353e69ca0d72b9fac5830abc631feda1ba3ae",
43
+ "gitHead": "40f0a6f6711d93762e0506b37e6e077e4bd9a541",
44
44
  "scripts": {
45
45
  "build": "expo-module build",
46
46
  "clean": "expo-module clean",