@fountain-ui/core 2.0.0-beta.75 → 2.0.0-beta.76

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 (49) hide show
  1. package/build/commonjs/Dialog/Dialog.js +30 -2
  2. package/build/commonjs/Dialog/Dialog.js.map +1 -1
  3. package/build/commonjs/Modal/Modal.js +7 -1
  4. package/build/commonjs/Modal/Modal.js.map +1 -1
  5. package/build/commonjs/Modal/ModalProps.js.map +1 -1
  6. package/build/commonjs/Snackbar/Snackbar.js +30 -7
  7. package/build/commonjs/Snackbar/Snackbar.js.map +1 -1
  8. package/build/commonjs/Tab/Tab.js +23 -14
  9. package/build/commonjs/Tab/Tab.js.map +1 -1
  10. package/build/commonjs/Tab/TabIndicator.js +5 -4
  11. package/build/commonjs/Tab/TabIndicator.js.map +1 -1
  12. package/build/commonjs/Tab/TabProps.js.map +1 -1
  13. package/build/commonjs/Tab/index.js.map +1 -1
  14. package/build/commonjs/Tabs/Tabs.js +46 -6
  15. package/build/commonjs/Tabs/Tabs.js.map +1 -1
  16. package/build/commonjs/Tabs/TabsProps.js.map +1 -1
  17. package/build/module/Dialog/Dialog.js +25 -3
  18. package/build/module/Dialog/Dialog.js.map +1 -1
  19. package/build/module/Modal/Modal.js +6 -1
  20. package/build/module/Modal/Modal.js.map +1 -1
  21. package/build/module/Modal/ModalProps.js.map +1 -1
  22. package/build/module/Snackbar/Snackbar.js +21 -5
  23. package/build/module/Snackbar/Snackbar.js.map +1 -1
  24. package/build/module/Tab/Tab.js +24 -15
  25. package/build/module/Tab/Tab.js.map +1 -1
  26. package/build/module/Tab/TabIndicator.js +5 -4
  27. package/build/module/Tab/TabIndicator.js.map +1 -1
  28. package/build/module/Tab/TabProps.js.map +1 -1
  29. package/build/module/Tab/index.js.map +1 -1
  30. package/build/module/Tabs/Tabs.js +48 -6
  31. package/build/module/Tabs/Tabs.js.map +1 -1
  32. package/build/module/Tabs/TabsProps.js.map +1 -1
  33. package/build/typescript/Modal/ModalProps.d.ts +10 -0
  34. package/build/typescript/Tab/TabIndicator.d.ts +3 -2
  35. package/build/typescript/Tab/TabProps.d.ts +14 -1
  36. package/build/typescript/Tab/index.d.ts +1 -1
  37. package/build/typescript/Tabs/Tabs.d.ts +1 -1
  38. package/build/typescript/Tabs/TabsProps.d.ts +14 -1
  39. package/package.json +2 -2
  40. package/src/Dialog/Dialog.tsx +26 -4
  41. package/src/Modal/Modal.tsx +7 -1
  42. package/src/Modal/ModalProps.ts +12 -0
  43. package/src/Snackbar/Snackbar.tsx +29 -7
  44. package/src/Tab/Tab.tsx +43 -17
  45. package/src/Tab/TabIndicator.tsx +7 -7
  46. package/src/Tab/TabProps.ts +16 -1
  47. package/src/Tab/index.ts +1 -1
  48. package/src/Tabs/Tabs.tsx +42 -4
  49. package/src/Tabs/TabsProps.ts +16 -1
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["TabsProps.ts"],"sourcesContent":["import type { ReactNode, Ref } from 'react';\nimport type { ViewProps } from 'react-native';\nimport type { TabIndicatorColor } from './TabIndicatorProps';\nimport type { TabVariant } from '../Tab';\nimport type { OverridableComponentProps, SyncAnimatedValue } from '../types';\nimport type { KeyboardDismissMode, KeyboardShouldPersistTaps, TabsInstance } from './types';\n\nexport default interface TabsProps extends OverridableComponentProps<ViewProps, {\n ref?: Ref<TabsInstance>;\n\n /**\n * Collection of Tab components.\n */\n children: ReactNode;\n\n /**\n * If `true`, the indicator is disabled.\n * @default false\n */\n disableIndicator?: boolean;\n\n /**\n * The color of tab indicator\n * @default 'primary'\n */\n indicatorColor?: TabIndicatorColor;\n\n /**\n * Index of initial tab that should be selected.\n * @default 0\n */\n initialIndex?: number;\n\n /**\n * keyboard dismissing condition of dragging.\n * @default 'none'\n */\n keyboardDismissMode?: KeyboardDismissMode,\n\n /**\n * keyboard persisting condition of tapping.\n * @default 'never'\n */\n keyboardShouldPersistTaps?: KeyboardShouldPersistTaps,\n\n /**\n * Callback fired when a tab is selected.\n */\n onChange?: (newIndex: number) => void;\n\n /**\n * If `true`, the component will be able to scroll.\n * @default false\n */\n scrollable?: boolean;\n\n /**\n * Unstable API.\n */\n UNSTABLE_sharedIndex?: SyncAnimatedValue;\n\n /**\n * The variant to use.\n * @default 'primary'\n */\n variant?: TabVariant;\n}> {}\n"],"mappings":""}
1
+ {"version":3,"names":[],"sources":["TabsProps.ts"],"sourcesContent":["import type { ReactNode, Ref } from 'react';\nimport type { ViewProps } from 'react-native';\nimport type { TabIndicatorColor } from './TabIndicatorProps';\nimport type { TabVariant, TabIndicatorSize } from '../Tab';\nimport type { OverridableComponentProps, SyncAnimatedValue } from '../types';\nimport type { KeyboardDismissMode, KeyboardShouldPersistTaps, TabsInstance } from './types';\n\nexport default interface TabsProps extends OverridableComponentProps<ViewProps, {\n ref?: Ref<TabsInstance>;\n\n /**\n * Collection of Tab components.\n */\n children: ReactNode;\n\n /**\n * If `true`, the indicator is disabled.\n * @default false\n */\n disableIndicator?: boolean;\n\n /**\n * The color of tab indicator\n * @default 'primary'\n */\n indicatorColor?: TabIndicatorColor;\n\n /**\n * The size of tab indicator.\n * 'full' adjusts the indicator to the size of the Tab,\n * while 'fit-content' adjusts the indicator to the size of the content inside the Tab.\n * @default 'full'\n */\n indicatorSize?: TabIndicatorSize;\n\n /**\n * Index of initial tab that should be selected.\n * @default 0\n */\n initialIndex?: number;\n\n /**\n * keyboard dismissing condition of dragging.\n * @default 'none'\n */\n keyboardDismissMode?: KeyboardDismissMode,\n\n /**\n * keyboard persisting condition of tapping.\n * @default 'never'\n */\n keyboardShouldPersistTaps?: KeyboardShouldPersistTaps,\n\n /**\n * Callback fired when a tab is selected.\n */\n onChange?: (newIndex: number) => void;\n\n /**\n * If `true`, the component will be able to scroll.\n * @default false\n */\n scrollable?: boolean;\n\n /**\n * Unstable API.\n */\n UNSTABLE_sharedIndex?: SyncAnimatedValue;\n\n /**\n * The variant to use.\n * @default 'primary'\n */\n variant?: TabVariant;\n\n /**\n * Callback function executed when a Tab is selected.\n * Executed even if the index does not change when a Tab is pressed.\n * Receives the next tab index and the current tab index as parameters.\n */\n onTabSelected?: (newIndex: number, currentIndex: number) => void;\n}> {}\n"],"mappings":""}
@@ -21,6 +21,16 @@ export default interface ModalProps extends OverridableComponentProps<ViewProps,
21
21
  * @default false
