Skip to content

Commit c4f70fb

Browse files
Merge branch 'navigation' into 'main'
Navigation See merge request react-native/react-native-material-components!19
2 parents fd1be1e + 3d8b36f commit c4f70fb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1107
-18
lines changed

README.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,200 @@ const onSubmitPress = async () => {
763763

764764
![divider](https://ik.imagekit.io/Computools/rn-material-components/divider.png?updatedAt=1705067870577)
765765
</details>
766+
<details><summary>Navigation</summary>
767+
<br />
768+
<details><summary>Nav Bar</summary>
769+
<br />
770+
771+
**Properties**
772+
773+
| name | description | type | default |
774+
| ------ | ------ | ------ | ---- |
775+
| routes | required | NavBarRoute<T, Y>[] | - |
776+
| activeRouteName | required | T | - |
777+
| onRoutePress | required | (route: T) => void | - |
778+
| damping | - | number | 20 |
779+
| fixedLabelVisibility | - | boolean | false |
780+
| scrollDirection | 'UP' or 'DOWN'. Use this propert to hide/show NavBar on scroll. | ScrollDirection | - |
781+
| containerStyle | - | ViewStyle | false |
782+
783+
```
784+
export interface NavBarRoute<T, Y> {
785+
name: T;
786+
icon: React.FC<Y>;
787+
selectedIcon: React.FC<Y>;
788+
789+
label?: string;
790+
badge?: string;
791+
showBadge?: boolean;
792+
badgeSize?: BadgeSize;
793+
iconProps?: Y;
794+
}
795+
```
796+
797+
To enable animation on scroll use ScrollView from Animated, create a shared value with a ScrollDirection value, scrollContext with a number value and manage them onScroll.
798+
799+
See the example:
800+
```
801+
export const MyComponent: React.FC = () => {
802+
const scrollDirection = useSharedValue(ScrollDirection.UP);
803+
const scrollContext = useSharedValue(0);
804+
805+
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
806+
const currentOffsetY = e.nativeEvent.contentOffset.y;
807+
808+
if (currentOffsetY <= 0 || currentOffsetY < scrollContext.value) {
809+
scrollDirection.value = ScrollDirection.UP;
810+
} else if (currentOffsetY >= scrollContext.value) {
811+
scrollDirection.value = ScrollDirection.DOWN;
812+
}
813+
814+
scrollContext.value = currentOffsetY;
815+
};
816+
817+
return (
818+
<>
819+
<Animated.ScrollView onScroll={onScroll}>
820+
<!-- scrollview content -->
821+
</Animated.ScrollView>
822+
<NavBar
823+
scrollDirection={scrollDirection}
824+
routes={routes}
825+
activeRouteName={activeRouteName}
826+
onRoutePress={setRoute}
827+
/>
828+
</>
829+
)
830+
}
831+
832+
```
833+
834+
![nav bar](https://ik.imagekit.io/Computools/rn-material-components/nav_bar.gif?updatedAt=1735922886681)
835+
![anim nav bar](https://ik.imagekit.io/Computools/rn-material-components/anim_nav_bar.gif?updatedAt=1735922886792)
836+
837+
</details>
838+
<details><summary>Tabs</summary>
839+
<br />
840+
841+
Each Tabs accepts the required property ```tabs```, an array of the Tab interface.
842+
843+
```
844+
export interface Tab<T, Y> extends Omit<TouchableOpacityProps, 'onPress'> {
845+
routeName: T;
846+
847+
title?: string;
848+
icon?: React.FC<Y>;
849+
iconProps?: Y;
850+
titleStyle?: TextStyle;
851+
852+
onPress: (routeName: T) => void;
853+
}
854+
```
855+
856+
<br />
857+
858+
To make the indicator responsive to scrolling, handle the scrollAnim state in the parent component and pass it as a prop to the Tabs component. This allows for seamless synchronization between the scrolling behavior and the indicator movement.
859+
860+
See the example:
861+
862+
```
863+
const ParentComponent = () => {
864+
const {width: windowWidth} = useWindowDimensions();
865+
866+
const acitveViewAnim = useSharedValue(0);
867+
const scrollViewRef = React.useRef<AnimatedScrollView>(null);
868+
869+
const tabs: Tab[] = [<--- your tabs --->]
870+
const maxOutput = 1 / tabs.lenght; // The maximum output is calculated as 1 / tabsCount, where tabsCount represents the total number of tabs.
871+
872+
const handleScrollToScreen1 = () => {
873+
acitveViewAnim.value = withTiming(0);
874+
scrollViewRef.current?.scrollTo({x: 0});
875+
};
876+
877+
const handleScrollToScreen2 = () => {
878+
acitveViewAnim.value = withTiming(maxOutput);
879+
scrollViewRef.current?.scrollToEnd();
880+
};
881+
882+
883+
const scrollHandler = useAnimatedScrollHandler(
884+
{
885+
onScroll: (e) => {
886+
acitveViewAnim.value = interpolate(e.contentOffset.x, [0, windowWidth], [0, maxOutput]);
887+
},
888+
onEndDrag: (e) => {
889+
if (e.contentOffset.x > maxOutput) {
890+
runOnJS(handleScrollToScreen2)();
891+
} else {
892+
runOnJS(handleScrollToScreen1)();
893+
}
894+
},
895+
},
896+
[windowWidth]
897+
);
898+
899+
return (
900+
<SecondaryTabs scrollAnim={acitveViewAnim} tabs={tabs}/>
901+
<Animated.ScrollView horizontal ref={scrollViewRef} bounces={false} showsHorizontalScrollIndicator={false} onScroll={scrollHandler}>
902+
<Text style={{width: windowWidth, paddingStart: 20}}>Screen 1</Text>
903+
<Text style={{width: windowWidth, paddingStart: 20}}>Screen 2</Text>
904+
</Animated.ScrollView>
905+
)
906+
};
907+
```
908+
909+
<details><summary>Primary Tabs</summary>
910+
<br />
911+
912+
**Properties**
913+
914+
| name | description | type | default |
915+
| ------ | ------ | ------ | ---- |
916+
| tabs | required | Tab<T, Y>[] | - |
917+
| activeTab | The active tab is managed through the state. Pass the activeTab prop to enable the active tab indicator animation when scrollAnim is not provided. | T | - |
918+
| scrollAnim | The indicator progresses from 0 to 1 / tabs.length. To make the indicator responsive to scrolling, refer to the "Tabs" section above for more details. | SharedValue<number> | - |
919+
| badgeSize | - | SMALL or BIG | BIG |
920+
| animConfig | - | (routeName: T) => void | - |
921+
| tabIconProps | - | Y | - |
922+
| tabStyle | - | ViewStyle | - |
923+
| badgeStyle | - | ViewStyle | - |
924+
| indicatorStyle | - | ViewStyle | - |
925+
| indicatorContainerStyle | - | ViewStyle | - |
926+
| tabsContainerStyle | - | ViewStyle | - |
927+
| tabInnerContentStyle | - | ViewStyle | - |
928+
| tabTitleStyle | - | TextStyle | - |
929+
930+
![primary tabs](https://ik.imagekit.io/Computools/rn-material-components/primary_tabs.gif?updatedAt=1735922886826)
931+
![primary tabs with badges](https://ik.imagekit.io/Computools/rn-material-components/secondary_tabs_with_badges.png?updatedAt=1735922619925)
932+
933+
</details>
934+
<details><summary>Secondary Tabs</summary>
935+
<br />
936+
937+
**Properties**
938+
939+
| name | description | type | default |
940+
| ------ | ------ | ------ | ---- |
941+
| tabs | required | Tab<T, Y>[] | - |
942+
| activeTab | The active tab is managed through the state. Pass the activeTab prop to enable the active tab indicator animation when scrollAnim is not provided. | T | - |
943+
| scrollAnim | The indicator progresses from 0 to 1 / tabs.length. To make the indicator responsive to scrolling, refer to the "Tabs" section above for more details. | SharedValue<number> | - |
944+
| badgeSize | - | SMALL or BIG | BIG |
945+
| animConfig | - | (routeName: T) => void | - |
946+
| tabIconProps | - | Y | - |
947+
| tabStyle | - | ViewStyle | - |
948+
| badgeStyle | - | ViewStyle | - |
949+
| indicatorStyle | - | ViewStyle | - |
950+
| tabsContainerStyle | - | ViewStyle | - |
951+
| tabInnerContentStyle | - | ViewStyle | - |
952+
| tabTitleStyle | - | TextStyle | - |
953+
954+
![secondary tabs](https://ik.imagekit.io/Computools/rn-material-components/secondart_tabs.gif?updatedAt=1735922886638)
955+
![secondary tabs with badges](https://ik.imagekit.io/Computools/rn-material-components/primary_tabs_with_badges.png?updatedAt=1735922619944)
956+
957+
</details>
958+
</details>
959+
</details>
766960
<details><summary>Sheets</summary>
767961
<br />
768962
<details><summary>Bottom Sheet</summary>

src/app-bars/bottom-app-bar/BottomAppBar.component.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,12 @@ import Animated, {useAnimatedStyle, withSpring, type SharedValue} from 'react-na
55

66
import {useTheme} from '../../theme/useTheme.hook';
77
import {type IconProps} from '../../icons/icon-props';
8+
import {ScrollDirection} from '../../types/scroll-direction.type';
89
import {BOTTOM_APP_BAR_PADDING_VARTICAL, styles} from './bottom-app-bar.styles';
910
import {type IconButtonProps} from '../../buttons/icon-buttons/icon-button.types';
1011
import {FloatingActionButton, FloatingActionButtonSize} from '../../buttons/floating-action-button/FloatingActionButton.component';
1112
import {AnimatedBottomAppBarActionButton} from './animated-bottom-app-bar-action-button/AnimatedBottomAppBarActionButton.component';
1213

13-
export enum ScrollDirection {
14-
UP = 'UP',
15-
DOWN = 'DOWN',
16-
}
17-
1814
export interface BottomAppBarProps<T extends IconProps> extends ViewProps {
1915
iconButtons: IconButtonProps<T>[];
2016

src/app-bars/bottom-app-bar/animated-bottom-app-bar-action-button/AnimatedBottomAppBarActionButton.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Animated, {FadeInDown, FadeOut, withDelay, useAnimatedStyle, withSpring,
33

44
import {useTheme} from '../../../theme/useTheme.hook';
55
import {type IconProps} from '../../../icons/icon-props';
6-
import {ScrollDirection} from '../BottomAppBar.component';
6+
import {ScrollDirection} from '../../../types/scroll-direction.type';
77
import {type IconButtonProps} from '../../../buttons/icon-buttons/icon-button.types';
88
import {StandartIconButton} from '../../../buttons/icon-buttons/standart-icon-button/StandardIconButton.component';
99

src/badge/Badge.component.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
11
import React from 'react';
2-
import {View, Text, type ViewProps, type StyleProp, type TextStyle} from 'react-native';
2+
import {View, Text, type ViewProps, type StyleProp, type ViewStyle, type TextStyle} from 'react-native';
33

44
import {styles} from './badge.styles';
55
import {useTheme} from '../theme/useTheme.hook';
66
import {useTypography} from '../typography/useTypography.component';
77

8-
export interface BadgeProps extends ViewProps {
9-
value: string;
8+
export enum BadgeSize {
9+
BIG = 'BIG',
10+
SMALL = 'SMALL',
11+
}
1012

13+
export interface BadgeProps extends ViewProps {
14+
value?: string;
15+
size?: BadgeSize;
1116
valueStyle?: StyleProp<TextStyle>;
1217
}
1318

14-
export const Badge: React.FC<BadgeProps> = ({value, style, valueStyle, ...props}) => {
19+
export const Badge: React.FC<BadgeProps> = ({value, size = BadgeSize.BIG, style, valueStyle, ...props}) => {
1520
const {error} = useTheme();
1621
const {labelSmall} = useTypography();
1722

23+
const shapeStyleMap: Record<BadgeSize, ViewStyle> = {
24+
[BadgeSize.BIG]: styles.big,
25+
[BadgeSize.SMALL]: styles.small,
26+
};
27+
1828
return (
19-
<View style={[styles.container, {backgroundColor: error.background}, style]} {...props}>
20-
<Text style={[labelSmall, {color: error.text}, valueStyle]}>{value}</Text>
29+
<View style={[styles.container, shapeStyleMap[size], {backgroundColor: error.background}, style]} {...props}>
30+
{size === BadgeSize.BIG && <Text style={[labelSmall, {color: error.text}, valueStyle]}>{value}</Text>}
2131
</View>
2232
);
2333
};

src/badge/badge.styles.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ export const styles = StyleSheet.create({
55
alignItems: 'center',
66
justifyContent: 'center',
77

8-
paddingHorizontal: 4,
8+
borderRadius: 100,
9+
},
10+
big: {
11+
alignSelf: 'center',
12+
913
minWidth: 16,
1014
height: 16,
11-
borderRadius: 100,
15+
paddingHorizontal: 4,
16+
},
17+
small: {
18+
width: 6,
19+
height: 6,
1220
},
1321
});

src/constants/icon.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const DEFAULT_ICON_SIZE = 24;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import Svg, {Path} from 'react-native-svg';
3+
4+
import {path} from './path.json';
5+
import type {IconProps} from '../icon-props';
6+
7+
const DEFAULT_SIZE = 24;
8+
const DEFAULT_COLOR = '#000';
9+
10+
export const BookmarkFilledIcon: React.FC<IconProps> = ({color = DEFAULT_COLOR, size = DEFAULT_SIZE, ...props}) => (
11+
<Svg viewBox="0 0 24 24" width={size} height={size} fill={'none'} {...props}>
12+
<Path fill={color} d={path} />
13+
</Svg>
14+
);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"path": "M5 21v-16c0-0.55 0.196-1.021 0.588-1.413s0.862-0.588 1.413-0.588h10c0.55 0 1.021 0.196 1.413 0.588s0.588 0.863 0.588 1.413v16l-7-3-7 3z"
3+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import Svg, {Path} from 'react-native-svg';
3+
4+
import {path} from './path.json';
5+
import type {IconProps} from '../icon-props';
6+
7+
const DEFAULT_SIZE = 24;
8+
const DEFAULT_COLOR = '#000';
9+
10+
export const CommuteFilledIcon: React.FC<IconProps> = ({color = DEFAULT_COLOR, size = DEFAULT_SIZE, ...props}) => (
11+
<Svg viewBox="0 0 24 24" width={size} height={size} fill={'none'} {...props}>
12+
<Path fill={color} d={path} />
13+
</Svg>
14+
);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"path": "M10 19.35v-5.7l1.4-4q0.125-0.275 0.363-0.462t0.588-0.188h7.3q0.35 0 0.588 0.188t0.363 0.462l1.4 4v5.7q0 0.275-0.188 0.462t-0.462 0.188h-0.7q-0.275 0-0.462-0.188t-0.188-0.462v-0.85h-8v0.85q0 0.275-0.188 0.462t-0.462 0.188h-0.7q-0.275 0-0.462-0.188t-0.188-0.462zM12 12.5h8l-0.7-2h-6.6l-0.7 2zM13 16.5q0.425 0 0.712-0.288t0.288-0.712-0.288-0.712-0.712-0.288-0.713 0.288-0.288 0.712 0.288 0.712 0.713 0.288zM19 16.5q0.425 0 0.712-0.288t0.288-0.712-0.288-0.712-0.712-0.288-0.712 0.288-0.288 0.712 0.288 0.712 0.712 0.288zM4 20v-1l1-1q-1.25 0-2.125-0.875t-0.875-2.125v-8q0-1.65 1.475-2.325t5.025-0.675q3.7 0 5.1 0.65t1.4 2.35v1h-2v-1h-9v6h5v7h-5zM5 16q0.425 0 0.713-0.288t0.288-0.712-0.288-0.712-0.713-0.288-0.713 0.288-0.288 0.712 0.288 0.712 0.713 0.288z"
3+
}

0 commit comments

Comments
 (0)