Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Third party dependencies.
import React, { useRef, useState } from 'react';
import { Animated, ScrollView, LayoutChangeEvent } from 'react-native';

// External dependencies.
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import {
Box,
BoxFlexDirection,
BoxAlignItems,
} from '@metamask/design-system-react-native';

// Internal dependencies.
import TabsIconTab from '../TabsIconTab/TabsIconTab';
import { TabsIconBarProps } from './TabsIconBar.types';
import { useTabsBarLayout } from '../hooks/useTabsBarLayout';

const TabsIconBar: React.FC<TabsIconBarProps> = ({
tabs,
activeIndex,
onTabPress,
testID,
twClassName,
fillWidth = false,
collapseAnim,
...boxProps
}) => {
const tw = useTailwind();

const scrollViewRef = useRef<ScrollView>(null);
const underlineAnimated = useRef(new Animated.Value(0)).current;
const [underlineWidth, setUnderlineWidth] = useState(0);

// Height collapse animation — icon tabs always have a border-b row
const [tabRowHeight, setTabRowHeight] = useState(0);
const animatedHeight =
collapseAnim && tabRowHeight > 0
? collapseAnim.interpolate({
inputRange: [0, 1],
outputRange: [tabRowHeight, 0],
})
: undefined;

const {
isInitialized,
scrollEnabled,
handleContainerLayout,
handleTabLayout,
} = useTabsBarLayout({
tabs,
activeIndex,
fillWidth,
scrollAnimated: false,
scrollViewRef,
onAnimateToTab: (layout, isFirstTime) => {
// Icon tabs: underline is 75% of the tab width, centered
const targetWidth = layout.width * 0.75;
const targetX = layout.x + layout.width * 0.125;

setUnderlineWidth(targetWidth);

if (isFirstTime) {
underlineAnimated.setValue(targetX);
return null;
}

return Animated.timing(underlineAnimated, {
toValue: targetX,
duration: 200,
useNativeDriver: true,
});
},
Comment thread
vinnyhoward marked this conversation as resolved.
});

const handleTabPress = (index: number) => {
const tab = tabs[index];
if (!tab?.isDisabled) {
onTabPress(index);
}
};

return (
<Box
twClassName={`relative overflow-hidden border-b border-border-muted ${twClassName || ''}`}
testID={testID}
onLayout={handleContainerLayout as (layoutEvent: unknown) => void}
{...boxProps}
>
{scrollEnabled ? (
<ScrollView
ref={scrollViewRef}
horizontal
showsHorizontalScrollIndicator={false}
style={tw.style('flex-grow-0')}
contentContainerStyle={tw.style('flex-row px-4')}
scrollsToTop={false}
>
<Box
flexDirection={BoxFlexDirection.Row}
alignItems={BoxAlignItems.Center}
twClassName="relative gap-6"
>
{tabs.map((tab, index) => (
<TabsIconTab
key={tab.key}
label={tab.label}
iconName={tab.iconName}
isActive={index === activeIndex}
isDisabled={tab.isDisabled}
onPress={() => handleTabPress(index)}
onLayout={(layoutEvent) => handleTabLayout(index, layoutEvent)}
testID={tab.testID ?? `${testID}-tab-${index}`}
style={tw.style('py-2')}
Comment thread
cursor[bot] marked this conversation as resolved.
/>
))}

{activeIndex >= 0 && isInitialized && (
<Animated.View
style={tw.style(
'absolute -bottom-2px h-1 bg-icon-default z-1',
{
width: underlineWidth,
transform: [{ translateX: underlineAnimated }],
},
)}
/>
)}
</Box>
</ScrollView>
) : (
<Animated.View
onLayout={({
nativeEvent,
}: {
nativeEvent: LayoutChangeEvent['nativeEvent'];
}) => {
if (tabRowHeight === 0 && nativeEvent.layout.height > 0) {
setTabRowHeight(nativeEvent.layout.height);
}
}}
style={[
tw.style(
`relative ${fillWidth ? 'flex-row items-center' : 'px-4 gap-6 flex-row items-center relative'} ${animatedHeight !== undefined ? 'overflow-hidden' : ''}`,
),
animatedHeight !== undefined
? { height: animatedHeight }
: undefined,
]}
>
{tabs.map((tab, index) => (
<TabsIconTab
key={tab.key}
label={tab.label}
iconName={tab.iconName}
isActive={index === activeIndex}
isDisabled={tab.isDisabled}
onPress={() => handleTabPress(index)}
onLayout={(layoutEvent) => handleTabLayout(index, layoutEvent)}
testID={tab.testID ?? `${testID}-tab-${index}`}
shouldFillWidth={fillWidth}
/>
))}

{activeIndex >= 0 && isInitialized && (
<Animated.View
style={tw.style('absolute -bottom-2px h-1 bg-icon-default z-1', {
width: underlineWidth,
transform: [{ translateX: underlineAnimated }],
})}
/>
)}
</Animated.View>
)}
</Box>
);
};

export default TabsIconBar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Third party dependencies.
import React from 'react';
import { Animated } from 'react-native';

// External dependencies.
import { Box } from '@metamask/design-system-react-native';

// TODO: @MetaMask/design-system-engineers

Check warning on line 8 in app/component-library/components-temp/Tabs/TabsIconBar/TabsIconBar.types.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=metamask-mobile&issues=AZ30tssZcfeLMCCGsLWa&open=AZ30tssZcfeLMCCGsLWa&pullRequest=29684
// Use the concrete Box component props here instead of BoxProps.
// https://github.com/MetaMask/metamask-design-system/issues/1115
type BoxComponentProps = React.ComponentProps<typeof Box>;

// Internal dependencies.
import { IconName } from '../../../components/Icons/Icon/Icon.types';

/**
* Individual tab item data interface for the icon tab bar.
* Icon is required — use the base TabsBar for text-only tabs.
*/
export interface TabsIconItem {
key: string;
label: string;
content: React.ReactNode;
iconName: IconName;
isDisabled?: boolean;
testID?: string;
}

/**
* TabsIconBar component props
*/
export interface TabsIconBarProps extends BoxComponentProps {
/**
* Array of tab items — each must include an iconName
*/
tabs: TabsIconItem[];
/**
* Current active tab index
*/
activeIndex: number;
/**
* Callback when a tab is selected
*/
onTabPress: (index: number) => void;
/**
* Test ID for the component
*/
testID?: string;
/**
* Tailwind CSS classes to apply to the main container
*/
twClassName?: string;
/**
* When true, each tab stretches equally to fill the full container width.
* Disables horizontal scrolling and gap spacing. Defaults to false.
*/
fillWidth?: boolean;
/**
* Optional Animated.Value (0=visible, 1=hidden) that collapses the tab row height to zero.
* Requires useNativeDriver: false on the driving animation.
*/
collapseAnim?: Animated.Value;
}
Loading
Loading