@momo-kits/foundation 0.168.1-beta.33 → 0.168.1-beta.34

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.
@@ -16,9 +16,7 @@ import {
16
16
  PanResponder,
17
17
  Platform,
18
18
  Pressable,
19
- StatusBar,
20
19
  StyleSheet,
21
- TouchableOpacity,
22
20
  useWindowDimensions,
23
21
  View,
24
22
  } from 'react-native';
@@ -27,7 +25,6 @@ import { ApplicationContext, MiniAppContext } from '../Context';
27
25
  import { BottomSheetParams } from './types';
28
26
  import { Colors, Radius, Spacing, Styles } from '../Consts';
29
27
  import { Text } from '../Text';
30
- import { Icon } from '../Icon';
31
28
  import { useHeaderHeight } from '@react-navigation/elements';
32
29
  import Animated, {
33
30
  Easing,
@@ -40,54 +37,26 @@ import Animated, {
40
37
  } from 'react-native-reanimated';
41
38
  import { scheduleOnRN } from 'react-native-worklets';
42
39
  import layoutStyles from '../Layout/styles';
43
- import { DuoMetrics, InnerEdgeStyles, useScreenLayout } from '../Layout/utils';
44
- import { getDuoTopInset, isIPhoneDuo } from './device';
45
- import { SideBarButton } from './Components/SideBarButton';
46
-
47
- // iPhone Duo inner display: center the sheet instead of spanning the width
48
- const DUO_REGULAR_MIN_WIDTH = 600;
40
+ import { isIPhoneDuo } from './device';
41
+ import { useModalLayout } from './modalLayout';
42
+ import { MODAL_ICON_BUTTON_SIZE, ModalIconButton } from './ModalIconButton';
49
43
 
50
44
  const INDICATOR_HEIGHT = 16;
51
45
  const HEADER_HEIGHT = 56;
52
46
  const DUO_ORIENTATIONS = ['portrait', 'landscape'];
53
- const SIDE_COLUMN_OFFSET =
54
- INDICATOR_HEIGHT + (HEADER_HEIGHT - DuoMetrics.controlSize) / 2;
55
-
56
- const getSideInset = (inset: number) => Math.max(inset - Spacing.M, 0);
57
47
 
58
48
  const BottomSheet: React.FC<BottomSheetParams> = props => {
59
49
  const { theme, navigator } = useContext(ApplicationContext);
60
50
  const context = useContext<any>(MiniAppContext);
61
51
  const windowSize = useWindowDimensions();
62
52
  const isDuo = isIPhoneDuo();
63
- const isDuoCentered =
64
- isDuo &&
65
- Math.min(windowSize.width, windowSize.height) >= DUO_REGULAR_MIN_WIDTH;
66
- const isDuoFloating = isDuo && !isDuoCentered;
67
- const { sideBar } = useScreenLayout();
68
- const sideColumnWidth = (sideBar?.width ?? 0) - Spacing.M;
69
- const sideColumn =
70
- isDuoFloating &&
71
- sideColumnWidth >= DuoMetrics.controlSize + Spacing.M &&
72
- sideBar
73
- ? sideBar
74
- : undefined;
75
- const hasSideColumn = !!sideColumn;
53
+ const { isFoldable, placement, sheet, scrimOpacity } = useModalLayout();
76
54
  // 'screen' is the outer display on iPhone Duo, even when unfolded
77
55
  const screenHeight = Dimensions.get('screen').height;
78
56
  const heightDevice = isDuo ? windowSize.height : screenHeight;
79
57
  const action = useRef<undefined | string>(undefined);
80
58
  const insets = useSafeAreaInsets();
81
59
  const heightHeader = useHeaderHeight();
82
- const topGap = Math.max(
83
- sideColumn
84
- ? insets.top
85
- : getDuoTopInset(insets, windowSize.width, windowSize.height),
86
- Spacing.XL,
87
- );
88
- const sideInsetLeft = isDuoFloating ? getSideInset(insets.left) : 0;
89
- const sideInsetRight = isDuoFloating ? getSideInset(insets.right) : 0;
90
- const indicatorShift = (sideInsetRight - sideInsetLeft) / 2;
91
60
  const isIOS = Platform.OS === 'ios';
92
61
  const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
93
62
  const keyboardOffset = heightHeader - bottomInset;
@@ -113,7 +82,6 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
113
82
  const onDismissRef = useRef(props.route.params?.onDismiss);
114
83
  onDismissRef.current = props.route.params?.onDismiss;
115
84
  const [keyboardLift, setKeyboardLift] = useState(0);
116
- const [sheetTop, setSheetTop] = useState(windowSize.height);
117
85
 
118
86
  // KeyboardAvoidingView lifts the whole sheet past the top on iPhone Duo,
119
87
  // so lift it here and shrink maxHeight by the same amount instead
@@ -269,39 +237,15 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
269
237
 
270
238
  const showBack = !!leftOptions?.onPressIconLeft;
271
239
  const iconBack = leftOptions?.iconLeft ?? 'ic_back';
272
- const sideColumnCount = showBack ? 2 : 1;
273
- const sideColumnHeight =
274
- sideColumnCount * DuoMetrics.controlSize +
275
- (sideColumnCount - 1) * Spacing.M;
276
- const coversStatusBar = !!sideColumn && sheetTop < sideColumn.top;
277
-
278
- useEffect(() => {
279
- if (!coversStatusBar) {
280
- return;
281
- }
282
- const entry = StatusBar.pushStackEntry({
283
- barStyle: theme.dark ? 'light-content' : 'dark-content',
284
- animated: true,
285
- });
286
- return () => StatusBar.popStackEntry(entry);
287
- }, [coversStatusBar, theme.dark]);
288
240
 
289
241
  /**
290
242
  * render header
291
243
  */
292
244
  const renderHeader = useCallback(() => {
293
- const showButtons = !hasSideColumn;
294
245
  return (
295
246
  <View {...panResponder.panHandlers}>
296
247
  <View style={{ height: INDICATOR_HEIGHT }}>
297
- <View
298
- style={[
299
- styles.indicator,
300
- indicatorShift !== 0 && {
301
- transform: [{ translateX: indicatorShift }],
302
- },
303
- ]}
304
- />
248
+ <View style={styles.indicator} />
305
249
  </View>
306
250
  <View
307
251
  style={[
@@ -312,25 +256,17 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
312
256
  },
313
257
  ]}
314
258
  >
315
- {showButtons && showBack ? (
316
- <TouchableOpacity style={styles.iconButton} onPress={onPressBack}>
317
- <Icon source={iconBack} />
318
- </TouchableOpacity>
259
+ {showBack ? (
260
+ <ModalIconButton icon={iconBack} onPress={onPressBack} />
319
261
  ) : null}
320
262
 
321
263
  {options.header ? (
322
264
  <View style={Styles.flex}>{options.header}</View>
323
265
  ) : (
324
266
  <>
325
- {showButtons && !leftOptions ? (
326
- <View style={styles.iconButton} />
327
- ) : null}
267
+ {!leftOptions ? <View style={styles.iconButtonSpace} /> : null}
328
268
  <Text
329
- style={[
330
- Styles.flex,
331
- styles.title,
332
- !showButtons && styles.titleInset,
333
- ]}
269
+ style={[Styles.flex, styles.title]}
334
270
  numberOfLines={1}
335
271
  typography="header_default_bold"
336
272
  >
@@ -338,18 +274,12 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
338
274
  </Text>
339
275
  </>
340
276
  )}
341
- {showButtons ? (
342
- <TouchableOpacity style={styles.iconButton} onPress={onPressClose}>
343
- <Icon source="navigation_close" />
344
- </TouchableOpacity>
345
- ) : null}
277
+ <ModalIconButton icon="navigation_close" onPress={onPressClose} />
346
278
  </View>
347
279
  </View>
348
280
  );
349
281
  }, [
350
- hasSideColumn,
351
282
  iconBack,
352
- indicatorShift,
353
283
  leftOptions,
354
284
  onPressBack,
355
285
  onPressClose,
@@ -361,41 +291,6 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
361
291
  useDivider,
362
292
  ]);
363
293
 
364
- /**
365
- * iPhone Duo folded: back and close move to the side bar column, drawn on
366
- * the part of the sheet under the side safe area. They line up with the
367
- * header row, stay below the status bar cluster on a tall sheet and above a
368
- * camera at the bottom end of the strip.
369
- */
370
- const renderSideColumn = () => {
371
- if (!sideColumn) {
372
- return null;
373
- }
374
- const lowestTop =
375
- windowSize.height - sideColumn.bottom - sideColumnHeight - sheetTop;
376
- const top = Math.min(
377
- Math.max(sideColumn.top - sheetTop, SIDE_COLUMN_OFFSET),
378
- lowestTop,
379
- );
380
-
381
- return (
382
- <View
383
- pointerEvents={'box-none'}
384
- style={[
385
- styles.sideColumn,
386
- sideColumn.edge === 'right' ? styles.alignRight : styles.alignLeft,
387
- InnerEdgeStyles[sideColumn.edge],
388
- { top, width: sideColumnWidth },
389
- ]}
390
- >
391
- {showBack ? (
392
- <SideBarButton icon={iconBack} onPress={onPressBack} />
393
- ) : null}
394
- <SideBarButton icon="navigation_close" onPress={onPressClose} />
395
- </View>
396
- );
397
- };
398
-
399
294
  const animatedStyle = useAnimatedStyle(() => {
400
295
  return {
401
296
  transform: [{ translateY: translateY.value }],
@@ -407,17 +302,12 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
407
302
  opacity: interpolate(
408
303
  translateY.value,
409
304
  [0, heightDevice],
410
- [1, 0],
305
+ [scrimOpacity, 0],
411
306
  Extrapolation.CLAMP,
412
307
  ),
413
308
  };
414
309
  });
415
310
 
416
- const sideInsetStyle = isDuoFloating && {
417
- paddingLeft: sideInsetLeft,
418
- paddingRight: sideInsetRight,
419
- };
420
-
421
311
  const screen = (
422
312
  <Screen
423
313
  {...props}
@@ -458,41 +348,31 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
458
348
  </Pressable>
459
349
 
460
350
  <Animated.View
461
- onLayout={
462
- isDuoFloating ? e => setSheetTop(e.nativeEvent.layout.y) : undefined
463
- }
464
351
  style={[
465
352
  animatedStyle,
466
353
  {
467
354
  backgroundColor,
468
- borderTopLeftRadius: isDuo ? Radius.XL : Radius.M,
469
- borderTopRightRadius: isDuo ? Radius.XL : Radius.M,
355
+ borderTopLeftRadius: sheet.radius,
356
+ borderTopRightRadius: sheet.radius,
470
357
  overflow: 'hidden',
471
358
  },
472
- isDuo && {
473
- maxHeight: windowSize.height - topGap - keyboardLift,
474
- marginBottom: keyboardLift,
475
- },
476
- isDuoFloating && { marginHorizontal: Spacing.M },
477
- sideColumn && {
478
- minHeight:
479
- SIDE_COLUMN_OFFSET + sideColumnHeight + sideColumn.bottom,
480
- },
481
- isDuoCentered && {
482
- // keeps the portrait width in landscape, like system sheets
483
- width: Math.min(
484
- Math.min(windowSize.width, windowSize.height) - Spacing.L * 2,
485
- windowSize.width -
486
- Math.max(insets.left, insets.right, Spacing.L) * 2,
487
- ),
488
- alignSelf: 'center',
489
- },
359
+ isDuo && { marginBottom: keyboardLift },
360
+ isFoldable &&
361
+ sheet.width !== undefined &&
362
+ sheet.maxHeight !== undefined && {
363
+ // centered in the placement area, clear of the iPhone Duo
364
+ // control column and on the right half when half folded
365
+ width: sheet.width,
366
+ marginLeft: placement.x + (placement.width - sheet.width) / 2,
367
+ maxHeight: sheet.maxHeight - keyboardLift,
368
+ minHeight: sheet.minHeight,
369
+ },
490
370
  showBaseLineDebug && styles.debugBaseLine,
491
371
  ]}
492
372
  >
493
- {isDuo ? (
373
+ {isFoldable ? (
494
374
  // content shrinks so a tall sheet keeps its header and footer
495
- <View style={[styles.shrink, sideInsetStyle]}>
375
+ <View style={styles.shrink}>
496
376
  {renderHeader()}
497
377
  <View style={styles.shrink}>{screen}</View>
498
378
  </View>
@@ -510,7 +390,6 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
510
390
  paddingBottom: Spacing.S,
511
391
  backgroundColor: theme.colors.background.surface,
512
392
  },
513
- sideInsetStyle,
514
393
  ]}
515
394
  >
516
395
  {footerComponent}
@@ -524,7 +403,6 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
524
403
  }}
525
404
  />
526
405
  )}
527
- {renderSideColumn()}
528
406
  </Animated.View>
529
407
  </KeyboardAvoidingView>
530
408
  </Container>
@@ -538,7 +416,7 @@ const styles = StyleSheet.create({
538
416
  },
539
417
  overlay: {
540
418
  flex: 1,
541
- backgroundColor: 'rgba(0, 0, 0, 0.6)',
419
+ backgroundColor: Colors.black_20,
542
420
  },
543
421
  indicator: {
544
422
  width: 40,
@@ -553,23 +431,15 @@ const styles = StyleSheet.create({
553
431
  height: HEADER_HEIGHT,
554
432
  flexDirection: 'row',
555
433
  alignItems: 'center',
434
+ paddingHorizontal: Spacing.M,
435
+ gap: Spacing.S,
556
436
  },
557
437
  title: { textAlign: 'center' },
558
- titleInset: { paddingHorizontal: Spacing.M },
559
- iconButton: {
560
- width: 24,
561
- height: 24,
562
- alignItems: 'center',
563
- justifyContent: 'center',
564
- marginHorizontal: Spacing.M,
438
+ iconButtonSpace: {
439
+ width: MODAL_ICON_BUTTON_SIZE,
440
+ height: MODAL_ICON_BUTTON_SIZE,
565
441
  },
566
442
  shrink: { flexShrink: 1 },
567
- sideColumn: {
568
- position: 'absolute',
569
- gap: Spacing.M,
570
- },
571
- alignRight: { right: 0 },
572
- alignLeft: { left: 0 },
573
443
  debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
574
444
  });
575
445
 
@@ -0,0 +1,66 @@
1
+ import React, { useContext } from 'react';
2
+ import {
3
+ StyleProp,
4
+ StyleSheet,
5
+ TouchableOpacity,
6
+ ViewStyle,
7
+ } from 'react-native';
8
+ import { ApplicationContext } from '../Context';
9
+ import { Radius, Spacing } from '../Consts';
10
+ import { Icon } from '../Icon';
11
+
12
+ // modal spec: icon button 32, icon 20, shared with the Compose / SwiftUI kits
13
+ const MODAL_ICON_BUTTON_SIZE = Spacing.XXL;
14
+ const MODAL_ICON_SIZE = 20;
15
+
16
+ type ModalIconButtonProps = {
17
+ icon: string;
18
+ onPress: () => void;
19
+ accessibilityLabel?: string;
20
+ style?: StyleProp<ViewStyle>;
21
+ };
22
+
23
+ /**
24
+ * Close / back button of popups and bottom sheet headers
25
+ */
26
+ const ModalIconButton: React.FC<ModalIconButtonProps> = ({
27
+ icon,
28
+ onPress,
29
+ accessibilityLabel,
30
+ style,
31
+ }) => {
32
+ const { theme } = useContext(ApplicationContext);
33
+ return (
34
+ <TouchableOpacity
35
+ accessibilityLabel={accessibilityLabel}
36
+ onPress={onPress}
37
+ style={[
38
+ styles.button,
39
+ {
40
+ backgroundColor: theme.colors.background.surface,
41
+ borderColor: theme.colors.border.default,
42
+ },
43
+ style,
44
+ ]}
45
+ >
46
+ <Icon
47
+ source={icon}
48
+ color={theme.colors.text.default}
49
+ size={MODAL_ICON_SIZE}
50
+ />
51
+ </TouchableOpacity>
52
+ );
53
+ };
54
+
55
+ const styles = StyleSheet.create({
56
+ button: {
57
+ width: MODAL_ICON_BUTTON_SIZE,
58
+ height: MODAL_ICON_BUTTON_SIZE,
59
+ borderRadius: Radius.L,
60
+ borderWidth: 1,
61
+ alignItems: 'center',
62
+ justifyContent: 'center',
63
+ },
64
+ });
65
+
66
+ export { ModalIconButton, MODAL_ICON_BUTTON_SIZE };
@@ -1,4 +1,10 @@
1
- import React, { useCallback, useContext, useEffect, useRef } from 'react';
1
+ import React, {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ } from 'react';
2
8
  import {
3
9
  Animated,
4
10
  BackHandler,
@@ -11,14 +17,14 @@ import {
11
17
  useWindowDimensions,
12
18
  View,
13
19
  } from 'react-native';
14
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
15
20
 
16
21
  import { ApplicationContext, MiniAppContext } from '../Context';
17
- import { Styles } from '../Consts';
22
+ import { Colors, Styles } from '../Consts';
18
23
  import Navigation from './Navigation';
19
24
  import { ModalParams } from './types';
20
25
  import BottomSheet from './BottomSheet';
21
- import { getDuoModalBounds, isIPhoneDuo } from './device';
26
+ import { isIPhoneDuo } from './device';
27
+ import { useModalLayout } from './modalLayout';
22
28
  import { scheduleOnRN } from 'react-native-worklets';
23
29
 
24
30
  const ModalScreen: React.FC<any> = props => {
@@ -40,9 +46,9 @@ const Modal: React.FC<ModalParams> = props => {
40
46
  title,
41
47
  } = props.route.params;
42
48
  const Component = useRef(screen).current;
43
- const insets = useSafeAreaInsets();
44
- const windowSize = useWindowDimensions();
49
+ const { width, height } = useWindowDimensions();
45
50
  const isDuo = isIPhoneDuo();
51
+ const { isFoldable, placement, popup, scrimOpacity } = useModalLayout();
46
52
 
47
53
  /**
48
54
  * Fire auto_popup_displayed tracking event on mount
@@ -61,6 +67,15 @@ const Modal: React.FC<ModalParams> = props => {
61
67
 
62
68
  const opacity = useRef(new Animated.Value(0)).current;
63
69
  const scale = useRef(new Animated.Value(0.8)).current;
70
+ const scrimStyle = useMemo(
71
+ () => ({
72
+ opacity: opacity.interpolate({
73
+ inputRange: [0, 1],
74
+ outputRange: [0, scrimOpacity],
75
+ }),
76
+ }),
77
+ [opacity, scrimOpacity],
78
+ );
64
79
 
65
80
  const navigation = new Navigation(props.navigation);
66
81
  const params = {
@@ -146,7 +161,7 @@ const Modal: React.FC<ModalParams> = props => {
146
161
  style={StyleSheet.absoluteFillObject}
147
162
  onPress={() => onDismiss(undefined, barrierDismissible)}
148
163
  >
149
- <Animated.View style={[styles.overlayContent, { opacity }]} />
164
+ <Animated.View style={[styles.overlayContent, scrimStyle]} />
150
165
  </Pressable>
151
166
  <Animated.View
152
167
  style={[
@@ -154,13 +169,15 @@ const Modal: React.FC<ModalParams> = props => {
154
169
  opacity,
155
170
  transform: [{ scale }],
156
171
  },
157
- isDuo && styles.shrink,
158
- isDuo && {
159
- marginTop: insets.top,
160
- marginBottom: insets.bottom,
161
- marginLeft: insets.left,
162
- marginRight: insets.right,
163
- ...getDuoModalBounds(insets, windowSize.width, windowSize.height),
172
+ // centered in the placement area, at most a popup wide
173
+ isFoldable && styles.shrink,
174
+ isFoldable && {
175
+ marginTop: placement.y,
176
+ marginBottom: height - placement.y - placement.height,
177
+ marginLeft: placement.x,
178
+ marginRight: width - placement.x - placement.width,
179
+ maxWidth: popup.width,
180
+ maxHeight: popup.maxHeight,
164
181
  },
165
182
  modalStyle,
166
183
  ]}
@@ -177,7 +194,7 @@ const DUO_ORIENTATIONS = ['portrait', 'landscape'];
177
194
  const styles = StyleSheet.create({
178
195
  overlayContent: {
179
196
  flex: 1,
180
- backgroundColor: 'rgba(0, 0, 0, 0.6)',
197
+ backgroundColor: Colors.black_20,
181
198
  },
182
199
  shrink: { flexShrink: 1 },
183
200
  });
@@ -0,0 +1,128 @@
1
+ // document/__mocks__ shadows react-native with react-native-web for the doc site
2
+ jest.mock('react-native', () => jest.requireActual('react-native'));
3
+
4
+ import { getModalLayout } from '../modalLayout';
5
+
6
+ const NO_INSETS = { top: 0, right: 0, bottom: 0, left: 0 };
7
+
8
+ describe('getModalLayout', () => {
9
+ it('keeps the phone look', () => {
10
+ const layout = getModalLayout({
11
+ width: 390,
12
+ height: 844,
13
+ insets: { top: 47, right: 0, bottom: 34, left: 0 },
14
+ isDuo: false,
15
+ });
16
+ expect(layout.isFoldable).toBe(false);
17
+ expect(layout.popup.width).toBe(342);
18
+ expect(layout.popup.maxHeight).toBeUndefined();
19
+ expect(layout.popup.radius).toBe(16);
20
+ expect(layout.sheet.width).toBeUndefined();
21
+ expect(layout.sheet.maxHeight).toBeUndefined();
22
+ expect(layout.sheet.radius).toBe(12);
23
+ expect(layout.scrimOpacity).toBe(0.6);
24
+ });
25
+
26
+ it('keeps the phone look in landscape', () => {
27
+ const layout = getModalLayout({
28
+ width: 844,
29
+ height: 390,
30
+ insets: { top: 0, right: 47, bottom: 21, left: 47 },
31
+ isDuo: false,
32
+ });
33
+ expect(layout.isFoldable).toBe(false);
34
+ expect(layout.popup.width).toBe(844 - 48);
35
+ expect(layout.popup.maxHeight).toBeUndefined();
36
+ expect(layout.popup.radius).toBe(16);
37
+ expect(layout.sheet.width).toBeUndefined();
38
+ expect(layout.scrimOpacity).toBe(0.6);
39
+ });
40
+
41
+ it('caps popup and sheet on a wide foldable window', () => {
42
+ const layout = getModalLayout({
43
+ width: 884,
44
+ height: 1104,
45
+ insets: { top: 24, right: 0, bottom: 20, left: 0 },
46
+ isDuo: false,
47
+ });
48
+ expect(layout.isFoldable).toBe(true);
49
+ expect(layout.popup).toMatchObject({
50
+ width: 450,
51
+ minWidth: 280,
52
+ maxWidth: 450,
53
+ maxHeight: 600,
54
+ margin: 24,
55
+ radius: 24,
56
+ });
57
+ expect(layout.sheet).toMatchObject({
58
+ width: 653,
59
+ maxHeight: 1104 - 24 - 12,
60
+ minHeight: 230,
61
+ margin: 12,
62
+ radius: 24,
63
+ });
64
+ expect(layout.scrimOpacity).toBe(0.2);
65
+ });
66
+
67
+ it('limits popup height to the safe area minus 48', () => {
68
+ const layout = getModalLayout({
69
+ width: 900,
70
+ height: 600,
71
+ insets: { top: 24, right: 0, bottom: 20, left: 0 },
72
+ isDuo: false,
73
+ });
74
+ expect(layout.popup.maxHeight).toBe(600 - 24 - 20 - 48);
75
+ });
76
+
77
+ it('places modals left of the control column on the iPhone Duo outer display', () => {
78
+ const layout = getModalLayout({
79
+ width: 420,
80
+ height: 760,
81
+ insets: { top: 0, right: 64, bottom: 20, left: 0 },
82
+ isDuo: true,
83
+ });
84
+ expect(layout.isFoldable).toBe(true);
85
+ expect(layout.placement).toEqual({ x: 0, y: 0, width: 356, height: 740 });
86
+ expect(layout.popup.margin).toBe(12);
87
+ expect(layout.popup.width).toBe(356 - 24);
88
+ expect(layout.sheet.width).toBe(356 - 24);
89
+ });
90
+
91
+ it('never grows past the margins when the space is under the min width', () => {
92
+ const layout = getModalLayout({
93
+ width: 300,
94
+ height: 600,
95
+ insets: NO_INSETS,
96
+ isDuo: true,
97
+ });
98
+ expect(layout.popup.width).toBe(300 - 24);
99
+ expect(layout.popup.minWidth).toBe(300 - 24);
100
+ expect(layout.sheet.width).toBe(300 - 24);
101
+ });
102
+
103
+ it('moves to the right half when iPhone Duo is half folded', () => {
104
+ const layout = getModalLayout({
105
+ width: 1000,
106
+ height: 800,
107
+ insets: NO_INSETS,
108
+ isDuo: true,
109
+ hinge: { x: 495, y: 0, width: 10, height: 800 },
110
+ });
111
+ expect(layout.placement).toEqual({ x: 505, y: 0, width: 495, height: 800 });
112
+ expect(layout.popup.margin).toBe(12);
113
+ expect(layout.popup.width).toBe(450);
114
+ expect(layout.sheet.width).toBe(495 - 24);
115
+ });
116
+
117
+ it('ignores a division off the iPhone Duo', () => {
118
+ const layout = getModalLayout({
119
+ width: 1000,
120
+ height: 800,
121
+ insets: NO_INSETS,
122
+ isDuo: false,
123
+ hinge: { x: 495, y: 0, width: 10, height: 800 },
124
+ });
125
+ expect(layout.placement.x).toBe(0);
126
+ expect(layout.popup.margin).toBe(24);
127
+ });
128
+ });
@@ -1,13 +1,8 @@
1
1
  import { Dimensions, Platform, ScaledSize } from 'react-native';
2
- import type { EdgeInsets } from 'react-native-safe-area-context';
3
- import { Spacing } from '../Consts';
4
2
 
5
3
  // widest short side of any single-display iPhone
6
4
  const IPHONE_MAX_SHORT_SIDE = 440;
7
5
 
8
- const DUO_MODAL_MAX_WIDTH = 440;
9
- const DUO_MODAL_MAX_HEIGHT = 640;
10
-
11
6
  // iPhone Duo reports idiom "phone"; 'screen' stays on the outer display, so it
12
7
  // still matches when Split View narrows the window
13
8
  const isIPhoneDuo = () => {
@@ -23,30 +18,4 @@ const isIPhoneDuo = () => {
23
18
  );
24
19
  };
25
20
 
26
- // portrait side status bar is about twice as tall as its inset is wide
27
- const getDuoTopInset = (insets: EdgeInsets, width: number, height: number) =>
28
- Math.max(
29
- insets.top,
30
- height > width ? Math.max(insets.left, insets.right) * 2 : 0,
31
- );
32
-
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 };
21
+ export { isIPhoneDuo };
@@ -18,6 +18,8 @@ import Navigation from './Navigation';
18
18
  import Navigator from './Navigator';
19
19
  import ScaleSizeProvider from './ScaleSizeProvider';
20
20
  import { useTooltipPortal } from './TooltipPortal';
21
+ import { getModalLayout, useModalLayout } from './modalLayout';
22
+ import type { ModalLayout, ModalRect } from './modalLayout';
21
23
 
22
24
  export {
23
25
  NavigationContainer,
@@ -31,4 +33,8 @@ export {
31
33
  exportHeaderTitle,
32
34
  ScaleSizeProvider,
33
35
  useTooltipPortal,
36
+ getModalLayout,
37
+ useModalLayout,
34
38
  };
39
+
40
+ export type { ModalLayout, ModalRect };
@@ -0,0 +1,180 @@
1
+ import { useWindowDimensions } from 'react-native';
2
+ import {
3
+ EdgeInsets,
4
+ useSafeAreaInsets,
5
+ } from 'react-native-safe-area-context';
6
+ import { Radius, Spacing } from '../Consts';
7
+ import { ReservedRegion, useHinge } from '../Layout/ReservedRegions';
8
+ import { isIPhoneDuo } from './device';
9
+
10
+ // Foldable modal spec, shared with the Compose / SwiftUI kits
11
+ const POPUP_MAX_WIDTH = 450;
12
+ const POPUP_MIN_WIDTH = 280;
13
+ const POPUP_MAX_HEIGHT = 600;
14
+ const SHEET_MAX_WIDTH = 653;
15
+ const SHEET_MIN_WIDTH = 296;
16
+ const SHEET_MIN_HEIGHT = 230;
17
+ const FOLDABLE_MIN_WIDTH = 600;
18
+
19
+ const SCRIM_OPACITY = 0.6;
20
+ const FOLDABLE_SCRIM_OPACITY = 0.2;
21
+
22
+ export type ModalRect = {
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ height: number;
27
+ };
28
+
29
+ export type ModalLayout = {
30
+ /**
31
+ * iPhone Duo, or a window whose short side is at least 600 (Galaxy Z Fold / Pixel
32
+ * Fold inner display). Phones, landscape included, keep the regular modal look.
33
+ */
34
+ isFoldable: boolean;
35
+
36
+ /**
37
+ * Area popups and sheets are placed in: the safe area, left of the control column on
38
+ * the iPhone Duo outer display, the right half when iPhone Duo is half folded.
39
+ */
40
+ placement: ModalRect;
41
+
42
+ popup: {
43
+ width: number;
44
+ minWidth: number;
45
+ maxWidth: number;
46
+ /**
47
+ * `undefined` on phones, the popup grows with its content.
48
+ */
49
+ maxHeight?: number;
50
+ margin: number;
51
+ radius: number;
52
+ };
53
+
54
+ sheet: {
55
+ /**
56
+ * `undefined` on phones, the sheet spans the window.
57
+ */
58
+ width?: number;
59
+ maxHeight?: number;
60
+ minHeight?: number;
61
+ margin: number;
62
+ radius: number;
63
+ };
64
+
65
+ scrimOpacity: number;
66
+ };
67
+
68
+ type ModalLayoutInput = {
69
+ width: number;
70
+ height: number;
71
+ insets: EdgeInsets;
72
+ isDuo: boolean;
73
+ /**
74
+ * vertical division reported by the system, iPhone Duo half folded
75
+ */
76
+ hinge?: ReservedRegion;
77
+ };
78
+
79
+ const clamp = (value: number, min: number, max: number) =>
80
+ Math.min(Math.max(value, min), max);
81
+
82
+ /**
83
+ * Modal geometry for a window, pure so it can be tested without a device
84
+ */
85
+ const getModalLayout = ({
86
+ width,
87
+ height,
88
+ insets,
89
+ isDuo,
90
+ hinge,
91
+ }: ModalLayoutInput): ModalLayout => {
92
+ const shortSide = Math.min(width, height);
93
+ // short side, so a phone in landscape stays a phone
94
+ const isFoldable = isDuo || shortSide >= FOLDABLE_MIN_WIDTH;
95
+ const isDuoOuter = isDuo && shortSide < FOLDABLE_MIN_WIDTH;
96
+ const hingeRight = hinge ? hinge.x + hinge.width : 0;
97
+ const isHalfFolded =
98
+ isDuo && !!hinge && hinge.x > 0 && hingeRight < width - insets.right;
99
+
100
+ const safeLeft = isHalfFolded ? hingeRight : insets.left;
101
+ const placement = {
102
+ x: safeLeft,
103
+ y: insets.top,
104
+ width: width - insets.right - safeLeft,
105
+ height: height - insets.top - insets.bottom,
106
+ };
107
+
108
+ if (!isFoldable) {
109
+ const popupWidth = width - Spacing.XL * 2;
110
+ return {
111
+ isFoldable,
112
+ placement,
113
+ popup: {
114
+ width: popupWidth,
115
+ minWidth: popupWidth,
116
+ maxWidth: popupWidth,
117
+ margin: Spacing.XL,
118
+ radius: Radius.L,
119
+ },
120
+ sheet: { margin: 0, radius: Radius.M },
121
+ scrimOpacity: SCRIM_OPACITY,
122
+ };
123
+ }
124
+
125
+ const popupMargin = isDuoOuter || isHalfFolded ? Spacing.M : Spacing.XL;
126
+ const popupSpace = placement.width - popupMargin * 2;
127
+ const popupMinWidth = Math.min(POPUP_MIN_WIDTH, popupSpace);
128
+
129
+ const sheetMargin = Spacing.M;
130
+ const sheetSpace = placement.width - sheetMargin * 2;
131
+ const sheetMaxHeight = height - insets.top - Spacing.M;
132
+
133
+ return {
134
+ isFoldable,
135
+ placement,
136
+ popup: {
137
+ width: clamp(popupSpace, popupMinWidth, POPUP_MAX_WIDTH),
138
+ minWidth: popupMinWidth,
139
+ maxWidth: POPUP_MAX_WIDTH,
140
+ maxHeight: Math.min(POPUP_MAX_HEIGHT, placement.height - Spacing.XL * 2),
141
+ margin: popupMargin,
142
+ radius: Radius.XL,
143
+ },
144
+ sheet: {
145
+ width: clamp(
146
+ sheetSpace,
147
+ Math.min(SHEET_MIN_WIDTH, sheetSpace),
148
+ SHEET_MAX_WIDTH,
149
+ ),
150
+ maxHeight: sheetMaxHeight,
151
+ minHeight: Math.min(SHEET_MIN_HEIGHT, sheetMaxHeight),
152
+ margin: sheetMargin,
153
+ radius: Radius.XL,
154
+ },
155
+ scrimOpacity: FOLDABLE_SCRIM_OPACITY,
156
+ };
157
+ };
158
+
159
+ /**
160
+ * Modal geometry for the current window, follows fold, unfold, rotation and the
161
+ * iPhone Duo hinge
162
+ */
163
+ const useModalLayout = (): ModalLayout => {
164
+ const { width, height } = useWindowDimensions();
165
+ const insets = useSafeAreaInsets();
166
+ const hinge = useHinge();
167
+ return getModalLayout({ width, height, insets, isDuo: isIPhoneDuo(), hinge });
168
+ };
169
+
170
+ export {
171
+ getModalLayout,
172
+ useModalLayout,
173
+ POPUP_MAX_WIDTH,
174
+ POPUP_MIN_WIDTH,
175
+ POPUP_MAX_HEIGHT,
176
+ SHEET_MAX_WIDTH,
177
+ SHEET_MIN_WIDTH,
178
+ SHEET_MIN_HEIGHT,
179
+ FOLDABLE_MIN_WIDTH,
180
+ };
@@ -46,7 +46,7 @@ const BadgeRibbon: FC<BadgeRibbonProps> = ({
46
46
  >
47
47
  <View
48
48
  collapsable={false}
49
- style={{ transform: textTransform, marginVertical: -2 }}
49
+ style={[styles.ribbonLabelWrapper, { transform: textTransform }]}
50
50
  >
51
51
  <Text
52
52
  style={styles.ribbonLabel}
@@ -60,7 +60,7 @@ const BadgeRibbon: FC<BadgeRibbonProps> = ({
60
60
  );
61
61
 
62
62
  const renderSkewContent = () => (
63
- <View style={{ flexDirection: 'row', marginTop: skewMargin }}>
63
+ <View style={[styles.ribbonSkewRow, { marginTop: skewMargin }]}>
64
64
  <View
65
65
  collapsable={false}
66
66
  style={[
@@ -70,7 +70,7 @@ const BadgeRibbon: FC<BadgeRibbonProps> = ({
70
70
  >
71
71
  <View
72
72
  collapsable={false}
73
- style={{ transform: textTransform, marginVertical: -2 }}
73
+ style={[styles.ribbonLabelWrapper, { transform: textTransform }]}
74
74
  >
75
75
  <Text
76
76
  style={styles.ribbonLabel}
package/Badge/styles.ts CHANGED
@@ -118,6 +118,8 @@ export default StyleSheet.create({
118
118
  ribbonLabel: {
119
119
  color: Colors.black_01,
120
120
  },
121
+ ribbonLabelWrapper: {marginVertical: -2},
122
+ ribbonSkewRow: {flexDirection: 'row'},
121
123
  skew: {width: 8, height: 16},
122
124
  tail: {width: 5, height: 20},
123
125
  debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
@@ -1,4 +1,4 @@
1
- import React, { createContext, ReactNode, useMemo } from 'react';
1
+ import React, { createContext, ReactNode, useContext, useMemo } from 'react';
2
2
  import { Platform, StyleSheet } from 'react-native';
3
3
 
4
4
  export type ReservedRegion = {
@@ -25,7 +25,15 @@ try {
25
25
  const ReservedRegionsContext = createContext<ReservedRegion[]>([]);
26
26
 
27
27
  /**
28
- * keeps the occlusion regions reported by the native provider, the only kind layout reads
28
+ * vertical seam splitting the display into left and right halves, e.g. iPhone Duo
29
+ * half folded. `undefined` while the display is flat or the system reports none
30
+ */
31
+ const HingeContext = createContext<ReservedRegion | undefined>(undefined);
32
+
33
+ const useHinge = () => useContext(HingeContext);
34
+
35
+ /**
36
+ * keeps the occlusion regions and the vertical division reported by the native provider
29
37
  */
30
38
  const OcclusionRegions: React.FC<{
31
39
  useRegions: NativeReservedRegions['useReservedRegions'];
@@ -39,10 +47,19 @@ const OcclusionRegions: React.FC<{
39
47
  .map(region => region.frame),
40
48
  [regions],
41
49
  );
50
+ const hinge = useMemo(
51
+ () =>
52
+ regions.find(
53
+ region =>
54
+ region.kind === 'division' &&
55
+ region.frame.height > region.frame.width,
56
+ )?.frame,
57
+ [regions],
58
+ );
42
59
 
43
60
  return (
44
61
  <ReservedRegionsContext.Provider value={occlusion}>
45
- {children}
62
+ <HingeContext.Provider value={hinge}>{children}</HingeContext.Provider>
46
63
  </ReservedRegionsContext.Provider>
47
64
  );
48
65
  };
@@ -75,4 +92,4 @@ const styles = StyleSheet.create({
75
92
  },
76
93
  });
77
94
 
78
- export { ReservedRegionsContext, ReservedRegionsProvider };
95
+ export { ReservedRegionsContext, ReservedRegionsProvider, useHinge };
package/Layout/index.ts CHANGED
@@ -10,6 +10,7 @@ import ItemSectionList from './ItemSectionList';
10
10
  import Screen from './Screen';
11
11
  import { TrackingScope } from './TrackingScope';
12
12
  import { ScreenProps } from './Screen';
13
+ import { useHinge } from './ReservedRegions';
13
14
 
14
15
  const GridContext = createContext<GridContextProps>({
15
16
  numberOfColumns: 12,
@@ -31,4 +32,5 @@ export {
31
32
  ItemSectionList,
32
33
  Screen,
33
34
  TrackingScope,
35
+ useHinge,
34
36
  };
@@ -1,19 +1,15 @@
1
1
  import React, { useContext, useRef, useState } from 'react';
2
- import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
2
+ import { ScrollView, StyleSheet, View } from 'react-native';
3
3
  import { ApplicationContext, MiniAppContext } from '../Context';
4
4
  import { Button } from '../Button';
5
- import { Colors, Radius, Spacing, Styles } from '../Consts';
6
- import { Icon } from '../Icon';
5
+ import { Colors, Spacing, Styles } from '../Consts';
7
6
  import { Image } from '../Image';
8
7
  import { Text, useScaleSize } from '../Text';
9
8
  import { PopupNotifyProps } from './types';
10
9
  import { scheduleOnRN } from 'react-native-worklets';
11
- import {
12
- getPopupRadius,
13
- splitHighlightText,
14
- usePopupMaxHeight,
15
- usePopupWidth,
16
- } from './utils';
10
+ import { useModalLayout } from '../Application/modalLayout';
11
+ import { ModalIconButton } from '../Application/ModalIconButton';
12
+ import { splitHighlightText } from './utils';
17
13
 
18
14
  const PopupNotify: React.FC<PopupNotifyProps> = ({
19
15
  image,
@@ -30,16 +26,19 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
30
26
  const context = useContext<any>(MiniAppContext);
31
27
  const { theme, navigator, translate } = useContext(ApplicationContext);
32
28
  const [scrollContent, setScrollContent] = useState(false);
33
- const popupWidth = usePopupWidth();
34
- const maxHeight = usePopupMaxHeight();
35
- const radius = getPopupRadius();
29
+ const { isFoldable, popup } = useModalLayout();
30
+ const { width: popupWidth, maxHeight, radius } = popup;
36
31
  const scaledMaxHeight = useScaleSize(172);
37
32
 
38
33
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
39
34
  const closeAction = useRef('touch_outside');
40
35
  let descriptionStyle = {};
41
36
  let Description: any = View;
42
- if (scrollContent) {
37
+ if (isFoldable) {
38
+ // foldables cap the popup height, the description takes what is left
39
+ Description = ScrollView;
40
+ descriptionStyle = styles.shrink;
41
+ } else if (scrollContent) {
43
42
  Description = ScrollView;
44
43
  descriptionStyle = { maxHeight: scaledMaxHeight };
45
44
  }
@@ -165,34 +164,20 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
165
164
  * build close action
166
165
  */
167
166
  const buildCloseIcon = () => {
168
- if (typeof onIconClose === 'function') {
169
- return (
170
- <TouchableOpacity
171
- accessibilityLabel={'ic_popup_close'}
172
- style={styles.iconCloseContainer}
173
- onPress={() => {
174
- closeAction.current = 'icon_close';
175
- onAction(onIconClose);
176
- }}
177
- >
178
- <View
179
- style={[
180
- styles.iconClose,
181
- {
182
- backgroundColor: theme.colors.text.default,
183
- borderColor: theme.colors.background.surface,
184
- },
185
- ]}
186
- >
187
- <Icon
188
- source="navigation_close"
189
- color={theme.colors.background.surface}
190
- size={14}
191
- />
192
- </View>
193
- </TouchableOpacity>
194
- );
167
+ if (typeof onIconClose !== 'function') {
168
+ return null;
195
169
  }
170
+ return (
171
+ <ModalIconButton
172
+ icon="navigation_close"
173
+ accessibilityLabel={'ic_popup_close'}
174
+ style={styles.iconClose}
175
+ onPress={() => {
176
+ closeAction.current = 'icon_close';
177
+ onAction(onIconClose);
178
+ }}
179
+ />
180
+ );
196
181
  };
197
182
 
198
183
  const body = (
@@ -206,11 +191,17 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
206
191
  ]}
207
192
  />
208
193
  )}
209
- <View style={styles.content}>
194
+ <View style={[styles.content, isFoldable && styles.shrink]}>
210
195
  <Text
211
196
  typography={'header_default_bold'}
212
197
  numberOfLines={3}
213
198
  accessibilityLabel={'title_popup_notify'}
199
+ // keeps the title clear of the close button on a text only popup
200
+ style={
201
+ !image &&
202
+ typeof onIconClose === 'function' &&
203
+ styles.titleClearClose
204
+ }
214
205
  >
215
206
  {title}
216
207
  </Text>
@@ -262,20 +253,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
262
253
  showBaseLineDebug && styles.debugBaseLine,
263
254
  ]}
264
255
  >
265
- {maxHeight === undefined ? (
266
- body
267
- ) : (
268
- // scrolls when iPhone Duo landscape is too short; actions stay visible
269
- <ScrollView
270
- style={[
271
- styles.body,
272
- { borderTopLeftRadius: radius, borderTopRightRadius: radius },
273
- ]}
274
- bounces={false}
275
- >
276
- {body}
277
- </ScrollView>
278
- )}
256
+ {body}
279
257
  <View style={styles.contentAction}>{buildAction()}</View>
280
258
  {buildCloseIcon()}
281
259
  </View>
@@ -288,7 +266,7 @@ const styles = StyleSheet.create({
288
266
  aspectRatio: 1.7777,
289
267
  overflow: 'hidden',
290
268
  },
291
- body: { flexShrink: 1, overflow: 'hidden' },
269
+ shrink: { flexShrink: 1 },
292
270
  content: { padding: Spacing.XL },
293
271
  description: {
294
272
  marginTop: Spacing.S,
@@ -304,19 +282,11 @@ const styles = StyleSheet.create({
304
282
  width: Spacing.S,
305
283
  height: Spacing.S,
306
284
  },
307
- iconCloseContainer: {
308
- position: 'absolute',
309
- top: -Spacing.M,
310
- right: -Spacing.M,
311
- padding: Spacing.XS,
312
- },
285
+ titleClearClose: { paddingRight: Spacing.XL },
313
286
  iconClose: {
314
- width: 20,
315
- height: 20,
316
- alignItems: 'center',
317
- justifyContent: 'center',
318
- borderRadius: Radius.M,
319
- borderWidth: 2,
287
+ position: 'absolute',
288
+ top: Spacing.M,
289
+ right: Spacing.M,
320
290
  },
321
291
  });
322
292
 
@@ -5,7 +5,7 @@ import { ApplicationContext, MiniAppContext } from '../Context';
5
5
  import { Image } from '../Image';
6
6
  import { Colors, Radius, Spacing } from '../Consts';
7
7
  import { Icon } from '../Icon';
8
- import { usePopupMaxHeight, usePopupWidth } from './utils';
8
+ import { useModalLayout } from '../Application/modalLayout';
9
9
 
10
10
  const ASPECT_RATIO = 0.72;
11
11
  const ICON_CLOSE_SIZE = 20;
@@ -20,9 +20,10 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
20
20
  }) => {
21
21
  const context = useContext<any>(MiniAppContext);
22
22
  const { theme, navigator } = useContext(ApplicationContext);
23
- const popupWidth = usePopupWidth();
24
- const maxHeight = usePopupMaxHeight();
25
- // fit the portrait image by height when iPhone Duo leaves too little room
23
+ const {
24
+ popup: { width: popupWidth, maxHeight },
25
+ } = useModalLayout();
26
+ // fit the portrait image by height when a foldable leaves too little room
26
27
  let imageWidth = popupWidth;
27
28
  if (maxHeight !== undefined) {
28
29
  const heightFitWidth = (maxHeight - CLOSE_ROW_HEIGHT) * ASPECT_RATIO;
package/Popup/utils.ts CHANGED
@@ -1,30 +1,5 @@
1
- import { Dimensions, useWindowDimensions } from 'react-native';
2
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
3
- import { Radius, Spacing } from '../Consts';
4
- import { getDuoModalBounds, isIPhoneDuo } from '../Application/device';
5
-
6
1
  type HighlightSegment = { text: string; highlight: boolean };
7
2
 
8
- const usePopupWidth = () => {
9
- const insets = useSafeAreaInsets();
10
- const { width, height } = useWindowDimensions();
11
- if (!isIPhoneDuo()) {
12
- return Dimensions.get('window').width - Spacing.XL * 2;
13
- }
14
- return getDuoModalBounds(insets, width, height).maxWidth;
15
- };
16
-
17
- const usePopupMaxHeight = () => {
18
- const insets = useSafeAreaInsets();
19
- const { width, height } = useWindowDimensions();
20
- if (!isIPhoneDuo()) {
21
- return undefined;
22
- }
23
- return getDuoModalBounds(insets, width, height).maxHeight;
24
- };
25
-
26
- const getPopupRadius = () => (isIPhoneDuo() ? Radius.XL : Radius.L);
27
-
28
3
  const isWordChar = (char?: string) => {
29
4
  if (!char) {
30
5
  return false;
@@ -76,4 +51,4 @@ const splitHighlightText = (
76
51
  return segments;
77
52
  };
78
53
 
79
- export { usePopupWidth, usePopupMaxHeight, getPopupRadius, splitHighlightText };
54
+ export { splitHighlightText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.168.1-beta.33",
3
+ "version": "0.168.1-beta.34",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},