22
22
  */
23
23
  disableAnimation?: boolean;
24
+ /**
25
+ * The number of milliseconds to enter animation.
26
+ * @default 300
27
+ */
28
+ enterDuration?: number;
29
+ /**
30
+ * The number of milliseconds to exit animation.
31
+ * @default 150
32
+ */
33
+ exitDuration?: number;
24
34
  /**
25
35
  * If `true`, the backdrop is not rendered.
26
36
  * @default false
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
- import type { TabIndicatorColor } from './TabProps';
3
- declare const _default: React.MemoExoticComponent<({ color }: {
2
+ import type { TabIndicatorColor, TabIndicatorSize } from './TabProps';
3
+ declare const _default: React.MemoExoticComponent<({ indicatorSize, color }: {
4
+ indicatorSize: TabIndicatorSize;
4
5
  color: TabIndicatorColor;
5
6
  }) => JSX.Element>;
6
7
  export default _default;
@@ -1,8 +1,10 @@
1
1
  import React from 'react';
2
+ import type { LayoutChangeEvent } from 'react-native';
2
3
  import type { TabBaseProps } from '../TabBase';
3
4
  import type { OverridableComponentProps } from '../types';
4
5
  export declare type TabVariant = 'primary' | 'secondary' | 'bottom-navigation';
5
6
  export declare type TabIndicatorColor = 'primary' | 'secondary';
7
+ export declare type TabIndicatorSize = 'full' | 'fit-content';
6
8
  export default interface TabProps extends OverridableComponentProps<TabBaseProps, {
7
9
  /**
8
10
  * If `true`, the badge is visible.
@@ -12,7 +14,7 @@ export default interface TabProps extends OverridableComponentProps<TabBaseProps
12
14
  /**
13
15
  * The label of the Tab.
14
16
  */
