@fountain-ui/core 2.0.0-beta.13 → 2.0.0-beta.14

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.
Files changed (47) hide show
  1. package/build/commonjs/Tab/Tab.js +9 -5
  2. package/build/commonjs/Tab/Tab.js.map +1 -1
  3. package/build/commonjs/Tabs/IndexAwareTab.js +28 -0
  4. package/build/commonjs/Tabs/IndexAwareTab.js.map +1 -0
  5. package/build/commonjs/Tabs/TabIndicator.js +18 -12
  6. package/build/commonjs/Tabs/TabIndicator.js.map +1 -1
  7. package/build/commonjs/Tabs/TabIndicatorProps.js.map +1 -1
  8. package/build/commonjs/Tabs/Tabs.js +41 -61
  9. package/build/commonjs/Tabs/Tabs.js.map +1 -1
  10. package/build/commonjs/Tabs/TabsProps.js.map +1 -1
  11. package/build/commonjs/Tabs/index.js.map +1 -1
  12. package/build/commonjs/Tabs/useScrollViewReaction.js +54 -0
  13. package/build/commonjs/Tabs/useScrollViewReaction.js.map +1 -0
  14. package/build/module/Tab/Tab.js +5 -5
  15. package/build/module/Tab/Tab.js.map +1 -1
  16. package/build/module/Tabs/IndexAwareTab.js +18 -0
  17. package/build/module/Tabs/IndexAwareTab.js.map +1 -0
  18. package/build/module/Tabs/TabIndicator.js +18 -13
  19. package/build/module/Tabs/TabIndicator.js.map +1 -1
  20. package/build/module/Tabs/TabIndicatorProps.js.map +1 -1
  21. package/build/module/Tabs/Tabs.js +40 -60
  22. package/build/module/Tabs/Tabs.js.map +1 -1
  23. package/build/module/Tabs/TabsProps.js.map +1 -1
  24. package/build/module/Tabs/index.js.map +1 -1
  25. package/build/module/Tabs/useScrollViewReaction.js +44 -0
  26. package/build/module/Tabs/useScrollViewReaction.js.map +1 -0
  27. package/build/typescript/Tabs/IndexAwareTab.d.ts +9 -0
  28. package/build/typescript/Tabs/TabIndicatorProps.d.ts +2 -2
  29. package/build/typescript/Tabs/Tabs.d.ts +4 -1
  30. package/build/typescript/Tabs/TabsProps.d.ts +19 -11
  31. package/build/typescript/Tabs/index.d.ts +1 -1
  32. package/build/typescript/Tabs/useScrollViewReaction.d.ts +9 -0
  33. package/package.json +2 -2
  34. package/src/Tab/Tab.tsx +5 -5
  35. package/src/Tabs/IndexAwareTab.tsx +30 -0
  36. package/src/Tabs/TabIndicator.tsx +17 -13
  37. package/src/Tabs/TabIndicatorProps.ts +2 -2
  38. package/src/Tabs/Tabs.tsx +47 -62
  39. package/src/Tabs/TabsProps.ts +22 -12
  40. package/src/Tabs/index.ts +1 -1
  41. package/src/Tabs/useScrollViewReaction.ts +55 -0
  42. package/build/commonjs/Tabs/useTabsWidth.js +0 -26
  43. package/build/commonjs/Tabs/useTabsWidth.js.map +0 -1
  44. package/build/module/Tabs/useTabsWidth.js +0 -18
  45. package/build/module/Tabs/useTabsWidth.js.map +0 -1
  46. package/build/typescript/Tabs/useTabsWidth.d.ts +0 -2
  47. package/src/Tabs/useTabsWidth.ts +0 -20
