react-native-terra-ui 0.10.0 → 0.10.2
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 +13 -0
- package/lib/module/components/button/Button.js +21 -13
- package/lib/module/components/button/Button.js.map +1 -1
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/divider/Divider.js +127 -0
- package/lib/module/components/divider/Divider.js.map +1 -0
- package/lib/module/components/divider/index.js +4 -0
- package/lib/module/components/divider/index.js.map +1 -0
- package/lib/module/components/index.js +2 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/components/pressable/index.js +4 -0
- package/lib/module/components/pressable/index.js.map +1 -0
- package/lib/module/components/pressable/variants/PressableScale/index.js +110 -0
- package/lib/module/components/pressable/variants/PressableScale/index.js.map +1 -0
- package/lib/module/components/toast/ToastProvider.js +9 -236
- package/lib/module/components/toast/ToastProvider.js.map +1 -1
- package/lib/module/components/toast/hooks/use-toast-manager.js +96 -0
- package/lib/module/components/toast/hooks/use-toast-manager.js.map +1 -0
- package/lib/module/components/toast/parts/ToastAnimatedItem.js +34 -50
- package/lib/module/components/toast/parts/ToastAnimatedItem.js.map +1 -1
- package/lib/module/components/toast/parts/ToastOverlay.ios.js +36 -0
- package/lib/module/components/toast/parts/ToastOverlay.ios.js.map +1 -0
- package/lib/module/components/toast/parts/ToastOverlay.js +34 -0
- package/lib/module/components/toast/parts/ToastOverlay.js.map +1 -0
- package/lib/module/components/toast/parts/ToastViewport.js +91 -0
- package/lib/module/components/toast/parts/ToastViewport.js.map +1 -0
- package/lib/typescript/src/components/button/Button.d.ts +7 -0
- package/lib/typescript/src/components/button/Button.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +1 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/divider/Divider.d.ts +49 -0
- package/lib/typescript/src/components/divider/Divider.d.ts.map +1 -0
- package/lib/typescript/src/components/divider/index.d.ts +3 -0
- package/lib/typescript/src/components/divider/index.d.ts.map +1 -0
- package/lib/typescript/src/components/index.d.ts +2 -0
- package/lib/typescript/src/components/index.d.ts.map +1 -1
- package/lib/typescript/src/components/pressable/index.d.ts +3 -0
- package/lib/typescript/src/components/pressable/index.d.ts.map +1 -0
- package/lib/typescript/src/components/pressable/variants/PressableScale/index.d.ts +47 -0
- package/lib/typescript/src/components/pressable/variants/PressableScale/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toast/ToastProvider.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/hooks/use-toast-manager.d.ts +6 -0
- package/lib/typescript/src/components/toast/hooks/use-toast-manager.d.ts.map +1 -0
- package/lib/typescript/src/components/toast/parts/ToastAnimatedItem.d.ts +3 -2
- package/lib/typescript/src/components/toast/parts/ToastAnimatedItem.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/parts/ToastOverlay.d.ts +28 -0
- package/lib/typescript/src/components/toast/parts/ToastOverlay.d.ts.map +1 -0
- package/lib/typescript/src/components/toast/parts/ToastOverlay.ios.d.ts +28 -0
- package/lib/typescript/src/components/toast/parts/ToastOverlay.ios.d.ts.map +1 -0
- package/lib/typescript/src/components/toast/parts/ToastViewport.d.ts +11 -0
- package/lib/typescript/src/components/toast/parts/ToastViewport.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/button/Button.tsx +32 -13
- package/src/components/button/index.ts +1 -0
- package/src/components/divider/Divider.tsx +164 -0
- package/src/components/divider/index.ts +7 -0
- package/src/components/index.ts +2 -0
- package/src/components/pressable/index.ts +5 -0
- package/src/components/pressable/variants/PressableScale/index.tsx +166 -0
- package/src/components/toast/ToastProvider.tsx +7 -288
- package/src/components/toast/hooks/use-toast-manager.tsx +114 -0
- package/src/components/toast/parts/ToastAnimatedItem.tsx +32 -50
- package/src/components/toast/parts/ToastOverlay.ios.tsx +30 -0
- package/src/components/toast/parts/ToastOverlay.tsx +28 -0
- package/src/components/toast/parts/ToastViewport.tsx +104 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { DefaultToast } from "../Toast";
|
|
4
|
+
import type {
|
|
5
|
+
ToastComponentProps,
|
|
6
|
+
ToastId,
|
|
7
|
+
ToastItem,
|
|
8
|
+
ToastManager,
|
|
9
|
+
ToastPlacement,
|
|
10
|
+
ToastShowConfig,
|
|
11
|
+
} from "../types";
|
|
12
|
+
import { setToastManager } from "../utils/controller";
|
|
13
|
+
|
|
14
|
+
function createDefaultToastComponent(config: ToastShowConfig) {
|
|
15
|
+
return ({ id, hide, show }: ToastComponentProps) => (
|
|
16
|
+
<DefaultToast
|
|
17
|
+
id={id}
|
|
18
|
+
hide={(toastId) => hide(String(toastId))}
|
|
19
|
+
variant={config.variant}
|
|
20
|
+
label={config.label}
|
|
21
|
+
description={config.description}
|
|
22
|
+
icon={config.icon}
|
|
23
|
+
actionLabel={config.actionLabel}
|
|
24
|
+
showCloseButton={config.showCloseButton}
|
|
25
|
+
onActionPress={() => config.onActionPress?.({ show, hide })}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useToastManager(defaultPlacement: ToastPlacement, defaultDuration: number) {
|
|
31
|
+
const [toasts, setToasts] = useState<ToastItem[]>([]);
|
|
32
|
+
// Synchronous state lets batched calls and lifecycle callbacks see the latest stack.
|
|
33
|
+
const toastsRef = useRef<ToastItem[]>([]);
|
|
34
|
+
const counterRef = useRef(0);
|
|
35
|
+
const timeoutsRef = useRef(new Map<ToastId, ReturnType<typeof setTimeout>>());
|
|
36
|
+
|
|
37
|
+
const commit = useCallback((next: ToastItem[]) => {
|
|
38
|
+
toastsRef.current = next;
|
|
39
|
+
setToasts(next);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const clearToastTimeout = useCallback((id: ToastId) => {
|
|
43
|
+
const timeout = timeoutsRef.current.get(id);
|
|
44
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
45
|
+
timeoutsRef.current.delete(id);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const hide = useCallback<ToastManager["hide"]>(
|
|
49
|
+
(id) => {
|
|
50
|
+
const current = toastsRef.current;
|
|
51
|
+
const ids = new Set(
|
|
52
|
+
id === undefined ? current.slice(-1).map((toast) => toast.id) : Array.isArray(id) ? id : [id]
|
|
53
|
+
);
|
|
54
|
+
const removed = id === "all" ? current : current.filter((toast) => ids.has(toast.id));
|
|
55
|
+
if (removed.length === 0) return;
|
|
56
|
+
|
|
57
|
+
for (const toast of removed) clearToastTimeout(toast.id);
|
|
58
|
+
commit(id === "all" ? [] : current.filter((toast) => !ids.has(toast.id)));
|
|
59
|
+
for (const toast of removed) toast.onHide?.();
|
|
60
|
+
},
|
|
61
|
+
[clearToastTimeout, commit]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const show = useCallback<ToastManager["show"]>(
|
|
65
|
+
(options) => {
|
|
66
|
+
const config = typeof options === "string" ? { label: options } : options;
|
|
67
|
+
const item: ToastItem = {
|
|
68
|
+
id: config.id ?? `toast-${Date.now()}-${counterRef.current++}`,
|
|
69
|
+
component: "component" in config ? config.component : createDefaultToastComponent(config),
|
|
70
|
+
duration: config.duration ?? defaultDuration,
|
|
71
|
+
placement: config.placement ?? defaultPlacement,
|
|
72
|
+
onHide: config.onHide,
|
|
73
|
+
};
|
|
74
|
+
const current = toastsRef.current;
|
|
75
|
+
const removed = current.filter((toast) => toast.placement !== item.placement);
|
|
76
|
+
for (const toast of removed) clearToastTimeout(toast.id);
|
|
77
|
+
clearToastTimeout(item.id);
|
|
78
|
+
commit([
|
|
79
|
+
...current.filter((toast) => toast.placement === item.placement && toast.id !== item.id),
|
|
80
|
+
item,
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
if (item.duration !== "persistent" && Number.isFinite(item.duration) && item.duration > 0) {
|
|
84
|
+
timeoutsRef.current.set(
|
|
85
|
+
item.id,
|
|
86
|
+
setTimeout(() => hide(item.id), item.duration)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Finish the transaction before callbacks can replace or dismiss this item.
|
|
91
|
+
for (const toast of removed) toast.onHide?.();
|
|
92
|
+
if (toastsRef.current.includes(item)) config.onShow?.();
|
|
93
|
+
return item.id;
|
|
94
|
+
},
|
|
95
|
+
[clearToastTimeout, commit, defaultDuration, defaultPlacement, hide]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const manager = useMemo<ToastManager>(() => ({ show, hide }), [show, hide]);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
setToastManager(manager);
|
|
102
|
+
return () => setToastManager(null);
|
|
103
|
+
}, [manager]);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const timeouts = timeoutsRef.current;
|
|
107
|
+
return () => {
|
|
108
|
+
for (const timeout of timeouts.values()) clearTimeout(timeout);
|
|
109
|
+
timeouts.clear();
|
|
110
|
+
};
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
return { toasts, manager };
|
|
114
|
+
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { View } from "react-native";
|
|
3
3
|
|
|
4
4
|
import Animated, {
|
|
5
5
|
Easing,
|
|
6
|
-
Extrapolation,
|
|
7
|
-
interpolate,
|
|
8
6
|
Keyframe,
|
|
9
7
|
type SharedValue,
|
|
10
8
|
useAnimatedStyle,
|
|
11
9
|
withSpring,
|
|
12
10
|
withTiming,
|
|
13
11
|
} from "react-native-reanimated";
|
|
12
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
14
13
|
|
|
15
14
|
import type { ToastId, ToastPlacement } from "../types";
|
|
16
15
|
|
|
@@ -21,7 +20,8 @@ export interface ToastAnimatedItemProps {
|
|
|
21
20
|
index: number;
|
|
22
21
|
maxVisibleToasts: number;
|
|
23
22
|
placement: ToastPlacement;
|
|
24
|
-
total:
|
|
23
|
+
total: number;
|
|
24
|
+
frontId: ToastId;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export const enteringTop = new Keyframe({
|
|
@@ -72,12 +72,7 @@ export const exitingBottom = new Keyframe({
|
|
|
72
72
|
},
|
|
73
73
|
}).duration(150);
|
|
74
74
|
|
|
75
|
-
const
|
|
76
|
-
const opacityTimingConfig = { duration: 300 };
|
|
77
|
-
const translateYValue = [0, 10] as [number, number];
|
|
78
|
-
const translateYTimingConfig = { duration: 300 };
|
|
79
|
-
const scaleValue = [1, 0.97] as [number, number];
|
|
80
|
-
const scaleTimingConfig = { duration: 300 };
|
|
75
|
+
const timingConfig = { duration: 300 };
|
|
81
76
|
|
|
82
77
|
export function ToastAnimatedItem({
|
|
83
78
|
children,
|
|
@@ -87,28 +82,14 @@ export function ToastAnimatedItem({
|
|
|
87
82
|
maxVisibleToasts,
|
|
88
83
|
placement,
|
|
89
84
|
total,
|
|
85
|
+
frontId,
|
|
90
86
|
}: ToastAnimatedItemProps) {
|
|
87
|
+
const depth = total - index - 1;
|
|
88
|
+
const isVisible = depth < maxVisibleToasts;
|
|
91
89
|
const containerStyle = useAnimatedStyle(() => {
|
|
92
|
-
const
|
|
93
|
-
const lastToastId = heightEntries[heightEntries.length - 1]?.[0];
|
|
94
|
-
const lastToastHeight = lastToastId ? heights.value[lastToastId] : undefined;
|
|
95
|
-
const totalValue = total.value;
|
|
90
|
+
const lastToastHeight = heights.value[frontId];
|
|
96
91
|
const sign = placement === "top" ? 1 : -1;
|
|
97
92
|
|
|
98
|
-
const inputRange = [totalValue - 1, totalValue - 2];
|
|
99
|
-
const opacityInputRange = [totalValue - maxVisibleToasts, totalValue - maxVisibleToasts - 1];
|
|
100
|
-
|
|
101
|
-
const opacity = interpolate(index, opacityInputRange, opacityValue);
|
|
102
|
-
const translateY = interpolate(
|
|
103
|
-
index,
|
|
104
|
-
inputRange,
|
|
105
|
-
[translateYValue[0], translateYValue[1] * sign],
|
|
106
|
-
Extrapolation.CLAMP
|
|
107
|
-
);
|
|
108
|
-
const scale = interpolate(index, inputRange, scaleValue, {
|
|
109
|
-
extrapolateLeft: Extrapolation.CLAMP,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
93
|
return {
|
|
113
94
|
height: lastToastHeight
|
|
114
95
|
? withSpring(lastToastHeight, {
|
|
@@ -117,11 +98,10 @@ export function ToastAnimatedItem({
|
|
|
117
98
|
mass: 3,
|
|
118
99
|
})
|
|
119
100
|
: undefined,
|
|
120
|
-
|
|
121
|
-
opacity: withTiming(opacity, opacityTimingConfig),
|
|
101
|
+
opacity: withTiming(isVisible ? 1 : 0, timingConfig),
|
|
122
102
|
transform: [
|
|
123
|
-
{ translateY: withTiming(
|
|
124
|
-
{ scale: withTiming(
|
|
103
|
+
{ translateY: withTiming(Math.min(depth, 1) * 10 * sign, timingConfig) },
|
|
104
|
+
{ scale: withTiming(Math.max(0, 1 - depth * 0.03), timingConfig) },
|
|
125
105
|
],
|
|
126
106
|
zIndex: 1000 + index,
|
|
127
107
|
};
|
|
@@ -134,23 +114,26 @@ export function ToastAnimatedItem({
|
|
|
134
114
|
pointerEvents="box-none"
|
|
135
115
|
style={[styles.position, placement === "top" ? styles.top : styles.bottom]}
|
|
136
116
|
>
|
|
137
|
-
<Animated.View
|
|
138
|
-
|
|
139
|
-
pointerEvents="none"
|
|
140
|
-
|
|
141
|
-
importantForAccessibility="no-hide-descendants"
|
|
142
|
-
|
|
143
|
-
style={styles.measurement}
|
|
144
|
-
onLayout={(event) => {
|
|
145
|
-
const measuredHeight = event.nativeEvent.layout.height;
|
|
146
|
-
heights.value = {
|
|
147
|
-
...heights.value,
|
|
148
|
-
[id]: measuredHeight,
|
|
149
|
-
};
|
|
150
|
-
}}
|
|
117
|
+
<Animated.View
|
|
118
|
+
style={containerStyle}
|
|
119
|
+
pointerEvents={isVisible ? "box-none" : "none"}
|
|
120
|
+
accessibilityElementsHidden={!isVisible}
|
|
121
|
+
importantForAccessibility={isVisible ? "auto" : "no-hide-descendants"}
|
|
122
|
+
aria-hidden={!isVisible}
|
|
151
123
|
>
|
|
152
|
-
{
|
|
153
|
-
|
|
124
|
+
{/* An unconstrained child measures natural height without mounting a second copy. */}
|
|
125
|
+
<View
|
|
126
|
+
style={[styles.content, placement === "top" ? styles.top : styles.bottom]}
|
|
127
|
+
onLayout={(event) => {
|
|
128
|
+
const height = event.nativeEvent.layout.height;
|
|
129
|
+
if (heights.value[id] !== height) {
|
|
130
|
+
heights.value = { ...heights.value, [id]: height };
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</View>
|
|
136
|
+
</Animated.View>
|
|
154
137
|
</Animated.View>
|
|
155
138
|
);
|
|
156
139
|
}
|
|
@@ -167,10 +150,9 @@ const styles = StyleSheet.create({
|
|
|
167
150
|
bottom: {
|
|
168
151
|
bottom: 0,
|
|
169
152
|
},
|
|
170
|
-
|
|
153
|
+
content: {
|
|
171
154
|
position: "absolute",
|
|
172
155
|
left: 0,
|
|
173
156
|
right: 0,
|
|
174
|
-
opacity: 0,
|
|
175
157
|
},
|
|
176
158
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { FullWindowOverlay } from "react-native-screens";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lifts the toast viewport into a window of its own, above every presented
|
|
7
|
+
* view controller.
|
|
8
|
+
*
|
|
9
|
+
* Without this, a toast raised from a screen presented as `modal` or
|
|
10
|
+
* `formSheet` is invisible. `ToastProvider` sits above the app's navigator, so
|
|
11
|
+
* its viewport belongs to the root view controller's view; UIKit presents a
|
|
12
|
+
* sheet as a *separate* view controller on top of that one. They are different
|
|
13
|
+
* presentation layers, so no `zIndex` or `elevation` can lift the viewport over
|
|
14
|
+
* the sheet — the toast is mounted and animating, just underneath. The reader
|
|
15
|
+
* presses something, the app reports the result, and nothing appears.
|
|
16
|
+
*
|
|
17
|
+
* `FullWindowOverlay` hosts its children in its own `UIWindow`, which sits
|
|
18
|
+
* above presented controllers, so the viewport paints over a sheet the same way
|
|
19
|
+
* it paints over an ordinary screen.
|
|
20
|
+
*
|
|
21
|
+
* It covers the whole window, but it does not swallow the app's touches:
|
|
22
|
+
* `RNSFullWindowOverlay`'s `pointInside:` walks its subviews and answers NO
|
|
23
|
+
* anywhere they are not, so presses fall through to whatever is underneath.
|
|
24
|
+
*
|
|
25
|
+
* Mounted only alongside a non-empty viewport — `ToastViewport` returns `null`
|
|
26
|
+
* before reaching this — so an idle app carries no extra window.
|
|
27
|
+
*/
|
|
28
|
+
export function ToastOverlay({ children }: { children: ReactNode }) {
|
|
29
|
+
return <FullWindowOverlay>{children}</FullWindowOverlay>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hosts the toast viewport where it already is, because on Android it is
|
|
5
|
+
* already in the right place.
|
|
6
|
+
*
|
|
7
|
+
* The iOS variant explains the problem: a sheet there is a separate view
|
|
8
|
+
* controller, so a toast in the root tree paints underneath it. Android does
|
|
9
|
+
* not have that problem for these screens. `react-native-screens` handles
|
|
10
|
+
* `formSheet` as a `StackPresentation` inside the screen stack (see
|
|
11
|
+
* `Screen.kt`), which lives in the Activity's own window — the same window as
|
|
12
|
+
* this provider — so the viewport's `elevation: 9999` puts it on top the
|
|
13
|
+
* ordinary way. Verified on an API 36 emulator: a toast raised from a
|
|
14
|
+
* `formSheet` route renders over that sheet.
|
|
15
|
+
*
|
|
16
|
+
* Not to be confused with `ScreenModalFragment`, which *is* a
|
|
17
|
+
* `BottomSheetDialogFragment` and so does carry its own `Window`. It backs a
|
|
18
|
+
* different presentation, not the `formSheet` route above. A toast raised from
|
|
19
|
+
* something hosted in a real `Dialog` — that fragment, or React Native's own
|
|
20
|
+
* `Modal` — would still be behind it, and `elevation` cannot cross windows.
|
|
21
|
+
* Untested, and left alone: `FullWindowOverlay` is iOS-only (elsewhere it warns
|
|
22
|
+
* and renders a plain `View`), so the remedy there would be to own a window
|
|
23
|
+
* too, via a React Native `Modal`, which brings its own back-button and
|
|
24
|
+
* touch-capture behaviour.
|
|
25
|
+
*/
|
|
26
|
+
export function ToastOverlay({ children }: { children: ReactNode }) {
|
|
27
|
+
return <>{children}</>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { Keyboard, Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
|
|
5
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
6
|
+
|
|
7
|
+
import type { ToastId, ToastItem, ToastManager, ToastPlacement, ToastProviderProps } from "../types";
|
|
8
|
+
import { ToastAnimatedItem } from "./ToastAnimatedItem";
|
|
9
|
+
import { ToastOverlay } from "./ToastOverlay";
|
|
10
|
+
|
|
11
|
+
interface ToastViewportProps {
|
|
12
|
+
placement: ToastPlacement;
|
|
13
|
+
toasts: ToastItem[];
|
|
14
|
+
manager: ToastManager;
|
|
15
|
+
maxVisibleToasts: number;
|
|
16
|
+
offset?: ToastProviderProps["offset"];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ToastViewport({ placement, toasts, manager, maxVisibleToasts, offset }: ToastViewportProps) {
|
|
20
|
+
const safeAreaInsets = useSafeAreaInsets();
|
|
21
|
+
const heights = useSharedValue<Record<ToastId, number>>({});
|
|
22
|
+
const keyboardHeight = useSharedValue(0);
|
|
23
|
+
|
|
24
|
+
const baseBottom = offset?.bottom ?? safeAreaInsets.bottom + (Platform.OS === "ios" ? 6 : 12);
|
|
25
|
+
const baseTop = offset?.top ?? safeAreaInsets.top + (Platform.OS === "ios" ? 0 : 12);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (placement !== "bottom") return;
|
|
29
|
+
const showEvent = Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
30
|
+
const hideEvent = Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
|
|
31
|
+
const showSub = Keyboard.addListener(showEvent, (e) => {
|
|
32
|
+
keyboardHeight.value = withTiming(e.endCoordinates.height);
|
|
33
|
+
});
|
|
34
|
+
const hideSub = Keyboard.addListener(hideEvent, () => {
|
|
35
|
+
keyboardHeight.value = withTiming(0);
|
|
36
|
+
});
|
|
37
|
+
return () => {
|
|
38
|
+
showSub.remove();
|
|
39
|
+
hideSub.remove();
|
|
40
|
+
};
|
|
41
|
+
}, [keyboardHeight, placement]);
|
|
42
|
+
|
|
43
|
+
const gap = Platform.OS === "ios" ? 6 : 12;
|
|
44
|
+
|
|
45
|
+
const keyboardStyle = useAnimatedStyle(() => {
|
|
46
|
+
if (placement === "top") return { top: baseTop };
|
|
47
|
+
if (keyboardHeight.value > 0) {
|
|
48
|
+
return { bottom: keyboardHeight.value + gap };
|
|
49
|
+
}
|
|
50
|
+
return { bottom: baseBottom };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const activeIds = new Set(toasts.map((toast) => toast.id));
|
|
55
|
+
const nextHeights: Record<ToastId, number> = {};
|
|
56
|
+
for (const [id, height] of Object.entries(heights.value)) {
|
|
57
|
+
if (activeIds.has(id)) {
|
|
58
|
+
nextHeights[id] = height;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
heights.value = nextHeights;
|
|
62
|
+
}, [heights, toasts]);
|
|
63
|
+
|
|
64
|
+
const frontToast = toasts[toasts.length - 1];
|
|
65
|
+
if (!frontToast) return null;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<ToastOverlay>
|
|
69
|
+
<Animated.View
|
|
70
|
+
pointerEvents="box-none"
|
|
71
|
+
style={[
|
|
72
|
+
{
|
|
73
|
+
position: "absolute",
|
|
74
|
+
zIndex: 9999,
|
|
75
|
+
elevation: 9999,
|
|
76
|
+
left: offset?.left ?? safeAreaInsets.left + 12,
|
|
77
|
+
right: offset?.right ?? safeAreaInsets.right + 12,
|
|
78
|
+
},
|
|
79
|
+
keyboardStyle,
|
|
80
|
+
]}
|
|
81
|
+
>
|
|
82
|
+
{toasts.map((toast, index) => (
|
|
83
|
+
<ToastAnimatedItem
|
|
84
|
+
key={toast.id}
|
|
85
|
+
heights={heights}
|
|
86
|
+
id={toast.id}
|
|
87
|
+
index={index}
|
|
88
|
+
maxVisibleToasts={maxVisibleToasts}
|
|
89
|
+
placement={placement}
|
|
90
|
+
total={toasts.length}
|
|
91
|
+
frontId={frontToast.id}
|
|
92
|
+
>
|
|
93
|
+
{toast.component({
|
|
94
|
+
id: toast.id,
|
|
95
|
+
index,
|
|
96
|
+
hide: manager.hide,
|
|
97
|
+
show: manager.show,
|
|
98
|
+
})}
|
|
99
|
+
</ToastAnimatedItem>
|
|
100
|
+
))}
|
|
101
|
+
</Animated.View>
|
|
102
|
+
</ToastOverlay>
|
|
103
|
+
);
|
|
104
|
+
}
|