@oxyhq/bloom 0.51.1 → 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/frosted-icon-button/shared.js +10 -0
- package/lib/commonjs/frosted-icon-button/shared.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 +46 -7
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/commonjs/tab-bar/glyph.js +9 -4
- package/lib/commonjs/tab-bar/glyph.js.map +1 -1
- package/lib/commonjs/tab-bar/glyph.native.js +7 -2
- package/lib/commonjs/tab-bar/glyph.native.js.map +1 -1
- package/lib/commonjs/tab-bar/index.js +7 -0
- package/lib/commonjs/tab-bar/index.js.map +1 -1
- package/lib/commonjs/tab-bar/index.web.js +7 -0
- package/lib/commonjs/tab-bar/index.web.js.map +1 -1
- package/lib/commonjs/tab-bar/shared.js +32 -1
- package/lib/commonjs/tab-bar/shared.js.map +1 -1
- 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/frosted-icon-button/shared.js +10 -0
- package/lib/module/frosted-icon-button/shared.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 +47 -8
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/tab-bar/glyph.js +9 -4
- package/lib/module/tab-bar/glyph.js.map +1 -1
- package/lib/module/tab-bar/glyph.native.js +7 -2
- package/lib/module/tab-bar/glyph.native.js.map +1 -1
- package/lib/module/tab-bar/index.js +1 -0
- package/lib/module/tab-bar/index.js.map +1 -1
- package/lib/module/tab-bar/index.web.js +1 -0
- package/lib/module/tab-bar/index.web.js.map +1 -1
- package/lib/module/tab-bar/shared.js +30 -0
- package/lib/module/tab-bar/shared.js.map +1 -1
- 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/frosted-icon-button/shared.d.ts +10 -0
- package/lib/typescript/commonjs/frosted-icon-button/shared.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/glyph.d.ts +1 -1
- package/lib/typescript/commonjs/tab-bar/glyph.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/glyph.native.d.ts +1 -1
- package/lib/typescript/commonjs/tab-bar/glyph.native.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/index.d.ts +1 -0
- package/lib/typescript/commonjs/tab-bar/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/index.web.d.ts +1 -0
- package/lib/typescript/commonjs/tab-bar/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/shared.d.ts +25 -0
- package/lib/typescript/commonjs/tab-bar/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/surface.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/types.d.ts +37 -1
- package/lib/typescript/commonjs/tab-bar/types.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/frosted-icon-button/shared.d.ts +10 -0
- package/lib/typescript/module/frosted-icon-button/shared.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/glyph.d.ts +1 -1
- package/lib/typescript/module/tab-bar/glyph.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/glyph.native.d.ts +1 -1
- package/lib/typescript/module/tab-bar/glyph.native.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/index.d.ts +1 -0
- package/lib/typescript/module/tab-bar/index.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/index.web.d.ts +1 -0
- package/lib/typescript/module/tab-bar/index.web.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/shared.d.ts +25 -0
- package/lib/typescript/module/tab-bar/shared.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/surface.web.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/types.d.ts +37 -1
- package/lib/typescript/module/tab-bar/types.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__/TabBar.test.tsx +82 -2
- package/src/__tests__/TabBarAnimatedShorthand.test.ts +135 -0
- package/src/__tests__/TabBarFootprint.test.tsx +172 -0
- package/src/__tests__/TabBarLongPress.test.tsx +258 -0
- package/src/__tests__/TabBarWebFork.test.ts +1 -0
- 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/frosted-icon-button/shared.ts +10 -0
- 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 +54 -15
- package/src/tab-bar/glyph.native.tsx +5 -2
- package/src/tab-bar/glyph.tsx +8 -4
- package/src/tab-bar/index.ts +1 -0
- package/src/tab-bar/index.web.tsx +1 -0
- package/src/tab-bar/shared.ts +38 -0
- package/src/tab-bar/surface.web.tsx +4 -3
- package/src/tab-bar/types.ts +37 -1
- 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
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { act, render } from '@testing-library/react-native';
|
|
3
|
+
|
|
4
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
5
|
+
import { TabBar, TabBarButton } from '../tab-bar';
|
|
6
|
+
import type { TabBarItem } from '../tab-bar/types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `onIndexLongPress` — the bar's only reachable long press.
|
|
10
|
+
*
|
|
11
|
+
* `TabBarButtonProps extends PressableProps`, so a consumer's `onLongPress`
|
|
12
|
+
* type-checks and silently does nothing: the bar's `GestureDetector` consumes
|
|
13
|
+
* the touches (that is what makes scrubbing possible), so the inner `Pressable`
|
|
14
|
+
* only ever fires for assistive technology and keyboard activation. A real held
|
|
15
|
+
* finger fails the tap gesture at 400ms, never activates the pan, and dies in
|
|
16
|
+
* `onFinalize`'s early return. The gesture below is the only path in.
|
|
17
|
+
*
|
|
18
|
+
* Like `BottomSheetGesture.test.tsx`, this file replaces the shared
|
|
19
|
+
* gesture-handler mock (which discards every registered callback) with a
|
|
20
|
+
* RECORDING one, so the gestures the bar builds can be inspected and their
|
|
21
|
+
* worklets invoked directly. What that CAN prove: which gestures are composed
|
|
22
|
+
* into the race, the duration windows they are configured with, and that the
|
|
23
|
+
* long-press worklet resolves the same index from the same x as the tap
|
|
24
|
+
* worklet. What it cannot prove is gesture-handler's own arbitration — that a
|
|
25
|
+
* held finger really reaches this handler on a device. The 400ms/500ms split
|
|
26
|
+
* asserted below is what makes that arbitration decidable rather than a race,
|
|
27
|
+
* and the behaviour itself is verified on a real build.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
interface MockGestureEvent {
|
|
31
|
+
x: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface MockGesture {
|
|
35
|
+
kind: string;
|
|
36
|
+
/** Values captured from the builder chain. */
|
|
37
|
+
minDurationMs?: number;
|
|
38
|
+
maxDurationMs?: number;
|
|
39
|
+
/** Worklets captured from the builder chain. */
|
|
40
|
+
start?: (event: MockGestureEvent) => void;
|
|
41
|
+
update?: (event: MockGestureEvent) => void;
|
|
42
|
+
end?: (event: MockGestureEvent, success: boolean) => void;
|
|
43
|
+
/** Members of a composed `Gesture.Race(…)`; empty for a leaf gesture. */
|
|
44
|
+
members: MockGesture[];
|
|
45
|
+
/** Builder chain — every method returns the same object. */
|
|
46
|
+
activeOffsetX: (range: number[]) => MockGesture;
|
|
47
|
+
failOffsetY: (range: number[]) => MockGesture;
|
|
48
|
+
maxDistance: (px: number) => MockGesture;
|
|
49
|
+
maxDuration: (ms: number) => MockGesture;
|
|
50
|
+
minDuration: (ms: number) => MockGesture;
|
|
51
|
+
onStart: (worklet: (event: MockGestureEvent) => void) => MockGesture;
|
|
52
|
+
onUpdate: (worklet: (event: MockGestureEvent) => void) => MockGesture;
|
|
53
|
+
onEnd: (worklet: (event: MockGestureEvent, success: boolean) => void) => MockGesture;
|
|
54
|
+
onFinalize: (worklet: () => void) => MockGesture;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Every gesture built since the last reset. Must be `mock`-prefixed so the
|
|
58
|
+
// hoisted `jest.mock` factory may reference it.
|
|
59
|
+
const mockGestures: MockGesture[] = [];
|
|
60
|
+
|
|
61
|
+
jest.mock('react-native-gesture-handler', () => {
|
|
62
|
+
const build = (kind: string): MockGesture => {
|
|
63
|
+
const gesture: MockGesture = {
|
|
64
|
+
kind,
|
|
65
|
+
members: [],
|
|
66
|
+
activeOffsetX: () => gesture,
|
|
67
|
+
failOffsetY: () => gesture,
|
|
68
|
+
maxDistance: () => gesture,
|
|
69
|
+
maxDuration: (ms) => {
|
|
70
|
+
gesture.maxDurationMs = ms;
|
|
71
|
+
return gesture;
|
|
72
|
+
},
|
|
73
|
+
minDuration: (ms) => {
|
|
74
|
+
gesture.minDurationMs = ms;
|
|
75
|
+
return gesture;
|
|
76
|
+
},
|
|
77
|
+
onStart: (worklet) => {
|
|
78
|
+
gesture.start = worklet;
|
|
79
|
+
return gesture;
|
|
80
|
+
},
|
|
81
|
+
onUpdate: (worklet) => {
|
|
82
|
+
gesture.update = worklet;
|
|
83
|
+
return gesture;
|
|
84
|
+
},
|
|
85
|
+
onEnd: (worklet) => {
|
|
86
|
+
gesture.end = worklet;
|
|
87
|
+
return gesture;
|
|
88
|
+
},
|
|
89
|
+
onFinalize: () => gesture,
|
|
90
|
+
};
|
|
91
|
+
mockGestures.push(gesture);
|
|
92
|
+
return gesture;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
Gesture: {
|
|
97
|
+
Pan: () => build('Pan'),
|
|
98
|
+
Tap: () => build('Tap'),
|
|
99
|
+
LongPress: () => build('LongPress'),
|
|
100
|
+
Race: (...members: MockGesture[]) => {
|
|
101
|
+
const race = build('Race');
|
|
102
|
+
race.members.push(...members);
|
|
103
|
+
return race;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
GestureDetector: ({ children }: { children: React.ReactNode }) => children,
|
|
107
|
+
GestureHandlerRootView: ({ children }: { children: React.ReactNode }) => children,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const ITEMS: TabBarItem[] = [
|
|
112
|
+
{ name: 'home', label: 'Home', icon: null },
|
|
113
|
+
{ name: 'search', label: 'Search', icon: null },
|
|
114
|
+
{ name: 'you', label: 'You', icon: null },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Touch x positions inside the bar, in the 375pt window the react-native mock
|
|
119
|
+
* reports. Named by the tab they land on rather than by the geometry that puts
|
|
120
|
+
* them there: an edge touch belongs to the first/last tab and a centered one to
|
|
121
|
+
* the middle tab whatever the exact item width turns out to be.
|
|
122
|
+
*/
|
|
123
|
+
const NEAR_LEFT_EDGE = 8;
|
|
124
|
+
const WINDOW_CENTER = 187;
|
|
125
|
+
const NEAR_RIGHT_EDGE = 340;
|
|
126
|
+
|
|
127
|
+
function renderBar(props: {
|
|
128
|
+
onIndexChange?: (index: number) => void;
|
|
129
|
+
onIndexLongPress?: (index: number) => void;
|
|
130
|
+
}) {
|
|
131
|
+
return render(
|
|
132
|
+
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
133
|
+
<TabBar activeIndex={0} {...props}>
|
|
134
|
+
{ITEMS.map((item, index) => (
|
|
135
|
+
<TabBarButton key={item.name} item={item} index={index} />
|
|
136
|
+
))}
|
|
137
|
+
</TabBar>
|
|
138
|
+
</BloomThemeProvider>,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const gesturesOfKind = (kind: string) => mockGestures.filter((gesture) => gesture.kind === kind);
|
|
143
|
+
|
|
144
|
+
/** The gesture the detector was actually given — the last race constructed. */
|
|
145
|
+
function race(): MockGesture {
|
|
146
|
+
const races = gesturesOfKind('Race');
|
|
147
|
+
const last = races[races.length - 1];
|
|
148
|
+
if (!last) throw new Error('The bar composed no gesture race');
|
|
149
|
+
return last;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function member(kind: string): MockGesture {
|
|
153
|
+
const found = race().members.find((gesture) => gesture.kind === kind);
|
|
154
|
+
if (!found) throw new Error(`No ${kind} gesture in the bar's race`);
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Hold a finger at `x` until the long press activates. */
|
|
159
|
+
function longPressAt(x: number) {
|
|
160
|
+
act(() => {
|
|
161
|
+
member('LongPress').start?.({ x });
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A tap that succeeded at `x`. */
|
|
166
|
+
function tapAt(x: number) {
|
|
167
|
+
act(() => {
|
|
168
|
+
member('Tap').end?.({ x }, true);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
describe('TabBar long press', () => {
|
|
173
|
+
beforeEach(() => {
|
|
174
|
+
mockGestures.length = 0;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('composes no long-press gesture when no handler is given', () => {
|
|
178
|
+
// An always-armed long press would cancel the pan for anyone who rests a
|
|
179
|
+
// finger before scrubbing — a regression for every existing consumer.
|
|
180
|
+
renderBar({});
|
|
181
|
+
expect(gesturesOfKind('LongPress')).toHaveLength(0);
|
|
182
|
+
expect(race().members.map((gesture) => gesture.kind)).toEqual(['Pan', 'Tap']);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('adds one to the race, alongside the pan and the tap, when a handler is given', () => {
|
|
186
|
+
renderBar({ onIndexLongPress: jest.fn() });
|
|
187
|
+
expect(race().members.map((gesture) => gesture.kind)).toEqual(['Pan', 'Tap', 'LongPress']);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('arms it after the tap gesture has already given up', () => {
|
|
191
|
+
// The two windows must not overlap, or arming a long press would sometimes
|
|
192
|
+
// cost the bar a tap. The tap gives up at 400ms; the long press activates
|
|
193
|
+
// at 500ms, so at no instant are both candidates.
|
|
194
|
+
renderBar({ onIndexLongPress: jest.fn() });
|
|
195
|
+
expect(member('LongPress').minDurationMs).toBe(500);
|
|
196
|
+
expect(member('Tap').maxDurationMs).toBe(400);
|
|
197
|
+
expect(member('Tap').maxDurationMs ?? 0).toBeLessThan(member('LongPress').minDurationMs ?? 0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('reports the index of the tab under the finger', () => {
|
|
201
|
+
const onIndexLongPress = jest.fn();
|
|
202
|
+
renderBar({ onIndexLongPress });
|
|
203
|
+
|
|
204
|
+
longPressAt(NEAR_LEFT_EDGE);
|
|
205
|
+
longPressAt(WINDOW_CENTER);
|
|
206
|
+
longPressAt(NEAR_RIGHT_EDGE);
|
|
207
|
+
|
|
208
|
+
expect(onIndexLongPress.mock.calls).toEqual([[0], [1], [2]]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('resolves the same index a tap at the same point would', () => {
|
|
212
|
+
// Long press, tap and scrub all share one `indexAtX` worklet; this is what
|
|
213
|
+
// keeps a long press from ever acting on a different tab than the one the
|
|
214
|
+
// finger is visibly on.
|
|
215
|
+
const onIndexChange = jest.fn();
|
|
216
|
+
const onIndexLongPress = jest.fn();
|
|
217
|
+
renderBar({ onIndexChange, onIndexLongPress });
|
|
218
|
+
|
|
219
|
+
for (const x of [NEAR_LEFT_EDGE, 100, WINDOW_CENTER, 250, NEAR_RIGHT_EDGE]) {
|
|
220
|
+
tapAt(x);
|
|
221
|
+
longPressAt(x);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
expect(onIndexLongPress.mock.calls).toEqual(onIndexChange.mock.calls);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('does not select the tab it long-presses', () => {
|
|
228
|
+
// It is a secondary action (an account sheet, say) — navigating as well
|
|
229
|
+
// would take the user off the screen they opened it from.
|
|
230
|
+
const onIndexChange = jest.fn();
|
|
231
|
+
renderBar({ onIndexChange, onIndexLongPress: jest.fn() });
|
|
232
|
+
|
|
233
|
+
longPressAt(NEAR_RIGHT_EDGE);
|
|
234
|
+
|
|
235
|
+
expect(onIndexChange).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('still selects on a short tap while the long press is armed', () => {
|
|
239
|
+
const onIndexChange = jest.fn();
|
|
240
|
+
const onIndexLongPress = jest.fn();
|
|
241
|
+
renderBar({ onIndexChange, onIndexLongPress });
|
|
242
|
+
|
|
243
|
+
tapAt(NEAR_RIGHT_EDGE);
|
|
244
|
+
|
|
245
|
+
expect(onIndexChange).toHaveBeenCalledTimes(1);
|
|
246
|
+
expect(onIndexChange).toHaveBeenCalledWith(2);
|
|
247
|
+
expect(onIndexLongPress).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('keeps the scrub gesture in the race', () => {
|
|
251
|
+
// The pan is what makes the highlight scrubbable; a long press must not
|
|
252
|
+
// have displaced it.
|
|
253
|
+
renderBar({ onIndexLongPress: jest.fn() });
|
|
254
|
+
// Its scrub worklet, not merely a pan object: this is the handler that
|
|
255
|
+
// drags the highlight under the finger.
|
|
256
|
+
expect(typeof member('Pan').update).toBe('function');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -71,10 +71,14 @@ describe('ToastHost platform split', () => {
|
|
|
71
71
|
expect(styleModule).toMatch(
|
|
72
72
|
/export const WEB_POSITION_FIXED = 'fixed' as ViewStyle\['position'\]/,
|
|
73
73
|
);
|
|
74
|
-
// Exactly one cast in the module, and
|
|
74
|
+
// Exactly one cast in the module, and it is the only RUNTIME export.
|
|
75
75
|
// (Counted against code — the doc comment discusses casts in prose.)
|
|
76
|
+
// The module's other export is the `WebCssStyle` interface, which covers the
|
|
77
|
+
// other half of the same RN/RNW gap and emits nothing — see
|
|
78
|
+
// `web-css-style.test.ts`.
|
|
76
79
|
expect(code('styles/web-view-style.ts').match(/ as /g)).toHaveLength(1);
|
|
77
|
-
expect(styleModule.match(/^export /gm)).toHaveLength(1);
|
|
80
|
+
expect(styleModule.match(/^export const /gm)).toHaveLength(1);
|
|
81
|
+
expect(styleModule.match(/^export (?!const |interface )/gm)).toBeNull();
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
it('no engine file other than the host reaches for a platform overlay', () => {
|
|
@@ -2,8 +2,12 @@ import React from 'react';
|
|
|
2
2
|
import { AppState, Text } from 'react-native';
|
|
3
3
|
import { act, fireEvent, render } from '@testing-library/react-native';
|
|
4
4
|
|
|
5
|
+
import { space } from '../styles/tokens';
|
|
5
6
|
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
ENTERING_ANIMATION_DURATION,
|
|
9
|
+
TOAST_MAX_ROW_WIDTH,
|
|
10
|
+
} from '../toast/constants';
|
|
7
11
|
import { toast, ToastOutlet } from '../toast';
|
|
8
12
|
import { toastStore } from '../toast/toast-store';
|
|
9
13
|
import type { ToasterProps, ToastPosition } from '../toast/types';
|
|
@@ -137,6 +141,16 @@ const anchorOf = ({ UNSAFE_root }: ReturnType<typeof renderOutlet>) =>
|
|
|
137
141
|
return style.position === 'absolute' && style.width === '100%';
|
|
138
142
|
})[0];
|
|
139
143
|
|
|
144
|
+
/**
|
|
145
|
+
* The capped, centred row box — `ToastSwipeHandler`'s wrapper, the only row view
|
|
146
|
+
* carrying the `aria-live` announcement.
|
|
147
|
+
*/
|
|
148
|
+
const rowBoxOf = ({ UNSAFE_root }: ReturnType<typeof renderOutlet>) =>
|
|
149
|
+
UNSAFE_root.find(
|
|
150
|
+
(node) =>
|
|
151
|
+
hostName(node) === 'Animated.View' && node.props['aria-live'] !== undefined,
|
|
152
|
+
);
|
|
153
|
+
|
|
140
154
|
describe('ToastOutlet', () => {
|
|
141
155
|
beforeEach(() => {
|
|
142
156
|
jest.useFakeTimers();
|
|
@@ -382,6 +396,53 @@ describe('ToastOutlet', () => {
|
|
|
382
396
|
expect(toastStore.getSnapshot().toastHeights[id]).toBeUndefined();
|
|
383
397
|
});
|
|
384
398
|
|
|
399
|
+
/**
|
|
400
|
+
* W12 — the row box is capped so a toast does not span a desktop viewport.
|
|
401
|
+
* The cap sits on the swipe wrapper rather than on `ToastContent`'s card so it
|
|
402
|
+
* also covers `toast.custom` JSX and `unstyled` rows, which never render that
|
|
403
|
+
* card.
|
|
404
|
+
*/
|
|
405
|
+
describe('row width cap', () => {
|
|
406
|
+
it('caps and centres the row box', () => {
|
|
407
|
+
const rendered = renderOutlet();
|
|
408
|
+
show(() => toast('Not a banner'));
|
|
409
|
+
|
|
410
|
+
expect(flattenStyle(rowBoxOf(rendered).props.style)).toEqual(
|
|
411
|
+
expect.objectContaining({
|
|
412
|
+
width: '100%',
|
|
413
|
+
maxWidth: TOAST_MAX_ROW_WIDTH,
|
|
414
|
+
// Without this the capped row sits against the left edge: a child with
|
|
415
|
+
// a definite cross size is not stretched by the anchor.
|
|
416
|
+
alignSelf: 'center',
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('caps a custom-jsx row too, not just the default card', () => {
|
|
422
|
+
const rendered = renderOutlet();
|
|
423
|
+
show(() => toast.custom(<Text>Fully custom</Text>));
|
|
424
|
+
|
|
425
|
+
expect(flattenStyle(rowBoxOf(rendered).props.style).maxWidth).toBe(
|
|
426
|
+
TOAST_MAX_ROW_WIDTH,
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('leaves the card its gutters inside the cap', () => {
|
|
431
|
+
// The cap sizes the ROW, which carries the card's two `space.lg` gutters,
|
|
432
|
+
// so the visible card lands on sonner's 356px reference width.
|
|
433
|
+
expect(TOAST_MAX_ROW_WIDTH - space.lg * 2).toBe(356);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('lets a consumer widen the cap through toastContainerStyle', () => {
|
|
437
|
+
const rendered = renderOutlet({
|
|
438
|
+
toastOptions: { toastContainerStyle: { maxWidth: 600 } },
|
|
439
|
+
});
|
|
440
|
+
show(() => toast('Wider please'));
|
|
441
|
+
|
|
442
|
+
expect(flattenStyle(rowBoxOf(rendered).props.style).maxWidth).toBe(600);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
385
446
|
/**
|
|
386
447
|
* The outlet's `toastOptions` style slots and a per-toast `styles` object are
|
|
387
448
|
* folded into ONE styles object (`mergeToastStyles`). Upstream threads all
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ESTIMATED_TOAST_HEIGHT,
|
|
3
3
|
MIN_STACK_SCALE_X,
|
|
4
|
+
TOAST_MAX_ROW_WIDTH,
|
|
4
5
|
} from '../toast/constants';
|
|
5
6
|
import {
|
|
6
7
|
calculateStackScaleX,
|
|
@@ -206,7 +207,7 @@ describe('calculateStackScaleX', () => {
|
|
|
206
207
|
position = 'bottom-center' as ToastPosition,
|
|
207
208
|
isExpanded = false,
|
|
208
209
|
stackGap = 8,
|
|
209
|
-
|
|
210
|
+
rowWidth = 400,
|
|
210
211
|
}: {
|
|
211
212
|
index: number;
|
|
212
213
|
numberOfToasts?: number;
|
|
@@ -214,7 +215,7 @@ describe('calculateStackScaleX', () => {
|
|
|
214
215
|
position?: ToastPosition;
|
|
215
216
|
isExpanded?: boolean;
|
|
216
217
|
stackGap?: number;
|
|
217
|
-
|
|
218
|
+
rowWidth?: number;
|
|
218
219
|
}) =>
|
|
219
220
|
calculateStackScaleX({
|
|
220
221
|
index,
|
|
@@ -223,14 +224,14 @@ describe('calculateStackScaleX', () => {
|
|
|
223
224
|
position,
|
|
224
225
|
isExpanded,
|
|
225
226
|
stackGap,
|
|
226
|
-
|
|
227
|
+
rowWidth,
|
|
227
228
|
});
|
|
228
229
|
|
|
229
230
|
it.each([
|
|
230
231
|
['stacking is off', { index: 0, enableStacking: false }],
|
|
231
232
|
['there is only one row', { index: 0, numberOfToasts: 1 }],
|
|
232
233
|
['the stack is expanded', { index: 0, isExpanded: true }],
|
|
233
|
-
['the
|
|
234
|
+
['the row has not been measured', { index: 0, rowWidth: 0 }],
|
|
234
235
|
])('does not squeeze when %s', (_label, params) => {
|
|
235
236
|
expect(scaleAt(params)).toBe(1);
|
|
236
237
|
});
|
|
@@ -249,11 +250,26 @@ describe('calculateStackScaleX', () => {
|
|
|
249
250
|
|
|
250
251
|
it('never squeezes past MIN_STACK_SCALE_X', () => {
|
|
251
252
|
expect(
|
|
252
|
-
scaleAt({ index: 0, numberOfToasts: 50, stackGap: 40,
|
|
253
|
+
scaleAt({ index: 0, numberOfToasts: 50, stackGap: 40, rowWidth: 200 }),
|
|
253
254
|
).toBe(MIN_STACK_SCALE_X);
|
|
254
255
|
});
|
|
255
256
|
|
|
256
|
-
it('scales the squeeze with the
|
|
257
|
-
expect(scaleAt({ index: 1,
|
|
257
|
+
it('scales the squeeze with the row width', () => {
|
|
258
|
+
expect(scaleAt({ index: 1, rowWidth: 800 })).toBe(1 - 16 / 800);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The squeeze is measured against the ROW, which is capped, not the window.
|
|
263
|
+
* Feeding it a desktop window width instead is the bug this pins: the buried
|
|
264
|
+
* row would narrow by 4 visible pixels rather than the intended 16.
|
|
265
|
+
*/
|
|
266
|
+
it('squeezes a capped row by the same visible amount on a desktop viewport', () => {
|
|
267
|
+
const desktopWindowWidth = 1280;
|
|
268
|
+
expect(scaleAt({ index: 1, rowWidth: TOAST_MAX_ROW_WIDTH })).toBe(
|
|
269
|
+
1 - 16 / TOAST_MAX_ROW_WIDTH,
|
|
270
|
+
);
|
|
271
|
+
expect(scaleAt({ index: 1, rowWidth: TOAST_MAX_ROW_WIDTH })).toBeLessThan(
|
|
272
|
+
scaleAt({ index: 1, rowWidth: desktopWindowWidth }),
|
|
273
|
+
);
|
|
258
274
|
});
|
|
259
275
|
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sibling of `web-position-fixed.test.ts`, guarding the OTHER half of the RN/RNW
|
|
6
|
+
* type gap.
|
|
7
|
+
*
|
|
8
|
+
* Eleven sites across seven files carried an inline `{ … } as ViewStyle` to smuggle
|
|
9
|
+
* web-only CSS (`animation`, `transition*`, `backdrop-filter`, `mask-image`,
|
|
10
|
+
* `scrollbar-*`, `user-select`) past React Native's `ViewStyle`. That is not a
|
|
11
|
+
* widening, it is SILENCING: `as` only requires the two types to be COMPARABLE, so
|
|
12
|
+
* `{ animatoin: '…' } as ViewStyle` compiles and the typo ships as a style key
|
|
13
|
+
* react-native-web drops on the floor.
|
|
14
|
+
*
|
|
15
|
+
* They now annotate with `WebCssStyle`, which restores excess-property checking
|
|
16
|
+
* AND erases completely — unlike a widening helper function, an annotation emits
|
|
17
|
+
* nothing, so the compiled output is the same as the cast's. Two of the eleven
|
|
18
|
+
* needed no widening at all once checked against RN 0.83, which models
|
|
19
|
+
* `boxShadow` and `cursor` on `ViewStyle` directly.
|
|
20
|
+
*
|
|
21
|
+
* This guard stops an inline cast coming back. It pins the FULL remaining set —
|
|
22
|
+
* per file and per count — rather than an allowlist of filenames, so a new cast
|
|
23
|
+
* inside an already-listed file fails too.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const SRC = join(__dirname, '..');
|
|
27
|
+
|
|
28
|
+
function sourceFiles(dir: string): string[] {
|
|
29
|
+
return readdirSync(dir).flatMap((entry) => {
|
|
30
|
+
const full = join(dir, entry);
|
|
31
|
+
if (statSync(full).isDirectory()) {
|
|
32
|
+
return entry === '__tests__' ? [] : sourceFiles(full);
|
|
33
|
+
}
|
|
34
|
+
return /\.tsx?$/.test(entry) ? [full] : [];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Source with comments stripped — they legitimately discuss the retired spelling. */
|
|
39
|
+
const codeOf = (file: string) =>
|
|
40
|
+
readFileSync(file, 'utf8')
|
|
41
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
42
|
+
.replace(/^\s*\/\/.*$/gm, '');
|
|
43
|
+
|
|
44
|
+
const files = sourceFiles(SRC);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The only `as ViewStyle` occurrences left in `src/`, and why each one is NOT a
|
|
48
|
+
* web-CSS cast. Anything else is a regression.
|
|
49
|
+
*/
|
|
50
|
+
const ALLOWED: Record<string, { count: number; reason: string }> = {
|
|
51
|
+
'styles/web-view-style.ts': {
|
|
52
|
+
count: 1,
|
|
53
|
+
reason:
|
|
54
|
+
"`WEB_POSITION_FIXED` — a VALUE-level cast on a key RN does model, just with a narrower union. It is the one documented crossing point and cannot be an annotation, because `position: 'fixed'` has to be writable inside an ordinary style object.",
|
|
55
|
+
},
|
|
56
|
+
'skeleton/index.tsx': {
|
|
57
|
+
count: 1,
|
|
58
|
+
reason:
|
|
59
|
+
'`(flattened as ViewStyle)?.width` narrows a `StyleSheet.flatten` result, which is legitimately `ViewStyle | TextStyle | ImageStyle`. Not a web-CSS cast.',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe('web-only CSS styles', () => {
|
|
64
|
+
it('finds source files to scan (guards against a broken walk)', () => {
|
|
65
|
+
expect(files.length).toBeGreaterThan(100);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('has no inline `as ViewStyle` outside the two documented exceptions', () => {
|
|
69
|
+
const found: Record<string, number> = {};
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const hits = codeOf(file).match(/\bas ViewStyle\b/g);
|
|
72
|
+
if (hits) {
|
|
73
|
+
found[file.replace(`${SRC}/`, '')] = hits.length;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const expected = Object.fromEntries(
|
|
77
|
+
Object.entries(ALLOWED).map(([file, { count }]) => [file, count]),
|
|
78
|
+
);
|
|
79
|
+
expect(found).toEqual(expected);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The properties `WebCssStyle` exists for. If one of these shows up in source
|
|
84
|
+
* next to a cast again, the test above catches it; this one catches the subtler
|
|
85
|
+
* regression of re-declaring the same widening locally instead of importing it.
|
|
86
|
+
*/
|
|
87
|
+
it('declares every web-only property in one place', () => {
|
|
88
|
+
const module = codeOf(join(SRC, 'styles/web-view-style.ts'));
|
|
89
|
+
for (const property of [
|
|
90
|
+
'animation',
|
|
91
|
+
'animationDelay',
|
|
92
|
+
'transitionProperty',
|
|
93
|
+
'transitionDuration',
|
|
94
|
+
'transitionTimingFunction',
|
|
95
|
+
'backdropFilter',
|
|
96
|
+
'WebkitBackdropFilter',
|
|
97
|
+
'maskImage',
|
|
98
|
+
'WebkitMaskImage',
|
|
99
|
+
'backgroundImage',
|
|
100
|
+
'scrollbarWidth',
|
|
101
|
+
'scrollbarColor',
|
|
102
|
+
'userSelect',
|
|
103
|
+
]) {
|
|
104
|
+
expect(module).toContain(`${property}?:`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('is a type-only import everywhere it is used, so nothing is emitted', () => {
|
|
109
|
+
const importers = files.filter(
|
|
110
|
+
(file) =>
|
|
111
|
+
file !== join(SRC, 'styles/web-view-style.ts') &&
|
|
112
|
+
codeOf(file).includes('WebCssStyle'),
|
|
113
|
+
);
|
|
114
|
+
// The seven forks the sweep converted.
|
|
115
|
+
expect(importers.length).toBeGreaterThanOrEqual(7);
|
|
116
|
+
for (const file of importers) {
|
|
117
|
+
const source = codeOf(file);
|
|
118
|
+
// Either `import type { WebCssStyle }` or an inline `type` specifier.
|
|
119
|
+
expect({
|
|
120
|
+
file: file.replace(`${SRC}/`, ''),
|
|
121
|
+
typeOnly:
|
|
122
|
+
/import type \{[^}]*\bWebCssStyle\b/.test(source) ||
|
|
123
|
+
/\btype WebCssStyle\b/.test(source),
|
|
124
|
+
}).toEqual({ file: file.replace(`${SRC}/`, ''), typeOnly: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* RN 0.83 models these on `ViewStyle` itself, so widening them would be cargo
|
|
130
|
+
* cult. Pinned because getting it wrong is invisible: the cast compiles either
|
|
131
|
+
* way and only the reader is misled.
|
|
132
|
+
*/
|
|
133
|
+
it('does not widen properties React Native already models', () => {
|
|
134
|
+
const module = codeOf(join(SRC, 'styles/web-view-style.ts'));
|
|
135
|
+
for (const property of ['cursor', 'boxShadow', 'filter', 'pointerEvents']) {
|
|
136
|
+
expect(module).not.toContain(`${property}?:`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type StyleProp,
|
|
12
12
|
} from 'react-native';
|
|
13
13
|
import { Gesture, GestureDetector, type GestureType } from 'react-native-gesture-handler';
|
|
14
|
+
import type { WebCssStyle } from '../styles/web-view-style';
|
|
14
15
|
import { Z_INDEX } from '../styles/z-index';
|
|
15
16
|
import Animated, {
|
|
16
17
|
type AnimatedStyle,
|
|
@@ -737,19 +738,22 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
737
738
|
</GestureDetector>
|
|
738
739
|
) : handleNode;
|
|
739
740
|
|
|
741
|
+
// `scrollbar-width` / `scrollbar-color` are web-only CSS RN does not model.
|
|
742
|
+
const webScrollbarStyle: WebCssStyle | undefined =
|
|
743
|
+
Platform.OS === 'web'
|
|
744
|
+
? {
|
|
745
|
+
scrollbarWidth: 'thin',
|
|
746
|
+
scrollbarColor: `${colors.border} transparent`,
|
|
747
|
+
}
|
|
748
|
+
: undefined;
|
|
749
|
+
|
|
740
750
|
// Inner content: scrollable wraps in Animated.ScrollView, non-scrollable
|
|
741
751
|
// renders children directly. In legacy mode the scrollview is also
|
|
742
752
|
// wrapped in the `nativeGesture` detector for scroll/pan coordination.
|
|
743
753
|
const scrollViewNode = (
|
|
744
754
|
<Animated.ScrollView
|
|
745
755
|
ref={scrollViewRef}
|
|
746
|
-
style={[
|
|
747
|
-
styles.scrollView,
|
|
748
|
-
Platform.OS === 'web' && ({
|
|
749
|
-
scrollbarWidth: 'thin',
|
|
750
|
-
scrollbarColor: `${colors.border} transparent`,
|
|
751
|
-
} as ViewStyle),
|
|
752
|
-
]}
|
|
756
|
+
style={[styles.scrollView, webScrollbarStyle]}
|
|
753
757
|
contentContainerStyle={dynamicStyles.scrollContent}
|
|
754
758
|
showsVerticalScrollIndicator={false}
|
|
755
759
|
keyboardShouldPersistTaps="handled"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { memo, useEffect, useState } from 'react';
|
|
2
2
|
import { View, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
|
+
import type { WebCssStyle } from '../styles/web-view-style';
|
|
4
5
|
import { useTheme } from '../theme/use-theme';
|
|
5
6
|
import { ConnectionDotsBase } from './ConnectionDotsBase';
|
|
6
7
|
import type { ConnectionDotsProps } from './types';
|
|
@@ -88,14 +89,14 @@ const ConnectionDotsComponent: React.FC<ConnectionDotsProps> = ({
|
|
|
88
89
|
borderRadius: dotSize / 2,
|
|
89
90
|
backgroundColor: color,
|
|
90
91
|
};
|
|
91
|
-
const animated:
|
|
92
|
+
const animated: WebCssStyle = reduce
|
|
92
93
|
? { opacity: 1 }
|
|
93
|
-
:
|
|
94
|
+
: {
|
|
94
95
|
// `animation` / `animationDelay` are real CSS props on web;
|
|
95
96
|
// react-native-web passes them through to the DOM node.
|
|
96
97
|
animation: `${WAVE_ANIMATION_NAME} ${WAVE_DURATION_MS}ms linear infinite`,
|
|
97
98
|
animationDelay: `${(i / count) * WAVE_DURATION_MS}ms`,
|
|
98
|
-
}
|
|
99
|
+
};
|
|
99
100
|
return (
|
|
100
101
|
<View
|
|
101
102
|
key={i}
|
|
@@ -36,7 +36,11 @@ export const SHADOW_BOX: Record<ShadowRole, string> = {
|
|
|
36
36
|
* On WEB this returns a `{ boxShadow }` style object (so it can also be applied
|
|
37
37
|
* inline without Tailwind). The `.native.ts` fork returns RN
|
|
38
38
|
* `shadowColor/shadowOffset/shadowOpacity/shadowRadius/elevation`.
|
|
39
|
+
*
|
|
40
|
+
* No widening needed: `boxShadow` is a first-class `ViewStyle` key on RN >= 0.76
|
|
41
|
+
* and takes a CSS string, so this is not one of the web-only properties
|
|
42
|
+
* `styles/web-view-style.ts` exists for.
|
|
39
43
|
*/
|
|
40
44
|
export function bloomShadowStyle(role: ShadowRole): ViewStyle {
|
|
41
|
-
return { boxShadow: SHADOW_BOX[role] }
|
|
45
|
+
return { boxShadow: SHADOW_BOX[role] };
|
|
42
46
|
}
|