@momo-kits/foundation 0.169.8 → 0.169.9
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.hooks.ts +27 -17
- package/Application/BottomSheet.tsx +35 -27
- package/Application/BottomTab/BottomTabBar.tsx +6 -6
- package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
- package/Application/BottomTab/TabScreen.tsx +11 -10
- package/Application/BottomTab/index.tsx +5 -5
- package/Application/BottomTab/usePressLift.ts +3 -3
- package/Application/Components/HeaderToolkitAction.tsx +26 -18
- package/Application/Components/useToolkitActions.tsx +22 -21
- package/Application/ModalScreen.tsx +19 -15
- package/Application/StackScreen.tsx +13 -9
- package/Application/modalLayout.ts +1 -3
- package/Context/index.ts +3 -1
- package/Image/index.tsx +6 -6
- package/Input/ErrorView.tsx +59 -0
- package/Input/FloatingView.tsx +101 -0
- package/Input/Input.tsx +40 -37
- package/Input/InputDropDown.tsx +13 -10
- package/Input/InputMoney.tsx +17 -17
- package/Input/InputMoneyView.tsx +12 -12
- package/Input/InputOTP.tsx +46 -34
- package/Input/InputPhoneNumber.tsx +48 -43
- package/Input/InputSearch.tsx +27 -24
- package/Input/InputTextArea.tsx +29 -23
- package/Input/RenderTrailing.tsx +79 -0
- package/Input/constants.ts +8 -0
- package/Input/utils.ts +126 -7
- package/Layout/FloatingButton.tsx +38 -24
- package/Layout/layoutMetrics.ts +43 -34
- package/Layout/screenTypes.ts +2 -3
- package/Pagination/PaginationScroll.tsx +5 -3
- package/Pagination/types.ts +3 -5
- package/Popup/PopupNotify.tsx +16 -13
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/types.ts +2 -4
- package/Skeleton/index.tsx +15 -12
- package/package.json +1 -1
- package/Input/common.tsx +0 -257
- package/Input/moneyUtils.ts +0 -23
package/Layout/layoutMetrics.ts
CHANGED
|
@@ -1,57 +1,66 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Header geometry
|
|
3
|
-
* - barHeight: navigation bar row below the status bar
|
|
4
|
-
* - iPhoneMinHeight: header never shorter than this on iPhone (landscape, iPhone Duo)
|
|
5
|
-
* - statusBarMinInset: a smaller top inset means the status bar is not on the top edge
|
|
6
|
-
* - largeWidth / largeMargin: @react-navigation/elements Header gives its content row
|
|
7
|
-
* 5pt side margins on iOS from 414pt wide
|
|
8
|
-
*/
|
|
1
|
+
/** Header geometry. */
|
|
9
2
|
export const HeaderMetrics = {
|
|
3
|
+
/** Navigation bar row below the status bar. */
|
|
10
4
|
barHeight: 52,
|
|
5
|
+
/** Header never shorter than this on iPhone (landscape, iPhone Duo). */
|
|
11
6
|
iPhoneMinHeight: 72,
|
|
7
|
+
/** A smaller top inset means the status bar is not on the top edge. */
|
|
12
8
|
statusBarMinInset: 20,
|
|
9
|
+
/**
|
|
10
|
+
* From this width @react-navigation/elements Header gives its content row
|
|
11
|
+
* `largeMargin` (5pt) side margins on iOS.
|
|
12
|
+
*/
|
|
13
13
|
largeWidth: 414,
|
|
14
14
|
largeMargin: 5,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* iPhone Duo geometry, iOS 27.1 (Xcode 27.1 simulator)
|
|
19
|
-
* - maxAspectRatio: both displays are near 1.42 (inner) and 1.45 (outer), every slab
|
|
20
|
-
* iPhone is 1.77 or taller
|
|
21
|
-
* - innerMinHeight: window height from which the inner display is in use (outer display
|
|
22
|
-
* is 466pt tall in landscape, inner display 669pt)
|
|
23
|
-
* - controlSize / itemPitch: vertical bar of 48pt circular controls, item groups as 48pt
|
|
24
|
-
* wide capsules at a 50pt pitch, 12pt from the strip's inner edge and 12pt apart
|
|
25
|
-
* - firstItemTop: with the status bar in the strip, the first control sits under the
|
|
26
|
-
* camera, clock and connectivity cluster (cluster ends near y 151 on the outer display,
|
|
27
|
-
* y 98 on the inner display in landscape)
|
|
28
|
-
* - outerHeaderHeight / outerCameraCenterY: on the outer display in portrait the header
|
|
29
|
-
* is 4pt taller than iPhoneMinHeight and its title lines up with the camera punchout
|
|
30
|
-
* (y 24–66 at the top of the strip)
|
|
31
|
-
* - edgeInset: on the outer display in landscape the strip holds no status bar, so its
|
|
32
|
-
* controls start this far from the top and stay this far above the bottom, unless a
|
|
33
|
-
* reserved region (the camera at either end) asks for more
|
|
34
|
-
* - innerStatusBarCenterY / innerStatusBarWidth: on the inner display in portrait the
|
|
35
|
-
* clock and connectivity cluster sits at the top right corner (x 550–643 of 669, clock
|
|
36
|
-
* digits centered on y 47); its reserved region, when reported, is taller than the glyphs
|
|
37
|
-
* - floatingTabBarHeight / floatingTabBarMargin / floatingTabBarInsetOverlap: the host
|
|
38
|
-
* app's iOS 26 system tab bar on the inner display in portrait, a 62pt Liquid Glass pill
|
|
39
|
-
* sized to its tabs with the QR as a pinned 62pt platter beside it, centered and at
|
|
40
|
-
* least 20pt from the side edges; it overlaps the home indicator inset by 13pt (21pt
|
|
41
|
-
* above the screen bottom with a 34pt inset)
|
|
42
|
-
*/
|
|
17
|
+
/** iPhone Duo geometry, iOS 27.1 (Xcode 27.1 simulator). */
|
|
43
18
|
export const DuoMetrics = {
|
|
19
|
+
/**
|
|
20
|
+
* Both displays are near 1.42 (inner) and 1.45 (outer); every slab iPhone is
|
|
21
|
+
* 1.77 or taller.
|
|
22
|
+
*/
|
|
44
23
|
maxAspectRatio: 1.6,
|
|
24
|
+
/**
|
|
25
|
+
* Window height from which the inner display is in use (outer display is
|
|
26
|
+
* 466pt tall in landscape, inner display 669pt).
|
|
27
|
+
*/
|
|
45
28
|
innerMinHeight: 600,
|
|
29
|
+
/**
|
|
30
|
+
* Vertical bar of 48pt circular controls; item groups are 48pt wide capsules
|
|
31
|
+
* at a 50pt pitch, 12pt from the strip's inner edge and 12pt apart.
|
|
32
|
+
*/
|
|
46
33
|
controlSize: 48,
|
|
47
34
|
itemPitch: 50,
|
|
35
|
+
/**
|
|
36
|
+
* With the status bar in the strip, the first control sits under the camera,
|
|
37
|
+
* clock and connectivity cluster (ends near y 151 outer, y 98 inner
|
|
38
|
+
* landscape).
|
|
39
|
+
*/
|
|
48
40
|
firstItemTop: 170,
|
|
49
41
|
firstItemTopLandscape: 120,
|
|
42
|
+
/**
|
|
43
|
+
* Outer display in portrait: the header is 4pt taller than iPhoneMinHeight
|
|
44
|
+
* and its title lines up with the camera punchout (y 24–66 at the strip's
|
|
45
|
+
* top).
|
|
46
|
+
*/
|
|
50
47
|
outerHeaderHeight: 76,
|
|
51
48
|
outerCameraCenterY: 45,
|
|
49
|
+
/**
|
|
50
|
+
* Outer display in landscape: no status bar in the strip, so controls keep
|
|
51
|
+
* this gap from both ends unless a reserved region (the camera) needs more.
|
|
52
|
+
*/
|
|
52
53
|
edgeInset: 24,
|
|
54
|
+
/**
|
|
55
|
+
* Inner display in portrait: clock and connectivity cluster at the top right
|
|
56
|
+
* (x 550–643 of 669, digits on y 47); a reported reserved region is taller.
|
|
57
|
+
*/
|
|
53
58
|
innerStatusBarCenterY: 47,
|
|
54
59
|
innerStatusBarWidth: 120,
|
|
60
|
+
/**
|
|
61
|
+
* Host app's iOS 26 tab bar, inner display in portrait: a 62pt Liquid Glass
|
|
62
|
+
* pill plus 62pt QR platter, ≥ 20pt from the sides, 13pt over the home inset.
|
|
63
|
+
*/
|
|
55
64
|
floatingTabBarHeight: 62,
|
|
56
65
|
floatingTabBarMargin: 20,
|
|
57
66
|
floatingTabBarInsetOverlap: 13,
|
package/Layout/screenTypes.ts
CHANGED
|
@@ -59,9 +59,8 @@ export interface ScreenProps extends ViewProps {
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Optional. If `true`, the footer is inset by the standard horizontal margin
|
|
62
|
-
* (12pt each side)
|
|
63
|
-
*
|
|
64
|
-
* Defaults to `true`, on every platform.
|
|
62
|
+
* (12pt each side); `false` spans the full width, e.g. for a footer with its
|
|
63
|
+
* own horizontal padding. Defaults to `true`, on every platform.
|
|
65
64
|
*/
|
|
66
65
|
marginFooter?: boolean;
|
|
67
66
|
|
|
@@ -13,6 +13,8 @@ import { ApplicationContext, MiniAppContext } from '../Context';
|
|
|
13
13
|
|
|
14
14
|
const INDICATOR_WIDTH = 24;
|
|
15
15
|
const INDICATOR_CONTAINER_WIDTH = 72;
|
|
16
|
+
// one frame at 60 fps
|
|
17
|
+
const SCROLL_EVENT_THROTTLE = 16;
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Horizontal scroll view with a small indicator that tracks the scroll offset.
|
|
@@ -31,7 +33,7 @@ const PaginationScroll: FC<ScrollIndicatorProps> = ({
|
|
|
31
33
|
|
|
32
34
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
33
35
|
|
|
34
|
-
const
|
|
36
|
+
const handleScroll = useAnimatedScrollHandler({
|
|
35
37
|
onScroll: (event) => {
|
|
36
38
|
left.value = event.contentOffset.x;
|
|
37
39
|
},
|
|
@@ -54,8 +56,8 @@ const PaginationScroll: FC<ScrollIndicatorProps> = ({
|
|
|
54
56
|
return (
|
|
55
57
|
<Animated.ScrollView
|
|
56
58
|
ref={scrollViewRef as any}
|
|
57
|
-
onScroll={
|
|
58
|
-
scrollEventThrottle={
|
|
59
|
+
onScroll={handleScroll}
|
|
60
|
+
scrollEventThrottle={SCROLL_EVENT_THROTTLE}
|
|
59
61
|
alwaysBounceHorizontal={false}
|
|
60
62
|
showsHorizontalScrollIndicator={false}
|
|
61
63
|
horizontal
|
package/Pagination/types.ts
CHANGED
|
@@ -3,11 +3,9 @@ import {Ref} from 'react';
|
|
|
3
3
|
|
|
4
4
|
export type PaginationProps = {
|
|
5
5
|
/**
|
|
6
|
-
* Optional.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - 'number': Represents a style where pages are represented by numbers.
|
|
10
|
-
* - 'scroll': Rendered as 'default'; use `PaginationScroll` instead.
|
|
6
|
+
* Optional. Visual style: 'default' dots, 'black_white' dots on a translucent
|
|
7
|
+
* dark background, 'number' page numbers. 'scroll' renders as 'default'; use
|
|
8
|
+
* `PaginationScroll` instead.
|
|
11
9
|
*/
|
|
12
10
|
type?: 'default' | 'black_white' | 'number' | 'scroll';
|
|
13
11
|
|
package/Popup/PopupNotify.tsx
CHANGED
|
@@ -12,6 +12,9 @@ import { ModalIconButton } from '../Application/ModalIconButton';
|
|
|
12
12
|
import { splitHighlightText } from './utils';
|
|
13
13
|
|
|
14
14
|
const MAX_ROW_TITLE_LENGTH = 12;
|
|
15
|
+
const DESCRIPTION_MAX_HEIGHT = 172;
|
|
16
|
+
const MAX_DESCRIPTION_LINES = 8.5;
|
|
17
|
+
const IMAGE_ASPECT_RATIO = 1.7777;
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Notify popup body: optional image, title, description, error code and one or
|
|
@@ -32,10 +35,10 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
32
35
|
}) => {
|
|
33
36
|
const context = useContext<any>(MiniAppContext);
|
|
34
37
|
const { theme, navigator, translate } = useContext(ApplicationContext);
|
|
35
|
-
const [
|
|
38
|
+
const [isDescriptionScrollable, setIsDescriptionScrollable] = useState(false);
|
|
36
39
|
const { isFoldable, popup } = useModalLayout();
|
|
37
40
|
const { width: popupWidth, maxHeight, radius } = popup;
|
|
38
|
-
const scaledMaxHeight = useScaleSize(
|
|
41
|
+
const scaledMaxHeight = useScaleSize(DESCRIPTION_MAX_HEIGHT);
|
|
39
42
|
|
|
40
43
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
41
44
|
const closeAction = useRef('touch_outside');
|
|
@@ -45,12 +48,12 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
45
48
|
// foldables cap the popup height, the description takes what is left
|
|
46
49
|
Description = ScrollView;
|
|
47
50
|
descriptionStyle = styles.shrink;
|
|
48
|
-
} else if (
|
|
51
|
+
} else if (isDescriptionScrollable) {
|
|
49
52
|
Description = ScrollView;
|
|
50
53
|
descriptionStyle = { maxHeight: scaledMaxHeight };
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
const
|
|
56
|
+
const handleAction = (callback?: () => void) => {
|
|
54
57
|
if (typeof onRequestClose === 'function') {
|
|
55
58
|
onRequestClose?.(callback);
|
|
56
59
|
} else {
|
|
@@ -72,7 +75,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
72
75
|
size="medium"
|
|
73
76
|
onPress={() => {
|
|
74
77
|
closeAction.current = 'button_action';
|
|
75
|
-
|
|
78
|
+
handleAction(secondary?.onPress);
|
|
76
79
|
}}
|
|
77
80
|
accessibilityLabel={'btn_popup_cancel'}
|
|
78
81
|
/>
|
|
@@ -86,7 +89,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
86
89
|
size="medium"
|
|
87
90
|
onPress={() => {
|
|
88
91
|
closeAction.current = 'button_action';
|
|
89
|
-
|
|
92
|
+
handleAction(primary.onPress);
|
|
90
93
|
}}
|
|
91
94
|
accessibilityLabel={'btn_popup_allow'}
|
|
92
95
|
/>
|
|
@@ -103,7 +106,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
103
106
|
size="medium"
|
|
104
107
|
onPress={() => {
|
|
105
108
|
closeAction.current = 'button_action';
|
|
106
|
-
|
|
109
|
+
handleAction(primary.onPress);
|
|
107
110
|
}}
|
|
108
111
|
/>
|
|
109
112
|
{!!secondary?.title && (
|
|
@@ -116,7 +119,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
116
119
|
size="medium"
|
|
117
120
|
onPress={() => {
|
|
118
121
|
closeAction.current = 'button_action';
|
|
119
|
-
|
|
122
|
+
handleAction(secondary?.onPress);
|
|
120
123
|
}}
|
|
121
124
|
/>
|
|
122
125
|
</>
|
|
@@ -172,7 +175,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
172
175
|
style={styles.iconClose}
|
|
173
176
|
onPress={() => {
|
|
174
177
|
closeAction.current = 'icon_close';
|
|
175
|
-
|
|
178
|
+
handleAction(onIconClose);
|
|
176
179
|
}}
|
|
177
180
|
/>
|
|
178
181
|
);
|
|
@@ -189,9 +192,9 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
|
|
|
189
192
|
accessibilityLabel={'description_popup_notify'}
|
|
190
193
|
typography={'body_default_regular'}
|
|
191
194
|
onTextLayout={({ nativeEvent: { lines } }) => {
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
+
const shouldScroll = lines.length > MAX_DESCRIPTION_LINES;
|
|
196
|
+
if (shouldScroll !== isDescriptionScrollable) {
|
|
197
|
+
setIsDescriptionScrollable(shouldScroll);
|
|
195
198
|
}
|
|
196
199
|
}}
|
|
197
200
|
>
|
|
@@ -260,7 +263,7 @@ const styles = StyleSheet.create({
|
|
|
260
263
|
// clips the image and content to the card radius
|
|
261
264
|
card: { overflow: 'hidden' },
|
|
262
265
|
image: {
|
|
263
|
-
aspectRatio:
|
|
266
|
+
aspectRatio: IMAGE_ASPECT_RATIO,
|
|
264
267
|
},
|
|
265
268
|
shrink: { flexShrink: 1 },
|
|
266
269
|
content: { padding: Spacing.XL },
|
package/Popup/PopupPromotion.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { useModalLayout } from '../Application/modalLayout';
|
|
|
9
9
|
|
|
10
10
|
const ASPECT_RATIO = 0.72;
|
|
11
11
|
const ICON_CLOSE_SIZE = 20;
|
|
12
|
+
const CLOSE_GLYPH_SIZE = 14;
|
|
12
13
|
// close button row under the image: marginTop + vertical padding + icon
|
|
13
14
|
const CLOSE_ROW_HEIGHT = Spacing.S * 3 + ICON_CLOSE_SIZE;
|
|
14
15
|
|
|
@@ -37,7 +38,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
37
38
|
|
|
38
39
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
39
40
|
|
|
40
|
-
const
|
|
41
|
+
const handleAction = (callback?: () => void) => {
|
|
41
42
|
if (typeof onRequestClose === 'function') {
|
|
42
43
|
onRequestClose?.(callback);
|
|
43
44
|
} else {
|
|
@@ -53,7 +54,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
53
54
|
accessibilityLabel={'ic_popup_close'}
|
|
54
55
|
onPress={() => {
|
|
55
56
|
closeAction.current = 'icon_close';
|
|
56
|
-
|
|
57
|
+
handleAction(onIconClose);
|
|
57
58
|
}}
|
|
58
59
|
style={styles.iconButton}
|
|
59
60
|
>
|
|
@@ -69,7 +70,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
69
70
|
<Icon
|
|
70
71
|
source="navigation_close"
|
|
71
72
|
color={theme.colors.background.surface}
|
|
72
|
-
size={
|
|
73
|
+
size={CLOSE_GLYPH_SIZE}
|
|
73
74
|
/>
|
|
74
75
|
</View>
|
|
75
76
|
</TouchableOpacity>
|
|
@@ -82,7 +83,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
|
|
|
82
83
|
accessibilityLabel={'popup_promotion'}
|
|
83
84
|
style={[{ width: imageWidth }, showBaseLineDebug && styles.debugBaseLine]}
|
|
84
85
|
>
|
|
85
|
-
<TouchableOpacity onPress={() =>
|
|
86
|
+
<TouchableOpacity onPress={() => handleAction(onActionClick)}>
|
|
86
87
|
<Image
|
|
87
88
|
source={{
|
|
88
89
|
uri: image,
|
package/Popup/types.ts
CHANGED
|
@@ -43,10 +43,8 @@ export type PopupNotifyProps = {
|
|
|
43
43
|
primary: PopupAction;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* `buttonDirection`: Optional.
|
|
47
|
-
*
|
|
48
|
-
* - 'column': Buttons are laid out vertically.
|
|
49
|
-
* - 'auto': The layout is automatically determined by title length.
|
|
46
|
+
* `buttonDirection`: Optional. Layout of the action buttons: 'row' horizontal,
|
|
47
|
+
* 'column' vertical, 'auto' column only when a title is too long for a row.
|
|
50
48
|
*/
|
|
51
49
|
buttonDirection?: PopupActionDirection;
|
|
52
50
|
|
package/Skeleton/index.tsx
CHANGED
|
@@ -21,6 +21,12 @@ import { Colors, Styles } from '../Consts';
|
|
|
21
21
|
import styles from './styles';
|
|
22
22
|
import { ScreenContext, SkeletonContext } from '../Context';
|
|
23
23
|
|
|
24
|
+
const PRIMARY_COLOR = Colors.black_05;
|
|
25
|
+
const HIGHLIGHT_COLOR1 = Colors.black_05;
|
|
26
|
+
const HIGHLIGHT_COLOR2 = Colors.black_03;
|
|
27
|
+
const SHIMMER_DURATION = 1000;
|
|
28
|
+
const LAYOUT_ANIMATION_DURATION = 500;
|
|
29
|
+
|
|
24
30
|
/**
|
|
25
31
|
* Shimmering placeholder that fills its parent. Reports loading to the screen
|
|
26
32
|
* context while mounted.
|
|
@@ -28,9 +34,6 @@ import { ScreenContext, SkeletonContext } from '../Context';
|
|
|
28
34
|
const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
|
|
29
35
|
const screen = useContext<any>(ScreenContext);
|
|
30
36
|
const [width, setWidth] = useState(0);
|
|
31
|
-
const PRIMARY_COLOR = Colors.black_05;
|
|
32
|
-
const HIGHLIGHT_COLOR1 = Colors.black_05;
|
|
33
|
-
const HIGHLIGHT_COLOR2 = Colors.black_03;
|
|
34
37
|
const progress = useSharedValue(0);
|
|
35
38
|
|
|
36
39
|
const shimmerColors = [HIGHLIGHT_COLOR1, HIGHLIGHT_COLOR2, HIGHLIGHT_COLOR1];
|
|
@@ -39,7 +42,7 @@ const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
|
|
|
39
42
|
progress.value = 0;
|
|
40
43
|
progress.value = withRepeat(
|
|
41
44
|
withTiming(1, {
|
|
42
|
-
duration:
|
|
45
|
+
duration: SHIMMER_DURATION,
|
|
43
46
|
easing: Easing.linear,
|
|
44
47
|
}),
|
|
45
48
|
-1,
|
|
@@ -57,7 +60,7 @@ const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
|
|
|
57
60
|
return { transform: [{ translateX }] };
|
|
58
61
|
});
|
|
59
62
|
|
|
60
|
-
const
|
|
63
|
+
const handleLayout = (newWidth: number) => {
|
|
61
64
|
if (newWidth !== width) {
|
|
62
65
|
setWidth(newWidth);
|
|
63
66
|
}
|
|
@@ -66,7 +69,7 @@ const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
|
|
|
66
69
|
return (
|
|
67
70
|
<View style={[styles.container, style]}>
|
|
68
71
|
<View
|
|
69
|
-
onLayout={e =>
|
|
72
|
+
onLayout={e => handleLayout(e.nativeEvent.layout.width)}
|
|
70
73
|
style={[Styles.flex, { backgroundColor: PRIMARY_COLOR }]}
|
|
71
74
|
>
|
|
72
75
|
<Animated.View style={[styles.shimmer, shimmerStyle]}>
|
|
@@ -96,28 +99,28 @@ const SkeletonProvider: React.FC<{ loading: boolean; children: any }> = ({
|
|
|
96
99
|
loading = true,
|
|
97
100
|
children,
|
|
98
101
|
}) => {
|
|
99
|
-
const [
|
|
102
|
+
const [isLoadingShown, setIsLoadingShown] = useState(loading);
|
|
100
103
|
|
|
101
104
|
useEffect(() => {
|
|
102
|
-
if (
|
|
105
|
+
if (isLoadingShown !== loading) {
|
|
103
106
|
if (Platform.OS === 'android') {
|
|
104
107
|
if (UIManager.setLayoutAnimationEnabledExperimental) {
|
|
105
108
|
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
111
|
LayoutAnimation.configureNext({
|
|
109
|
-
duration:
|
|
112
|
+
duration: LAYOUT_ANIMATION_DURATION,
|
|
110
113
|
create: {
|
|
111
114
|
type: LayoutAnimation.Types.easeInEaseOut,
|
|
112
115
|
property: LayoutAnimation.Properties.opacity,
|
|
113
116
|
},
|
|
114
117
|
});
|
|
115
|
-
|
|
118
|
+
setIsLoadingShown(loading);
|
|
116
119
|
}
|
|
117
|
-
}, [loading,
|
|
120
|
+
}, [loading, isLoadingShown]);
|
|
118
121
|
|
|
119
122
|
return (
|
|
120
|
-
<SkeletonContext.Provider value={{ loading:
|
|
123
|
+
<SkeletonContext.Provider value={{ loading: isLoadingShown }}>
|
|
121
124
|
{children}
|
|
122
125
|
</SkeletonContext.Provider>
|
|
123
126
|
);
|
package/package.json
CHANGED
package/Input/common.tsx
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import React, { FC, ReactNode, useContext } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
GestureResponderEvent,
|
|
4
|
-
TouchableOpacity,
|
|
5
|
-
View,
|
|
6
|
-
ViewStyle,
|
|
7
|
-
} from 'react-native';
|
|
8
|
-
import { ApplicationContext } from '../Context';
|
|
9
|
-
import { Theme } from '../Application/types';
|
|
10
|
-
import { Styles } from '../Consts';
|
|
11
|
-
import { Icon } from '../Icon';
|
|
12
|
-
import { useScaleSize, Text } from '../Text';
|
|
13
|
-
import styles from './styles';
|
|
14
|
-
import { Loader } from '../Loader';
|
|
15
|
-
import IconSources from '../Assets/icon.json';
|
|
16
|
-
|
|
17
|
-
type FloatingViewProps = {
|
|
18
|
-
floatingValue?: string;
|
|
19
|
-
floatingIconColor?: string;
|
|
20
|
-
componentId?: string;
|
|
21
|
-
disabled?: boolean;
|
|
22
|
-
floatingIcon?: string;
|
|
23
|
-
required?: boolean;
|
|
24
|
-
style?: ViewStyle;
|
|
25
|
-
onPress?: (e: GestureResponderEvent) => void;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type TrailingProps = {
|
|
29
|
-
loading?: boolean;
|
|
30
|
-
color?: string;
|
|
31
|
-
icon?: string;
|
|
32
|
-
componentId?: string;
|
|
33
|
-
onPressIcon?: () => void;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const MAX_LENGTH = 300;
|
|
37
|
-
|
|
38
|
-
/** Border color by state; priority is disabled > error > focused > default. */
|
|
39
|
-
export const getBorderColor = (
|
|
40
|
-
theme: Theme,
|
|
41
|
-
focused: boolean,
|
|
42
|
-
errorMessage?: string,
|
|
43
|
-
disabled?: boolean,
|
|
44
|
-
) => {
|
|
45
|
-
let borderColor = theme.colors.border.default;
|
|
46
|
-
|
|
47
|
-
if (focused) {
|
|
48
|
-
borderColor = theme.colors.primary;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (errorMessage) {
|
|
52
|
-
borderColor = theme.colors.error.primary;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (disabled) {
|
|
56
|
-
borderColor = theme.colors.border.disable;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return { borderColor };
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Container style for `size`; multiline uses `height` as minHeight so the
|
|
64
|
-
* field can grow with its content.
|
|
65
|
-
*/
|
|
66
|
-
export const getSizeStyle = (
|
|
67
|
-
size?: 'small' | 'large',
|
|
68
|
-
multiline: boolean = false,
|
|
69
|
-
height?: number,
|
|
70
|
-
) => {
|
|
71
|
-
let scaleStyle: ViewStyle = {};
|
|
72
|
-
if (height) {
|
|
73
|
-
scaleStyle = { height };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (multiline) {
|
|
77
|
-
return [
|
|
78
|
-
styles.multilineContainer,
|
|
79
|
-
{
|
|
80
|
-
minHeight: height,
|
|
81
|
-
},
|
|
82
|
-
];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (size === 'small') {
|
|
86
|
-
return [styles.smallContainer, scaleStyle];
|
|
87
|
-
}
|
|
88
|
-
return [styles.container, scaleStyle];
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Error or hint row under an input (error wins); with neither, `errorSpacing`
|
|
93
|
-
* keeps an empty row so the layout doesn't jump when an error appears.
|
|
94
|
-
*/
|
|
95
|
-
export const ErrorView: FC<{
|
|
96
|
-
errorMessage?: string;
|
|
97
|
-
errorSpacing?: boolean;
|
|
98
|
-
hintText?: string;
|
|
99
|
-
componentId?: string;
|
|
100
|
-
}> = ({ errorMessage, errorSpacing, hintText, componentId }) => {
|
|
101
|
-
const { theme } = useContext(ApplicationContext);
|
|
102
|
-
const errorColor = theme.colors.error.primary;
|
|
103
|
-
const hintColor = theme.colors.text.hint;
|
|
104
|
-
const hintTextDefault = hintText ?? 'Không thể chỉnh sửa';
|
|
105
|
-
const scaledIconSize = useScaleSize(16);
|
|
106
|
-
|
|
107
|
-
if (errorMessage || hintText) {
|
|
108
|
-
const messageColor = errorMessage ? errorColor : hintColor;
|
|
109
|
-
const messageType = errorMessage ? 'error' : 'hint';
|
|
110
|
-
return (
|
|
111
|
-
<View style={styles.errorView}>
|
|
112
|
-
<View style={styles.errorIcon}>
|
|
113
|
-
<Icon color={messageColor} source="ic_error" size={scaledIconSize} />
|
|
114
|
-
</View>
|
|
115
|
-
<Text
|
|
116
|
-
accessibilityLabel={`${componentId}|${messageType}-text`}
|
|
117
|
-
style={Styles.flex}
|
|
118
|
-
color={messageColor}
|
|
119
|
-
typography={'description_default_regular'}
|
|
120
|
-
>
|
|
121
|
-
{errorMessage ?? hintTextDefault}
|
|
122
|
-
</Text>
|
|
123
|
-
</View>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
if (errorSpacing) {
|
|
127
|
-
return <View style={[styles.errorView, styles.errorSpacing]} />;
|
|
128
|
-
}
|
|
129
|
-
return <View />;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Floating label on the input's top border with a required `*` and an
|
|
134
|
-
* optional pressable icon; renders nothing without `floatingValue`.
|
|
135
|
-
*/
|
|
136
|
-
export const FloatingView: FC<FloatingViewProps> = ({
|
|
137
|
-
floatingValue,
|
|
138
|
-
floatingIconColor,
|
|
139
|
-
disabled,
|
|
140
|
-
floatingIcon,
|
|
141
|
-
required,
|
|
142
|
-
style,
|
|
143
|
-
onPress,
|
|
144
|
-
componentId,
|
|
145
|
-
}) => {
|
|
146
|
-
const { theme } = useContext(ApplicationContext);
|
|
147
|
-
const scaledTop = useScaleSize(10);
|
|
148
|
-
if (floatingValue) {
|
|
149
|
-
let floatingTextColor = theme.colors.text.hint;
|
|
150
|
-
let floatingIconTintColor = floatingIconColor;
|
|
151
|
-
if (disabled) {
|
|
152
|
-
floatingTextColor = theme.colors.text.disable;
|
|
153
|
-
floatingIconTintColor = theme.colors.text.disable;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
<View
|
|
158
|
-
style={[
|
|
159
|
-
styles.floatingView,
|
|
160
|
-
styles.floatingViewLayer,
|
|
161
|
-
{
|
|
162
|
-
backgroundColor: theme.colors.background.surface,
|
|
163
|
-
top: -scaledTop,
|
|
164
|
-
},
|
|
165
|
-
style,
|
|
166
|
-
]}
|
|
167
|
-
>
|
|
168
|
-
<Text
|
|
169
|
-
color={floatingTextColor}
|
|
170
|
-
typography={'label_s_medium'}
|
|
171
|
-
accessibilityLabel={`${componentId}|floating-text`}
|
|
172
|
-
>
|
|
173
|
-
{floatingValue}
|
|
174
|
-
{required && (
|
|
175
|
-
<Text
|
|
176
|
-
typography={'label_s_medium'}
|
|
177
|
-
color={theme.colors.error.primary}
|
|
178
|
-
>
|
|
179
|
-
*
|
|
180
|
-
</Text>
|
|
181
|
-
)}
|
|
182
|
-
</Text>
|
|
183
|
-
{!!floatingIcon && (
|
|
184
|
-
<TouchableOpacity
|
|
185
|
-
activeOpacity={onPress ? 0.6 : 1}
|
|
186
|
-
onPress={onPress}
|
|
187
|
-
accessibilityLabel={`${componentId}|floating-icon-touch`}
|
|
188
|
-
>
|
|
189
|
-
<Icon
|
|
190
|
-
color={floatingIconTintColor}
|
|
191
|
-
source={floatingIcon}
|
|
192
|
-
size={16}
|
|
193
|
-
style={styles.floatingIcon}
|
|
194
|
-
accessibilityLabel={`${componentId}|floating-icon`}
|
|
195
|
-
/>
|
|
196
|
-
</TouchableOpacity>
|
|
197
|
-
)}
|
|
198
|
-
</View>
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
return null;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Trailing slot: spinner while `loading`, an icon when `icon` is a known icon
|
|
206
|
-
* name or URL, otherwise `icon` as a text action (first 15 chars).
|
|
207
|
-
*/
|
|
208
|
-
export const RenderTrailing: FC<TrailingProps> = ({
|
|
209
|
-
loading,
|
|
210
|
-
color,
|
|
211
|
-
icon,
|
|
212
|
-
onPressIcon,
|
|
213
|
-
componentId,
|
|
214
|
-
}) => {
|
|
215
|
-
const { theme } = useContext(ApplicationContext);
|
|
216
|
-
if (loading) {
|
|
217
|
-
return <Loader type={'spinner'} color={color} style={styles.icon} />;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (icon) {
|
|
221
|
-
const isIconName = !!(IconSources as any)?.[icon]?.uri;
|
|
222
|
-
const isIcon = isIconName || icon.includes('http');
|
|
223
|
-
const renderIconTouchable = (content: ReactNode) => {
|
|
224
|
-
return (
|
|
225
|
-
<TouchableOpacity
|
|
226
|
-
accessibilityLabel={`${componentId}|trailing-touch`}
|
|
227
|
-
onPress={onPressIcon}
|
|
228
|
-
style={styles.icon}
|
|
229
|
-
>
|
|
230
|
-
{content}
|
|
231
|
-
</TouchableOpacity>
|
|
232
|
-
);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
if (isIcon) {
|
|
236
|
-
return renderIconTouchable(
|
|
237
|
-
<Icon
|
|
238
|
-
color={color}
|
|
239
|
-
source={icon as string}
|
|
240
|
-
size={24}
|
|
241
|
-
accessibilityLabel={`${componentId}|trailing-icon`}
|
|
242
|
-
/>,
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
return renderIconTouchable(
|
|
246
|
-
<Text
|
|
247
|
-
typography="action_xs_bold"
|
|
248
|
-
color={color ?? theme.colors.primary}
|
|
249
|
-
numberOfLines={1}
|
|
250
|
-
accessibilityLabel={`${componentId}|trailing-text`}
|
|
251
|
-
>
|
|
252
|
-
{icon!.substring(0, 15)}
|
|
253
|
-
</Text>,
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
return null;
|
|
257
|
-
};
|