15
- children: string;
17
+ children: string | React.ReactElement;
16
18
  /**
17
19
  * If `true`, the indicator is enabled.
18
20
  * @default false
@@ -32,6 +34,13 @@ export default interface TabProps extends OverridableComponentProps<TabBaseProps
32
34
  * @default 'primary'
33
35
  */
34
36
  indicatorColor?: TabIndicatorColor;
37
+ /**
38
+ * The size of tab indicator.
39
+ * 'full' adjusts the indicator to the size of the Tab,
40
+ * while 'fit-content' adjusts the indicator to the size of the content inside the Tab.
41
+ * @default 'full'
42
+ */
43
+ indicatorSize?: TabIndicatorSize;
35
44
  /**
36
45
  * If supplied, use this icon on selected state.
37
46
  */
@@ -41,5 +50,9 @@ export default interface TabProps extends OverridableComponentProps<TabBaseProps
41
50
  * @default 'primary'
42
51
  */
43
52
  variant?: TabVariant;
53
+ /**
54
+ * Function to be passed to the child component's onLayout prop.
55
+ */
56
+ onTabInnerLayout?: (event: LayoutChangeEvent) => void;
44
57
  }> {
45
58
  }
@@ -1,2 +1,2 @@
1
1
  export { default } from './Tab';
2
- export type { default as TabProps, TabVariant } from './TabProps';
2
+ export type { default as TabProps, TabVariant, TabIndicatorSize } from './TabProps';
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
2
  import type TabsProps from './TabsProps';
3
3
  import type { TabsInstance } from './types';
4
- declare const Tabs: React.ForwardRefExoticComponent<Pick<TabsProps, "testID" | "style" | "onLayout" | "keyboardDismissMode" | "children" | "pointerEvents" | "onStartShouldSetResponder" | "onMoveShouldSetResponder" | "onResponderEnd" | "onResponderGrant" | "onResponderReject" | "onResponderMove" | "onResponderRelease" | "onResponderStart" | "onResponderTerminationRequest" | "onResponderTerminate" | "onStartShouldSetResponderCapture" | "onMoveShouldSetResponderCapture" | "accessibilityLabel" | "accessible" | "hitSlop" | "removeClippedSubviews" | "nativeID" | "collapsable" | "needsOffscreenAlphaCompositing" | "renderToHardwareTextureAndroid" | "focusable" | "shouldRasterizeIOS" | "isTVSelectable" | "hasTVPreferredFocus" | "tvParallaxProperties" | "tvParallaxShiftDistanceX" | "tvParallaxShiftDistanceY" | "tvParallaxTiltAngle" | "tvParallaxMagnification" | "onTouchStart" | "onTouchMove" | "onTouchEnd" | "onTouchCancel" | "onTouchEndCapture" | "accessibilityActions" | "accessibilityRole" | "accessibilityState" | "accessibilityHint" | "accessibilityValue" | "onAccessibilityAction" | "accessibilityLabelledBy" | "accessibilityLiveRegion" | "importantForAccessibility" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityViewIsModal" | "onAccessibilityEscape" | "onAccessibilityTap" | "onMagicTap" | "accessibilityIgnoresInvertColors" | "variant" | "keyboardShouldPersistTaps" | "onChange" | "indicatorColor" | "initialIndex" | "scrollable" | "disableIndicator" | "UNSTABLE_sharedIndex"> & React.RefAttributes<TabsInstance>>;
4
+ declare const Tabs: React.ForwardRefExoticComponent<Pick<TabsProps, "testID" | "style" | "onLayout" | "keyboardDismissMode" | "children" | "pointerEvents" | "onStartShouldSetResponder" | "onMoveShouldSetResponder" | "onResponderEnd" | "onResponderGrant" | "onResponderReject" | "onResponderMove" | "onResponderRelease" | "onResponderStart" | "onResponderTerminationRequest" | "onResponderTerminate" | "onStartShouldSetResponderCapture" | "onMoveShouldSetResponderCapture" | "accessibilityLabel" | "accessible" | "hitSlop" | "removeClippedSubviews" | "nativeID" | "collapsable" | "needsOffscreenAlphaCompositing" | "renderToHardwareTextureAndroid" | "focusable" | "shouldRasterizeIOS" | "isTVSelectable" | "hasTVPreferredFocus" | "tvParallaxProperties" | "tvParallaxShiftDistanceX" | "tvParallaxShiftDistanceY" | "tvParallaxTiltAngle" | "tvParallaxMagnification" | "onTouchStart" | "onTouchMove" | "onTouchEnd" | "onTouchCancel" | "onTouchEndCapture" | "accessibilityActions" | "accessibilityRole" | "accessibilityState" | "accessibilityHint" | "accessibilityValue" | "onAccessibilityAction" | "accessibilityLabelledBy" | "accessibilityLiveRegion" | "importantForAccessibility" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityViewIsModal" | "onAccessibilityEscape" | "onAccessibilityTap" | "onMagicTap" | "accessibilityIgnoresInvertColors" | "variant" | "keyboardShouldPersistTaps" | "onChange" | "indicatorColor" | "indicatorSize" | "initialIndex" | "scrollable" | "disableIndicator" | "UNSTABLE_sharedIndex" | "onTabSelected"> & React.RefAttributes<TabsInstance>>;
5
5
  export default Tabs;
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode, Ref } from 'react';
2
2
  import type { ViewProps } from 'react-native';
