Skip to content

Commit

Permalink
feat: custom label component
Browse files Browse the repository at this point in the history
Allows specifying a ReactNode to render as a label

Useful to add icons or other animations to tab labels
  • Loading branch information
andreialecu committed May 19, 2022
1 parent 8d79765 commit 51a7234
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 30 deletions.
2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -34,6 +35,7 @@ import { ExampleComponentType } from './types'

const EXAMPLE_COMPONENTS: ExampleComponentType[] = [
Default,
DefaultCustomLabels,
Snap,
RevealHeaderOnScroll,
RevealHeaderOnScrollSnap,
Expand Down
17 changes: 17 additions & 0 deletions example/src/DefaultCustomLabels.tsx
Original file line number Diff line number Diff line change
@@ -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 <ExampleComponent renderHeader={Header} />
}

DefaultExampleCustomLabels.title = title

export default DefaultExampleCustomLabels
130 changes: 130 additions & 0 deletions example/src/Shared/ExampleComponentCustomLabels.tsx
Original file line number Diff line number Diff line change
@@ -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<CollapsibleProps>

function TabItem<T extends TabName>({
index,
indexDecimal,
label,
}: Pick<TabItemProps<T>, '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 (
<View style={styles.tabItemContainer}>
<Animated.View style={[styles.tabItemDot, dotStyle]} />
<Animated.Text style={textStyle}>{label}</Animated.Text>
</View>
)
}

const Example = React.forwardRef<CollapsibleRef, Props>(
({ emptyContacts, ...props }, ref) => {
const makeLabel = useCallback(
<T extends TabName>(label: string) => (props: TabItemProps<T>) => (
<TabItem
index={props.index}
indexDecimal={props.indexDecimal}
label={label}
/>
),
[]
)

return (
<Tabs.Container ref={ref} headerHeight={HEADER_HEIGHT} {...props}>
<Tabs.Tab name="article" label={makeLabel('Article')}>
<Article />
</Tabs.Tab>
<Tabs.Tab name="albums" label={makeLabel('Albums')}>
<Albums />
</Tabs.Tab>
<Tabs.Tab name="contacts" label={makeLabel('Contacts')}>
<Contacts emptyContacts={emptyContacts} />
</Tabs.Tab>
<Tabs.Tab name="ordered" label={makeLabel('Ordered')}>
<SectionContacts emptyContacts={emptyContacts} />
</Tabs.Tab>
</Tabs.Container>
)
}
)

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',
},
})
2 changes: 1 addition & 1 deletion src/MaterialTabBar/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const TABBAR_HEIGHT = 48
* </Tabs.Container>
* ```
*/
const MaterialTabBar = <T extends TabName = any>({
const MaterialTabBar = <T extends TabName = TabName>({
tabNames,
indexDecimal,
scrollEnabled = false,
Expand Down
56 changes: 35 additions & 21 deletions src/MaterialTabBar/TabItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useMemo } from 'react'
import { StyleSheet, Pressable, Platform } from 'react-native'
import Animated, {
interpolate,
Expand All @@ -14,23 +14,27 @@ const DEFAULT_COLOR = 'rgba(0, 0, 0, 1)'
/**
* Any additional props are passed to the pressable component.
*/
export const MaterialTabItem = <T extends TabName = any>({
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<T>): React.ReactElement => {
export const MaterialTabItem = <T extends TabName = string>(
props: MaterialTabItemProps<T>
): 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(
Expand All @@ -46,6 +50,18 @@ export const MaterialTabItem = <T extends TabName = any>({
}
})

const renderedLabel = useMemo(() => {
if (typeof label === 'string') {
return (
<Animated.Text style={[styles.label, stylez, labelStyle]}>
{label}
</Animated.Text>
)
}

return label(props)
}, [label, labelStyle, props, stylez])

return (
<Pressable
onLayout={onLayout}
Expand All @@ -62,9 +78,7 @@ export const MaterialTabItem = <T extends TabName = any>({
}}
{...rest}
>
<Animated.Text style={[styles.label, stylez, labelStyle]}>
{label}
</Animated.Text>
{renderedLabel}
</Pressable>
)
}
Expand Down
9 changes: 4 additions & 5 deletions src/MaterialTabBar/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react'
import {
LayoutChangeEvent,
PressableProps,
Expand All @@ -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<Animated.AnimateStyle<ViewStyle>>
type AnimatedTextStyle = StyleProp<Animated.AnimateStyle<TextStyle>>

export type MaterialTabItemProps<T extends TabName> = {
name: T
index: number
indexDecimal: Animated.SharedValue<number>
export type MaterialTabItemProps<T extends TabName> = TabItemProps<T> & {
onPress: (name: T) => void
onLayout?: (event: LayoutChangeEvent) => void
scrollEnabled?: boolean
label: string

style?: StyleProp<ViewStyle>
/**
* Style to apply to the tab item label
Expand Down
11 changes: 10 additions & 1 deletion src/Tab.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import React from 'react'
import Animated from 'react-native-reanimated'

import { TabName } from './types'

export type TabItemProps<T extends TabName> = {
name: T
index: number
indexDecimal: Animated.SharedValue<number>

label: string | ((props: TabItemProps<T>) => React.ReactNode)
}

export type TabProps<T extends TabName> = {
readonly name: T
label?: string
label?: TabItemProps<T>['label']
children: React.ReactNode
}

Expand Down
4 changes: 3 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,6 +23,8 @@ export type {
MaterialTabItemProps,
CollapsibleRef,
OnTabChangeCallback,
TabItemProps,
TabProps
}

export const Tabs = {
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type RefComponent =

export type Ref<T extends RefComponent> = React.RefObject<T>

export type TabName = string | number
export type TabName = string

export type RefHandler<T extends TabName = TabName> = {
jumpToTab: (name: T) => boolean
Expand Down

0 comments on commit 51a7234

Please sign in to comment.