@momo-kits/foundation 0.168.1-beta.2 → 0.168.1-beta.21
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/Application/BottomSheet.tsx +142 -37
- package/Application/BottomTab/DuoTabBar.tsx +191 -151
- package/Application/BottomTab/useLiquidSelection.ts +374 -0
- package/Application/Components/DuoGlassBackground.tsx +5 -8
- package/Application/ModalScreen.tsx +12 -8
- package/Application/device.ts +24 -1
- package/Application/utils.tsx +5 -9
- package/Input/InputMoney.tsx +130 -178
- package/Input/styles.ts +9 -0
- package/Layout/Screen.tsx +18 -39
- package/Layout/utils.ts +1 -0
- package/Popup/PopupNotify.tsx +20 -10
- package/Popup/utils.ts +9 -15
- package/package.json +37 -37
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { Gesture } from 'react-native-gesture-handler';
|
|
3
|
+
import {
|
|
4
|
+
clamp,
|
|
5
|
+
useAnimatedReaction,
|
|
6
|
+
useAnimatedStyle,
|
|
7
|
+
useReducedMotion,
|
|
8
|
+
useSharedValue,
|
|
9
|
+
withSequence,
|
|
10
|
+
withSpring,
|
|
11
|
+
withTiming,
|
|
12
|
+
} from 'react-native-reanimated';
|
|
13
|
+
import { scheduleOnRN } from 'react-native-worklets';
|
|
14
|
+
import { ApplicationContext } from '../../Context';
|
|
15
|
+
|
|
16
|
+
type Axis = 'x' | 'y';
|
|
17
|
+
|
|
18
|
+
type Options = {
|
|
19
|
+
count: number;
|
|
20
|
+
// distance between two neighbouring tab centers
|
|
21
|
+
pitch: number;
|
|
22
|
+
// position of the first tab's center inside the gesture view
|
|
23
|
+
firstCenter: number;
|
|
24
|
+
axis: Axis;
|
|
25
|
+
index: number;
|
|
26
|
+
onSelect: (index: number) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const SPRING = { damping: 18, stiffness: 220, mass: 0.8 };
|
|
30
|
+
// the lens trails the finger a little while dragged, like the system tab bar
|
|
31
|
+
const DRAG_SPRING = { damping: 24, stiffness: 600, mass: 0.6 };
|
|
32
|
+
const REDUCED_TIMING = { duration: 200 };
|
|
33
|
+
// while pressed the selection bubble grows into a lens past the bar's edges (80x54 to about 96x70)
|
|
34
|
+
const LENS_SCALE_X = 1.2;
|
|
35
|
+
const LENS_SCALE_Y = 1.3;
|
|
36
|
+
// a moving lens stretches along its path and thins across it, eased per frame so the
|
|
37
|
+
// shape follows the speed smoothly
|
|
38
|
+
const STRETCH_PER_POINT = 0.012;
|
|
39
|
+
const MAX_STRETCH = 0.25;
|
|
40
|
+
const STRETCH_EASING = 0.25;
|
|
41
|
+
// once eased below this the lens is round again
|
|
42
|
+
const REST_STRETCH = 0.002;
|
|
43
|
+
// a tap is shorter than the lift: the lens finishes rising, then settles, one smooth pulse
|
|
44
|
+
const TAP_LIFT_DURATION = 120;
|
|
45
|
+
// the bar swells a little under the finger
|
|
46
|
+
const BAR_SWELL = 0.04;
|
|
47
|
+
// the tab the lens lands on pops
|
|
48
|
+
const ICON_POP = 1.15;
|
|
49
|
+
// movement before a press turns into a drag, below it the tab's touchable handles the tap
|
|
50
|
+
const DRAG_SLOP = 10;
|
|
51
|
+
// UIScrollView rubber band constant
|
|
52
|
+
const RUBBER_BAND = 0.55;
|
|
53
|
+
// a cancelled press or a prevented switch: the lens returns to the selected tab after this
|
|
54
|
+
const RESYNC_DELAY = 100;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* resistance past the first and last tab, the further the finger goes the less the lens follows
|
|
58
|
+
*/
|
|
59
|
+
const rubberBand = (value: number, max: number, dimension: number) => {
|
|
60
|
+
'worklet';
|
|
61
|
+
const band = (overshoot: number) =>
|
|
62
|
+
(1 - 1 / ((overshoot * RUBBER_BAND) / dimension + 1)) * dimension;
|
|
63
|
+
if (value < 0) {
|
|
64
|
+
return -band(-value);
|
|
65
|
+
}
|
|
66
|
+
if (value > max) {
|
|
67
|
+
return max + band(value - max);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* iOS 26 tab bar selection motion, without the glass material:
|
|
74
|
+
* - press lifts the selection into a lens over the pressed tab and swells the bar, release
|
|
75
|
+
* settles it back (the tab's touchable drives this through pressIn / pressOut)
|
|
76
|
+
* - dragging slides the lens under the finger, stretched by its speed and rubber banded
|
|
77
|
+
* past the ends, with a tick on each tab it enters; releasing selects the tab under it.
|
|
78
|
+
* The drag cancels the touchable once it activates, so a press is either a tap or a drag
|
|
79
|
+
*/
|
|
80
|
+
const useLiquidSelection = ({
|
|
81
|
+
count,
|
|
82
|
+
pitch,
|
|
83
|
+
firstCenter,
|
|
84
|
+
axis,
|
|
85
|
+
index,
|
|
86
|
+
onSelect,
|
|
87
|
+
}: Options) => {
|
|
88
|
+
const { navigator } = useContext<any>(ApplicationContext);
|
|
89
|
+
const reduceMotion = useReducedMotion();
|
|
90
|
+
|
|
91
|
+
const offset = useSharedValue(index * pitch);
|
|
92
|
+
// where the lens is heading, so a re-render doesn't restart a spring already going there
|
|
93
|
+
const destination = useSharedValue(index * pitch);
|
|
94
|
+
const selected = useSharedValue(index);
|
|
95
|
+
const hovered = useSharedValue(index);
|
|
96
|
+
const lift = useSharedValue(0);
|
|
97
|
+
const dragging = useSharedValue(false);
|
|
98
|
+
const stretch = useSharedValue(0);
|
|
99
|
+
const metrics = useSharedValue({ count, pitch, firstCenter });
|
|
100
|
+
const measured = useRef(pitch > 0);
|
|
101
|
+
|
|
102
|
+
const indexRef = useRef(index);
|
|
103
|
+
const pitchRef = useRef(pitch);
|
|
104
|
+
// read through refs so the gesture is never rebuilt mid drag
|
|
105
|
+
const callbacks = useRef({ onSelect, navigator });
|
|
106
|
+
indexRef.current = index;
|
|
107
|
+
pitchRef.current = pitch;
|
|
108
|
+
callbacks.current = { onSelect, navigator };
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
metrics.value = { count, pitch, firstCenter };
|
|
112
|
+
}, [count, pitch, firstCenter, metrics]);
|
|
113
|
+
|
|
114
|
+
const move = useCallback(
|
|
115
|
+
(target: number, velocity: number, config: typeof SPRING) => {
|
|
116
|
+
'worklet';
|
|
117
|
+
if (destination.value === target && velocity === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
destination.value = target;
|
|
121
|
+
offset.value = reduceMotion
|
|
122
|
+
? withTiming(target, REDUCED_TIMING)
|
|
123
|
+
: withSpring(target, { ...config, velocity });
|
|
124
|
+
},
|
|
125
|
+
[offset, destination, reduceMotion],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const target = index * pitch;
|
|
130
|
+
selected.value = index;
|
|
131
|
+
// snap on first layout so the lens doesn't slide in from the leading edge
|
|
132
|
+
if (measured.current) {
|
|
133
|
+
move(target, 0, SPRING);
|
|
134
|
+
} else {
|
|
135
|
+
destination.value = target;
|
|
136
|
+
offset.value = target;
|
|
137
|
+
}
|
|
138
|
+
measured.current = pitch > 0;
|
|
139
|
+
}, [index, pitch, offset, destination, selected, move]);
|
|
140
|
+
|
|
141
|
+
useAnimatedReaction(
|
|
142
|
+
() => offset.value,
|
|
143
|
+
(current, previous) => {
|
|
144
|
+
if (previous === null || reduceMotion) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const target = Math.min(
|
|
148
|
+
Math.abs(current - previous) * STRETCH_PER_POINT,
|
|
149
|
+
MAX_STRETCH,
|
|
150
|
+
);
|
|
151
|
+
// eased both ways: a slow frame (e.g. the spring turning at its overshoot) must not
|
|
152
|
+
// snap the shape
|
|
153
|
+
const next = stretch.value + (target - stretch.value) * STRETCH_EASING;
|
|
154
|
+
stretch.value = next < REST_STRETCH ? 0 : next;
|
|
155
|
+
},
|
|
156
|
+
[reduceMotion],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* returns the lens to the selected tab unless a switch to where it is heading happened
|
|
161
|
+
*/
|
|
162
|
+
const resync = useCallback(() => {
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
const target = indexRef.current * pitchRef.current;
|
|
165
|
+
if (!dragging.value && destination.value !== target) {
|
|
166
|
+
move(target, 0, SPRING);
|
|
167
|
+
}
|
|
168
|
+
}, RESYNC_DELAY);
|
|
169
|
+
}, [destination, dragging, move]);
|
|
170
|
+
|
|
171
|
+
const pressIn = (itemIndex: number) => {
|
|
172
|
+
// the lens heads to the pressed tab, not only the selected one
|
|
173
|
+
move(itemIndex * pitch, 0, SPRING);
|
|
174
|
+
if (!reduceMotion) {
|
|
175
|
+
lift.value = withSpring(1, SPRING);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* false when a drag took the press over
|
|
181
|
+
*/
|
|
182
|
+
const pressOut = () => {
|
|
183
|
+
// a drag taking over cancels the touchable, the drag owns the lens from there
|
|
184
|
+
if (dragging.value) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (!reduceMotion) {
|
|
188
|
+
lift.value = withSequence(
|
|
189
|
+
withTiming(1, { duration: TAP_LIFT_DURATION }),
|
|
190
|
+
withSpring(0, SPRING),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
// the lens keeps heading to the pressed tab: onPress switches to it, and a cancelled
|
|
194
|
+
// press brings it back
|
|
195
|
+
resync();
|
|
196
|
+
return true;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const tick = useCallback(() => {
|
|
200
|
+
callbacks.current.navigator?.maxApi?.triggerEventVibration?.('light');
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
const select = useCallback(
|
|
204
|
+
(target: number) => {
|
|
205
|
+
callbacks.current.onSelect(target);
|
|
206
|
+
resync();
|
|
207
|
+
},
|
|
208
|
+
[resync],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const gesture = useMemo(() => {
|
|
212
|
+
const position = (e: { x: number; y: number }) => {
|
|
213
|
+
'worklet';
|
|
214
|
+
return (axis === 'x' ? e.x : e.y) - metrics.value.firstCenter;
|
|
215
|
+
};
|
|
216
|
+
const nearest = (value: number) => {
|
|
217
|
+
'worklet';
|
|
218
|
+
const { count: tabs, pitch: step } = metrics.value;
|
|
219
|
+
return step > 0 ? clamp(Math.round(value / step), 0, tabs - 1) : 0;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return Gesture.Pan()
|
|
223
|
+
.minDistance(DRAG_SLOP)
|
|
224
|
+
.shouldCancelWhenOutside(false)
|
|
225
|
+
.onStart(e => {
|
|
226
|
+
dragging.value = true;
|
|
227
|
+
hovered.value = nearest(position(e));
|
|
228
|
+
if (!reduceMotion) {
|
|
229
|
+
lift.value = withSpring(1, SPRING);
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
.onUpdate(e => {
|
|
233
|
+
const { count: tabs, pitch: step } = metrics.value;
|
|
234
|
+
const value = position(e);
|
|
235
|
+
const target = nearest(value);
|
|
236
|
+
if (target !== hovered.value) {
|
|
237
|
+
hovered.value = target;
|
|
238
|
+
scheduleOnRN(tick);
|
|
239
|
+
}
|
|
240
|
+
const follow = rubberBand(value, (tabs - 1) * step, step);
|
|
241
|
+
if (reduceMotion) {
|
|
242
|
+
destination.value = follow;
|
|
243
|
+
offset.value = follow;
|
|
244
|
+
} else {
|
|
245
|
+
move(follow, 0, DRAG_SPRING);
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
.onEnd((e, success) => {
|
|
249
|
+
dragging.value = false;
|
|
250
|
+
lift.value = reduceMotion ? 0 : withSpring(0, SPRING);
|
|
251
|
+
const raw = axis === 'x' ? e.velocityX : e.velocityY;
|
|
252
|
+
const velocity = Number.isFinite(raw) ? raw : 0;
|
|
253
|
+
if (!success) {
|
|
254
|
+
move(selected.value * metrics.value.pitch, velocity, SPRING);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const target = nearest(position(e));
|
|
258
|
+
move(target * metrics.value.pitch, velocity, SPRING);
|
|
259
|
+
scheduleOnRN(select, target);
|
|
260
|
+
});
|
|
261
|
+
}, [
|
|
262
|
+
axis,
|
|
263
|
+
reduceMotion,
|
|
264
|
+
offset,
|
|
265
|
+
destination,
|
|
266
|
+
selected,
|
|
267
|
+
hovered,
|
|
268
|
+
lift,
|
|
269
|
+
dragging,
|
|
270
|
+
metrics,
|
|
271
|
+
move,
|
|
272
|
+
tick,
|
|
273
|
+
select,
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
const bubbleStyle = useAnimatedStyle(() => {
|
|
277
|
+
const along = 1 + stretch.value;
|
|
278
|
+
const across = 1 - stretch.value * 0.5;
|
|
279
|
+
const lensX = 1 + lift.value * (LENS_SCALE_X - 1);
|
|
280
|
+
const lensY = 1 + lift.value * (LENS_SCALE_Y - 1);
|
|
281
|
+
return {
|
|
282
|
+
transform:
|
|
283
|
+
axis === 'x'
|
|
284
|
+
? [
|
|
285
|
+
{ translateX: offset.value },
|
|
286
|
+
{ scaleX: lensX * along },
|
|
287
|
+
{ scaleY: lensY * across },
|
|
288
|
+
]
|
|
289
|
+
: [
|
|
290
|
+
{ translateY: offset.value },
|
|
291
|
+
{ scaleX: lensX * across },
|
|
292
|
+
{ scaleY: lensY * along },
|
|
293
|
+
],
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const barStyle = useAnimatedStyle(() => ({
|
|
298
|
+
transform: [{ scale: 1 + lift.value * BAR_SWELL }],
|
|
299
|
+
}));
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
gesture,
|
|
303
|
+
bubbleStyle,
|
|
304
|
+
barStyle,
|
|
305
|
+
pressIn,
|
|
306
|
+
pressOut,
|
|
307
|
+
offset,
|
|
308
|
+
selected,
|
|
309
|
+
pitch,
|
|
310
|
+
reduceMotion,
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
type LiquidSelection = ReturnType<typeof useLiquidSelection>;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* active tint of a tab follows the lens over it, and the tab the lens lands on pops
|
|
318
|
+
*/
|
|
319
|
+
const useLiquidItem = (
|
|
320
|
+
{ offset, selected, pitch, reduceMotion }: LiquidSelection,
|
|
321
|
+
itemIndex: number,
|
|
322
|
+
) => {
|
|
323
|
+
const pop = useSharedValue(1);
|
|
324
|
+
|
|
325
|
+
useAnimatedReaction(
|
|
326
|
+
() => selected.value === itemIndex,
|
|
327
|
+
(isSelected, wasSelected) => {
|
|
328
|
+
if (isSelected && wasSelected === false && !reduceMotion) {
|
|
329
|
+
pop.value = withSequence(
|
|
330
|
+
withTiming(ICON_POP, { duration: 120 }),
|
|
331
|
+
withSpring(1, SPRING),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
[itemIndex, reduceMotion],
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const activeStyle = useAnimatedStyle(() => {
|
|
339
|
+
if (pitch <= 0) {
|
|
340
|
+
return { opacity: selected.value === itemIndex ? 1 : 0 };
|
|
341
|
+
}
|
|
342
|
+
const distance = Math.abs(offset.value - itemIndex * pitch) / pitch;
|
|
343
|
+
return { opacity: clamp(1 - distance, 0, 1) };
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const popStyle = useAnimatedStyle(() => ({
|
|
347
|
+
transform: [{ scale: pop.value }],
|
|
348
|
+
}));
|
|
349
|
+
|
|
350
|
+
return { activeStyle, popStyle };
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* lift for a single button, e.g. the compressed rail
|
|
355
|
+
*/
|
|
356
|
+
const usePressLift = () => {
|
|
357
|
+
const reduceMotion = useReducedMotion();
|
|
358
|
+
const lift = useSharedValue(0);
|
|
359
|
+
const style = useAnimatedStyle(() => ({
|
|
360
|
+
transform: [{ scale: 1 + lift.value * (LENS_SCALE_X - 1) * 0.5 }],
|
|
361
|
+
}));
|
|
362
|
+
const onPressIn = () => {
|
|
363
|
+
if (!reduceMotion) {
|
|
364
|
+
lift.value = withSpring(1, SPRING);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const onPressOut = () => {
|
|
368
|
+
lift.value = withSpring(0, SPRING);
|
|
369
|
+
};
|
|
370
|
+
return { style, onPressIn, onPressOut };
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
export type { LiquidSelection };
|
|
374
|
+
export { useLiquidSelection, useLiquidItem, usePressLift };
|
|
@@ -10,12 +10,10 @@ import { ApplicationContext } from '../../Context';
|
|
|
10
10
|
import { Shadow } from '../../Consts';
|
|
11
11
|
import { Image } from '../../Image';
|
|
12
12
|
import { AnimatedCompatValue, useNormalizedSharedValue } from '../utils';
|
|
13
|
+
import { useScreenLayout } from '../../Layout';
|
|
13
14
|
|
|
14
|
-
// extended headers run their gradient this far below the header, as off Duo
|
|
15
15
|
const EXTENDED_GRADIENT_EXTRA = 52;
|
|
16
|
-
// scroll distance over which the extended gradient collapses to the header
|
|
17
16
|
const EXTENDED_COLLAPSE_DISTANCE = 100;
|
|
18
|
-
// scroll distance over which the bar material fades in
|
|
19
17
|
const FADE_DISTANCE = 16;
|
|
20
18
|
const TRANSLUCENT_ALPHA = 'CC';
|
|
21
19
|
|
|
@@ -25,11 +23,12 @@ const TRANSLUCENT_ALPHA = 'CC';
|
|
|
25
23
|
*/
|
|
26
24
|
const DuoGlassBackground: React.FC<{
|
|
27
25
|
animatedValue?: AnimatedCompatValue;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}> = ({ animatedValue, fadeStart = 0 }) => {
|
|
26
|
+
bannerBottom?: number;
|
|
27
|
+
}> = ({ animatedValue, bannerBottom = 0 }) => {
|
|
31
28
|
const { theme } = useContext(ApplicationContext);
|
|
29
|
+
const { headerHeight } = useScreenLayout();
|
|
32
30
|
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
31
|
+
const fadeStart = Math.max(bannerBottom - headerHeight, 0);
|
|
33
32
|
|
|
34
33
|
const fadeStyle = useAnimatedStyle(() => ({
|
|
35
34
|
opacity: interpolate(
|
|
@@ -68,7 +67,6 @@ const DuoScrollGradient: React.FC<{
|
|
|
68
67
|
headerBackground?: string;
|
|
69
68
|
heightHeader: number;
|
|
70
69
|
extended?: boolean;
|
|
71
|
-
// quick return header: the gradient leaves with the content instead of fading
|
|
72
70
|
followScroll?: boolean;
|
|
73
71
|
}> = ({
|
|
74
72
|
animatedValue,
|
|
@@ -147,7 +145,6 @@ const styles = StyleSheet.create({
|
|
|
147
145
|
right: 0,
|
|
148
146
|
overflow: 'hidden',
|
|
149
147
|
},
|
|
150
|
-
// banner image keeps its 375:154 shape, the gradient box clips it
|
|
151
148
|
gradientFill: {
|
|
152
149
|
width: '100%',
|
|
153
150
|
position: 'absolute',
|
|
@@ -18,7 +18,7 @@ import { Styles } from '../Consts';
|
|
|
18
18
|
import Navigation from './Navigation';
|
|
19
19
|
import { ModalParams } from './types';
|
|
20
20
|
import BottomSheet from './BottomSheet';
|
|
21
|
-
import {
|
|
21
|
+
import { getDuoModalBounds, isIPhoneDuo } from './device';
|
|
22
22
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
23
23
|
|
|
24
24
|
const ModalScreen: React.FC<any> = props => {
|
|
@@ -42,6 +42,7 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
42
42
|
const Component = useRef(screen).current;
|
|
43
43
|
const insets = useSafeAreaInsets();
|
|
44
44
|
const windowSize = useWindowDimensions();
|
|
45
|
+
const isDuo = isIPhoneDuo();
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Fire auto_popup_displayed tracking event on mount
|
|
@@ -135,6 +136,7 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
135
136
|
onRequestClose={onDismiss}
|
|
136
137
|
style={StyleSheet.absoluteFillObject}
|
|
137
138
|
isModalKit={true}
|
|
139
|
+
supportedOrientations={isDuo ? DUO_ORIENTATIONS : undefined}
|
|
138
140
|
>
|
|
139
141
|
<KeyboardAvoidingView
|
|
140
142
|
style={Styles.flexCenter}
|
|
@@ -152,14 +154,13 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
152
154
|
opacity,
|
|
153
155
|
transform: [{ scale }],
|
|
154
156
|
},
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
marginTop:
|
|
158
|
-
insets,
|
|
159
|
-
windowSize.width,
|
|
160
|
-
windowSize.height,
|
|
161
|
-
),
|
|
157
|
+
isDuo && styles.shrink,
|
|
158
|
+
isDuo && {
|
|
159
|
+
marginTop: insets.top,
|
|
162
160
|
marginBottom: insets.bottom,
|
|
161
|
+
marginLeft: insets.left,
|
|
162
|
+
marginRight: insets.right,
|
|
163
|
+
...getDuoModalBounds(insets, windowSize.width, windowSize.height),
|
|
163
164
|
},
|
|
164
165
|
modalStyle,
|
|
165
166
|
]}
|
|
@@ -171,11 +172,14 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
171
172
|
);
|
|
172
173
|
};
|
|
173
174
|
|
|
175
|
+
const DUO_ORIENTATIONS = ['portrait', 'landscape'];
|
|
176
|
+
|
|
174
177
|
const styles = StyleSheet.create({
|
|
175
178
|
overlayContent: {
|
|
176
179
|
flex: 1,
|
|
177
180
|
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
178
181
|
},
|
|
182
|
+
shrink: { flexShrink: 1 },
|
|
179
183
|
});
|
|
180
184
|
|
|
181
185
|
export default ModalScreen;
|
package/Application/device.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Dimensions, Platform, ScaledSize } from 'react-native';
|
|
2
2
|
import type { EdgeInsets } from 'react-native-safe-area-context';
|
|
3
|
+
import { Spacing } from '../Consts';
|
|
3
4
|
|
|
4
5
|
// widest short side of any single-display iPhone
|
|
5
6
|
const IPHONE_MAX_SHORT_SIDE = 440;
|
|
6
7
|
|
|
8
|
+
const DUO_MODAL_MAX_WIDTH = 440;
|
|
9
|
+
const DUO_MODAL_MAX_HEIGHT = 640;
|
|
10
|
+
|
|
7
11
|
// iPhone Duo reports idiom "phone"; 'screen' stays on the outer display, so it
|
|
8
12
|
// still matches when Split View narrows the window
|
|
9
13
|
const isIPhoneDuo = () => {
|
|
@@ -26,4 +30,23 @@ const getDuoTopInset = (insets: EdgeInsets, width: number, height: number) =>
|
|
|
26
30
|
height > width ? Math.max(insets.left, insets.right) * 2 : 0,
|
|
27
31
|
);
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Max modal size on iPhone Duo: inside the safe area, so clear of the side
|
|
35
|
+
* toolbar strip, and capped so it stays a dialog on the inner display.
|
|
36
|
+
*/
|
|
37
|
+
const getDuoModalBounds = (
|
|
38
|
+
insets: EdgeInsets,
|
|
39
|
+
width: number,
|
|
40
|
+
height: number,
|
|
41
|
+
) => ({
|
|
42
|
+
maxWidth: Math.min(
|
|
43
|
+
width - insets.left - insets.right - Spacing.XL * 2,
|
|
44
|
+
DUO_MODAL_MAX_WIDTH,
|
|
45
|
+
),
|
|
46
|
+
maxHeight: Math.min(
|
|
47
|
+
height - insets.top - insets.bottom - Spacing.XL * 2,
|
|
48
|
+
DUO_MODAL_MAX_HEIGHT,
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export { isIPhoneDuo, getDuoTopInset, getDuoModalBounds };
|
package/Application/utils.tsx
CHANGED
|
@@ -32,6 +32,8 @@ import { HeaderBackground } from './Components/HeaderBackground';
|
|
|
32
32
|
import { HeaderLeft } from './Components/HeaderLeft';
|
|
33
33
|
import { HeaderRight } from './Components/HeaderRight';
|
|
34
34
|
|
|
35
|
+
const SWIPE_BACK_HAPTIC_THRESHOLD = 50;
|
|
36
|
+
|
|
35
37
|
const forStackTransitionWithOverlay = (
|
|
36
38
|
props: StackCardInterpolationProps,
|
|
37
39
|
): StackCardInterpolatedStyle => {
|
|
@@ -374,13 +376,6 @@ const useNormalizedSharedValue = (
|
|
|
374
376
|
return fallback;
|
|
375
377
|
};
|
|
376
378
|
|
|
377
|
-
/**
|
|
378
|
-
* Travel, in points, at which the swipe back gesture ticks. Flat and well short of
|
|
379
|
-
* react-navigation's own `distance / 2` commit rule: the tick marks "the gesture is
|
|
380
|
-
* recognised", not "you are past the point of no return".
|
|
381
|
-
*/
|
|
382
|
-
const SWIPE_BACK_HAPTIC_THRESHOLD = 50;
|
|
383
|
-
|
|
384
379
|
/**
|
|
385
380
|
* `current.progress` is `gesture.interpolate(...)` built by CardStack
|
|
386
381
|
* (@react-navigation/stack 7.4.2, views/Stack/CardStack.tsx `getProgressFromGesture`).
|
|
@@ -478,8 +473,9 @@ const SwipeBackHaptic = () => {
|
|
|
478
473
|
return null;
|
|
479
474
|
};
|
|
480
475
|
|
|
481
|
-
const isWhiteListDesignSystem = (context: any): boolean =>
|
|
482
|
-
context?.designSystemConfig?.isWhiteListDesignSystem === true;
|
|
476
|
+
const isWhiteListDesignSystem = (context: any): boolean => {
|
|
477
|
+
return context?.designSystemConfig?.isWhiteListDesignSystem === true;
|
|
478
|
+
};
|
|
483
479
|
|
|
484
480
|
export {
|
|
485
481
|
getStackOptions,
|