3
3
  import type { TabIndicatorColor } from './TabIndicatorProps';
4
- import type { TabVariant } from '../Tab';
4
+ import type { TabVariant, TabIndicatorSize } from '../Tab';
5
5
  import type { OverridableComponentProps, SyncAnimatedValue } from '../types';
6
6
  import type { KeyboardDismissMode, KeyboardShouldPersistTaps, TabsInstance } from './types';
7
7
  export default interface TabsProps extends OverridableComponentProps<ViewProps, {
@@ -20,6 +20,13 @@ export default interface TabsProps extends OverridableComponentProps<ViewProps,
20
20
  * @default 'primary'
21
21
  */
22
22
  indicatorColor?: TabIndicatorColor;
23
+ /**
24
+ * The size of tab indicator.
25
+ * 'full' adjusts the indicator to the size of the Tab,
26
+ * while 'fit-content' adjusts the indicator to the size of the content inside the Tab.
27
+ * @default 'full'
28
+ */
29
+ indicatorSize?: TabIndicatorSize;
23
30
  /**
24
31
  * Index of initial tab that should be selected.
25
32
  * @default 0
@@ -53,5 +60,11 @@ export default interface TabsProps extends OverridableComponentProps<ViewProps,
53
60
  * @default 'primary'
54
61
  */
55
62
  variant?: TabVariant;
63
+ /**
64
+ * Callback function executed when a Tab is selected.
65
+ * Executed even if the index does not change when a Tab is pressed.
66
+ * Receives the next tab index and the current tab index as parameters.
67
+ */
68
+ onTabSelected?: (newIndex: number, currentIndex: number) => void;
56
69
  }> {
57
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fountain-ui/core",
3
- "version": "2.0.0-beta.75",
3
+ "version": "2.0.0-beta.76",
4
4
  "author": "Fountain-UI Team",
5
5
  "description": "React components that implement Tappytoon's Fountain Design.",
6
6
  "license": "MIT",
@@ -67,5 +67,5 @@
67
67
  "publishConfig": {
68
68
  "access": "public"
69
69
  },
70
- "gitHead": "474e7eda06ca67e0986459cb8426e9323eb328a5"
70
+ "gitHead": "cc48805efae8ceeab7e10b5e39dba30136d3a6e4"
71
71
  }
