-
Notifications
You must be signed in to change notification settings - Fork 374
Expand file tree
/
Copy pathPortalWhileClosingView.tsx
More file actions
155 lines (137 loc) · 5.4 KB
/
PortalWhileClosingView.tsx
File metadata and controls
155 lines (137 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import React, { ReactNode, useEffect, useMemo, useRef } from 'react';
import { Platform, View } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Portal } from 'react-native-teleport';
import { useStableCallback } from '../../hooks';
import {
clearClosingPortalLayout,
createClosingPortalLayoutRegistrationId,
setClosingPortalLayout,
useShouldTeleportToClosingPortal,
useHasActiveId,
} from '../../state-store';
type PortalWhileClosingViewProps = {
/**
* Content that should render in place normally and teleport to a closing portal host
* while the message overlay is closing.
*/
children: ReactNode;
/**
* Name of the closing `PortalHost` in `MessageOverlayHostLayer`.
* This key is also used in the closing layout registry.
*/
portalHostName: string;
/**
* Stable portal instance name used by `react-native-teleport` to move this content
* between the in-place tree and the closing host.
*/
portalName: string;
};
/**
* Keeps wrapped UI above the message overlay during close animation by teleporting it to a closing portal host.
*
* Why this is needed:
*
* When the overlay closes, the animated message can visually pass over fixed UI (for example composer/header).
* This wrapper moves that UI into the overlay host layer for the closing phase, so stacking stays correct.
*
* To use it, simply wrap any view that should remain on top while the overlay is closing, and pass a `portalHostName`
* and a `portalName`. Once the wrapped view has a valid measured layout, it can participate in the closing host layer.
*
* Behavior:
* - renders children in place during normal operation
* - registers absolute layout for `portalHostName` once a valid measurement exists
* - while overlay state is `closing`, teleports children to the matching closing host
* - renders a local placeholder while closing to preserve original layout space
*
* Stack presence only starts after first valid measurement. That prevents unmeasured entries from taking over a host
* slot and rendering with incomplete geometry.
*
* Note: As the `PortalWhileClosingView` relies heavily on being able to calculate the layout and positioning
* properties of its children automatically, make sure that you do not wrap absolutely positioned views with
* it as positioning parameters specifically will not be calculated correctly as the absolute position of
* the immediate child will be towards its immediate parent (which is our `Portal` view). Instead, wrap its
* children directly (the non-absolutely positioned ones). Since we use `measureInWindow` to get a hold of
* the initial measurements, we'll always have the correct position of the relevant content.
*
* @param props.children content to render and teleport while closing
* @param props.portalHostName closing host slot name used for layout registration and portal target
* @param props.portalName stable portal instance name for `react-native-teleport`
*/
export const PortalWhileClosingView = ({
children,
portalHostName,
portalName,
}: PortalWhileClosingViewProps) => {
const registrationIdRef = useRef<string | null>(null);
if (!registrationIdRef.current) {
registrationIdRef.current = createClosingPortalLayoutRegistrationId();
}
const registrationId = registrationIdRef.current;
const { syncPortalLayout, containerRef, placeholderLayout } = useSyncingApi(
portalHostName,
registrationId,
);
const shouldTeleport = useShouldTeleportToClosingPortal(portalHostName, registrationId);
useEffect(() => {
return () => {
clearClosingPortalLayout(portalHostName, registrationId);
};
}, [portalHostName, registrationId]);
const placeholderStyle = useAnimatedStyle(() => ({
height: placeholderLayout.value.h,
width: placeholderLayout.value.w > 0 ? placeholderLayout.value.w : '100%',
}));
return (
<>
<Portal hostName={shouldTeleport ? portalHostName : undefined} name={portalName}>
<View collapsable={false} ref={containerRef} onLayout={syncPortalLayout}>
{children}
</View>
</Portal>
{shouldTeleport ? (
<Animated.View
pointerEvents='none'
style={placeholderStyle}
testID={`portal-while-closing-placeholder-${portalName}`}
/>
) : null}
</>
);
};
const useSyncingApi = (portalHostName: string, registrationId: string) => {
const containerRef = useRef<View | null>(null);
const placeholderLayout = useSharedValue({ h: 0, w: 0 });
const insets = useSafeAreaInsets();
const hasActiveId = useHasActiveId();
const syncPortalLayout = useStableCallback(() => {
if (!hasActiveId) {
return;
}
containerRef.current?.measureInWindow((x, y, width, height) => {
const absolute = {
x,
y: y + (Platform.OS === 'android' ? insets.top : 0),
};
if (!width || !height) {
return;
}
placeholderLayout.value = { h: height, w: width };
setClosingPortalLayout(portalHostName, registrationId, {
...absolute,
h: height,
w: width,
});
});
});
useEffect(() => {
if (hasActiveId) {
syncPortalLayout();
}
}, [insets.bottom, hasActiveId, syncPortalLayout]);
return useMemo(
() => ({ syncPortalLayout, containerRef, placeholderLayout }),
[placeholderLayout, syncPortalLayout],
);
};