package/src/Tabs/Tabs.tsx CHANGED
@@ -1,13 +1,15 @@
1
- import React, { cloneElement, useEffect, useMemo, useRef } from 'react';
2
- import { GestureResponderEvent, LayoutChangeEvent, ScrollView, View } from 'react-native';
3
- import type { WithTimingConfig } from 'react-native-reanimated';
4
- import { Easing, useSharedValue, withTiming } from 'react-native-reanimated';
1
+ import React, { cloneElement, forwardRef, useImperativeHandle } from 'react';
2
+ import type { GestureResponderEvent, LayoutChangeEvent } from 'react-native';
3
+ import { ScrollView, View } from 'react-native';
4
+ import { useSharedValue } from 'react-native-reanimated';
5
5
  import { NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';
6
6
  import { css, useTheme } from '../styles';
7
7
  import type TabsProps from './TabsProps';
8
+ import type { TabsInstance } from './TabsProps';
8
9
  import TabIndicator from './TabIndicator';
9
- import useTabsWidth from './useTabsWidth';
10
+ import IndexAwareTab from './IndexAwareTab';
10
11
  import useTabCoordinates from './useTabCoordinates';
12
+ import useScrollViewReaction from './useScrollViewReaction';
11
13
 
12
14
  type TabsStyleKeys =
13
15
  | 'root'
@@ -34,65 +36,43 @@ const useStyles: UseStyles<TabsStyles> = function (): TabsStyles {
34
36
  };
35
37
  };
36
38
 
