@momo-kits/foundation 0.168.1-beta.35 → 0.168.1-beta.37

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,43 +16,24 @@ import {
16
16
  } from 'react-native';
17
17
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
18
18
  import { useHeaderHeight } from '@react-navigation/elements';
19
- import { Spacing } from '../Consts';
20
- import { DuoMetrics, useScreenLayout } from '../Layout/utils';
21
19
  import { scheduleOnRN } from 'react-native-worklets';
22
20
  import { BottomSheetParams, NavigatorProps } from './types';
23
- import { getDuoTopInset, isIPhoneDuo } from './utils';
24
- import { DUO_REGULAR_MIN_WIDTH, getSideInset } from './BottomSheet.styles';
21
+ import { isIPhoneDuo } from './utils';
22
+ import { useModalLayout } from './modalLayout';
25
23
 
24
+ /**
25
+ * Sheet geometry for the current window: foldable placement and size from
26
+ * `useModalLayout`, plus the insets and offsets the keyboard handling needs.
27
+ */
26
28
  export const useBottomSheetLayout = () => {
27
29
  const windowSize = useWindowDimensions();
28
30
  const isDuo = isIPhoneDuo();
29
- const isDuoCentered =
30
- isDuo &&
31
- Math.min(windowSize.width, windowSize.height) >= DUO_REGULAR_MIN_WIDTH;
32
- const isDuoFloating = isDuo && !isDuoCentered;
33
- const { sideBar } = useScreenLayout();
34
- const sideColumnWidth = (sideBar?.width ?? 0) - Spacing.M;
35
- const sideColumn =
36
- isDuoFloating &&
37
- sideColumnWidth >= DuoMetrics.controlSize + Spacing.M &&
38
- sideBar
39
- ? sideBar
40
- : undefined;
41
- const hasSideColumn = !!sideColumn;
31
+ const { isFoldable, placement, sheet, scrimOpacity } = useModalLayout();
42
32
  // 'screen' is the outer display on iPhone Duo, even when unfolded
43
33
  const screenHeight = Dimensions.get('screen').height;
44
34
  const heightDevice = isDuo ? windowSize.height : screenHeight;
45
35
  const insets = useSafeAreaInsets();
46
36
  const heightHeader = useHeaderHeight();
47
- const topGap = Math.max(
48
- sideColumn
49
- ? insets.top
50
- : getDuoTopInset(insets, windowSize.width, windowSize.height),
51
- Spacing.XL,
52
- );
53
- const sideInsetLeft = isDuoFloating ? getSideInset(insets.left) : 0;
54
- const sideInsetRight = isDuoFloating ? getSideInset(insets.right) : 0;
55
- const indicatorShift = (sideInsetRight - sideInsetLeft) / 2;
56
37
  const isIOS = Platform.OS === 'ios';
57
38
  const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
58
39
  const keyboardOffset = heightHeader - bottomInset;
@@ -60,17 +41,11 @@ export const useBottomSheetLayout = () => {
60
41
  return {
61
42
  windowSize,
62
43
  isDuo,
63
- isDuoCentered,
64
- isDuoFloating,
65
- sideColumn,
66
- sideColumnWidth,
67
- hasSideColumn,
44
+ isFoldable,
45
+ placement,
46
+ sheet,
47
+ scrimOpacity,
68
48
  heightDevice,
69
- insets,
70
- topGap,
71
- sideInsetLeft,
72
- sideInsetRight,
73
- indicatorShift,
74
49
  bottomInset,
75
50
  keyboardOffset,
76
51
  };
@@ -124,9 +99,14 @@ export const useDuoKeyboardLift = (
124
99
  return keyboardLift;
125
100
  };
126
101
 
102
+ /**
103
+ * Slide of the sheet and fade of the scrim behind it, up to the
104
+ * `scrimOpacity` of the modal spec (lighter on foldables).
105
+ */
127
106
  export const useSheetAnimatedStyles = (
128
107
  translateY: SharedValue<number>,
129
108
  heightDevice: number,
109
+ scrimOpacity: number,
130
110
  ) => {
131
111
  const animatedStyle = useAnimatedStyle(() => {
132
112
  return {
@@ -139,7 +119,7 @@ export const useSheetAnimatedStyles = (
139
119
  opacity: interpolate(
140
120
  translateY.value,
141
121
  [0, heightDevice],
142
- [1, 0],
122
+ [scrimOpacity, 0],
143
123
  Extrapolation.CLAMP,
144
124
  ),
145
125
  };
@@ -1,169 +1,90 @@
1
1
  import React from 'react';
2
- import {
3
- PanResponderInstance,
4
- ScaledSize,
5
- TouchableOpacity,
6
- View,
7
- } from 'react-native';
8
- import type { EdgeInsets } from 'react-native-safe-area-context';
9
- import { Radius, Spacing, Styles } from '../Consts';
2
+ import { PanResponderInstance, View, ViewStyle } from 'react-native';
3
+ import { Spacing, Styles } from '../Consts';
10
4
  import layoutStyles from '../Layout/styles';
11
5
  import { Text } from '../Text';
12
- import { Icon } from '../Icon';
13
- import { InnerEdgeStyles, type SideBarLayout } from '../Layout/utils';
14
- import { SideBarButton } from './Components/SideBarButton';
15
- import {
16
- INDICATOR_HEIGHT,
17
- SIDE_COLUMN_OFFSET,
18
- styles,
19
- } from './BottomSheet.styles';
6
+ import { INDICATOR_HEIGHT, styles } from './BottomSheet.styles';
7
+ import { ModalIconButton } from './ModalIconButton';
8
+ import { type ModalLayout } from './modalLayout';
20
9
  import { BottomSheetParams, Theme } from './types';
21
10
 
22
- type SheetPartsParams = {
11
+ type SheetHeaderParams = {
23
12
  panHandlers: PanResponderInstance['panHandlers'];
24
- indicatorShift: number;
25
13
  useDivider: boolean;
26
14
  theme: Theme;
27
- hasSideColumn: boolean;
28
15
  showBack: boolean;
29
16
  onPressBack: () => void;
30
17
  onPressClose: () => void;
31
18
  iconBack: string;
32
19
  options: BottomSheetParams['options'];
33
20
  leftOptions: BottomSheetParams['leftOptions'];
34
- sideColumn?: SideBarLayout;
35
- windowHeight: number;
36
- sheetTop: number;
37
- sideColumnHeight: number;
38
- sideColumnWidth: number;
39
21
  };
40
22
 
23
+ /**
24
+ * Drag handle and header row: back, title (or a custom header) and close, with
25
+ * the 32 modal icon buttons on every device.
26
+ */
41
27
  export const renderSheetHeader = ({
42
28
  panHandlers,
43
- indicatorShift,
44
29
  useDivider,
45
30
  theme,
46
- hasSideColumn,
47
31
  showBack,
48
32
  onPressBack,
49
33
  onPressClose,
50
34
  iconBack,
51
35
  options,
52
36
  leftOptions,
53
- }: SheetPartsParams) => {
54
- const showButtons = !hasSideColumn;
55
- return (
56
- <View {...panHandlers}>
57
- <View style={{ height: INDICATOR_HEIGHT }}>
58
- <View
59
- style={[
60
- styles.indicator,
61
- indicatorShift !== 0 && {
62
- transform: [{ translateX: indicatorShift }],
63
- },
64
- ]}
65
- />
66
- </View>
67
- <View
68
- style={[
69
- styles.header,
70
- useDivider && styles.headerDivider,
71
- useDivider && {
72
- borderBottomColor: theme.colors.border.default,
73
- },
74
- ]}
75
- >
76
- {showButtons && showBack ? (
77
- <TouchableOpacity style={styles.iconButton} onPress={onPressBack}>
78
- <Icon source={iconBack} />
79
- </TouchableOpacity>
80
- ) : null}
81
-
82
- {options.header ? (
83
- <View style={Styles.flex}>{options.header}</View>
84
- ) : (
85
- <>
86
- {showButtons && !leftOptions ? (
87
- <View style={styles.iconButton} />
88
- ) : null}
89
- <Text
90
- style={[
91
- Styles.flex,
92
- styles.title,
93
- !showButtons && styles.titleInset,
94
- ]}
95
- numberOfLines={1}
96
- typography="header_default_bold"
97
- >
98
- {options.title ?? ''}
99
- </Text>
100
- </>
101
- )}
102
- {showButtons ? (
103
- <TouchableOpacity style={styles.iconButton} onPress={onPressClose}>
104
- <Icon source="navigation_close" />
105
- </TouchableOpacity>
106
- ) : null}
107
- </View>
37
+ }: SheetHeaderParams) => (
38
+ <View {...panHandlers}>
39
+ <View style={{ height: INDICATOR_HEIGHT }}>
40
+ <View style={styles.indicator} />
108
41
  </View>
109
- );
110
- };
111
-
112
- /**
113
- * iPhone Duo folded: back and close move to the side bar column, drawn on
114
- * the part of the sheet under the side safe area. They line up with the
115
- * header row, stay below the status bar cluster on a tall sheet and above a
116
- * camera at the bottom end of the strip.
117
- */
118
- export const renderSheetSideColumn = ({
119
- sideColumn,
120
- windowHeight,
121
- sheetTop,
122
- sideColumnHeight,
123
- sideColumnWidth,
124
- showBack,
125
- iconBack,
126
- onPressBack,
127
- onPressClose,
128
- }: SheetPartsParams) => {
129
- if (!sideColumn) {
130
- return null;
131
- }
132
- const lowestTop =
133
- windowHeight - sideColumn.bottom - sideColumnHeight - sheetTop;
134
- const top = Math.min(
135
- Math.max(sideColumn.top - sheetTop, SIDE_COLUMN_OFFSET),
136
- lowestTop,
137
- );
138
-
139
- return (
140
42
  <View
141
- pointerEvents={'box-none'}
142
43
  style={[
143
- styles.sideColumn,
144
- sideColumn.edge === 'right' ? styles.alignRight : styles.alignLeft,
145
- InnerEdgeStyles[sideColumn.edge],
146
- { top, width: sideColumnWidth },
44
+ styles.header,
45
+ useDivider && styles.headerDivider,
46
+ useDivider && {
47
+ borderBottomColor: theme.colors.border.default,
48
+ },
147
49
  ]}
148
50
  >
149
51
  {showBack ? (
150
- <SideBarButton icon={iconBack} onPress={onPressBack} />
52
+ <ModalIconButton icon={iconBack} onPress={onPressBack} />
151
53
  ) : null}
152
- <SideBarButton icon="navigation_close" onPress={onPressClose} />
54
+
55
+ {options.header ? (
56
+ <View style={Styles.flex}>{options.header}</View>
57
+ ) : (
58
+ <>
59
+ {!leftOptions ? <View style={styles.iconButtonSpace} /> : null}
60
+ <Text
61
+ style={[Styles.flex, styles.title]}
62
+ numberOfLines={1}
63
+ typography="header_default_bold"
64
+ >
65
+ {options.title ?? ''}
66
+ </Text>
67
+ </>
68
+ )}
69
+ <ModalIconButton icon="navigation_close" onPress={onPressClose} />
153
70
  </View>
154
- );
155
- };
71
+ </View>
72
+ );
156
73
 
74
+ /**
75
+ * Footer pinned under the sheet content, `null` when the sheet has none.
76
+ */
157
77
  export const renderSheetFooter = ({
158
78
  footerComponent,
159
79
  theme,
160
- sideInsetStyle,
161
80
  }: {
162
81
  footerComponent: BottomSheetParams['footerComponent'];
163
82
  theme: Theme;
164
- sideInsetStyle: any;
165
- }) =>
166
- footerComponent && (
83
+ }) => {
84
+ if (!footerComponent) {
85
+ return null;
86
+ }
87
+ return (
167
88
  <View
168
89
  style={[
169
90
  layoutStyles.shadow,
@@ -171,61 +92,56 @@ export const renderSheetFooter = ({
171
92
  paddingBottom: Spacing.S,
172
93
  backgroundColor: theme.colors.background.surface,
173
94
  },
174
- sideInsetStyle,
175
95
  ]}
176
96
  >
177
97
  {footerComponent}
178
98
  </View>
179
99
  );
100
+ };
180
101
 
181
102
  type SheetStyleParams = {
182
103
  backgroundColor: string;
183
104
  isDuo: boolean;
184
- isDuoFloating: boolean;
185
- isDuoCentered: boolean;
186
- windowSize: ScaledSize;
187
- topGap: number;
105
+ isFoldable: boolean;
106
+ placement: ModalLayout['placement'];
107
+ sheet: ModalLayout['sheet'];
188
108
  keyboardLift: number;
189
- sideColumn?: SideBarLayout;
190
- sideColumnHeight: number;
191
- insets: EdgeInsets;
192
109
  showBaseLineDebug: boolean;
193
110
  };
194
111
 
112
+ /**
113
+ * Sheet container style. On foldables the sheet is centered in the placement
114
+ * area, clear of the iPhone Duo control column and on the right half when half
115
+ * folded; the keyboard lift comes off its max height.
116
+ */
195
117
  export const getSheetStyle = ({
196
118
  backgroundColor,
197
119
  isDuo,
198
- isDuoFloating,
199
- isDuoCentered,
200
- windowSize,
201
- topGap,
120
+ isFoldable,
121
+ placement,
122
+ sheet,
202
123
  keyboardLift,
203
- sideColumn,
204
- sideColumnHeight,
205
- insets,
206
124
  showBaseLineDebug,
207
- }: SheetStyleParams) => [
208
- styles.sheetContainer,
209
- {
210
- backgroundColor,
211
- borderTopLeftRadius: isDuo ? Radius.XL : Radius.M,
212
- borderTopRightRadius: isDuo ? Radius.XL : Radius.M,
213
- },
214
- isDuo && {
215
- maxHeight: windowSize.height - topGap - keyboardLift,
216
- marginBottom: keyboardLift,
217
- },
218
- isDuoFloating && { marginHorizontal: Spacing.M },
219
- sideColumn && {
220
- minHeight: SIDE_COLUMN_OFFSET + sideColumnHeight + sideColumn.bottom,
221
- },
222
- isDuoCentered && styles.centered,
223
- isDuoCentered && {
224
- // keeps the portrait width in landscape, like system sheets
225
- width: Math.min(
226
- Math.min(windowSize.width, windowSize.height) - Spacing.L * 2,
227
- windowSize.width - Math.max(insets.left, insets.right, Spacing.L) * 2,
228
- ),
229
- },
230
- showBaseLineDebug && styles.debugBaseLine,
231
- ];
125
+ }: SheetStyleParams) => {
126
+ const { width, maxHeight } = sheet;
127
+ let foldableStyle: ViewStyle | undefined;
128
+ if (isFoldable && width !== undefined && maxHeight !== undefined) {
129
+ foldableStyle = {
130
+ width,
131
+ marginLeft: placement.x + (placement.width - width) / 2,
132
+ maxHeight: maxHeight - keyboardLift,
133
+ minHeight: sheet.minHeight,
134
+ };
135
+ }
136
+ return [
137
+ styles.sheetContainer,
138
+ {
139
+ backgroundColor,
140
+ borderTopLeftRadius: sheet.radius,
141
+ borderTopRightRadius: sheet.radius,
142
+ },
143
+ isDuo && { marginBottom: keyboardLift },
144
+ foldableStyle,
145
+ showBaseLineDebug && styles.debugBaseLine,
146
+ ];
147
+ };
@@ -1,17 +1,10 @@
1
1
  import { StyleSheet } from 'react-native';
2
2
  import { Colors, Radius, Spacing } from '../Consts';
3
- import { DuoMetrics } from '../Layout/utils';
4
-
5
- // iPhone Duo inner display: center the sheet instead of spanning the width
6
- export const DUO_REGULAR_MIN_WIDTH = 600;
3
+ import { MODAL_ICON_BUTTON_SIZE } from './ModalIconButton';
7
4
 
8
5
  export const INDICATOR_HEIGHT = 16;
9
6
  export const HEADER_HEIGHT = 56;
10
7
  export const DUO_ORIENTATIONS = ['portrait', 'landscape'];
11
- export const SIDE_COLUMN_OFFSET =
12
- INDICATOR_HEIGHT + (HEADER_HEIGHT - DuoMetrics.controlSize) / 2;
13
-
14
- export const getSideInset = (inset: number) => Math.max(inset - Spacing.M, 0);
15
8
 
16
9
  export const styles = StyleSheet.create({
17
10
  container: {
@@ -20,7 +13,7 @@ export const styles = StyleSheet.create({
20
13
  },
21
14
  overlay: {
22
15
  flex: 1,
23
- backgroundColor: 'rgba(0, 0, 0, 0.6)',
16
+ backgroundColor: Colors.black_20,
24
17
  },
25
18
  indicator: {
26
19
  width: 40,
@@ -35,25 +28,16 @@ export const styles = StyleSheet.create({
35
28
  height: HEADER_HEIGHT,
36
29
  flexDirection: 'row',
37
30
  alignItems: 'center',
31
+ paddingHorizontal: Spacing.M,
32
+ gap: Spacing.S,
38
33
  },
39
34
  title: { textAlign: 'center' },
40
- titleInset: { paddingHorizontal: Spacing.M },
41
- iconButton: {
42
- width: 24,
43
- height: 24,
44
- alignItems: 'center',
45
- justifyContent: 'center',
46
- marginHorizontal: Spacing.M,
35
+ iconButtonSpace: {
36
+ width: MODAL_ICON_BUTTON_SIZE,
37
+ height: MODAL_ICON_BUTTON_SIZE,
47
38
  },
48
39
  shrink: { flexShrink: 1 },
49
- sideColumn: {
50
- position: 'absolute',
51
- gap: Spacing.M,
52
- },
53
- alignRight: { right: 0 },
54
- alignLeft: { left: 0 },
55
40
  debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
56
41
  headerDivider: { borderBottomWidth: 1 },
57
42
  sheetContainer: { overflow: 'hidden' },
58
- centered: { alignSelf: 'center' },
59
43
  });