From 51a7234fec8f19f384ed771789d883aee247260f Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Thu, 19 May 2022 13:03:06 +0300 Subject: [PATCH] feat: custom label component Allows specifying a ReactNode to render as a label Useful to add icons or other animations to tab labels --- example/src/App.tsx | 2 + example/src/DefaultCustomLabels.tsx | 17 +++ .../Shared/ExampleComponentCustomLabels.tsx | 130 ++++++++++++++++++ src/MaterialTabBar/TabBar.tsx | 2 +- src/MaterialTabBar/TabItem.tsx | 56 +++++--- src/MaterialTabBar/types.ts | 9 +- src/Tab.tsx | 11 +- src/index.tsx | 4 +- src/types.ts | 2 +- 9 files changed, 203 insertions(+), 30 deletions(-) create mode 100644 example/src/DefaultCustomLabels.tsx create mode 100644 example/src/Shared/ExampleComponentCustomLabels.tsx diff --git a/example/src/App.tsx b/example/src/App.tsx index ca9c0eef..2b061276 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -16,6 +16,7 @@ import AndroidSharedPullToRefresh from './AndroidSharedPullToRefresh' import AnimatedHeader from './AnimatedHeader' import CenteredEmptyList from './CenteredEmptyList' import Default from './Default' +import DefaultCustomLabels from './DefaultCustomLabels' import DynamicTabs from './DynamicTabs' import HeaderOverscrollExample from './HeaderOverscroll' import Lazy from './Lazy' @@ -34,6 +35,7 @@ import { ExampleComponentType } from './types' const EXAMPLE_COMPONENTS: ExampleComponentType[] = [ Default, + DefaultCustomLabels, Snap, RevealHeaderOnScroll, RevealHeaderOnScrollSnap, diff --git a/example/src/DefaultCustomLabels.tsx b/example/src/DefaultCustomLabels.tsx new file mode 100644 index 00000000..f31515d8 --- /dev/null +++ b/example/src/DefaultCustomLabels.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +import ExampleComponent from './Shared/ExampleComponentCustomLabels' +import { buildHeader } from './Shared/Header' +import { ExampleComponentType } from './types' + +const title = 'Default w/ Custom Labels' + +const Header = buildHeader(title) + +const DefaultExampleCustomLabels: ExampleComponentType = () => { + return +} + +DefaultExampleCustomLabels.title = title + +export default DefaultExampleCustomLabels diff --git a/example/src/Shared/ExampleComponentCustomLabels.tsx b/example/src/Shared/ExampleComponentCustomLabels.tsx new file mode 100644 index 00000000..bcca4801 --- /dev/null +++ b/example/src/Shared/ExampleComponentCustomLabels.tsx @@ -0,0 +1,130 @@ +import React, { useCallback } from 'react' +import { View, StyleSheet } from 'react-native' +import { + Tabs, + CollapsibleRef, + CollapsibleProps, + TabItemProps, +} from 'react-native-collapsible-tab-view' +import Animated, { + interpolate, + interpolateColor, + useAnimatedStyle, +} from 'react-native-reanimated' + +import { TabName } from '../../../src/types' +import Albums from './Albums' +import Article from './Article' +import Contacts from './Contacts' +import { HEADER_HEIGHT } from './Header' +import SectionContacts from './SectionContacts' + +type Props = { + emptyContacts?: boolean +} & Partial + +function TabItem({ + index, + indexDecimal, + label, +}: Pick, 'index' | 'indexDecimal'> & { label: string }) { + const dotStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateX: interpolate( + indexDecimal.value, + [index - 1, index, index + 1], + [0, -8, 0], + Animated.Extrapolate.CLAMP + ), + }, + ], + opacity: interpolate( + indexDecimal.value, + [index - 1, index, index + 1], + [0, 1, 0], + Animated.Extrapolate.CLAMP + ), + } + }) + + const textStyle = useAnimatedStyle(() => { + return { + fontWeight: + Math.abs(index - indexDecimal.value) < 0.5 ? 'bold' : undefined, + transform: [ + { + translateX: interpolate( + indexDecimal.value, + [index - 1, index, index + 1], + [0, 8, 0], + Animated.Extrapolate.CLAMP + ), + }, + ], + color: interpolateColor( + indexDecimal.value, + [index - 1, index, index + 1], + ['black', '#2196f3', 'black'] + ), + } + }) + + return ( + + + {label} + + ) +} + +const Example = React.forwardRef( + ({ emptyContacts, ...props }, ref) => { + const makeLabel = useCallback( + (label: string) => (props: TabItemProps) => ( + + ), + [] + ) + + return ( + + +
+ + + + + + + + + + + + ) + } +) + +export default Example + +const styles = StyleSheet.create({ + tabItemDot: { + position: 'absolute', + + width: 10, + height: 10, + backgroundColor: '#2196f3', + marginRight: 5, + borderRadius: 10, + }, + tabItemContainer: { + flexDirection: 'row', + alignItems: 'center', + }, +}) diff --git a/src/MaterialTabBar/TabBar.tsx b/src/MaterialTabBar/TabBar.tsx index e36ffded..2adf7200 100644 --- a/src/MaterialTabBar/TabBar.tsx +++ b/src/MaterialTabBar/TabBar.tsx @@ -41,7 +41,7 @@ export const TABBAR_HEIGHT = 48 * * ``` */ -const MaterialTabBar = ({ +const MaterialTabBar = ({ tabNames, indexDecimal, scrollEnabled = false, diff --git a/src/MaterialTabBar/TabItem.tsx b/src/MaterialTabBar/TabItem.tsx index 0142e95c..1578508b 100644 --- a/src/MaterialTabBar/TabItem.tsx +++ b/src/MaterialTabBar/TabItem.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useMemo } from 'react' import { StyleSheet, Pressable, Platform } from 'react-native' import Animated, { interpolate, @@ -14,23 +14,27 @@ const DEFAULT_COLOR = 'rgba(0, 0, 0, 1)' /** * Any additional props are passed to the pressable component. */ -export const MaterialTabItem = ({ - name, - index, - onPress, - onLayout, - scrollEnabled, - indexDecimal, - label, - style, - labelStyle, - activeColor = DEFAULT_COLOR, - inactiveColor = DEFAULT_COLOR, - inactiveOpacity = 0.7, - pressColor = '#DDDDDD', - pressOpacity = Platform.OS === 'ios' ? 0.2 : 1, - ...rest -}: MaterialTabItemProps): React.ReactElement => { +export const MaterialTabItem = ( + props: MaterialTabItemProps +): React.ReactElement => { + const { + name, + index, + onPress, + onLayout, + scrollEnabled, + indexDecimal, + label, + style, + labelStyle, + activeColor = DEFAULT_COLOR, + inactiveColor = DEFAULT_COLOR, + inactiveOpacity = 0.7, + pressColor = '#DDDDDD', + pressOpacity = Platform.OS === 'ios' ? 0.2 : 1, + ...rest + } = props + const stylez = useAnimatedStyle(() => { return { opacity: interpolate( @@ -46,6 +50,18 @@ export const MaterialTabItem = ({ } }) + const renderedLabel = useMemo(() => { + if (typeof label === 'string') { + return ( + + {label} + + ) + } + + return label(props) + }, [label, labelStyle, props, stylez]) + return ( ({ }} {...rest} > - - {label} - + {renderedLabel} ) } diff --git a/src/MaterialTabBar/types.ts b/src/MaterialTabBar/types.ts index 886f73e0..0bb0cb9d 100644 --- a/src/MaterialTabBar/types.ts +++ b/src/MaterialTabBar/types.ts @@ -1,3 +1,4 @@ +import React from 'react' import { LayoutChangeEvent, PressableProps, @@ -7,19 +8,17 @@ import { } from 'react-native' import Animated from 'react-native-reanimated' +import { TabItemProps } from '../Tab' import { TabBarProps, TabName } from '../types' type AnimatedStyle = StyleProp> type AnimatedTextStyle = StyleProp> -export type MaterialTabItemProps = { - name: T - index: number - indexDecimal: Animated.SharedValue +export type MaterialTabItemProps = TabItemProps & { onPress: (name: T) => void onLayout?: (event: LayoutChangeEvent) => void scrollEnabled?: boolean - label: string + style?: StyleProp /** * Style to apply to the tab item label diff --git a/src/Tab.tsx b/src/Tab.tsx index 096c9e00..0b35b524 100644 --- a/src/Tab.tsx +++ b/src/Tab.tsx @@ -1,10 +1,19 @@ import React from 'react' +import Animated from 'react-native-reanimated' import { TabName } from './types' +export type TabItemProps = { + name: T + index: number + indexDecimal: Animated.SharedValue + + label: string | ((props: TabItemProps) => React.ReactNode) +} + export type TabProps = { readonly name: T - label?: string + label?: TabItemProps['label'] children: React.ReactNode } diff --git a/src/index.tsx b/src/index.tsx index 4b1c5ca1..28583a1d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,7 @@ import { Lazy } from './Lazy' import { MaterialTabBarProps, MaterialTabItemProps } from './MaterialTabBar' import { ScrollView } from './ScrollView' import { SectionList } from './SectionList' -import { Tab } from './Tab' +import { Tab, TabItemProps, TabProps } from './Tab' import { TabBarProps, CollapsibleProps, @@ -23,6 +23,8 @@ export type { MaterialTabItemProps, CollapsibleRef, OnTabChangeCallback, + TabItemProps, + TabProps } export const Tabs = { diff --git a/src/types.ts b/src/types.ts index 8c6e134c..50340d30 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,7 +21,7 @@ export type RefComponent = export type Ref = React.RefObject -export type TabName = string | number +export type TabName = string export type RefHandler = { jumpToTab: (name: T) => boolean