@oxyhq/bloom 0.52.0 → 0.52.1
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/lib/commonjs/bottom-sheet/BottomSheetBase.js +7 -4
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/connection-dots/ConnectionDots.web.js.map +1 -1
- package/lib/commonjs/design-tokens/shadows.js +4 -0
- package/lib/commonjs/design-tokens/shadows.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +26 -10
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/index.js +66 -66
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +90 -90
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/loading/Loading.web.js.map +1 -1
- package/lib/commonjs/loading/SpinnerIcon.web.js +10 -6
- package/lib/commonjs/loading/SpinnerIcon.web.js.map +1 -1
- package/lib/commonjs/motion/index.js +1 -1
- package/lib/commonjs/motion/index.web.js +32 -0
- package/lib/commonjs/motion/index.web.js.map +1 -0
- package/lib/commonjs/motion/motion.js +10 -2
- package/lib/commonjs/motion/motion.js.map +1 -1
- package/lib/commonjs/motion/motion.web.js +137 -0
- package/lib/commonjs/motion/motion.web.js.map +1 -0
- package/lib/commonjs/progressive-blur/index.web.js.map +1 -1
- package/lib/commonjs/styles/web-view-style.js +33 -0
- package/lib/commonjs/styles/web-view-style.js.map +1 -1
- package/lib/commonjs/tab-bar/TabBarBase.js +18 -4
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/commonjs/tab-bar/index.js +5 -5
- package/lib/commonjs/tab-bar/index.js.map +1 -1
- package/lib/commonjs/tab-bar/index.web.js +5 -5
- package/lib/commonjs/tab-bar/index.web.js.map +1 -1
- package/lib/commonjs/tab-bar/shared.js +3 -3
- package/lib/commonjs/tab-bar/surface.web.js.map +1 -1
- package/lib/commonjs/tab-bar/use-footprint.js +56 -0
- package/lib/commonjs/tab-bar/use-footprint.js.map +1 -0
- package/lib/commonjs/toast/ToastRow.js +7 -3
- package/lib/commonjs/toast/ToastRow.js.map +1 -1
- package/lib/commonjs/toast/ToastSwipeHandler.js +29 -13
- package/lib/commonjs/toast/ToastSwipeHandler.js.map +1 -1
- package/lib/commonjs/toast/constants.js +29 -1
- package/lib/commonjs/toast/constants.js.map +1 -1
- package/lib/commonjs/toast/position-utils.js +9 -4
- package/lib/commonjs/toast/position-utils.js.map +1 -1
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +12 -9
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +7 -4
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/connection-dots/ConnectionDots.web.js.map +1 -1
- package/lib/module/design-tokens/shadows.js +4 -0
- package/lib/module/design-tokens/shadows.js.map +1 -1
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +26 -10
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/loading/Loading.web.js.map +1 -1
- package/lib/module/loading/SpinnerIcon.web.js +10 -6
- package/lib/module/loading/SpinnerIcon.web.js.map +1 -1
- package/lib/module/motion/index.js +1 -1
- package/lib/module/motion/index.js.map +1 -1
- package/lib/module/motion/index.web.js +10 -0
- package/lib/module/motion/index.web.js.map +1 -0
- package/lib/module/motion/motion.js +10 -2
- package/lib/module/motion/motion.js.map +1 -1
- package/lib/module/motion/motion.web.js +133 -0
- package/lib/module/motion/motion.web.js.map +1 -0
- package/lib/module/progressive-blur/index.web.js.map +1 -1
- package/lib/module/styles/web-view-style.js +33 -0
- package/lib/module/styles/web-view-style.js.map +1 -1
- package/lib/module/tab-bar/TabBarBase.js +18 -4
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/tab-bar/index.js +1 -1
- package/lib/module/tab-bar/index.js.map +1 -1
- package/lib/module/tab-bar/index.web.js +1 -1
- package/lib/module/tab-bar/index.web.js.map +1 -1
- package/lib/module/tab-bar/shared.js +3 -3
- package/lib/module/tab-bar/surface.web.js.map +1 -1
- package/lib/module/tab-bar/use-footprint.js +52 -0
- package/lib/module/tab-bar/use-footprint.js.map +1 -0
- package/lib/module/toast/ToastRow.js +8 -4
- package/lib/module/toast/ToastRow.js.map +1 -1
- package/lib/module/toast/ToastSwipeHandler.js +29 -13
- package/lib/module/toast/ToastSwipeHandler.js.map +1 -1
- package/lib/module/toast/constants.js +28 -1
- package/lib/module/toast/constants.js.map +1 -1
- package/lib/module/toast/position-utils.js +9 -4
- package/lib/module/toast/position-utils.js.map +1 -1
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +12 -9
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/shadows.d.ts +4 -0
- package/lib/typescript/commonjs/design-tokens/shadows.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/Loading.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/motion/index.web.d.ts +9 -0
- package/lib/typescript/commonjs/motion/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/motion.d.ts +14 -6
- package/lib/typescript/commonjs/motion/motion.d.ts.map +1 -1
- package/lib/typescript/commonjs/motion/motion.web.d.ts +79 -0
- package/lib/typescript/commonjs/motion/motion.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/progressive-blur/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/web-view-style.d.ts +49 -0
- package/lib/typescript/commonjs/styles/web-view-style.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/index.d.ts +1 -1
- package/lib/typescript/commonjs/tab-bar/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/index.web.d.ts +1 -1
- package/lib/typescript/commonjs/tab-bar/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/shared.d.ts +3 -3
- package/lib/typescript/commonjs/tab-bar/surface.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts +31 -0
- package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts.map +1 -0
- package/lib/typescript/commonjs/toast/ToastRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/ToastSwipeHandler.d.ts +12 -6
- package/lib/typescript/commonjs/toast/ToastSwipeHandler.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/constants.d.ts +26 -8
- package/lib/typescript/commonjs/toast/constants.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/position-utils.d.ts +8 -3
- package/lib/typescript/commonjs/toast/position-utils.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/shadows.d.ts +4 -0
- package/lib/typescript/module/design-tokens/shadows.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/loading/Loading.web.d.ts.map +1 -1
- package/lib/typescript/module/loading/SpinnerIcon.web.d.ts.map +1 -1
- package/lib/typescript/module/motion/index.web.d.ts +9 -0
- package/lib/typescript/module/motion/index.web.d.ts.map +1 -0
- package/lib/typescript/module/motion/motion.d.ts +14 -6
- package/lib/typescript/module/motion/motion.d.ts.map +1 -1
- package/lib/typescript/module/motion/motion.web.d.ts +79 -0
- package/lib/typescript/module/motion/motion.web.d.ts.map +1 -0
- package/lib/typescript/module/progressive-blur/index.web.d.ts.map +1 -1
- package/lib/typescript/module/styles/web-view-style.d.ts +49 -0
- package/lib/typescript/module/styles/web-view-style.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/index.d.ts +1 -1
- package/lib/typescript/module/tab-bar/index.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/index.web.d.ts +1 -1
- package/lib/typescript/module/tab-bar/index.web.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/shared.d.ts +3 -3
- package/lib/typescript/module/tab-bar/surface.web.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/use-footprint.d.ts +31 -0
- package/lib/typescript/module/tab-bar/use-footprint.d.ts.map +1 -0
- package/lib/typescript/module/toast/ToastRow.d.ts.map +1 -1
- package/lib/typescript/module/toast/ToastSwipeHandler.d.ts +12 -6
- package/lib/typescript/module/toast/ToastSwipeHandler.d.ts.map +1 -1
- package/lib/typescript/module/toast/constants.d.ts +26 -8
- package/lib/typescript/module/toast/constants.d.ts.map +1 -1
- package/lib/typescript/module/toast/position-utils.d.ts +8 -3
- package/lib/typescript/module/toast/position-utils.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__tests__/Motion.test.tsx +31 -3
- package/src/__tests__/MotionWebFork.test.ts +140 -0
- package/src/__tests__/TabBarAnimatedShorthand.test.ts +135 -0
- package/src/__tests__/TabBarFootprint.test.tsx +172 -0
- package/src/__tests__/TabBarWebFork.test.ts +1 -1
- package/src/__tests__/ToastHostWebFork.test.ts +6 -2
- package/src/__tests__/Toaster.test.tsx +62 -1
- package/src/__tests__/toast-position-utils.test.ts +23 -7
- package/src/__tests__/web-css-style.test.ts +139 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +11 -7
- package/src/connection-dots/ConnectionDots.web.tsx +4 -3
- package/src/design-tokens/shadows.ts +5 -1
- package/src/dialog/Dialog.tsx +1 -1
- package/src/dialog/Dialog.web.tsx +41 -26
- package/src/index.web.ts +2 -2
- package/src/loading/Loading.web.tsx +7 -5
- package/src/loading/SpinnerIcon.web.tsx +12 -9
- package/src/motion/index.web.ts +8 -0
- package/src/motion/motion.ts +14 -6
- package/src/motion/motion.web.ts +99 -0
- package/src/progressive-blur/index.web.tsx +4 -3
- package/src/styles/web-view-style.ts +50 -0
- package/src/tab-bar/TabBarBase.tsx +17 -12
- package/src/tab-bar/index.ts +1 -1
- package/src/tab-bar/index.web.tsx +1 -1
- package/src/tab-bar/shared.ts +3 -3
- package/src/tab-bar/surface.web.tsx +4 -3
- package/src/tab-bar/use-footprint.ts +50 -0
- package/src/toast/ToastRow.tsx +8 -3
- package/src/toast/ToastSwipeHandler.tsx +30 -12
- package/src/toast/constants.ts +28 -0
- package/src/toast/position-utils.ts +10 -5
- package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +9 -8
- package/lib/commonjs/tab-bar/use-reserved-space.js +0 -50
- package/lib/commonjs/tab-bar/use-reserved-space.js.map +0 -1
- package/lib/module/tab-bar/use-reserved-space.js +0 -46
- package/lib/module/tab-bar/use-reserved-space.js.map +0 -1
- package/lib/typescript/commonjs/tab-bar/use-reserved-space.d.ts +0 -25
- package/lib/typescript/commonjs/tab-bar/use-reserved-space.d.ts.map +0 -1
- package/lib/typescript/module/tab-bar/use-reserved-space.d.ts +0 -25
- package/lib/typescript/module/tab-bar/use-reserved-space.d.ts.map +0 -1
- package/src/__tests__/TabBarReservedSpace.test.tsx +0 -150
- package/src/tab-bar/use-reserved-space.ts +0 -44
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much room the floating bar physically takes at the bottom of the window.
|
|
3
|
+
*
|
|
4
|
+
* The bar is `position: absolute` over the content, so nothing accounts for it
|
|
5
|
+
* automatically — every screen under a tab layout has to keep its own content
|
|
6
|
+
* out from under the pill. That number is built from the bar's private geometry
|
|
7
|
+
* (its expanded height and the gap it holds off the window's bottom edge),
|
|
8
|
+
* which is exactly why it is exported as a hook: hardcoding it in an app
|
|
9
|
+
* silently drifts the moment the bar changes by a pixel.
|
|
10
|
+
*
|
|
11
|
+
* Platform-neutral, like the rest of the family — `react-native-safe-area-context`
|
|
12
|
+
* is already a required Bloom peer and is what the bar itself measures with.
|
|
13
|
+
*/
|
|
14
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
15
|
+
|
|
16
|
+
import { EXPANDED_HEIGHT, tabBarBottomGap } from './shared';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The vertical space the tab bar OCCUPIES above the bottom of the window, in
|
|
20
|
+
* px: its own height plus its own bottom offset, and nothing else.
|
|
21
|
+
*
|
|
22
|
+
* It is deliberately the raw footprint, with no clearance folded in. Clearance
|
|
23
|
+
* is a consumer decision and it differs per call site — a scroll view wants the
|
|
24
|
+
* footprint plus a breathing gap so the last row clears the pill, while a
|
|
25
|
+
* floating action button anchored by `bottom` already supplies its own gap and
|
|
26
|
+
* wants the footprint bare. Baking a margin in here would place that FAB too
|
|
27
|
+
* high and make one word mean two things inside a single file. Add your own:
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* const footprint = useTabBarFootprint();
|
|
31
|
+
*
|
|
32
|
+
* <Animated.ScrollView contentContainerStyle={{ paddingBottom: footprint + 12 }} />
|
|
33
|
+
* <Fab style={{ position: 'absolute', right: 16, bottom: footprint }} />
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
|
|
37
|
+
* inset into its own gap, so this is measured from the true window edge, not
|
|
38
|
+
* from the safe area. Never write `footprint + insets.bottom`: that counts the
|
|
39
|
+
* home indicator twice and strands a visible band of dead space under every
|
|
40
|
+
* list (114px accounted for a 76px bar on an iOS device or PWA). Whatever you
|
|
41
|
+
* add on top is your own clearance — never the inset a second time.
|
|
42
|
+
*
|
|
43
|
+
* It is the EXPANDED height, deliberately. The bar minimizes while scrolling
|
|
44
|
+
* down and re-expands on the way up, so measuring the minimized height would
|
|
45
|
+
* hide the end of a list the moment the user scrolled back to it.
|
|
46
|
+
*/
|
|
47
|
+
export function useTabBarFootprint(): number {
|
|
48
|
+
const insets = useSafeAreaInsets();
|
|
49
|
+
return tabBarBottomGap(insets.bottom) + EXPANDED_HEIGHT;
|
|
50
|
+
}
|
package/src/toast/ToastRow.tsx
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
CLOSE_BUTTON_HIT_AREA,
|
|
56
56
|
ENTERING_ANIMATION_DURATION,
|
|
57
57
|
STACKING_ANIMATION_DURATION,
|
|
58
|
+
TOAST_MAX_ROW_WIDTH,
|
|
58
59
|
toastDefaults,
|
|
59
60
|
} from './constants';
|
|
60
61
|
import { useDynamicToastContext, useToastContext } from './context';
|
|
@@ -224,6 +225,8 @@ export const ToastRow = React.forwardRef<ToastRef, ToastRowProps>(
|
|
|
224
225
|
});
|
|
225
226
|
|
|
226
227
|
const { width: screenWidth } = useWindowDimensions();
|
|
228
|
+
// The row is `width: '100%'` up to the cap, so this is its real width.
|
|
229
|
+
const rowWidth = Math.min(screenWidth, TOAST_MAX_ROW_WIDTH);
|
|
227
230
|
|
|
228
231
|
const stackScaleX = useAnimatedTarget(
|
|
229
232
|
calculateStackScaleX({
|
|
@@ -233,7 +236,7 @@ export const ToastRow = React.forwardRef<ToastRef, ToastRowProps>(
|
|
|
233
236
|
position,
|
|
234
237
|
isExpanded,
|
|
235
238
|
stackGap,
|
|
236
|
-
|
|
239
|
+
rowWidth,
|
|
237
240
|
}),
|
|
238
241
|
{ duration: STACKING_ANIMATION_DURATION, easing: easeOutQuartFn },
|
|
239
242
|
);
|
|
@@ -356,7 +359,9 @@ export const ToastRow = React.forwardRef<ToastRef, ToastRowProps>(
|
|
|
356
359
|
if (enableStacking && numberOfToasts > 1 && position !== 'center') {
|
|
357
360
|
// On Android the RNGH tap wins the race against the nested Pressable,
|
|
358
361
|
// so a press in the close-button strip has to dismiss explicitly.
|
|
359
|
-
|
|
362
|
+
// `x` is relative to the row, which is capped — measuring the strip
|
|
363
|
+
// from the window edge would put it outside the row on a wide viewport.
|
|
364
|
+
if (x > rowWidth - CLOSE_BUTTON_HIT_AREA) {
|
|
360
365
|
if (isExpanded && closeButton && dismissible) {
|
|
361
366
|
onDismiss(id);
|
|
362
367
|
}
|
|
@@ -370,7 +375,7 @@ export const ToastRow = React.forwardRef<ToastRef, ToastRowProps>(
|
|
|
370
375
|
enableStacking,
|
|
371
376
|
numberOfToasts,
|
|
372
377
|
position,
|
|
373
|
-
|
|
378
|
+
rowWidth,
|
|
374
379
|
isExpanded,
|
|
375
380
|
closeButton,
|
|
376
381
|
dismissible,
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
* Derived from sonner-native v0.26.4 — src/gestures.tsx
|
|
3
3
|
* (MIT © Gunnar Torfi Steinarsson). See the top-level NOTICE.
|
|
4
4
|
*
|
|
5
|
-
* W2 — the animated style reads `translate`, `direction`, `position`,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* W2 — the animated style reads `translate`, `direction`, `position`, `rowWidth`
|
|
6
|
+
* and `isAndroid`; ALL of them are in the dependency array. On web without the
|
|
7
|
+
* worklets babel plugin reanimated re-runs the mapper on its deps rather than on
|
|
8
|
+
* auto-tracked reads, so a missing dep freezes the swipe transform at its first
|
|
9
|
+
* frame (upstream omits `position` and `isAndroid`, so a toast that changes
|
|
10
|
+
* position mid-life swipes the wrong way).
|
|
11
|
+
*
|
|
12
|
+
* W12 — this wrapper is THE row box, so it is where `TOAST_MAX_ROW_WIDTH` caps
|
|
13
|
+
* and centres the toast. Capping here rather than on `ToastContent`'s card
|
|
14
|
+
* covers `toast.custom` JSX and `unstyled` rows too, which never reach that
|
|
15
|
+
* card. Every distance the gesture measures is a fraction of the capped
|
|
16
|
+
* `rowWidth`, not of the window — see the constant.
|
|
11
17
|
*/
|
|
12
18
|
import * as React from 'react';
|
|
13
19
|
import {
|
|
@@ -28,10 +34,11 @@ import Animated, {
|
|
|
28
34
|
} from 'react-native-reanimated';
|
|
29
35
|
|
|
30
36
|
import { easeInOutCircFn } from './animations';
|
|
37
|
+
import { TOAST_MAX_ROW_WIDTH } from './constants';
|
|
31
38
|
import { useToastContext } from './context';
|
|
32
39
|
import type { ToastPosition, ToastProps } from './types';
|
|
33
40
|
|
|
34
|
-
/** Fraction of the
|
|
41
|
+
/** Fraction of the row a horizontal swipe must cross to dismiss. */
|
|
35
42
|
const HORIZONTAL_DISMISS_FRACTION = 0.25;
|
|
36
43
|
/** Below this the gesture reads as a tap, so the row springs back. */
|
|
37
44
|
const TAP_SLOP = 16;
|
|
@@ -66,6 +73,8 @@ export const ToastSwipeHandler: React.FC<
|
|
|
66
73
|
onPress,
|
|
67
74
|
}) => {
|
|
68
75
|
const { width: windowWidth } = useWindowDimensions();
|
|
76
|
+
// The row is `width: '100%'` up to the cap, so this is its real width.
|
|
77
|
+
const rowWidth = Math.min(windowWidth, TOAST_MAX_ROW_WIDTH);
|
|
69
78
|
const translate = useSharedValue(0);
|
|
70
79
|
const { swipeToDismissDirection: direction, position: positionCtx } =
|
|
71
80
|
useToastContext();
|
|
@@ -137,7 +146,7 @@ export const ToastSwipeHandler: React.FC<
|
|
|
137
146
|
if (direction === 'left') {
|
|
138
147
|
if (Math.abs(translate.value) < TAP_SLOP) {
|
|
139
148
|
springBack();
|
|
140
|
-
} else if (translate.value < -
|
|
149
|
+
} else if (translate.value < -rowWidth * HORIZONTAL_DISMISS_FRACTION) {
|
|
141
150
|
dismiss();
|
|
142
151
|
} else {
|
|
143
152
|
springBack();
|
|
@@ -175,11 +184,11 @@ export const ToastSwipeHandler: React.FC<
|
|
|
175
184
|
? 1
|
|
176
185
|
: interpolate(
|
|
177
186
|
translate.value,
|
|
178
|
-
[0, direction === 'left' ? -
|
|
187
|
+
[0, direction === 'left' ? -rowWidth : -VERTICAL_FADE_DISTANCE],
|
|
179
188
|
[1, 0],
|
|
180
189
|
),
|
|
181
190
|
};
|
|
182
|
-
}, [direction, translate,
|
|
191
|
+
}, [direction, translate, rowWidth, position, isAndroid]);
|
|
183
192
|
|
|
184
193
|
return (
|
|
185
194
|
<GestureDetector gesture={Gesture.Race(tap, pan)}>
|
|
@@ -187,7 +196,7 @@ export const ToastSwipeHandler: React.FC<
|
|
|
187
196
|
style={[
|
|
188
197
|
animatedStyle,
|
|
189
198
|
unstyled ? undefined : styles.centered,
|
|
190
|
-
styles.
|
|
199
|
+
styles.rowBox,
|
|
191
200
|
style,
|
|
192
201
|
]}
|
|
193
202
|
// W9 — on web `LinearTransition`'s easing degrades to CSS `ease` because
|
|
@@ -205,7 +214,16 @@ export const ToastSwipeHandler: React.FC<
|
|
|
205
214
|
|
|
206
215
|
const styles = StyleSheet.create({
|
|
207
216
|
centered: { justifyContent: 'center' },
|
|
208
|
-
|
|
217
|
+
/**
|
|
218
|
+
* `alignSelf` is load-bearing: the parent anchor stretches its children, but a
|
|
219
|
+
* child with a definite cross size (`width: '100%'` capped by `maxWidth`) is
|
|
220
|
+
* not stretched — it would sit against the left edge without this.
|
|
221
|
+
*/
|
|
222
|
+
rowBox: {
|
|
223
|
+
width: '100%',
|
|
224
|
+
maxWidth: TOAST_MAX_ROW_WIDTH,
|
|
225
|
+
alignSelf: 'center',
|
|
226
|
+
},
|
|
209
227
|
});
|
|
210
228
|
|
|
211
229
|
/**
|
package/src/toast/constants.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* `toast-store.ts` never has to import a Reanimated-dependent module: the store
|
|
7
7
|
* stays pure JS and its suite needs zero React Native mocks.
|
|
8
8
|
*/
|
|
9
|
+
import { space } from '../styles/tokens';
|
|
9
10
|
import type {
|
|
10
11
|
AutoWiggle,
|
|
11
12
|
ToastPosition,
|
|
@@ -21,6 +22,33 @@ export const OUTSIDE_PRESS_PADDING = 20;
|
|
|
21
22
|
/** However deep a stack gets, a buried row never narrows past this scale. */
|
|
22
23
|
export const MIN_STACK_SCALE_X = 0.8;
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* W12 — HOW WIDE A TOAST ROW IS ALLOWED TO GET.
|
|
27
|
+
*
|
|
28
|
+
* sonner-native has no cap: the row is `width: '100%'` and the card inside it is
|
|
29
|
+
* inset by `marginHorizontal: space.lg`, so the card is always
|
|
30
|
+
* screen-width-minus-32. That is correct for the platform it was written for and
|
|
31
|
+
* wrong everywhere else — on a 1280px desktop viewport it produces a ~1248px
|
|
32
|
+
* card, which reads as a page banner rather than a toast. sonner (web) instead
|
|
33
|
+
* fixes the card at `--width: 356px`.
|
|
34
|
+
*
|
|
35
|
+
* The cap therefore sizes the ROW BOX, which carries the card's two `space.lg`
|
|
36
|
+
* gutters, so the visible card lands on sonner's 356px on any viewport wide
|
|
37
|
+
* enough to reach it. `maxWidth` needs no platform fork because it degrades by
|
|
38
|
+
* itself: below a 388px viewport the row is still screen-width and the card is
|
|
39
|
+
* still screen-width-minus-32, exactly as before, so phone portrait — the case
|
|
40
|
+
* the mobile sizing was right for — is untouched. Above it (desktop, tablet,
|
|
41
|
+
* phone landscape) the row caps and centres.
|
|
42
|
+
*
|
|
43
|
+
* `ToastSwipeHandler`'s dismiss threshold and `calculateStackScaleX`'s depth cue
|
|
44
|
+
* are fractions of this row width, NOT of the window: at 1280px a
|
|
45
|
+
* window-relative threshold would demand a 320px drag from a 388px card, and a
|
|
46
|
+
* window-relative squeeze would shrink a buried row by 4px instead of 16px.
|
|
47
|
+
* A consumer who overrides the cap through `toastOptions.toastContainerStyle`
|
|
48
|
+
* moves the card only; both of those keep measuring against this default.
|
|
49
|
+
*/
|
|
50
|
+
export const TOAST_MAX_ROW_WIDTH = 356 + space.lg * 2;
|
|
51
|
+
|
|
24
52
|
/** Enter animation length; also the auto-close timer's head start and the overlay teardown delay. */
|
|
25
53
|
export const ENTERING_ANIMATION_DURATION = 300;
|
|
26
54
|
export const STACKING_ANIMATION_DURATION = 600;
|
|
@@ -34,7 +34,12 @@ export const getOrderedToastIds = (
|
|
|
34
34
|
*
|
|
35
35
|
* Scale rather than width: changing the real width would rewrap the text. Returns
|
|
36
36
|
* 1 (no squeeze) for a single row, an expanded stack, stacking turned off, or
|
|
37
|
-
* before the
|
|
37
|
+
* before the row width is known.
|
|
38
|
+
*
|
|
39
|
+
* `rowWidth` is the row's OWN width, not the window's. Upstream can use the
|
|
40
|
+
* screen width because its row is always full-bleed; Bloom's is capped at
|
|
41
|
+
* `TOAST_MAX_ROW_WIDTH`, and dividing a fixed `stackGap` inset by a 1280px window
|
|
42
|
+
* would shrink a buried row by 4 visible pixels instead of the intended 16.
|
|
38
43
|
*/
|
|
39
44
|
export const calculateStackScaleX = ({
|
|
40
45
|
index,
|
|
@@ -43,7 +48,7 @@ export const calculateStackScaleX = ({
|
|
|
43
48
|
position,
|
|
44
49
|
isExpanded,
|
|
45
50
|
stackGap,
|
|
46
|
-
|
|
51
|
+
rowWidth,
|
|
47
52
|
}: {
|
|
48
53
|
index: number;
|
|
49
54
|
numberOfToasts: number;
|
|
@@ -51,16 +56,16 @@ export const calculateStackScaleX = ({
|
|
|
51
56
|
position: ToastPosition;
|
|
52
57
|
isExpanded: boolean;
|
|
53
58
|
stackGap: number;
|
|
54
|
-
|
|
59
|
+
rowWidth: number;
|
|
55
60
|
}): number => {
|
|
56
|
-
if (!enableStacking || numberOfToasts <= 1 || isExpanded ||
|
|
61
|
+
if (!enableStacking || numberOfToasts <= 1 || isExpanded || rowWidth <= 0) {
|
|
57
62
|
return 1;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
const distanceFromFront =
|
|
61
66
|
position === 'top-center' ? index : numberOfToasts - index - 1;
|
|
62
67
|
const narrowAmount = stackGap * distanceFromFront * 2;
|
|
63
|
-
return Math.max(MIN_STACK_SCALE_X, 1 - narrowAmount /
|
|
68
|
+
return Math.max(MIN_STACK_SCALE_X, 1 - narrowAmount / rowWidth);
|
|
64
69
|
};
|
|
65
70
|
|
|
66
71
|
const heightOf = (
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
ArrowOutOfBox_Stroke2_Corner0_Rounded,
|
|
37
37
|
} from '../icons';
|
|
38
38
|
import { Z_INDEX } from '../styles/z-index';
|
|
39
|
-
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
39
|
+
import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
|
|
40
40
|
import {
|
|
41
41
|
getAspectRatio,
|
|
42
42
|
fetchAspectRatio,
|
|
@@ -76,14 +76,15 @@ const AnimatedImage = Animated.createAnimatedComponent(Image);
|
|
|
76
76
|
|
|
77
77
|
// Web-only interaction hints (no-ops on native): the zoom surfaces are
|
|
78
78
|
// tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
|
|
79
|
-
// image. `Platform.select` keeps them off native
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
// image. `Platform.select` keeps them off native. `cursor` is a real
|
|
80
|
+
// `ViewStyle` key on RN 0.83; `userSelect` is declared on `TextStyle` only,
|
|
81
|
+
// which is what `WebCssStyle` covers.
|
|
82
|
+
const WEB_POINTER: WebCssStyle = { userSelect: 'none', cursor: 'pointer' };
|
|
83
|
+
const WEB_USER_SELECT_NONE: WebCssStyle = { userSelect: 'none' };
|
|
84
|
+
|
|
85
|
+
const webPointerStyle = Platform.select({ web: WEB_POINTER, default: undefined });
|
|
85
86
|
const webUserSelectNoneStyle = Platform.select({
|
|
86
|
-
web:
|
|
87
|
+
web: WEB_USER_SELECT_NONE,
|
|
87
88
|
default: undefined,
|
|
88
89
|
});
|
|
89
90
|
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.useTabBarReservedSpace = useTabBarReservedSpace;
|
|
7
|
-
var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
|
|
8
|
-
var _shared = require("./shared.js");
|
|
9
|
-
/**
|
|
10
|
-
* How much room a screen must leave at the bottom for the floating bar.
|
|
11
|
-
*
|
|
12
|
-
* The bar is `position: absolute` over the content, so nothing reserves space
|
|
13
|
-
* for it automatically — every scrollable screen under a tab layout has to pad
|
|
14
|
-
* its own content out from under the pill. That number is built from the bar's
|
|
15
|
-
* private geometry (its expanded height and the gap it keeps from the window's
|
|
16
|
-
* bottom edge), which is exactly why it is exported as a hook: hardcoding it in
|
|
17
|
-
* an app silently drifts the moment the bar changes by a pixel.
|
|
18
|
-
*
|
|
19
|
-
* Platform-neutral, like the rest of the family — `react-native-safe-area-context`
|
|
20
|
-
* is already a required Bloom peer and is what the bar itself measures with.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The vertical space the tab bar occupies above the bottom of the window, in
|
|
25
|
-
* px. Use it directly as the bottom padding (or bottom offset) of anything that
|
|
26
|
-
* would otherwise sit under the bar:
|
|
27
|
-
*
|
|
28
|
-
* ```tsx
|
|
29
|
-
* const reserved = useTabBarReservedSpace();
|
|
30
|
-
*
|
|
31
|
-
* <Animated.ScrollView contentContainerStyle={{ paddingBottom: reserved }} />
|
|
32
|
-
* <Fab style={{ position: 'absolute', right: 16, bottom: reserved + 12 }} />
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
|
|
36
|
-
* inset into its own bottom gap, so it is measured from the true window edge,
|
|
37
|
-
* not from the safe area. Never write `reserved + insets.bottom`: that counts
|
|
38
|
-
* the home indicator twice and strands a visible band of dead space under every
|
|
39
|
-
* list (a 76px bar reserving 110px on an iOS device or PWA). It is the whole
|
|
40
|
-
* number; nothing needs adding to it.
|
|
41
|
-
*
|
|
42
|
-
* It is the EXPANDED height, deliberately. The bar minimizes while scrolling
|
|
43
|
-
* down and re-expands on the way up, so reserving the minimized height would
|
|
44
|
-
* hide the end of a list the moment the user scrolled back to it.
|
|
45
|
-
*/
|
|
46
|
-
function useTabBarReservedSpace() {
|
|
47
|
-
const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
|
|
48
|
-
return (0, _shared.tabBarBottomGap)(insets.bottom) + _shared.EXPANDED_HEIGHT;
|
|
49
|
-
}
|
|
50
|
-
//# sourceMappingURL=use-reserved-space.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["_reactNativeSafeAreaContext","require","_shared","useTabBarReservedSpace","insets","useSafeAreaInsets","tabBarBottomGap","bottom","EXPANDED_HEIGHT"],"sourceRoot":"../../../src","sources":["tab-bar/use-reserved-space.ts"],"mappings":";;;;;;AAaA,IAAAA,2BAAA,GAAAC,OAAA;AAEA,IAAAC,OAAA,GAAAD,OAAA;AAfA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASE,sBAAsBA,CAAA,EAAW;EAC/C,MAAMC,MAAM,GAAG,IAAAC,6CAAiB,EAAC,CAAC;EAClC,OAAO,IAAAC,uBAAe,EAACF,MAAM,CAACG,MAAM,CAAC,GAAGC,uBAAe;AACzD","ignoreList":[]}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* How much room a screen must leave at the bottom for the floating bar.
|
|
5
|
-
*
|
|
6
|
-
* The bar is `position: absolute` over the content, so nothing reserves space
|
|
7
|
-
* for it automatically — every scrollable screen under a tab layout has to pad
|
|
8
|
-
* its own content out from under the pill. That number is built from the bar's
|
|
9
|
-
* private geometry (its expanded height and the gap it keeps from the window's
|
|
10
|
-
* bottom edge), which is exactly why it is exported as a hook: hardcoding it in
|
|
11
|
-
* an app silently drifts the moment the bar changes by a pixel.
|
|
12
|
-
*
|
|
13
|
-
* Platform-neutral, like the rest of the family — `react-native-safe-area-context`
|
|
14
|
-
* is already a required Bloom peer and is what the bar itself measures with.
|
|
15
|
-
*/
|
|
16
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
17
|
-
import { EXPANDED_HEIGHT, tabBarBottomGap } from "./shared.js";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The vertical space the tab bar occupies above the bottom of the window, in
|
|
21
|
-
* px. Use it directly as the bottom padding (or bottom offset) of anything that
|
|
22
|
-
* would otherwise sit under the bar:
|
|
23
|
-
*
|
|
24
|
-
* ```tsx
|
|
25
|
-
* const reserved = useTabBarReservedSpace();
|
|
26
|
-
*
|
|
27
|
-
* <Animated.ScrollView contentContainerStyle={{ paddingBottom: reserved }} />
|
|
28
|
-
* <Fab style={{ position: 'absolute', right: 16, bottom: reserved + 12 }} />
|
|
29
|
-
* ```
|
|
30
|
-
*
|
|
31
|
-
* The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
|
|
32
|
-
* inset into its own bottom gap, so it is measured from the true window edge,
|
|
33
|
-
* not from the safe area. Never write `reserved + insets.bottom`: that counts
|
|
34
|
-
* the home indicator twice and strands a visible band of dead space under every
|
|
35
|
-
* list (a 76px bar reserving 110px on an iOS device or PWA). It is the whole
|
|
36
|
-
* number; nothing needs adding to it.
|
|
37
|
-
*
|
|
38
|
-
* It is the EXPANDED height, deliberately. The bar minimizes while scrolling
|
|
39
|
-
* down and re-expands on the way up, so reserving the minimized height would
|
|
40
|
-
* hide the end of a list the moment the user scrolled back to it.
|
|
41
|
-
*/
|
|
42
|
-
export function useTabBarReservedSpace() {
|
|
43
|
-
const insets = useSafeAreaInsets();
|
|
44
|
-
return tabBarBottomGap(insets.bottom) + EXPANDED_HEIGHT;
|
|
45
|
-
}
|
|
46
|
-
//# sourceMappingURL=use-reserved-space.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["useSafeAreaInsets","EXPANDED_HEIGHT","tabBarBottomGap","useTabBarReservedSpace","insets","bottom"],"sourceRoot":"../../../src","sources":["tab-bar/use-reserved-space.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,iBAAiB,QAAQ,gCAAgC;AAElE,SAASC,eAAe,EAAEC,eAAe,QAAQ,aAAU;;AAE3D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,sBAAsBA,CAAA,EAAW;EAC/C,MAAMC,MAAM,GAAGJ,iBAAiB,CAAC,CAAC;EAClC,OAAOE,eAAe,CAACE,MAAM,CAACC,MAAM,CAAC,GAAGJ,eAAe;AACzD","ignoreList":[]}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The vertical space the tab bar occupies above the bottom of the window, in
|
|
3
|
-
* px. Use it directly as the bottom padding (or bottom offset) of anything that
|
|
4
|
-
* would otherwise sit under the bar:
|
|
5
|
-
*
|
|
6
|
-
* ```tsx
|
|
7
|
-
* const reserved = useTabBarReservedSpace();
|
|
8
|
-
*
|
|
9
|
-
* <Animated.ScrollView contentContainerStyle={{ paddingBottom: reserved }} />
|
|
10
|
-
* <Fab style={{ position: 'absolute', right: 16, bottom: reserved + 12 }} />
|
|
11
|
-
* ```
|
|
12
|
-
*
|
|
13
|
-
* The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
|
|
14
|
-
* inset into its own bottom gap, so it is measured from the true window edge,
|
|
15
|
-
* not from the safe area. Never write `reserved + insets.bottom`: that counts
|
|
16
|
-
* the home indicator twice and strands a visible band of dead space under every
|
|
17
|
-
* list (a 76px bar reserving 110px on an iOS device or PWA). It is the whole
|
|
18
|
-
* number; nothing needs adding to it.
|
|
19
|
-
*
|
|
20
|
-
* It is the EXPANDED height, deliberately. The bar minimizes while scrolling
|
|
21
|
-
* down and re-expands on the way up, so reserving the minimized height would
|
|
22
|
-
* hide the end of a list the moment the user scrolled back to it.
|
|
23
|
-
*/
|
|
24
|
-
export declare function useTabBarReservedSpace(): number;
|
|
25
|
-
//# sourceMappingURL=use-reserved-space.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-reserved-space.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/use-reserved-space.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAG/C"}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The vertical space the tab bar occupies above the bottom of the window, in
|
|
3
|
-
* px. Use it directly as the bottom padding (or bottom offset) of anything that
|
|
4
|
-
* would otherwise sit under the bar:
|
|
5
|
-
*
|
|
6
|
-
* ```tsx
|
|
7
|
-
* const reserved = useTabBarReservedSpace();
|
|
8
|
-
*
|
|
9
|
-
* <Animated.ScrollView contentContainerStyle={{ paddingBottom: reserved }} />
|
|
10
|
-
* <Fab style={{ position: 'absolute', right: 16, bottom: reserved + 12 }} />
|
|
11
|
-
* ```
|
|
12
|
-
*
|
|
13
|
-
* The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
|
|
14
|
-
* inset into its own bottom gap, so it is measured from the true window edge,
|
|
15
|
-
* not from the safe area. Never write `reserved + insets.bottom`: that counts
|
|
16
|
-
* the home indicator twice and strands a visible band of dead space under every
|
|
17
|
-
* list (a 76px bar reserving 110px on an iOS device or PWA). It is the whole
|
|
18
|
-
* number; nothing needs adding to it.
|
|
19
|
-
*
|
|
20
|
-
* It is the EXPANDED height, deliberately. The bar minimizes while scrolling
|
|
21
|
-
* down and re-expands on the way up, so reserving the minimized height would
|
|
22
|
-
* hide the end of a list the moment the user scrolled back to it.
|
|
23
|
-
*/
|
|
24
|
-
export declare function useTabBarReservedSpace(): number;
|
|
25
|
-
//# sourceMappingURL=use-reserved-space.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-reserved-space.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/use-reserved-space.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAG/C"}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { ReactTestInstance } from 'react-test-renderer';
|
|
3
|
-
import { render } from '@testing-library/react-native';
|
|
4
|
-
|
|
5
|
-
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
6
|
-
import { TabBar, TabBarButton, useTabBarReservedSpace } from '../tab-bar';
|
|
7
|
-
import type { TabBarItem } from '../tab-bar/types';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* `useTabBarReservedSpace()` — the number a screen pads its content by so a
|
|
11
|
-
* list does not end underneath the floating bar.
|
|
12
|
-
*
|
|
13
|
-
* The bar is absolutely positioned, so nothing reserves that space
|
|
14
|
-
* automatically, and the geometry it is built from (expanded height, bottom
|
|
15
|
-
* gap) is private. Without the hook a consumer hardcodes both — which drifts
|
|
16
|
-
* silently the first time the bar changes by a pixel — and, worse, tends to add
|
|
17
|
-
* `insets.bottom` on top, because that is what reserving space for bottom
|
|
18
|
-
* chrome normally requires. The bar already folds the inset into its own gap,
|
|
19
|
-
* so that double-counts the home indicator.
|
|
20
|
-
*
|
|
21
|
-
* The suite therefore asserts three separate things: the absolute pixel values
|
|
22
|
-
* (so a change to the constants must be deliberate), the inset relationship (so
|
|
23
|
-
* the double-count can never creep back in), and equality with the bar's own
|
|
24
|
-
* rendered geometry (so the hook cannot drift from where the bar actually sits).
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
// Mutated per test; must be `mock`-prefixed for the hoisted factory.
|
|
28
|
-
const mockInsets = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
29
|
-
|
|
30
|
-
jest.mock('react-native-safe-area-context', () => ({
|
|
31
|
-
...jest.requireActual('react-native-safe-area-context'),
|
|
32
|
-
useSafeAreaInsets: () => mockInsets,
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
const ITEMS: TabBarItem[] = [
|
|
36
|
-
{ name: 'home', label: 'Home', icon: null },
|
|
37
|
-
{ name: 'search', label: 'Search', icon: null },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
/** A device with a home indicator (iPhone-class bottom inset). */
|
|
41
|
-
const HOME_INDICATOR_INSET = 34;
|
|
42
|
-
|
|
43
|
-
function ReservedProbe({ onValue }: { onValue: (space: number) => void }) {
|
|
44
|
-
onValue(useTabBarReservedSpace());
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function withTheme(ui: React.ReactElement) {
|
|
49
|
-
return (
|
|
50
|
-
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
51
|
-
{ui}
|
|
52
|
-
</BloomThemeProvider>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** The space the hook reserves on a device whose bottom inset is `inset`. */
|
|
57
|
-
function reservedFor(inset: number): number {
|
|
58
|
-
mockInsets.bottom = inset;
|
|
59
|
-
let space: number | undefined;
|
|
60
|
-
render(withTheme(<ReservedProbe onValue={(value) => (space = value)} />));
|
|
61
|
-
if (space === undefined) throw new Error('ReservedProbe never ran');
|
|
62
|
-
return space;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function flattenStyle(style: unknown): Record<string, unknown> {
|
|
66
|
-
const out: Record<string, unknown> = {};
|
|
67
|
-
const visit = (value: unknown): void => {
|
|
68
|
-
if (Array.isArray(value)) value.forEach(visit);
|
|
69
|
-
else if (value && typeof value === 'object') Object.assign(out, value);
|
|
70
|
-
};
|
|
71
|
-
visit(style);
|
|
72
|
-
return out;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The flattened style of every styled node in the tree.
|
|
77
|
-
*
|
|
78
|
-
* Restricted to HOST nodes: the RN mock renders each component as a composite
|
|
79
|
-
* wrapping a host element of the same name, and both carry the same `style`, so
|
|
80
|
-
* counting all of them would double every match.
|
|
81
|
-
*/
|
|
82
|
-
function styledNodes(root: ReactTestInstance): Record<string, unknown>[] {
|
|
83
|
-
return root
|
|
84
|
-
.findAll((node) => typeof node.type === 'string' && node.props?.style !== undefined)
|
|
85
|
-
.map((node) => flattenStyle(node.props.style));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
describe('useTabBarReservedSpace', () => {
|
|
89
|
-
beforeEach(() => {
|
|
90
|
-
mockInsets.bottom = 0;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('reserves the bar plus its gap where there is no bottom inset', () => {
|
|
94
|
-
// 58pt bar + the 12pt floor the gap never goes below.
|
|
95
|
-
expect(reservedFor(0)).toBe(70);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('reserves more on a device with a home indicator', () => {
|
|
99
|
-
expect(reservedFor(HOME_INDICATOR_INSET)).toBe(76);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('already includes the inset — adding insets.bottom would double-count it', () => {
|
|
103
|
-
// The bar absorbs 16pt of the inset into its own gap, so the space grows by
|
|
104
|
-
// the REMAINDER, never by the whole inset. A consumer writing
|
|
105
|
-
// `reserved + insets.bottom` would reserve 110pt for a 76pt bar.
|
|
106
|
-
const grown = reservedFor(HOME_INDICATOR_INSET) - reservedFor(0);
|
|
107
|
-
expect(grown).toBe(6);
|
|
108
|
-
expect(grown).toBeLessThan(HOME_INDICATOR_INSET);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('does not shrink below the floor for a small inset', () => {
|
|
112
|
-
// An inset under what the gap absorbs must not pull the bar down toward the
|
|
113
|
-
// window edge — the gap is clamped, and the reserved space with it.
|
|
114
|
-
expect(reservedFor(8)).toBe(reservedFor(0));
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('equals the space the rendered bar actually occupies', () => {
|
|
118
|
-
// The invariant that makes the hook worth exporting: its number is the bar's
|
|
119
|
-
// own geometry (the gap it holds off the window edge plus its expanded
|
|
120
|
-
// height), not a second copy that can drift.
|
|
121
|
-
mockInsets.bottom = HOME_INDICATOR_INSET;
|
|
122
|
-
let reserved: number | undefined;
|
|
123
|
-
const { UNSAFE_root } = render(
|
|
124
|
-
withTheme(
|
|
125
|
-
<>
|
|
126
|
-
<ReservedProbe onValue={(value) => (reserved = value)} />
|
|
127
|
-
<TabBar activeIndex={0}>
|
|
128
|
-
{ITEMS.map((item, index) => (
|
|
129
|
-
<TabBarButton key={item.name} item={item} index={index} />
|
|
130
|
-
))}
|
|
131
|
-
</TabBar>
|
|
132
|
-
</>,
|
|
133
|
-
),
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const styles = styledNodes(UNSAFE_root);
|
|
137
|
-
// The wrapper holding the pill off the bottom edge — the only node with a
|
|
138
|
-
// bottom margin.
|
|
139
|
-
const gaps = styles.filter((style) => typeof style.marginBottom === 'number');
|
|
140
|
-
expect(gaps).toHaveLength(1);
|
|
141
|
-
// The pill itself: the animated capsule sizes BOTH its height and its
|
|
142
|
-
// horizontal inset, which nothing else in the bar does.
|
|
143
|
-
const pills = styles.filter(
|
|
144
|
-
(style) => typeof style.height === 'number' && typeof style.marginHorizontal === 'number',
|
|
145
|
-
);
|
|
146
|
-
expect(pills).toHaveLength(1);
|
|
147
|
-
|
|
148
|
-
expect(reserved).toBe(Number(gaps[0]?.marginBottom) + Number(pills[0]?.height));
|
|
149
|
-
});
|
|
150
|
-
});
|