@@ -1,9 +1,11 @@
1
- import React from 'react';
2
- import { useWindowDimensions } from 'react-native';
1
+ import React, { useEffect } from 'react';
2
+ import { useWindowDimensions, Animated } from 'react-native';
3
3
  import { css, NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';
4
4
  import Column from '../Column';
5
5
  import Modal from '../Modal';
6
6
  import Paper from '../Paper';
7
+ import { useAnimatedValue } from '../hooks';
8
+ import { isNotAndroid12 } from '../utils';
7
9
  import { useTheme } from '../styles';
8
10
  import type DialogProps from './DialogProps';
9
11
 
@@ -20,6 +22,10 @@ type DialogStyles = NamedStylesStringUnion<DialogStyleKeys>;
20
22
 
21
23
  const DIALOG_MAX_WIDTH = 328;
22
24
 
25
+ const fadeInDuration = 300;
26
+ const fadeOutDuration = 100;
27
+ const fadeAnimationDelay = 50;
28
+
23
29
  const useStyles: UseStyles<DialogStyles> = function (): DialogStyles {
24
30
  const theme = useTheme();
25
31
 
@@ -74,16 +80,32 @@ export default function Dialog(props: DialogProps) {
74
80
 
75
81
  const styles = useStyles();
76
82
  const theme = useTheme();
83
+ const animatedOpacity = useAnimatedValue(fullScreen ? 1 : 0);
84
+
85
+ useEffect(() => {
86
+ if(!fullScreen){
87
+ Animated.timing(animatedOpacity, {
88
+ toValue: visible ? 1 : 0,
89
+ duration: visible ? fadeInDuration: fadeOutDuration,
90
+ delay: visible ? fadeAnimationDelay : 0,
91
+ useNativeDriver: isNotAndroid12,
92
+ }).start();
93
+ }
94
+ }, [fullScreen, visible]);
77
95
 
78
96
  return (
79
97
  <Modal
80
98
  animationStyle={fullScreen ? styles.animationFullScreen : styles.animation}
81
99
  onClose={onClose}
100
+ exitDuration={fullScreen ? 150 : 300}
82
101
  visible={visible}
83
102
  style={styles.root}
84
103
  {...otherProps}
85
104
  >
86
- <React.Fragment>
105
+ <Animated.View
106
+ needsOffscreenAlphaCompositing={true}
107
+ style={{ opacity: animatedOpacity }}
108
+ >
87
109
  {topElement ? (
88
110
  <Column style={fullScreen ? undefined : styles.topElementSize}>
89
111
  <Column style={styles.topElementPosition}>
@@ -103,7 +125,7 @@ export default function Dialog(props: DialogProps) {
103
125
  >
104
126
  {children}
105
127
  </Paper>
106
- </React.Fragment>
128
+ </Animated.View>
107
129
  </Modal>
108
130
  );
109
131
  };
@@ -14,6 +14,9 @@ export interface ModalCloseEvent {
14
14
  };
15
15
  }
16
16
 
17
+ const defaultEnterDuration = 300;
18
+ const defaultExitDuration = 150;
19
+
17
20
  export const createModalCloseEvent = (reason: ModalCloseReasonType) => ({
18
21
  metadata: {
19
22
  reason,
@@ -26,6 +29,8 @@ export default function Modal(props: ModalProps) {
26
29
  backdropOpacity = 0.5,
27
30
  children,
28
31
  disableAnimation = false,
32
+ enterDuration = defaultEnterDuration,
33
+ exitDuration = defaultExitDuration,
29
34
  hideBackdrop = false,
30
35
  onClose,
31
36
  style,
@@ -68,7 +73,8 @@ export default function Modal(props: ModalProps) {
68
73
  {!disableAnimation ? (
69
74
  <Slide
70
75
  appear={visible}
71
- exitDuration={150}
76
+ enterDuration={enterDuration}
77
+ exitDuration={exitDuration}
72
78
  onEnter={() => setExited(false)}
73
79
  onExited={() => setExited(true)}
74
80
  style={animationStyle}
@@ -26,6 +26,18 @@ export default interface ModalProps extends OverridableComponentProps<ViewProps,
26
26
  */
27
27
  disableAnimation?: boolean;
28
28
 
29
+ /**
30
+ * The number of milliseconds to enter animation.
31
+ * @default 300
32
+ */
33
+ enterDuration?: number;
34
+
35
+ /**
36
+ * The number of milliseconds to exit animation.
37
+ * @default 150
38
+ */
39
+ exitDuration?: number;
40
+
29
41
  /**
30
42
  * If `true`, the backdrop is not rendered.
31
43
  * @default false
@@ -1,14 +1,20 @@
1
- import React from 'react';
2
- import { View } from 'react-native';
1
+ import React, { useEffect } from 'react';
2
+ import { Animated } from 'react-native';
3
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
- import { css, NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';
4
+ import { NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';
5
5
  import Slide from '../Slide';
6
6
  import SnackbarContent from '../SnackbarContent';
7
+ import { useAnimatedValue } from '../hooks';
7
8
  import { useTheme } from '../styles';
9
+ import { isNotAndroid12 } from '../utils';
8
10
  import type SnackbarProps from './SnackbarProps';
9
11
 
10
12
  type SnackBarStyles = NamedStylesStringUnion<'root'>;
11
13
 
14
+ const fadeInDuration = 300;
15
+ const fadeOutDuration = 110;
16
+ const fadeAnimationDelay = 100;
17
+
12
18
  const useStyles: UseStyles<SnackBarStyles> = function (): SnackBarStyles {
13
19
  const theme = useTheme();
14
20
  const insets = useSafeAreaInsets();
@@ -75,16 +81,32 @@ export default function Snackbar(props: SnackbarProps) {
75
81
 
76
82
  const [exited, setExited] = React.useState(true);
77
83
 
84
+ const animatedOpacity = useAnimatedValue(0);
85
+
86
+ const animatedStyle = {
87
+ opacity: animatedOpacity,
88
+ };
89
+
90
+ useEffect(() => {
91
+ Animated.timing(animatedOpacity, {
92
+ toValue: visible ? 1 : 0,
93
+ duration: visible ? fadeInDuration : fadeOutDuration,
94
+ delay: visible ? fadeAnimationDelay : 0,
95
+ useNativeDriver: isNotAndroid12,
96
+ }).start();
97
+ }, [visible]);
98
+
78
99
  if (!visible && exited) {
79
100
  return null;
80
101
  }
81
102
 
82
103
  return (
83
- <View
84
- style={css([
104
+ <Animated.View
105
+ style={[
106
+ animatedStyle,
85
107
  styles.root,
86
108
  style,
87
- ])}
109
+ ]}
88
110
  {...otherProps}
89
111
  >
90
112
  <Slide
@@ -103,6 +125,6 @@ export default function Snackbar(props: SnackbarProps) {
103
125
  />
104
126
  )}
105
127
  </Slide>
106
- </View>
128
+ </Animated.View>
107
129
  );
108
130
  };
package/src/Tab/Tab.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { cloneElement } from 'react';
2
- import { Platform, Text } from 'react-native';
2
+ import { Platform, Text, View } from 'react-native';
3
3
  import Badge from '../Badge';
4
4
  import TabBase from '../TabBase';
5
5
  import type TabProps from './TabProps';
@@ -10,16 +10,14 @@ const styles = StyleSheet.create({
10
10
  root: {
11
11
  // TODO: Remove redundant platform checking
12
12
  ...(Platform.OS === 'web' ? { minWidth: 'auto' } : {}),
13
- },
14
- primary: {
15
- minHeight: 40,
16
- },
17
- secondary: {
18
13
  minHeight: 40,
19
14
  },
20
15
  bottomNavigation: {
21
16
  minHeight: 56,
22
17
  },
18
+ filledInner: {
19
+ justifyContent: 'center',
20
+ }
23
21
  });
24
22
 
25
23
  export default function Tab(props: TabProps) {
@@ -29,10 +27,12 @@ export default function Tab(props: TabProps) {
29
27
  enableIndicator = false,
30
28
  icon: defaultIcon,
31
29
  indicatorColor = 'primary',
30
+ indicatorSize = 'full',
32
31
  selected = false,
33
32
  selectedIcon,
34
33
  variant = 'primary',
35
34
  style,
35
+ onTabInnerLayout,
36
36
  ...otherProps
37
37
  } = props;
38
38
 
@@ -44,12 +44,15 @@ export default function Tab(props: TabProps) {
44
44
 
45
45
  const tabBaseStyle = css([
46
46
  styles.root,
47
- variant === 'primary'
48
- ? styles.primary
49
- : (variant === 'secondary' ? styles.secondary : styles.bottomNavigation),
47
+ variant === 'bottom-navigation' && styles.bottomNavigation,
50
48
  style,
51
49
  ]);
52
50
 
51
+ const tabInnerStyle = css([
52
+ styles.root,
53
+ styles.filledInner,
54
+ ]);
55
+
53
56
  const fontStyle = createFontStyle(theme, {
54
57
  selector: (typo) => variant === 'primary'
55
58
  ? typo.h2
@@ -62,13 +65,12 @@ export default function Tab(props: TabProps) {
62
65
  const icon = selected ? (selectedIcon || defaultIcon) : defaultIcon;
63
66
  const iconElement = icon ? cloneElement(icon, { fill: color }) : null;
64
67
 
65
- return (
66
- <TabBase
67
- pressEffect={pressEffect}
68
- style={tabBaseStyle}
69
- vertical={vertical}
70
- {...otherProps}
71
- >
68
+ const tabElement = typeof children !== 'string' ? (
69
+ React.cloneElement(children, {
70
+ selected,
71
+ })
72
+ ) : (
73
+ <React.Fragment>
72
74
  <Badge
73
75
  children={iconElement}
74
76
  invisible={!badgeVisible}
@@ -78,8 +80,32 @@ export default function Tab(props: TabProps) {
78
80
  children={children}
79
81
  style={css(fontStyle)}
80
82
  />
83
+ </React.Fragment>
84
+ );
85
+ const tabIndicator = (enableIndicator && selected)
86
+ ? <TabIndicator indicatorSize={indicatorSize} color={indicatorColor}/>
87
+ : null;
88
+
89
+ return (
90
+ <TabBase
91
+ pressEffect={pressEffect}
92
+ style={tabBaseStyle}
93
+ vertical={vertical}
94
+ {...otherProps}
95
+ >
96
+ {indicatorSize === 'fit-content' ? (
97
+ <View onLayout={onTabInnerLayout} style={tabInnerStyle}>
98
+ {tabElement}
99
+
100
+ {tabIndicator}
101
+ </View>
102
+ ) : (
103
+ <React.Fragment>
104
+ {tabElement}
81
105
 
82
- {(enableIndicator && selected) ? <TabIndicator color={indicatorColor}/> : null}
106
+ {tabIndicator}
107
+ </React.Fragment>
108
+ )}
83
109
  </TabBase>
84
110
  );
85
111
  };
@@ -2,12 +2,12 @@ import React from 'react';
2
2
  import { View } from 'react-native';
3
3
  import { NamedStylesStringUnion } from '@fountain-ui/styles';
4
4
  import { useTheme } from '../styles';
5
- import type { TabIndicatorColor } from './TabProps';
5
+ import type { TabIndicatorColor, TabIndicatorSize } from './TabProps';
6
6
 
7
7
  type TabIndicatorStyles = NamedStylesStringUnion<'root'>;
8
8
 
9
- const useStyles: (color: TabIndicatorColor) => TabIndicatorStyles =
10
- function (color: TabIndicatorColor): TabIndicatorStyles {
9
+ const useStyles: (color: TabIndicatorColor, indicatorSize: TabIndicatorSize) => TabIndicatorStyles =
10
+ function (color: TabIndicatorColor, indicatorSize): TabIndicatorStyles {
11
11
  const theme = useTheme();
12
12
 
13
13
  return {
@@ -15,15 +15,15 @@ const useStyles: (color: TabIndicatorColor) => TabIndicatorStyles =
15
15
  backgroundColor: theme.palette[color].main,
16
16
  bottom: 0,
17
17
  height: 2,
18
- left: 12,
19
- right: 12,
18
+ left: indicatorSize === 'fit-content' ? 0 : 12,
19
+ right: indicatorSize === 'fit-content' ? 0 : 12,
20
20
  position: 'absolute',
21
21
  },
22
22
  };
23
23
  };
24
24
 
25
- const TabIndicator = function TabIndicator({ color }: { color: TabIndicatorColor }) {
26
- const styles = useStyles(color);
25
+ const TabIndicator = function TabIndicator({ indicatorSize, color }: { indicatorSize: TabIndicatorSize; color: TabIndicatorColor }) {
26
+ const styles = useStyles(color, indicatorSize);
27
27
 
28
28
  return (
29
29
  <View
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
+ import type { LayoutChangeEvent } from 'react-native';
2
3
  import type { TabBaseProps } from '../TabBase';
3
4
  import type { OverridableComponentProps } from '../types';
4
5
 
5
6
  export type TabVariant = 'primary' | 'secondary' | 'bottom-navigation';
6
7
  export type TabIndicatorColor = 'primary' | 'secondary'
8
+ export type TabIndicatorSize = 'full' | 'fit-content';
7
9
 
8
10
  export default interface TabProps extends OverridableComponentProps<TabBaseProps, {
9
11
  /**
@@ -15,7 +17,7 @@ export default interface TabProps extends OverridableComponentProps<TabBaseProps
15
17
  /**
16
18
  * The label of the Tab.
17
19
  */
18
- children: string;
20
+ children: string | React.ReactElement;
19
21
 
20
22
  /**
21
23
  * If `true`, the indicator is enabled.
@@ -40,6 +42,14 @@ export default interface TabProps extends OverridableComponentProps<TabBaseProps
40
42
  */
41
43
  indicatorColor?: TabIndicatorColor;
42
44
 
45
+ /**
46
+ * The size of tab indicator.
47
+ * 'full' adjusts the indicator to the size of the Tab,
48
+ * while 'fit-content' adjusts the indicator to the size of the content inside the Tab.
49
+ * @default 'full'
50
+ */
51
+ indicatorSize?: TabIndicatorSize;
52
+
43
53
  /**
44
54
  * If supplied, use this icon on selected state.
45
55
  */
@@ -50,4 +60,9 @@ export default interface TabProps extends OverridableComponentProps<TabBaseProps
50
60
  * @default 'primary'
51
61
  */
52
62
  variant?: TabVariant;
63
+
64
+ /**
65
+ * Function to be passed to the child component's onLayout prop.
66
+ */
67
+ onTabInnerLayout?: (event: LayoutChangeEvent) => void;
53
68
  }> {}
package/src/Tab/index.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { default } from './Tab';
2
- export type { default as TabProps, TabVariant } from './TabProps';
2
+ export type { default as TabProps, TabVariant, TabIndicatorSize } from './TabProps';
package/src/Tabs/Tabs.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { cloneElement, forwardRef, useEffect, useImperativeHandle } from 'react';
1
+ import React, { cloneElement, forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
2
2
  import type { GestureResponderEvent, LayoutChangeEvent } from 'react-native';
3
3
  import { View } from 'react-native';
4
4
  import { NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';
@@ -45,6 +45,7 @@ const Tabs = forwardRef<TabsInstance, TabsProps>(function Tabs(props, ref) {
45
45
  indicatorColor = 'primary',
46
46
  initialIndex = 0,
47
47
  disableIndicator = false,
48
+ indicatorSize = 'full',
48
49
  keyboardDismissMode = 'none',
49
50
  keyboardShouldPersistTaps = 'never',
50
51
  onChange,
@@ -52,13 +53,19 @@ const Tabs = forwardRef<TabsInstance, TabsProps>(function Tabs(props, ref) {
52
53
  style,
53
54
  variant = 'primary',
54
55
  UNSTABLE_sharedIndex,
56
+ onTabSelected,
55
57
  } = props;
56
58
 
57
59
  const fallbackSharedIndex = useSyncAnimatedValue({ initialValue: initialIndex });
58
60
  const sharedIndex = UNSTABLE_sharedIndex ?? fallbackSharedIndex;
59
61
  const realInitialIndex = sharedIndex.initialValue;
60
62
 
63
+ const currentIndexRef = useRef(initialIndex);
64
+
61
65
  const setTab = (newIndex: number) => {
66
+ const currentIndex = currentIndexRef.current;
67
+ onTabSelected?.(newIndex, currentIndex);
68
+
62
69
  sharedIndex.animatedValue.setValue(newIndex);
63
70
  };
64
71
 
@@ -72,19 +79,48 @@ const Tabs = forwardRef<TabsInstance, TabsProps>(function Tabs(props, ref) {
72
79
 
73
80
  const styles = useStyles();
74
81
 
75
- const [coordinates, updateCoordinate] = useTabCoordinates(children);
82
+ const [outerCoordinates, updateCoordinate] = useTabCoordinates(children);
83
+ const [innerCoordinates, updateInnerCoordinate] = useTabCoordinates(children);
76
84
 
77
- const canRenderIndicator = isEveryTabCoordinatesDefined(coordinates, children);
85
+ const canRenderIndicator = indicatorSize === 'fit-content'
86
+ ? isEveryTabCoordinatesDefined(innerCoordinates, children)
87
+ : isEveryTabCoordinatesDefined(outerCoordinates, children);
78
88
 
79
89
  const indexStore = useIndexStore(sharedIndex);
80
90
 
91
+ const coordinates = useMemo(() => {
92
+ if (indicatorSize !== 'fit-content') {
93
+ return outerCoordinates;
94
+ }
95
+
96
+ return innerCoordinates.map((coordinate, idx) => {
97
+ const { x1, x2 } = coordinate;
98
+ const { x1: outerX1 } = outerCoordinates[idx];
99
+ return {
100
+ x1: x1 + outerX1,
101
+ x2: x2 + outerX1,
102
+ }
103
+ });
104
+ }, [outerCoordinates, innerCoordinates, variant]);
105
+
81
106
  useEffect(() => {
82
107
  return indexStore.subscribe(newIndex => {
83
108
  onChange?.(newIndex);
109
+ currentIndexRef.current = newIndex;
84
110
  });
85
111
  }, [indexStore, onChange]);
86
112
 
87
113
  const tabElements = React.Children.map(children, (child, index) => {
114
+ if (!child) {
115
+ return null;
116
+ }
117
+
118
+ const onTabInnerLayout = (event: LayoutChangeEvent) => {
119
+ const { x, width } = event.nativeEvent.layout;
120
+
121
+ updateInnerCoordinate(index, x, width);
122
+ };
123
+
88
124
  const onLayout = (event: LayoutChangeEvent) => {
89
125
  const { x, width } = event.nativeEvent.layout;
90
126
 
@@ -111,10 +147,12 @@ const Tabs = forwardRef<TabsInstance, TabsProps>(function Tabs(props, ref) {
111
147
  const tabElement = cloneElement(child, {
112
148
  enableIndicator: !disableIndicator && !canRenderIndicator,
113
149
  indicatorColor,
150
+ onTabInnerLayout,
114
151
  onLayout,
115
152
  onPress,
116
153
  onMouseDown,
117
154
  variant,
155
+ indicatorSize,
118
156
  style: scrollable ? undefined : styles.fixedTab,
119
157
  });
120
158
 
@@ -125,7 +163,7 @@ const Tabs = forwardRef<TabsInstance, TabsProps>(function Tabs(props, ref) {
125
163
  initialIndex={realInitialIndex}
126
164
  />
127
165
  );
128
- });
166
+ })?.filter(Boolean);
129
167
 
130
168
  const tabIndicator = canRenderIndicator ? (
131
169
  <TabIndicator