37
- const ANIMATION_CONFIG: Readonly<WithTimingConfig> = {
38
- duration: 200,
39
- easing: Easing.out(Easing.exp),
40
- };
41
-
42
- export default function Tabs(props: TabsProps) {
39
+ const Tabs = forwardRef<TabsInstance, TabsProps>(function Tabs(props, ref) {
43
40
  const {
44
41
  children,
45
- index: indexProp,
42
+ initialIndex = 0,
46
43
  disableIndicator = false,
47
44
  keyboardDismissMode = 'none',
48
45
  keyboardShouldPersistTaps = 'never',
49
46
  onChange,
50
47
  scrollable = false,
51
- scrollValue: scrollValueProp,
52
48
  style,
53
49
  variant = 'primary',
54
- ...otherProps
55
50
  } = props;
56
51
 
57
- const styles = useStyles();
58
-
59
- const [containerWidth, handleLayout] = useTabsWidth();
60
-
61
- const scrollViewRef = useRef<ScrollView | null>(null);
52
+ const sharedIndex = useSharedValue<number>(initialIndex);
62
53
 
63
- const { coordinates, updateCoordinate } = useTabCoordinates(children);
64
-
65
- const internalScrollValue = useSharedValue(0);
66
- const scrollValue = scrollValueProp ?? internalScrollValue;
67
-
68
- const isReadyToRenderIndicator = coordinates.length > 0;
69
-
70
- useEffect(() => {
71
- const animateTab = (index: number) => {
72
- scrollValue.value = withTiming(index, ANIMATION_CONFIG);
73
- };
54
+ const getCurrentIndex = (): number => sharedIndex.value;
74
55
 
75
- animateTab(indexProp);
76
- }, [indexProp, scrollValue]);
56
+ const setTab = (newIndex: number) => {
57
+ sharedIndex.value = newIndex;
58
+ };
77
59
 
78
- const scrollPosition = useMemo<number>(() => {
79
- const coordinate = coordinates[indexProp - 1];
60
+ useImperativeHandle(
61
+ ref,
62
+ () => ({
63
+ getCurrentIndex,
64
+ setTab,
65
+ }),
66
+ [sharedIndex],
67
+ );
80
68
 
81
- if (coordinate) {
82
- const tabWidth = coordinate.x2 - coordinate.x1;
83
- return Math.floor(coordinate.x1 + tabWidth / 2);
84
- }
69
+ const styles = useStyles();
85
70
 
86
- return 0;
87
- }, [indexProp, coordinates]);
71
+ const { coordinates, updateCoordinate } = useTabCoordinates(children);
88
72
 
89
- useEffect(() => {
90
- const scrollView = scrollViewRef.current;
73
+ const { scrollViewRef, onLayout } = useScrollViewReaction(sharedIndex, coordinates);
91
74
 
92
- if (scrollView) {
93
- scrollView.scrollTo({ x: scrollPosition, y: 0, animated: true });
94
- }
95
- }, [scrollPosition, containerWidth]);
75
+ const isReadyToRenderIndicator = coordinates.length > 0;
96
76
 
97
77
  const tabElements = React.Children.map(children, (child, index) => {
98
78
  const onLayout = (event: LayoutChangeEvent) => {
@@ -108,46 +88,49 @@ export default function Tabs(props: TabsProps) {
108
88
  };
109
89
 
110
90
  const onPress = () => {
91
+ setTab(index);
92
+
111
93
  onChange?.(index);
112
94
  // @ts-ignore
113
95
  child.props.onPress?.();
114
96
  };
115
97
 
116
- const selected = index === indexProp;
117
- const enableIndicatorPlaceholder = disableIndicator
118
- ? false
119
- : (isReadyToRenderIndicator ? false : selected);
120
-
121
- //@ts-ignore
122
- return cloneElement(child, {
123
- enableIndicator: enableIndicatorPlaceholder,
98
+ // @ts-ignore
99
+ const tabElement = cloneElement(child, {
100
+ enableIndicator: !disableIndicator && isReadyToRenderIndicator,
124
101
  onLayout,
125
102
  onPress,
126
103
  onMouseDown,
127
104
  variant,
128
- selected,
129
105
  style: scrollable ? undefined : styles.fixedTab,
130
106
  });
107
+
108
+ return (
109
+ <IndexAwareTab
110
+ children={tabElement}
111
+ index={index}
112
+ sharedIndex={sharedIndex}
113
+ />
114
+ );
131
115
  });
132
116
 
133
- const indicator = (
117
+ const tabIndicator = (
134
118
  <TabIndicator
135
119
  coordinates={coordinates}
136
120
  disabled={disableIndicator}
137
121
  scrollable={scrollable}
138
- scrollValue={scrollValue}
122
+ sharedIndex={sharedIndex}
139
123
  />
140
124
  );
141
125
 
142
126
  return (
143
127
  <View
144
- onLayout={handleLayout}
128
+ onLayout={onLayout}
145
129
  style={css([
146
130
  styles.root,
147
131
  scrollable ? undefined : styles.fixedRoot,
148
132
  style,
149
133
  ])}
150
- {...otherProps}
151
134
  >
152
135
  {scrollable ? (
153
136
  <ScrollView
@@ -164,14 +147,16 @@ export default function Tabs(props: TabsProps) {
164
147
  keyboardShouldPersistTaps={keyboardShouldPersistTaps}
165
148
  >
166
149
  {tabElements}
167
- {indicator}
150
+ {tabIndicator}
168
151
  </ScrollView>
169
152
  ) : (
170
153
  <React.Fragment>
171
154
  {tabElements}
172
- {indicator}
155
+ {tabIndicator}
173
156
  </React.Fragment>
174
157
  )}
175
158
  </View>
176
159
  );
177
- };
160
+ });
161
+
162
+ export default Tabs;
@@ -1,6 +1,5 @@
1
- import React from 'react';
1
+ import type { ReactNode, Ref } from 'react';
2
2
  import type { ViewProps } from 'react-native';
3
- import Animated from 'react-native-reanimated';
4
3
  import type { TabVariant } from '../Tab';
5
4
  import type { OverridableComponentProps } from '../types';
6
5
 
@@ -15,16 +14,26 @@ export type KeyboardShouldPersistTaps =
15
14
  | 'always'
16
15
  | 'handled'; // app only
17
16
 
18
- export default interface TabsProps extends OverridableComponentProps<ViewProps, {
17
+ export interface TabsInstance {
19
18
  /**
20
- * Collection of Tab components.
19
+ * Get current tab index.
21
20
  */
22
- children: React.ReactNode;
21
+ getCurrentIndex: () => number;
22
+
23
+ /**
24
+ * Function to scroll to a specific tab. Invalid index is ignored.
25
+ * @param index
26
+ */
27
+ setTab: (index: number) => void;
28
+ }
29
+
30
+ export default interface TabsProps extends OverridableComponentProps<ViewProps, {
31
+ ref?: Ref<TabsInstance>;
23
32
 
24
33
  /**
25
- * Selected index.
34
+ * Collection of Tab components.
26
35
  */
27
- index: number;
36
+ children: ReactNode;
28
37
 
29
38
  /**
30
39
  * If `true`, the indicator is disabled.
@@ -32,6 +41,12 @@ export default interface TabsProps extends OverridableComponentProps<ViewProps,
32
41
  */
33
42
  disableIndicator?: boolean;
34
43
 
44
+ /**
45
+ * Index of initial tab that should be selected.
46
+ * @default 0
47
+ */
48
+ initialIndex?: number;
49
+
35
50
  /**
36
51
  * keyboard dismissing condition of dragging.
37
52
  * @default 'none'
@@ -55,11 +70,6 @@ export default interface TabsProps extends OverridableComponentProps<ViewProps,
55
70
  */
56
71
  scrollable?: boolean;
57
72
 
58
- /**
59
- * Scrollable value for using animated interpolation.
60
- */
61
- scrollValue?: Animated.SharedValue<number>;
62
-
63
73
  /**
64
74
  * The variant to use.
65
75
  * @default 'primary'
package/src/Tabs/index.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { default } from './Tabs';
2
- export type { default as TabsProps } from './TabsProps';
2
+ export type { default as TabsProps, TabsInstance } from './TabsProps';
@@ -0,0 +1,55 @@
1
+ import type { MutableRefObject } from 'react';
2
+ import { useCallback, useRef } from 'react';
3
+ import type { ScrollView, ViewProps } from 'react-native';
4
+ import { runOnJS, SharedValue, useAnimatedReaction } from 'react-native-reanimated';
5
+ import type TabCoordinate from './TabCoordinate';
6
+
7
+ export interface UseScrollViewReaction {
8
+ scrollViewRef: MutableRefObject<ScrollView | null>;
9
+ onLayout: ViewProps['onLayout'];
10
+ }
11
+
12
+ export default function useScrollViewReaction(
13
+ sharedIndex: SharedValue<number>,
14
+ coordinates: TabCoordinate[],
15
+ ): UseScrollViewReaction {
16
+ const scrollViewRef = useRef<ScrollView | null>(null);
17
+
18
+ const lastScrolledPositionRef = useRef<number>(NaN);
19
+
20
+ const scrollTo = (scrollPosition: number) => {
21
+ const scrollView = scrollViewRef.current;
22
+
23
+ if (scrollView && Number.isFinite(scrollPosition)) {
24
+ scrollView.scrollTo({ x: scrollPosition, y: 0, animated: true });
25
+
26
+ lastScrolledPositionRef.current = scrollPosition;
27
+ }
28
+ };
29
+
30
+ const onLayout = useCallback(() => {
31
+ scrollTo(lastScrolledPositionRef.current);
32
+ }, []);
33
+
34
+ useAnimatedReaction(
35
+ () => {
36
+ const prevIndex = sharedIndex.value - 1;
37
+ const prevCoordinate = coordinates[prevIndex];
38
+
39
+ if (prevCoordinate) {
40
+ const prevTabWidth = prevCoordinate.x2 - prevCoordinate.x1;
41
+ return Math.floor(prevCoordinate.x1 + prevTabWidth / 2);
42
+ }
43
+
44
+ return 0;
45
+ },
46
+ (scrollPosition, prevScrollPosition) => {
47
+ if (scrollPosition !== prevScrollPosition) {
48
+ runOnJS(scrollTo)(scrollPosition);
49
+ }
50
+ },
51
+ [coordinates],
52
+ );
53
+
54
+ return { scrollViewRef, onLayout };
55
+ };
@@ -1,26 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = useTabsWidth;
7
-
8
- var _react = require("react");
9
-
10
- var _reactNative = require("react-native");
11
-
12
- const assumeInitialWidth = () => _reactNative.Dimensions.get('window').width;
13
-
14
- const isIntegerPartEquals = (a, b) => Math.round(a) === Math.round(b);
15
-
16
- const isIntegerPartDifferent = (a, b) => !isIntegerPartEquals(a, b);
17
-
18
- function useTabsWidth() {
19
- const [width, setWidth] = (0, _react.useState)(assumeInitialWidth);
20
- const onLayout = (0, _react.useCallback)(e => {
21
- const newWidth = e.nativeEvent.layout.width;
22
- setWidth(prevWidth => isIntegerPartDifferent(prevWidth, newWidth) ? newWidth : prevWidth);
23
- }, []);
24
- return [width, onLayout];
25
- }
26
- //# sourceMappingURL=useTabsWidth.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["assumeInitialWidth","Dimensions","get","width","isIntegerPartEquals","a","b","Math","round","isIntegerPartDifferent","useTabsWidth","setWidth","useState","onLayout","useCallback","e","newWidth","nativeEvent","layout","prevWidth"],"sources":["useTabsWidth.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { Dimensions, LayoutChangeEvent, ViewProps } from 'react-native';\n\nconst assumeInitialWidth = (): number => Dimensions.get('window').width;\n\nconst isIntegerPartEquals = (a: number, b: number) => Math.round(a) === Math.round(b);\n\nconst isIntegerPartDifferent = (a: number, b: number) => !isIntegerPartEquals(a, b);\n\nexport default function useTabsWidth(): [number, ViewProps['onLayout']] {\n const [width, setWidth] = useState(assumeInitialWidth);\n\n const onLayout = useCallback((e: LayoutChangeEvent) => {\n const newWidth = e.nativeEvent.layout.width;\n\n setWidth((prevWidth) => isIntegerPartDifferent(prevWidth, newWidth) ? newWidth : prevWidth);\n }, []);\n\n return [width, onLayout];\n}\n"],"mappings":";;;;;;;AAAA;;AACA;;AAEA,MAAMA,kBAAkB,GAAG,MAAcC,uBAAA,CAAWC,GAAX,CAAe,QAAf,EAAyBC,KAAlE;;AAEA,MAAMC,mBAAmB,GAAG,CAACC,CAAD,EAAYC,CAAZ,KAA0BC,IAAI,CAACC,KAAL,CAAWH,CAAX,MAAkBE,IAAI,CAACC,KAAL,CAAWF,CAAX,CAAxE;;AAEA,MAAMG,sBAAsB,GAAG,CAACJ,CAAD,EAAYC,CAAZ,KAA0B,CAACF,mBAAmB,CAACC,CAAD,EAAIC,CAAJ,CAA7E;;AAEe,SAASI,YAAT,GAAyD;EACpE,MAAM,CAACP,KAAD,EAAQQ,QAAR,IAAoB,IAAAC,eAAA,EAASZ,kBAAT,CAA1B;EAEA,MAAMa,QAAQ,GAAG,IAAAC,kBAAA,EAAaC,CAAD,IAA0B;IACnD,MAAMC,QAAQ,GAAGD,CAAC,CAACE,WAAF,CAAcC,MAAd,CAAqBf,KAAtC;IAEAQ,QAAQ,CAAEQ,SAAD,IAAeV,sBAAsB,CAACU,SAAD,EAAYH,QAAZ,CAAtB,GAA8CA,QAA9C,GAAyDG,SAAzE,CAAR;EACH,CAJgB,EAId,EAJc,CAAjB;EAMA,OAAO,CAAChB,KAAD,EAAQU,QAAR,CAAP;AACH"}
@@ -1,18 +0,0 @@
1
- import { useCallback, useState } from 'react';
2
- import { Dimensions } from 'react-native';
3
-
4
- const assumeInitialWidth = () => Dimensions.get('window').width;
5
-
6
- const isIntegerPartEquals = (a, b) => Math.round(a) === Math.round(b);
7
-
8
- const isIntegerPartDifferent = (a, b) => !isIntegerPartEquals(a, b);
9
-
10
- export default function useTabsWidth() {
11
- const [width, setWidth] = useState(assumeInitialWidth);
12
- const onLayout = useCallback(e => {
13
- const newWidth = e.nativeEvent.layout.width;
14
- setWidth(prevWidth => isIntegerPartDifferent(prevWidth, newWidth) ? newWidth : prevWidth);
15
- }, []);
16
- return [width, onLayout];
17
- }
18
- //# sourceMappingURL=useTabsWidth.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["useCallback","useState","Dimensions","assumeInitialWidth","get","width","isIntegerPartEquals","a","b","Math","round","isIntegerPartDifferent","useTabsWidth","setWidth","onLayout","e","newWidth","nativeEvent","layout","prevWidth"],"sources":["useTabsWidth.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { Dimensions, LayoutChangeEvent, ViewProps } from 'react-native';\n\nconst assumeInitialWidth = (): number => Dimensions.get('window').width;\n\nconst isIntegerPartEquals = (a: number, b: number) => Math.round(a) === Math.round(b);\n\nconst isIntegerPartDifferent = (a: number, b: number) => !isIntegerPartEquals(a, b);\n\nexport default function useTabsWidth(): [number, ViewProps['onLayout']] {\n const [width, setWidth] = useState(assumeInitialWidth);\n\n const onLayout = useCallback((e: LayoutChangeEvent) => {\n const newWidth = e.nativeEvent.layout.width;\n\n setWidth((prevWidth) => isIntegerPartDifferent(prevWidth, newWidth) ? newWidth : prevWidth);\n }, []);\n\n return [width, onLayout];\n}\n"],"mappings":"AAAA,SAASA,WAAT,EAAsBC,QAAtB,QAAsC,OAAtC;AACA,SAASC,UAAT,QAAyD,cAAzD;;AAEA,MAAMC,kBAAkB,GAAG,MAAcD,UAAU,CAACE,GAAX,CAAe,QAAf,EAAyBC,KAAlE;;AAEA,MAAMC,mBAAmB,GAAG,CAACC,CAAD,EAAYC,CAAZ,KAA0BC,IAAI,CAACC,KAAL,CAAWH,CAAX,MAAkBE,IAAI,CAACC,KAAL,CAAWF,CAAX,CAAxE;;AAEA,MAAMG,sBAAsB,GAAG,CAACJ,CAAD,EAAYC,CAAZ,KAA0B,CAACF,mBAAmB,CAACC,CAAD,EAAIC,CAAJ,CAA7E;;AAEA,eAAe,SAASI,YAAT,GAAyD;EACpE,MAAM,CAACP,KAAD,EAAQQ,QAAR,IAAoBZ,QAAQ,CAACE,kBAAD,CAAlC;EAEA,MAAMW,QAAQ,GAAGd,WAAW,CAAEe,CAAD,IAA0B;IACnD,MAAMC,QAAQ,GAAGD,CAAC,CAACE,WAAF,CAAcC,MAAd,CAAqBb,KAAtC;IAEAQ,QAAQ,CAAEM,SAAD,IAAeR,sBAAsB,CAACQ,SAAD,EAAYH,QAAZ,CAAtB,GAA8CA,QAA9C,GAAyDG,SAAzE,CAAR;EACH,CAJ2B,EAIzB,EAJyB,CAA5B;EAMA,OAAO,CAACd,KAAD,EAAQS,QAAR,CAAP;AACH"}
@@ -1,2 +0,0 @@
1
- import { ViewProps } from 'react-native';
2
- export default function useTabsWidth(): [number, ViewProps['onLayout']];
@@ -1,20 +0,0 @@
1
- import { useCallback, useState } from 'react';
2
- import { Dimensions, LayoutChangeEvent, ViewProps } from 'react-native';
3
-
4
- const assumeInitialWidth = (): number => Dimensions.get('window').width;
5
-
6
- const isIntegerPartEquals = (a: number, b: number) => Math.round(a) === Math.round(b);
7
-
8
- const isIntegerPartDifferent = (a: number, b: number) => !isIntegerPartEquals(a, b);
9
-
10
- export default function useTabsWidth(): [number, ViewProps['onLayout']] {
11
- const [width, setWidth] = useState(assumeInitialWidth);
12
-
13
- const onLayout = useCallback((e: LayoutChangeEvent) => {
14
- const newWidth = e.nativeEvent.layout.width;
15
-
16
- setWidth((prevWidth) => isIntegerPartDifferent(prevWidth, newWidth) ? newWidth : prevWidth);
17
- }, []);
18
-
19
- return [width, onLayout];
20
- }