@momo-kits/foundation 0.168.1-beta.32 → 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.
- package/Application/BottomSheet.tsx +9 -28
- package/Application/ModalIconButton.tsx +66 -0
- package/Badge/BadgeRibbon.tsx +3 -3
- package/Badge/styles.ts +2 -0
- package/Popup/PopupNotify.tsx +31 -65
- package/Popup/types.ts +7 -0
- package/Popup/utils.ts +54 -0
- package/package.json +1 -1
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
Platform,
|
|
18
18
|
Pressable,
|
|
19
19
|
StyleSheet,
|
|
20
|
-
TouchableOpacity,
|
|
21
20
|
useWindowDimensions,
|
|
22
21
|
View,
|
|
23
22
|
} from 'react-native';
|
|
@@ -26,7 +25,6 @@ import { ApplicationContext, MiniAppContext } from '../Context';
|
|
|
26
25
|
import { BottomSheetParams } from './types';
|
|
27
26
|
import { Colors, Radius, Spacing, Styles } from '../Consts';
|
|
28
27
|
import { Text } from '../Text';
|
|
29
|
-
import { Icon } from '../Icon';
|
|
30
28
|
import { useHeaderHeight } from '@react-navigation/elements';
|
|
31
29
|
import Animated, {
|
|
32
30
|
Easing,
|
|
@@ -41,13 +39,11 @@ import { scheduleOnRN } from 'react-native-worklets';
|
|
|
41
39
|
import layoutStyles from '../Layout/styles';
|
|
42
40
|
import { isIPhoneDuo } from './device';
|
|
43
41
|
import { useModalLayout } from './modalLayout';
|
|
42
|
+
import { MODAL_ICON_BUTTON_SIZE, ModalIconButton } from './ModalIconButton';
|
|
44
43
|
|
|
45
44
|
const INDICATOR_HEIGHT = 16;
|
|
46
45
|
const HEADER_HEIGHT = 56;
|
|
47
46
|
const DUO_ORIENTATIONS = ['portrait', 'landscape'];
|
|
48
|
-
// foldable spec: icon button 32, icon 20
|
|
49
|
-
const FOLDABLE_ICON_BUTTON_SIZE = 32;
|
|
50
|
-
const FOLDABLE_ICON_SIZE = 20;
|
|
51
47
|
|
|
52
48
|
const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
53
49
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
@@ -246,11 +242,6 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
246
242
|
* render header
|
|
247
243
|
*/
|
|
248
244
|
const renderHeader = useCallback(() => {
|
|
249
|
-
const iconButtonStyle = [
|
|
250
|
-
styles.iconButton,
|
|
251
|
-
isFoldable && styles.foldableIconButton,
|
|
252
|
-
];
|
|
253
|
-
const iconSize = isFoldable ? FOLDABLE_ICON_SIZE : undefined;
|
|
254
245
|
return (
|
|
255
246
|
<View {...panResponder.panHandlers}>
|
|
256
247
|
<View style={{ height: INDICATOR_HEIGHT }}>
|
|
@@ -266,16 +257,14 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
266
257
|
]}
|
|
267
258
|
>
|
|
268
259
|
{showBack ? (
|
|
269
|
-
<
|
|
270
|
-
<Icon source={iconBack} size={iconSize} />
|
|
271
|
-
</TouchableOpacity>
|
|
260
|
+
<ModalIconButton icon={iconBack} onPress={onPressBack} />
|
|
272
261
|
) : null}
|
|
273
262
|
|
|
274
263
|
{options.header ? (
|
|
275
264
|
<View style={Styles.flex}>{options.header}</View>
|
|
276
265
|
) : (
|
|
277
266
|
<>
|
|
278
|
-
{!leftOptions ? <View style={
|
|
267
|
+
{!leftOptions ? <View style={styles.iconButtonSpace} /> : null}
|
|
279
268
|
<Text
|
|
280
269
|
style={[Styles.flex, styles.title]}
|
|
281
270
|
numberOfLines={1}
|
|
@@ -285,15 +274,12 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
285
274
|
</Text>
|
|
286
275
|
</>
|
|
287
276
|
)}
|
|
288
|
-
<
|
|
289
|
-
<Icon source="navigation_close" size={iconSize} />
|
|
290
|
-
</TouchableOpacity>
|
|
277
|
+
<ModalIconButton icon="navigation_close" onPress={onPressClose} />
|
|
291
278
|
</View>
|
|
292
279
|
</View>
|
|
293
280
|
);
|
|
294
281
|
}, [
|
|
295
282
|
iconBack,
|
|
296
|
-
isFoldable,
|
|
297
283
|
leftOptions,
|
|
298
284
|
onPressBack,
|
|
299
285
|
onPressClose,
|
|
@@ -445,18 +431,13 @@ const styles = StyleSheet.create({
|
|
|
445
431
|
height: HEADER_HEIGHT,
|
|
446
432
|
flexDirection: 'row',
|
|
447
433
|
alignItems: 'center',
|
|
434
|
+
paddingHorizontal: Spacing.M,
|
|
435
|
+
gap: Spacing.S,
|
|
448
436
|
},
|
|
449
437
|
title: { textAlign: 'center' },
|
|
450
|
-
|
|
451
|
-
width:
|
|
452
|
-
height:
|
|
453
|
-
alignItems: 'center',
|
|
454
|
-
justifyContent: 'center',
|
|
455
|
-
marginHorizontal: Spacing.M,
|
|
456
|
-
},
|
|
457
|
-
foldableIconButton: {
|
|
458
|
-
width: FOLDABLE_ICON_BUTTON_SIZE,
|
|
459
|
-
height: FOLDABLE_ICON_BUTTON_SIZE,
|
|
438
|
+
iconButtonSpace: {
|
|
439
|
+
width: MODAL_ICON_BUTTON_SIZE,
|
|
440
|
+
height: MODAL_ICON_BUTTON_SIZE,
|
|
460
441
|
},
|
|
461
442
|
shrink: { flexShrink: 1 },
|
|
462
443
|
debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
|
|
@@ -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 };
|
package/Badge/BadgeRibbon.tsx
CHANGED
|
@@ -46,7 +46,7 @@ const BadgeRibbon: FC<BadgeRibbonProps> = ({
|
|
|
46
46
|
>
|
|
47
47
|
<View
|
|
48
48
|
collapsable={false}
|
|
49
|
-
style={{ transform: textTransform
|
|
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={{
|
|
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
|
|
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 },
|
package/Popup/PopupNotify.tsx
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import React, { useContext, useRef, useState } from 'react';
|
|
2
|
-
import { ScrollView, StyleSheet,
|
|
2
|
+
import { ScrollView, StyleSheet, View } from 'react-native';
|
|
3
3
|
import { ApplicationContext, MiniAppContext } from '../Context';
|
|
4
4
|
import { Button } from '../Button';
|
|
5
|
-
import { Colors,
|
|
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
10
|
import { useModalLayout } from '../Application/modalLayout';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const FOLDABLE_CLOSE_SIZE = 32;
|
|
15
|
-
const FOLDABLE_CLOSE_ICON_SIZE = 20;
|
|
11
|
+
import { ModalIconButton } from '../Application/ModalIconButton';
|
|
12
|
+
import { splitHighlightText } from './utils';
|
|
16
13
|
|
|
17
14
|
const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
18
15
|
image,
|
|
19
16
|
title = 'Title',
|
|
20
17
|
description = 'Description',
|
|
18
|
+
highlightText,
|
|
21
19
|
error,
|
|
22
20
|
secondary,
|
|
23
21
|
primary,
|
|
@@ -142,6 +140,26 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
142
140
|
return renderColumn();
|
|
143
141
|
};
|
|
144
142
|
|
|
143
|
+
/**
|
|
144
|
+
* build description text, highlighted keywords keep size/lineHeight and use weight 600
|
|
145
|
+
*/
|
|
146
|
+
const renderDescription = (text: string) => {
|
|
147
|
+
const segments = splitHighlightText(text, highlightText);
|
|
148
|
+
if (!segments) {
|
|
149
|
+
return text;
|
|
150
|
+
}
|
|
151
|
+
return segments.map((segment, index) => {
|
|
152
|
+
if (segment.highlight) {
|
|
153
|
+
return (
|
|
154
|
+
<Text key={index} typography={'header_s_semibold'}>
|
|
155
|
+
{segment.text}
|
|
156
|
+
</Text>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return segment.text;
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
145
163
|
/**
|
|
146
164
|
* build close action
|
|
147
165
|
*/
|
|
@@ -149,49 +167,16 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
149
167
|
if (typeof onIconClose !== 'function') {
|
|
150
168
|
return null;
|
|
151
169
|
}
|
|
152
|
-
if (isFoldable) {
|
|
153
|
-
return (
|
|
154
|
-
<TouchableOpacity
|
|
155
|
-
accessibilityLabel={'ic_popup_close'}
|
|
156
|
-
style={styles.foldableClose}
|
|
157
|
-
onPress={() => {
|
|
158
|
-
closeAction.current = 'icon_close';
|
|
159
|
-
onAction(onIconClose);
|
|
160
|
-
}}
|
|
161
|
-
>
|
|
162
|
-
<Icon
|
|
163
|
-
source="navigation_close"
|
|
164
|
-
color={theme.colors.text.default}
|
|
165
|
-
size={FOLDABLE_CLOSE_ICON_SIZE}
|
|
166
|
-
/>
|
|
167
|
-
</TouchableOpacity>
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
170
|
return (
|
|
171
|
-
<
|
|
171
|
+
<ModalIconButton
|
|
172
|
+
icon="navigation_close"
|
|
172
173
|
accessibilityLabel={'ic_popup_close'}
|
|
173
|
-
style={styles.
|
|
174
|
+
style={styles.iconClose}
|
|
174
175
|
onPress={() => {
|
|
175
176
|
closeAction.current = 'icon_close';
|
|
176
177
|
onAction(onIconClose);
|
|
177
178
|
}}
|
|
178
|
-
|
|
179
|
-
<View
|
|
180
|
-
style={[
|
|
181
|
-
styles.iconClose,
|
|
182
|
-
{
|
|
183
|
-
backgroundColor: theme.colors.text.default,
|
|
184
|
-
borderColor: theme.colors.background.surface,
|
|
185
|
-
},
|
|
186
|
-
]}
|
|
187
|
-
>
|
|
188
|
-
<Icon
|
|
189
|
-
source="navigation_close"
|
|
190
|
-
color={theme.colors.background.surface}
|
|
191
|
-
size={14}
|
|
192
|
-
/>
|
|
193
|
-
</View>
|
|
194
|
-
</TouchableOpacity>
|
|
179
|
+
/>
|
|
195
180
|
);
|
|
196
181
|
};
|
|
197
182
|
|
|
@@ -213,7 +198,6 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
213
198
|
accessibilityLabel={'title_popup_notify'}
|
|
214
199
|
// keeps the title clear of the close button on a text only popup
|
|
215
200
|
style={
|
|
216
|
-
isFoldable &&
|
|
217
201
|
!image &&
|
|
218
202
|
typeof onIconClose === 'function' &&
|
|
219
203
|
styles.titleClearClose
|
|
@@ -233,7 +217,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
233
217
|
}
|
|
234
218
|
}}
|
|
235
219
|
>
|
|
236
|
-
{description}
|
|
220
|
+
{renderDescription(description)}
|
|
237
221
|
</Text>
|
|
238
222
|
) : (
|
|
239
223
|
description
|
|
@@ -298,29 +282,11 @@ const styles = StyleSheet.create({
|
|
|
298
282
|
width: Spacing.S,
|
|
299
283
|
height: Spacing.S,
|
|
300
284
|
},
|
|
301
|
-
iconCloseContainer: {
|
|
302
|
-
position: 'absolute',
|
|
303
|
-
top: -Spacing.M,
|
|
304
|
-
right: -Spacing.M,
|
|
305
|
-
padding: Spacing.XS,
|
|
306
|
-
},
|
|
307
285
|
titleClearClose: { paddingRight: Spacing.XL },
|
|
308
|
-
|
|
286
|
+
iconClose: {
|
|
309
287
|
position: 'absolute',
|
|
310
288
|
top: Spacing.M,
|
|
311
289
|
right: Spacing.M,
|
|
312
|
-
width: FOLDABLE_CLOSE_SIZE,
|
|
313
|
-
height: FOLDABLE_CLOSE_SIZE,
|
|
314
|
-
alignItems: 'center',
|
|
315
|
-
justifyContent: 'center',
|
|
316
|
-
},
|
|
317
|
-
iconClose: {
|
|
318
|
-
width: 20,
|
|
319
|
-
height: 20,
|
|
320
|
-
alignItems: 'center',
|
|
321
|
-
justifyContent: 'center',
|
|
322
|
-
borderRadius: Radius.M,
|
|
323
|
-
borderWidth: 2,
|
|
324
290
|
},
|
|
325
291
|
});
|
|
326
292
|
|
package/Popup/types.ts
CHANGED
|
@@ -20,6 +20,13 @@ export type PopupNotifyProps = {
|
|
|
20
20
|
*/
|
|
21
21
|
description?: string | React.ReactNode;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* `highlightText`: Optional. List of words/phrases within `description` to be rendered in semibold.
|
|
25
|
+
* Only applies when `description` is a string. Matching is whole-word and case-sensitive;
|
|
26
|
+
* every occurrence of each string is highlighted.
|
|
27
|
+
*/
|
|
28
|
+
highlightText?: string[];
|
|
29
|
+
|
|
23
30
|
/**
|
|
24
31
|
* `error`: Optional. Represents additional error text to be displayed in the PopupNotify component.
|
|
25
32
|
*/
|
package/Popup/utils.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
type HighlightSegment = { text: string; highlight: boolean };
|
|
2
|
+
|
|
3
|
+
const isWordChar = (char?: string) => {
|
|
4
|
+
if (!char) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
const isLetter = char.toLowerCase() !== char.toUpperCase();
|
|
8
|
+
return isLetter || /[0-9_]/.test(char);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Split `text` into plain/highlighted segments (same matching as Information `highlightText`):
|
|
13
|
+
* whole-word, case-sensitive, every occurrence, longest keyword first.
|
|
14
|
+
* Returns `undefined` when there is nothing to highlight.
|
|
15
|
+
*/
|
|
16
|
+
const splitHighlightText = (
|
|
17
|
+
text: string,
|
|
18
|
+
highlightText?: string[],
|
|
19
|
+
): HighlightSegment[] | undefined => {
|
|
20
|
+
const keywords = highlightText?.filter(keyword => {
|
|
21
|
+
if (typeof keyword !== 'string') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return keyword.length > 0;
|
|
25
|
+
});
|
|
26
|
+
if (!keywords?.length) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const pattern = [...keywords]
|
|
30
|
+
.sort((a, b) => b.length - a.length)
|
|
31
|
+
.map(keyword => keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
32
|
+
.join('|');
|
|
33
|
+
const regex = new RegExp(pattern, 'g');
|
|
34
|
+
const segments: HighlightSegment[] = [];
|
|
35
|
+
let lastIndex = 0;
|
|
36
|
+
let match;
|
|
37
|
+
while ((match = regex.exec(text))) {
|
|
38
|
+
const start = match.index;
|
|
39
|
+
const end = start + match[0].length;
|
|
40
|
+
if (isWordChar(text[start - 1]) || isWordChar(text[end])) {
|
|
41
|
+
regex.lastIndex = start + 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (start > lastIndex) {
|
|
45
|
+
segments.push({ text: text.slice(lastIndex, start), highlight: false });
|
|
46
|
+
}
|
|
47
|
+
segments.push({ text: match[0], highlight: true });
|
|
48
|
+
lastIndex = end;
|
|
49
|
+
}
|
|
50
|
+
segments.push({ text: text.slice(lastIndex), highlight: false });
|
|
51
|
+
return segments;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { splitHighlightText };
|