Fix mobile sidebars reappearing ghost-open after heavy commits
On Android (Fabric), a large React commit landing after a sidebar open/close settles — e.g. a long agent thread finishing its first mount — re-applies React's stale committed props onto the native view, reverting the UI-thread transform so the sidebar reappears with no transition while store and worklet state correctly say "closed" (software-mansion/react-native-reanimated#9635). Work around it by bumping a settled-generation counter after every settle so the animated styles refresh React's committed props from the settled shared values. The backdrop's pointerEvents moves from the worklet to React state for the same reason: Reanimated's prop re-application doesn't cover it, and a reverted "auto" wedges an invisible tap-eating backdrop. Animations still run entirely on the UI thread; the bump only fires after settle and writes no shared values.
This commit is contained in:
@@ -97,6 +97,7 @@ export function ExplorerSidebar({
|
||||
windowWidth,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
settledGeneration,
|
||||
isGesturing,
|
||||
gestureAnimatingRef,
|
||||
closeGestureRef,
|
||||
@@ -259,22 +260,37 @@ export function ExplorerSidebar({
|
||||
[isMobile, explorerWidth, resizeWidth, setExplorerWidth, viewportWidth],
|
||||
);
|
||||
|
||||
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: translateX.value }],
|
||||
}));
|
||||
// settledGeneration as deps: after each settle the updater is rebuilt so the
|
||||
// shared values are re-applied to the view, protecting against a heavy Fabric
|
||||
// commit reverting the transform to stale React-committed props (#9635).
|
||||
const sidebarAnimatedStyle = useAnimatedStyle(
|
||||
() => ({
|
||||
transform: [{ translateX: translateX.value }],
|
||||
}),
|
||||
[settledGeneration],
|
||||
);
|
||||
|
||||
const backdropAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: backdropOpacity.value,
|
||||
pointerEvents: backdropOpacity.value > 0.01 ? "auto" : "none",
|
||||
}));
|
||||
// pointerEvents comes from React state, not the worklet: the Fabric revert
|
||||
// protection above does not cover pointerEvents, so a worklet-driven value
|
||||
// can wedge an invisible tap-eating backdrop after a heavy commit.
|
||||
const backdropAnimatedStyle = useAnimatedStyle(
|
||||
() => ({
|
||||
opacity: backdropOpacity.value,
|
||||
}),
|
||||
[settledGeneration],
|
||||
);
|
||||
|
||||
const resizeAnimatedStyle = useAnimatedStyle(() => ({
|
||||
width: resizeWidth.value,
|
||||
}));
|
||||
|
||||
const backdropCombinedStyle = useMemo(
|
||||
() => [explorerStaticStyles.backdrop, backdropAnimatedStyle],
|
||||
[backdropAnimatedStyle],
|
||||
() => [
|
||||
explorerStaticStyles.backdrop,
|
||||
backdropAnimatedStyle,
|
||||
{ pointerEvents: isOpen ? ("auto" as const) : ("none" as const) },
|
||||
],
|
||||
[backdropAnimatedStyle, isOpen],
|
||||
);
|
||||
const mobileSidebarStyle = useMemo(
|
||||
() => [
|
||||
|
||||
@@ -615,6 +615,7 @@ function MobileSidebar({
|
||||
windowWidth,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
settledGeneration,
|
||||
isGesturing,
|
||||
mobilePanelState,
|
||||
gestureAnimatingRef,
|
||||
@@ -748,14 +749,25 @@ function MobileSidebar({
|
||||
[activeHostStatusColor],
|
||||
);
|
||||
|
||||
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: translateX.value }],
|
||||
}));
|
||||
// settledGeneration as deps: after each settle the updater is rebuilt so the
|
||||
// shared values are re-applied to the view, protecting against a heavy Fabric
|
||||
// commit reverting the transform to stale React-committed props (#9635).
|
||||
const sidebarAnimatedStyle = useAnimatedStyle(
|
||||
() => ({
|
||||
transform: [{ translateX: translateX.value }],
|
||||
}),
|
||||
[settledGeneration],
|
||||
);
|
||||
|
||||
const backdropAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: backdropOpacity.value,
|
||||
pointerEvents: backdropOpacity.value > 0.01 ? "auto" : "none",
|
||||
}));
|
||||
// pointerEvents comes from React state, not the worklet: the Fabric revert
|
||||
// protection above does not cover pointerEvents, so a worklet-driven value
|
||||
// can wedge an invisible tap-eating backdrop after a heavy commit.
|
||||
const backdropAnimatedStyle = useAnimatedStyle(
|
||||
() => ({
|
||||
opacity: backdropOpacity.value,
|
||||
}),
|
||||
[settledGeneration],
|
||||
);
|
||||
|
||||
let overlayPointerEvents: "auto" | "none" | "box-none";
|
||||
if (!isWeb) overlayPointerEvents = "box-none";
|
||||
@@ -763,8 +775,12 @@ function MobileSidebar({
|
||||
else overlayPointerEvents = "none";
|
||||
|
||||
const backdropStyle = useMemo(
|
||||
() => [staticStyles.backdrop, backdropAnimatedStyle],
|
||||
[backdropAnimatedStyle],
|
||||
() => [
|
||||
staticStyles.backdrop,
|
||||
backdropAnimatedStyle,
|
||||
{ pointerEvents: isOpen ? ("auto" as const) : ("none" as const) },
|
||||
],
|
||||
[backdropAnimatedStyle, isOpen],
|
||||
);
|
||||
const mobileSidebarStyle = useMemo(
|
||||
() => [
|
||||
|
||||
Reference in New Issue
Block a user