@momo-kits/foundation 0.167.1-beta.5 → 0.167.1-duo-test.1
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 +115 -14
- package/Application/BottomTab/BottomTabBar.tsx +6 -3
- package/Application/BottomTab/DuoBarCompression.tsx +135 -0
- package/Application/BottomTab/DuoTabBar.tsx +462 -0
- package/Application/BottomTab/index.tsx +150 -134
- package/Application/Components/DuoGlassBackground.tsx +179 -0
- package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
- package/Application/Components/HeaderExtendHeader.tsx +75 -28
- package/Application/Components/HeaderLeft.tsx +38 -0
- package/Application/Components/HeaderRight.tsx +142 -9
- package/Application/Components/HeaderTitle.tsx +67 -17
- package/Application/Components/OptionalGlassView.ts +11 -0
- package/Application/Components/OptionalScrollObserver.ts +12 -0
- package/Application/Components/SearchHeader.tsx +22 -15
- package/Application/Components/SideBarButton.tsx +139 -0
- package/Application/ModalScreen.tsx +16 -2
- package/Application/NavigationContainer.tsx +36 -45
- package/Application/Navigator.ts +1 -3
- package/Application/StackScreen.tsx +63 -9
- package/Application/WidgetContainer.tsx +9 -7
- package/Application/device.ts +29 -0
- package/Application/types.ts +27 -1
- package/Application/utils.tsx +255 -7
- package/Consts/index.ts +0 -1
- package/Consts/theme.ts +5 -6
- package/Input/Input.tsx +3 -4
- package/Input/InputMoney.tsx +31 -51
- package/Input/InputPhoneNumber.tsx +3 -4
- package/Input/InputSearch.tsx +3 -4
- package/Input/InputTextArea.tsx +3 -4
- package/Layout/Card.tsx +8 -26
- package/Layout/FloatingButton.tsx +3 -3
- package/Layout/Item.tsx +1 -10
- package/Layout/ReservedRegions.tsx +47 -0
- package/Layout/Screen.tsx +199 -52
- package/Layout/Section.tsx +5 -18
- package/Layout/index.ts +4 -4
- package/Layout/styles.ts +5 -0
- package/Layout/utils.ts +465 -5
- package/Popup/PopupNotify.tsx +33 -22
- package/Popup/PopupPromotion.tsx +20 -8
- package/Popup/utils.ts +32 -0
- package/Text/index.tsx +2 -11
- package/Text/utils.ts +1 -2
- package/package.json +3 -2
- package/Consts/designSystemFont.ts +0 -38
- package/Layout/GridSystem.tsx +0 -137
package/Application/types.ts
CHANGED
|
@@ -117,7 +117,6 @@ export type Context = {
|
|
|
117
117
|
[key: string]: any;
|
|
118
118
|
theme: Theme;
|
|
119
119
|
navigator?: NavigatorProps;
|
|
120
|
-
showGrid?: boolean;
|
|
121
120
|
translate?: (data: string | { vi: string; en: string }) => string;
|
|
122
121
|
};
|
|
123
122
|
|
|
@@ -258,6 +257,11 @@ export interface HeaderBackProps extends NavigationButtonProps {
|
|
|
258
257
|
preventBack?: PopupNotifyProps;
|
|
259
258
|
onPressLeftHeader?: () => void;
|
|
260
259
|
onBackHandler?: (goBack: () => void) => void;
|
|
260
|
+
/**
|
|
261
|
+
* Optional. If `true`, the button floats into the iPhone Duo side bar while the
|
|
262
|
+
* device shows one. Set when the button is rendered by the navigation header.
|
|
263
|
+
*/
|
|
264
|
+
floating?: boolean;
|
|
261
265
|
}
|
|
262
266
|
|
|
263
267
|
export type HeaderBackgroundProps = {
|
|
@@ -329,6 +333,11 @@ export type BottomTabProps = {
|
|
|
329
333
|
blur?: (e: EventArg<'blur', false, unknown>) => void;
|
|
330
334
|
};
|
|
331
335
|
floatingButton?: FloatingButtonProps;
|
|
336
|
+
/**
|
|
337
|
+
* iPhone Duo: when the side bar can't hold both bars, which one starts compressed;
|
|
338
|
+
* tapping the compressed one swaps them. Defaults to 'toolbar'
|
|
339
|
+
*/
|
|
340
|
+
compressionBehavior?: 'toolbar' | 'tabBar';
|
|
332
341
|
};
|
|
333
342
|
|
|
334
343
|
export type FloatingButtonProps = {
|
|
@@ -352,3 +361,20 @@ export interface SearchHeaderProps extends InputSearchProps {
|
|
|
352
361
|
renderButtons?: () => ReactNode;
|
|
353
362
|
hiddenBack?: boolean;
|
|
354
363
|
}
|
|
364
|
+
|
|
365
|
+
export type HeaderScrollOptions = {
|
|
366
|
+
/**
|
|
367
|
+
* Navigation of the screen whose header follows the scroll.
|
|
368
|
+
*/
|
|
369
|
+
navigation?: Navigation;
|
|
370
|
+
useShadowHeader?: boolean;
|
|
371
|
+
gradientColor?: string;
|
|
372
|
+
headerBackground?: string;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
export type ScrollChange = {
|
|
376
|
+
y: number;
|
|
377
|
+
contentHeight: number;
|
|
378
|
+
viewportHeight: number;
|
|
379
|
+
phase: 'scroll' | 'dragEnd' | 'momentumEnd' | 'takeOver';
|
|
380
|
+
};
|
package/Application/utils.tsx
CHANGED
|
@@ -16,12 +16,28 @@ import type {
|
|
|
16
16
|
StackCardInterpolationProps,
|
|
17
17
|
} from '@react-navigation/stack';
|
|
18
18
|
import type { NavigationState, PartialState } from '@react-navigation/native';
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
HeaderScrollOptions,
|
|
21
|
+
ScrollChange,
|
|
22
|
+
HeaderTitleProps,
|
|
23
|
+
NavigationOptions,
|
|
24
|
+
} from './types';
|
|
20
25
|
import { ApplicationContext } from '../Context';
|
|
21
26
|
import { Colors, Spacing } from '../Consts';
|
|
22
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
Animated,
|
|
29
|
+
AppState,
|
|
30
|
+
NativeModules,
|
|
31
|
+
Platform,
|
|
32
|
+
} from 'react-native';
|
|
33
|
+
import { useHeaderHeight } from '@react-navigation/elements';
|
|
23
34
|
import type { SharedValue } from 'react-native-reanimated';
|
|
24
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
useAnimatedReaction,
|
|
37
|
+
useAnimatedStyle,
|
|
38
|
+
useSharedValue,
|
|
39
|
+
withTiming,
|
|
40
|
+
} from 'react-native-reanimated';
|
|
25
41
|
import {
|
|
26
42
|
MiniAppContext,
|
|
27
43
|
ScreenContext,
|
|
@@ -31,6 +47,12 @@ import { HeaderTitle, TitleCustom } from './Components/HeaderTitle';
|
|
|
31
47
|
import { HeaderBackground } from './Components/HeaderBackground';
|
|
32
48
|
import { HeaderLeft } from './Components/HeaderLeft';
|
|
33
49
|
import { HeaderRight } from './Components/HeaderRight';
|
|
50
|
+
import {
|
|
51
|
+
DuoGlassBackground,
|
|
52
|
+
DuoScrollGradient,
|
|
53
|
+
} from './Components/DuoGlassBackground';
|
|
54
|
+
import { DuoQuickReturnHeader } from './Components/DuoQuickReturnHeader';
|
|
55
|
+
import { useScreenLayout } from '../Layout/utils';
|
|
34
56
|
|
|
35
57
|
const forStackTransitionWithOverlay = (
|
|
36
58
|
props: StackCardInterpolationProps,
|
|
@@ -73,8 +95,8 @@ const getStackOptions = (): StackNavigationOptions => {
|
|
|
73
95
|
left: Spacing.S,
|
|
74
96
|
},
|
|
75
97
|
headerBackground: (props: any) => <HeaderBackground {...props} />,
|
|
76
|
-
headerLeft: (props: any) => <HeaderLeft {...props} />,
|
|
77
|
-
headerRight: (props: any) => <HeaderRight {...props} />,
|
|
98
|
+
headerLeft: (props: any) => <HeaderLeft {...props} floating />,
|
|
99
|
+
headerRight: (props: any) => <HeaderRight {...props} floating />,
|
|
78
100
|
headerTintColor: Colors.black_17,
|
|
79
101
|
gestureEnabled: Platform.OS === 'ios',
|
|
80
102
|
...TransitionPresets.SlideFromRightIOS,
|
|
@@ -129,6 +151,7 @@ const getOptions = (
|
|
|
129
151
|
preventBack={params.preventBack}
|
|
130
152
|
onPressLeftHeader={params.onPressLeftHeader}
|
|
131
153
|
onBackHandler={params.onBackHandler}
|
|
154
|
+
floating
|
|
132
155
|
/>
|
|
133
156
|
);
|
|
134
157
|
if (params.hiddenBack) {
|
|
@@ -167,7 +190,9 @@ const getOptions = (
|
|
|
167
190
|
...options,
|
|
168
191
|
...{
|
|
169
192
|
headerRight: (props: any) => {
|
|
170
|
-
return
|
|
193
|
+
return (
|
|
194
|
+
<HeaderRight {...props} headerRight={params.headerRight} floating />
|
|
195
|
+
);
|
|
171
196
|
},
|
|
172
197
|
},
|
|
173
198
|
};
|
|
@@ -401,7 +426,9 @@ const getSwipeGestureValue = (
|
|
|
401
426
|
};
|
|
402
427
|
|
|
403
428
|
const readAnimatedValue = (node: unknown, fallback: number): number => {
|
|
404
|
-
const value = (
|
|
429
|
+
const value = (
|
|
430
|
+
node as { __getValue?: () => unknown } | undefined
|
|
431
|
+
)?.__getValue?.();
|
|
405
432
|
return typeof value === 'number' ? value : fallback;
|
|
406
433
|
};
|
|
407
434
|
|
|
@@ -475,6 +502,224 @@ const SwipeBackHaptic = () => {
|
|
|
475
502
|
return null;
|
|
476
503
|
};
|
|
477
504
|
|
|
505
|
+
const isWhiteListDesignSystem = (context: any): boolean =>
|
|
506
|
+
context?.designSystemConfig?.isWhiteListDesignSystem === true;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* iPhone Duo: the header leaves with the content and returns on a reverse scroll
|
|
510
|
+
*/
|
|
511
|
+
const useDuoQuickReturn = (
|
|
512
|
+
scrollY: SharedValue<number>,
|
|
513
|
+
enabled: boolean,
|
|
514
|
+
heightHeader: number,
|
|
515
|
+
) => {
|
|
516
|
+
const headerHide = useSharedValue(0);
|
|
517
|
+
const lastScrollY = useSharedValue(0);
|
|
518
|
+
|
|
519
|
+
useAnimatedReaction(
|
|
520
|
+
() => scrollY.value,
|
|
521
|
+
y => {
|
|
522
|
+
const delta = y - lastScrollY.value;
|
|
523
|
+
lastScrollY.value = y;
|
|
524
|
+
if (!enabled || y <= 0) {
|
|
525
|
+
headerHide.value = 0;
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
headerHide.value = Math.min(
|
|
529
|
+
Math.max(headerHide.value + delta, 0),
|
|
530
|
+
heightHeader,
|
|
531
|
+
);
|
|
532
|
+
},
|
|
533
|
+
[enabled, heightHeader],
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const quickReturnOptions: StackNavigationOptions = useMemo(
|
|
537
|
+
() =>
|
|
538
|
+
enabled
|
|
539
|
+
? {
|
|
540
|
+
header: (props: any) => (
|
|
541
|
+
<DuoQuickReturnHeader {...props} offset={headerHide} />
|
|
542
|
+
),
|
|
543
|
+
}
|
|
544
|
+
: { header: undefined },
|
|
545
|
+
[enabled, headerHide],
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* a half-shown quick return header settles fully in or out
|
|
550
|
+
*/
|
|
551
|
+
const snap = useCallback(
|
|
552
|
+
(offsetY: number) => {
|
|
553
|
+
const hide = headerHide.value;
|
|
554
|
+
// above heightHeader the header still sits on the content
|
|
555
|
+
if (
|
|
556
|
+
!enabled ||
|
|
557
|
+
offsetY < heightHeader ||
|
|
558
|
+
hide <= 0 ||
|
|
559
|
+
hide >= heightHeader
|
|
560
|
+
) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
headerHide.value = withTiming(
|
|
564
|
+
hide > heightHeader / 2 ? heightHeader : 0,
|
|
565
|
+
{ duration: 200 },
|
|
566
|
+
);
|
|
567
|
+
},
|
|
568
|
+
[enabled, heightHeader, headerHide],
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* moves the reference point without counting it as a scroll
|
|
573
|
+
*/
|
|
574
|
+
const resync = useCallback(
|
|
575
|
+
(y: number) => {
|
|
576
|
+
lastScrollY.value = y;
|
|
577
|
+
},
|
|
578
|
+
[lastScrollY],
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
return { headerHide, quickReturnOptions, snap, resync };
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Header scroll state for screens that render their own scroll views instead of
|
|
586
|
+
* `<Screen scrollable>`, such as one ScrollView per TabView page.
|
|
587
|
+
*/
|
|
588
|
+
/**
|
|
589
|
+
* header scroll state fed by any scroll source; off, it leaves the header and content alone
|
|
590
|
+
*/
|
|
591
|
+
const useHeaderScrollState = (
|
|
592
|
+
{
|
|
593
|
+
navigation,
|
|
594
|
+
useShadowHeader = true,
|
|
595
|
+
gradientColor,
|
|
596
|
+
headerBackground,
|
|
597
|
+
}: HeaderScrollOptions,
|
|
598
|
+
enabled: boolean,
|
|
599
|
+
) => {
|
|
600
|
+
const layout = useScreenLayout();
|
|
601
|
+
const heightHeader = useHeaderHeight();
|
|
602
|
+
const scrollY = useSharedValue(0);
|
|
603
|
+
const hasSideBar = !!layout.sideBar;
|
|
604
|
+
const duo = enabled && hasSideBar;
|
|
605
|
+
|
|
606
|
+
const viewportHeight = useRef<number | undefined>(undefined);
|
|
607
|
+
const { headerHide, quickReturnOptions, snap, resync } = useDuoQuickReturn(
|
|
608
|
+
scrollY,
|
|
609
|
+
duo,
|
|
610
|
+
heightHeader,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
if (!enabled) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const options: StackNavigationOptions = hasSideBar
|
|
618
|
+
? {
|
|
619
|
+
...quickReturnOptions,
|
|
620
|
+
headerTransparent: true,
|
|
621
|
+
headerTintColor: Colors.black_17,
|
|
622
|
+
headerShown: true,
|
|
623
|
+
headerBackground: () => (
|
|
624
|
+
<DuoGlassBackground animatedValue={scrollY} />
|
|
625
|
+
),
|
|
626
|
+
headerTitle: (props: any) => <HeaderTitle {...props} />,
|
|
627
|
+
}
|
|
628
|
+
: {
|
|
629
|
+
header: undefined,
|
|
630
|
+
headerTransparent: false,
|
|
631
|
+
headerTintColor: Colors.black_17,
|
|
632
|
+
headerShown: true,
|
|
633
|
+
headerBackground: (props: any) => (
|
|
634
|
+
<HeaderBackground
|
|
635
|
+
{...props}
|
|
636
|
+
animatedValue={scrollY}
|
|
637
|
+
useShadowHeader={useShadowHeader}
|
|
638
|
+
headerBackground={headerBackground}
|
|
639
|
+
gradientColor={gradientColor}
|
|
640
|
+
/>
|
|
641
|
+
),
|
|
642
|
+
headerTitle: (props: any) => <HeaderTitle {...props} />,
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
navigation?.instance?.setOptions(options);
|
|
646
|
+
}, [
|
|
647
|
+
enabled,
|
|
648
|
+
navigation?.instance,
|
|
649
|
+
hasSideBar,
|
|
650
|
+
quickReturnOptions,
|
|
651
|
+
scrollY,
|
|
652
|
+
useShadowHeader,
|
|
653
|
+
headerBackground,
|
|
654
|
+
gradientColor,
|
|
655
|
+
]);
|
|
656
|
+
|
|
657
|
+
const setScrollOffset = useCallback(
|
|
658
|
+
(y: number) => {
|
|
659
|
+
resync(y);
|
|
660
|
+
scrollY.value = y;
|
|
661
|
+
},
|
|
662
|
+
[scrollY, resync],
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
const onScrollChange = useCallback(
|
|
666
|
+
({
|
|
667
|
+
y: offsetY,
|
|
668
|
+
contentHeight,
|
|
669
|
+
viewportHeight: height,
|
|
670
|
+
phase,
|
|
671
|
+
}: ScrollChange) => {
|
|
672
|
+
// the bottom bounce is not a reverse scroll
|
|
673
|
+
const maxY = Math.max(contentHeight - height, 0);
|
|
674
|
+
const y = Math.min(offsetY, maxY);
|
|
675
|
+
if (phase === 'takeOver') {
|
|
676
|
+
viewportHeight.current = height;
|
|
677
|
+
setScrollOffset(y);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (phase !== 'scroll') {
|
|
681
|
+
snap(offsetY);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
// the quick return resizes the viewport; pinned at the bottom, the offset
|
|
685
|
+
// iOS clamps to the new size is not a scroll
|
|
686
|
+
const resized = height !== viewportHeight.current;
|
|
687
|
+
viewportHeight.current = height;
|
|
688
|
+
if (resized && offsetY >= maxY - 1) {
|
|
689
|
+
resync(y);
|
|
690
|
+
}
|
|
691
|
+
scrollY.value = y;
|
|
692
|
+
},
|
|
693
|
+
[scrollY, snap, resync, setScrollOffset],
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
const { insetLeft, insetRight } = layout;
|
|
697
|
+
const contentStyle = useAnimatedStyle(
|
|
698
|
+
() =>
|
|
699
|
+
enabled
|
|
700
|
+
? {
|
|
701
|
+
paddingLeft: insetLeft,
|
|
702
|
+
paddingRight: insetRight,
|
|
703
|
+
paddingTop: hasSideBar ? heightHeader - headerHide.value : 0,
|
|
704
|
+
}
|
|
705
|
+
: { paddingLeft: 0, paddingRight: 0, paddingTop: 0 },
|
|
706
|
+
[enabled, hasSideBar, heightHeader, insetLeft, insetRight],
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
const background = duo ? (
|
|
710
|
+
<DuoScrollGradient
|
|
711
|
+
animatedValue={scrollY}
|
|
712
|
+
gradientColor={gradientColor}
|
|
713
|
+
headerBackground={headerBackground}
|
|
714
|
+
heightHeader={heightHeader}
|
|
715
|
+
followScroll
|
|
716
|
+
/>
|
|
717
|
+
) : null;
|
|
718
|
+
|
|
719
|
+
return { scrollY, onScrollChange, setScrollOffset, background, contentStyle };
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
|
|
478
723
|
export {
|
|
479
724
|
getStackOptions,
|
|
480
725
|
getModalOptions,
|
|
@@ -486,5 +731,8 @@ export {
|
|
|
486
731
|
useAppState,
|
|
487
732
|
useNativeCanGoBack,
|
|
488
733
|
useNormalizedSharedValue,
|
|
734
|
+
useDuoQuickReturn,
|
|
735
|
+
useHeaderScrollState,
|
|
489
736
|
SwipeBackHaptic,
|
|
737
|
+
isWhiteListDesignSystem,
|
|
490
738
|
};
|
package/Consts/index.ts
CHANGED
package/Consts/theme.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {Context, Theme} from '../Application/types';
|
|
2
|
-
import {Colors} from './colors+spacing+radius';
|
|
1
|
+
import type { Context, Theme } from '../Application/types';
|
|
2
|
+
import { Colors } from './colors+spacing+radius';
|
|
3
3
|
|
|
4
4
|
const defaultTheme: Theme = {
|
|
5
5
|
dark: false,
|
|
@@ -51,7 +51,7 @@ const defaultTheme: Theme = {
|
|
|
51
51
|
container: Colors.blue_08,
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
|
-
font: '
|
|
54
|
+
font: 'SFProText',
|
|
55
55
|
assets: {},
|
|
56
56
|
};
|
|
57
57
|
|
|
@@ -105,17 +105,16 @@ const defaultDarkTheme: Theme = {
|
|
|
105
105
|
container: Colors.blue_08,
|
|
106
106
|
},
|
|
107
107
|
},
|
|
108
|
-
font: '
|
|
108
|
+
font: 'SFProText',
|
|
109
109
|
assets: {},
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
const defaultContext: Context = {
|
|
113
113
|
theme: defaultTheme,
|
|
114
114
|
navigator: undefined,
|
|
115
|
-
showGrid: false,
|
|
116
115
|
translate: () => {
|
|
117
116
|
return '';
|
|
118
117
|
},
|
|
119
118
|
};
|
|
120
119
|
|
|
121
|
-
export {defaultContext, defaultTheme, defaultDarkTheme};
|
|
120
|
+
export { defaultContext, defaultTheme, defaultDarkTheme };
|
package/Input/Input.tsx
CHANGED
|
@@ -6,9 +6,8 @@ import React, {
|
|
|
6
6
|
useState,
|
|
7
7
|
} from 'react';
|
|
8
8
|
import {
|
|
9
|
-
NativeSyntheticEvent,
|
|
10
9
|
TextInput,
|
|
11
|
-
|
|
10
|
+
TextInputFocusEvent,
|
|
12
11
|
TouchableOpacity,
|
|
13
12
|
View,
|
|
14
13
|
} from 'react-native';
|
|
@@ -102,12 +101,12 @@ const Input = forwardRef(
|
|
|
102
101
|
onChangeText?.(text);
|
|
103
102
|
};
|
|
104
103
|
|
|
105
|
-
const _onFocus = (e:
|
|
104
|
+
const _onFocus = (e: TextInputFocusEvent) => {
|
|
106
105
|
setFocused(true);
|
|
107
106
|
onFocus?.(e);
|
|
108
107
|
};
|
|
109
108
|
|
|
110
|
-
const _onBlur = (e:
|
|
109
|
+
const _onBlur = (e: TextInputFocusEvent) => {
|
|
111
110
|
setFocused(false);
|
|
112
111
|
onBlur?.(e);
|
|
113
112
|
};
|
package/Input/InputMoney.tsx
CHANGED
|
@@ -9,11 +9,9 @@ import React, {
|
|
|
9
9
|
useState,
|
|
10
10
|
} from 'react';
|
|
11
11
|
import {
|
|
12
|
-
NativeSyntheticEvent,
|
|
13
|
-
Platform,
|
|
14
12
|
TextInput,
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
TextInputFocusEvent,
|
|
14
|
+
TextInputSelectionChangeEvent,
|
|
17
15
|
TouchableOpacity,
|
|
18
16
|
View,
|
|
19
17
|
} from 'react-native';
|
|
@@ -57,13 +55,10 @@ const caretFromDigitIndex = (display: string, digitIndex: number) => {
|
|
|
57
55
|
return 0;
|
|
58
56
|
};
|
|
59
57
|
|
|
60
|
-
// Finds which range of `old` was replaced to produce `next`. `selection` is
|
|
61
|
-
// the pre-edit selection on iOS and already the post-edit caret on Android,
|
|
62
|
-
// where onSelectionChange fires before onChangeText.
|
|
63
58
|
const detectEdit = (old: string, next: string, selection: Selection): Edit => {
|
|
64
59
|
const { start, end } = selection;
|
|
65
60
|
const removed = old.length - next.length;
|
|
66
|
-
const
|
|
61
|
+
const candidates: Edit[] = [
|
|
67
62
|
{
|
|
68
63
|
start,
|
|
69
64
|
end,
|
|
@@ -72,26 +67,6 @@ const detectEdit = (old: string, next: string, selection: Selection): Edit => {
|
|
|
72
67
|
{ start: start - removed, end: start, inserted: '' },
|
|
73
68
|
{ start, end: start + removed, inserted: '' },
|
|
74
69
|
];
|
|
75
|
-
const postEdit: Edit[] = [];
|
|
76
|
-
const tail = next.length - start;
|
|
77
|
-
if (start === end && tail <= old.length) {
|
|
78
|
-
let prefix = 0;
|
|
79
|
-
while (
|
|
80
|
-
prefix < Math.min(start, old.length - tail) &&
|
|
81
|
-
old[prefix] === next[prefix]
|
|
82
|
-
) {
|
|
83
|
-
prefix += 1;
|
|
84
|
-
}
|
|
85
|
-
postEdit.push({
|
|
86
|
-
start: prefix,
|
|
87
|
-
end: old.length - tail,
|
|
88
|
-
inserted: next.slice(prefix, start),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
const candidates =
|
|
92
|
-
Platform.OS === 'android'
|
|
93
|
-
? [...postEdit, ...preEdit]
|
|
94
|
-
: [...preEdit, ...postEdit];
|
|
95
70
|
const match = candidates.find(
|
|
96
71
|
e =>
|
|
97
72
|
e.start >= 0 &&
|
|
@@ -196,9 +171,8 @@ const InputMoney = forwardRef(
|
|
|
196
171
|
const selectionRef = useRef<Selection>({ start: 0, end: 0 });
|
|
197
172
|
// Caret to push to native once the committed text is rendered.
|
|
198
173
|
const pendingCaretRef = useRef<number | null>(null);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const rawCaretRef = useRef<number | null>(null);
|
|
174
|
+
const pendingSelectionRef = useRef<Selection | null>(null);
|
|
175
|
+
const editingRef = useRef(false);
|
|
202
176
|
const inputRef = useRef<TextInput>(null);
|
|
203
177
|
const componentName = 'InputMoney';
|
|
204
178
|
|
|
@@ -265,10 +239,13 @@ const InputMoney = forwardRef(
|
|
|
265
239
|
const _onChangeText = (text: string) => {
|
|
266
240
|
let digits = '';
|
|
267
241
|
let caretDigit = 0;
|
|
268
|
-
|
|
242
|
+
pendingSelectionRef.current = null;
|
|
243
|
+
editingRef.current = true;
|
|
244
|
+
setTimeout(() => {
|
|
245
|
+
editingRef.current = false;
|
|
246
|
+
});
|
|
269
247
|
if (text) {
|
|
270
248
|
const edit = detectEdit(displayRef.current, text, selectionRef.current);
|
|
271
|
-
rawCaretRef.current = edit.start + edit.inserted.length;
|
|
272
249
|
({ digits, caretDigit } = applyEdit(
|
|
273
250
|
displayRef.current,
|
|
274
251
|
edit,
|
|
@@ -281,31 +258,34 @@ const InputMoney = forwardRef(
|
|
|
281
258
|
};
|
|
282
259
|
|
|
283
260
|
// Keeps the caret out of the currency suffix.
|
|
284
|
-
const _onSelectionChange = (
|
|
285
|
-
e: NativeSyntheticEvent<TextInputSelectionChangeEventData>,
|
|
286
|
-
) => {
|
|
261
|
+
const _onSelectionChange = (e: TextInputSelectionChangeEvent) => {
|
|
287
262
|
props.onSelectionChange?.(e);
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
263
|
+
if (editingRef.current) return;
|
|
264
|
+
const selection = e.nativeEvent.selection;
|
|
265
|
+
pendingSelectionRef.current = selection;
|
|
266
|
+
setTimeout(() => {
|
|
267
|
+
if (pendingSelectionRef.current !== selection) return;
|
|
268
|
+
pendingSelectionRef.current = null;
|
|
269
|
+
const { start, end } = selection;
|
|
270
|
+
const display = displayRef.current;
|
|
271
|
+
const max = display ? display.length - currency.length : 0;
|
|
272
|
+
const clamped = {
|
|
273
|
+
start: Math.min(start, max),
|
|
274
|
+
end: Math.min(end, max),
|
|
275
|
+
};
|
|
276
|
+
selectionRef.current = clamped;
|
|
277
|
+
if (clamped.start !== start || clamped.end !== end) {
|
|
278
|
+
inputRef.current?.setSelection(clamped.start, clamped.end);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
301
281
|
};
|
|
302
282
|
|
|
303
|
-
const _onFocus = (e:
|
|
283
|
+
const _onFocus = (e: TextInputFocusEvent) => {
|
|
304
284
|
setFocused(true);
|
|
305
285
|
onFocus?.(e);
|
|
306
286
|
};
|
|
307
287
|
|
|
308
|
-
const _onBlur = (e:
|
|
288
|
+
const _onBlur = (e: TextInputFocusEvent) => {
|
|
309
289
|
setFocused(false);
|
|
310
290
|
onBlur?.(e);
|
|
311
291
|
};
|
|
@@ -6,10 +6,9 @@ import React, {
|
|
|
6
6
|
useState,
|
|
7
7
|
} from 'react';
|
|
8
8
|
import {
|
|
9
|
-
NativeSyntheticEvent,
|
|
10
9
|
StyleSheet,
|
|
11
10
|
TextInput,
|
|
12
|
-
|
|
11
|
+
TextInputFocusEvent,
|
|
13
12
|
TouchableOpacity,
|
|
14
13
|
View,
|
|
15
14
|
} from 'react-native';
|
|
@@ -93,12 +92,12 @@ const InputPhoneNumber = forwardRef(
|
|
|
93
92
|
onChangeText?.(text);
|
|
94
93
|
};
|
|
95
94
|
|
|
96
|
-
const _onFocus = (e:
|
|
95
|
+
const _onFocus = (e: TextInputFocusEvent) => {
|
|
97
96
|
setFocused(true);
|
|
98
97
|
onFocus?.(e);
|
|
99
98
|
};
|
|
100
99
|
|
|
101
|
-
const _onBlur = (e:
|
|
100
|
+
const _onBlur = (e: TextInputFocusEvent) => {
|
|
102
101
|
setFocused(false);
|
|
103
102
|
onBlur?.(e);
|
|
104
103
|
};
|
package/Input/InputSearch.tsx
CHANGED
|
@@ -10,9 +10,8 @@ import React, {
|
|
|
10
10
|
useState,
|
|
11
11
|
} from 'react';
|
|
12
12
|
import {
|
|
13
|
-
NativeSyntheticEvent,
|
|
14
13
|
TextInput,
|
|
15
|
-
|
|
14
|
+
TextInputFocusEvent,
|
|
16
15
|
TouchableOpacity,
|
|
17
16
|
View,
|
|
18
17
|
} from 'react-native';
|
|
@@ -166,12 +165,12 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
|
|
|
166
165
|
props.accessibilityLabel,
|
|
167
166
|
);
|
|
168
167
|
|
|
169
|
-
const _onFocus = (e:
|
|
168
|
+
const _onFocus = (e: TextInputFocusEvent) => {
|
|
170
169
|
setFocused(true);
|
|
171
170
|
onFocus?.(e);
|
|
172
171
|
};
|
|
173
172
|
|
|
174
|
-
const _onBlur = (e:
|
|
173
|
+
const _onBlur = (e: TextInputFocusEvent) => {
|
|
175
174
|
setFocused(false);
|
|
176
175
|
onBlur?.(e);
|
|
177
176
|
};
|
package/Input/InputTextArea.tsx
CHANGED
|
@@ -7,9 +7,8 @@ import React, {
|
|
|
7
7
|
useState,
|
|
8
8
|
} from 'react';
|
|
9
9
|
import {
|
|
10
|
-
NativeSyntheticEvent,
|
|
11
10
|
TextInput,
|
|
12
|
-
|
|
11
|
+
TextInputFocusEvent,
|
|
13
12
|
TouchableOpacity,
|
|
14
13
|
View,
|
|
15
14
|
} from 'react-native';
|
|
@@ -79,12 +78,12 @@ const InputTextArea = forwardRef(
|
|
|
79
78
|
setInputValue(text);
|
|
80
79
|
};
|
|
81
80
|
|
|
82
|
-
const _onFocus = (e:
|
|
81
|
+
const _onFocus = (e: TextInputFocusEvent) => {
|
|
83
82
|
setFocused(true);
|
|
84
83
|
onFocus?.(e);
|
|
85
84
|
};
|
|
86
85
|
|
|
87
|
-
const _onBlur = (e:
|
|
86
|
+
const _onBlur = (e: TextInputFocusEvent) => {
|
|
88
87
|
setFocused(false);
|
|
89
88
|
onBlur?.(e);
|
|
90
89
|
};
|