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.
Files changed (65) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/module/components/button/Button.js +21 -13
  3. package/lib/module/components/button/Button.js.map +1 -1
  4. package/lib/module/components/button/index.js.map +1 -1
  5. package/lib/module/components/divider/Divider.js +127 -0
  6. package/lib/module/components/divider/Divider.js.map +1 -0
  7. package/lib/module/components/divider/index.js +4 -0
  8. package/lib/module/components/divider/index.js.map +1 -0
  9. package/lib/module/components/index.js +2 -0
  10. package/lib/module/components/index.js.map +1 -1
  11. package/lib/module/components/pressable/index.js +4 -0
  12. package/lib/module/components/pressable/index.js.map +1 -0
  13. package/lib/module/components/pressable/variants/PressableScale/index.js +110 -0
  14. package/lib/module/components/pressable/variants/PressableScale/index.js.map +1 -0
  15. package/lib/module/components/toast/ToastProvider.js +9 -236
  16. package/lib/module/components/toast/ToastProvider.js.map +1 -1
  17. package/lib/module/components/toast/hooks/use-toast-manager.js +96 -0
  18. package/lib/module/components/toast/hooks/use-toast-manager.js.map +1 -0
  19. package/lib/module/components/toast/parts/ToastAnimatedItem.js +34 -50
  20. package/lib/module/components/toast/parts/ToastAnimatedItem.js.map +1 -1
  21. package/lib/module/components/toast/parts/ToastOverlay.ios.js +36 -0
  22. package/lib/module/components/toast/parts/ToastOverlay.ios.js.map +1 -0
  23. package/lib/module/components/toast/parts/ToastOverlay.js +34 -0
  24. package/lib/module/components/toast/parts/ToastOverlay.js.map +1 -0
  25. package/lib/module/components/toast/parts/ToastViewport.js +91 -0
  26. package/lib/module/components/toast/parts/ToastViewport.js.map +1 -0
  27. package/lib/typescript/src/components/button/Button.d.ts +7 -0
  28. package/lib/typescript/src/components/button/Button.d.ts.map +1 -1
  29. package/lib/typescript/src/components/button/index.d.ts +1 -1
  30. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  31. package/lib/typescript/src/components/divider/Divider.d.ts +49 -0
  32. package/lib/typescript/src/components/divider/Divider.d.ts.map +1 -0
  33. package/lib/typescript/src/components/divider/index.d.ts +3 -0
  34. package/lib/typescript/src/components/divider/index.d.ts.map +1 -0
  35. package/lib/typescript/src/components/index.d.ts +2 -0
  36. package/lib/typescript/src/components/index.d.ts.map +1 -1
  37. package/lib/typescript/src/components/pressable/index.d.ts +3 -0
  38. package/lib/typescript/src/components/pressable/index.d.ts.map +1 -0
  39. package/lib/typescript/src/components/pressable/variants/PressableScale/index.d.ts +47 -0
  40. package/lib/typescript/src/components/pressable/variants/PressableScale/index.d.ts.map +1 -0
  41. package/lib/typescript/src/components/toast/ToastProvider.d.ts.map +1 -1
  42. package/lib/typescript/src/components/toast/hooks/use-toast-manager.d.ts +6 -0
  43. package/lib/typescript/src/components/toast/hooks/use-toast-manager.d.ts.map +1 -0
  44. package/lib/typescript/src/components/toast/parts/ToastAnimatedItem.d.ts +3 -2
  45. package/lib/typescript/src/components/toast/parts/ToastAnimatedItem.d.ts.map +1 -1
  46. package/lib/typescript/src/components/toast/parts/ToastOverlay.d.ts +28 -0
  47. package/lib/typescript/src/components/toast/parts/ToastOverlay.d.ts.map +1 -0
  48. package/lib/typescript/src/components/toast/parts/ToastOverlay.ios.d.ts +28 -0
  49. package/lib/typescript/src/components/toast/parts/ToastOverlay.ios.d.ts.map +1 -0
  50. package/lib/typescript/src/components/toast/parts/ToastViewport.d.ts +11 -0
  51. package/lib/typescript/src/components/toast/parts/ToastViewport.d.ts.map +1 -0
  52. package/package.json +1 -1
  53. package/src/components/button/Button.tsx +32 -13
  54. package/src/components/button/index.ts +1 -0
  55. package/src/components/divider/Divider.tsx +164 -0
  56. package/src/components/divider/index.ts +7 -0
  57. package/src/components/index.ts +2 -0
  58. package/src/components/pressable/index.ts +5 -0
  59. package/src/components/pressable/variants/PressableScale/index.tsx +166 -0
  60. package/src/components/toast/ToastProvider.tsx +7 -288
  61. package/src/components/toast/hooks/use-toast-manager.tsx +114 -0
  62. package/src/components/toast/parts/ToastAnimatedItem.tsx +32 -50
  63. package/src/components/toast/parts/ToastOverlay.ios.tsx +30 -0
  64. package/src/components/toast/parts/ToastOverlay.tsx +28 -0
  65. 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 { StyleSheet, View } from "react-native";
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: SharedValue<number>;
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 opacityValue = [1, 0] as [number, number];
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 heightEntries = Object.entries(heights.value);
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
- pointerEvents: opacity === 0 ? "none" : "auto",
121
- opacity: withTiming(opacity, opacityTimingConfig),
101
+ opacity: withTiming(isVisible ? 1 : 0, timingConfig),
122
102
  transform: [
123
- { translateY: withTiming(translateY, translateYTimingConfig) },
124
- { scale: withTiming(scale, scaleTimingConfig) },
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 style={containerStyle}>{children}</Animated.View>
138
- <View
139
- pointerEvents="none"
140
- aria-hidden
141
- importantForAccessibility="no-hide-descendants"
142
- accessibilityElementsHidden
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
- {process.env.NODE_ENV === "test" ? null : children}
153
- </View>
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
- measurement: {
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
+ }