Skip to content

Commit fb1b788

Browse files
authored
Merge branch 'main' into style/musd-calc
2 parents d69e85e + 455d908 commit fb1b788

37 files changed

Lines changed: 1695 additions & 131 deletions

.github/workflows/update-e2e-fixtures.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ jobs:
3636
}}
3737
runs-on: ubuntu-latest
3838
timeout-minutes: 5
39+
permissions:
40+
actions: write
41+
contents: read
42+
pull-requests: write
3943
steps:
4044
- uses: actions/checkout@v4
4145

app/component-library/components-temp/Tabs/TabsIconBar/TabsIconBar.test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,41 @@ describe('TabsIconBar', () => {
365365
});
366366
expect(getByTestId('icon-bar')).toBeOnTheScreen();
367367
});
368+
369+
it('renders without throwing when collapseBy is provided', () => {
370+
const collapseAnim = new Animated.Value(0);
371+
expect(() =>
372+
render(
373+
<TabsIconBar
374+
tabs={mockTabs}
375+
activeIndex={0}
376+
onTabPress={jest.fn()}
377+
collapseAnim={collapseAnim}
378+
collapseBy={28}
379+
testID="icon-bar"
380+
/>,
381+
),
382+
).not.toThrow();
383+
});
384+
385+
it('renders without throwing when collapseBy exceeds tabRowHeight', () => {
386+
const collapseAnim = new Animated.Value(0);
387+
const { getByTestId } = render(
388+
<TabsIconBar
389+
tabs={mockTabs}
390+
activeIndex={0}
391+
onTabPress={jest.fn()}
392+
collapseAnim={collapseAnim}
393+
collapseBy={9999}
394+
testID="icon-bar"
395+
/>,
396+
);
397+
// Tab row height is clamped to 0 (no negative height); should not crash.
398+
act(() => {
399+
fireEvent(getByTestId('icon-bar'), 'onLayout', mockLayoutEvent(400));
400+
});
401+
expect(getByTestId('icon-bar')).toBeOnTheScreen();
402+
});
368403
});
369404

370405
describe('Tab Array Changes', () => {

app/component-library/components-temp/Tabs/TabsIconBar/TabsIconBar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const TabsIconBar: React.FC<TabsIconBarProps> = ({
2323
twClassName,
2424
fillWidth = false,
2525
collapseAnim,
26+
collapseBy,
2627
...boxProps
2728
}) => {
2829
const tw = useTailwind();
@@ -33,11 +34,13 @@ const TabsIconBar: React.FC<TabsIconBarProps> = ({
3334

3435
// Height collapse animation — icon tabs always have a border-b row
3536
const [tabRowHeight, setTabRowHeight] = useState(0);
37+
const collapsedHeight =
38+
collapseBy !== undefined ? Math.max(0, tabRowHeight - collapseBy) : 0;
3639
const animatedHeight =
3740
collapseAnim && tabRowHeight > 0
3841
? collapseAnim.interpolate({
3942
inputRange: [0, 1],
40-
outputRange: [tabRowHeight, 0],
43+
outputRange: [tabRowHeight, collapsedHeight],
4144
})
4245
: undefined;
4346

app/component-library/components-temp/Tabs/TabsIconBar/TabsIconBar.types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,10 @@ export interface TabsIconBarProps extends BoxComponentProps {
6060
* Requires useNativeDriver: false on the driving animation.
6161
*/
6262
collapseAnim?: Animated.Value;
63+
/**
64+
* When provided alongside `collapseAnim`, the tab row collapses BY this many pixels at the
65+
* fully-hidden state (1.0) instead of collapsing all the way to 0. Use this to shrink the
66+
* row by just the icon area (for example) while keeping labels visible.
67+
*/
68+
collapseBy?: number;
6369
}

app/component-library/components-temp/Tabs/TabsIconList/TabsIconList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const TabsIconList = forwardRef<TabsIconListRef, TabsIconListProps>(
8585

8686
<GestureDetector gesture={swipeGesture}>
8787
<Box
88-
twClassName={`flex-1 mt-2 px-4 ${tabsListContentTwClassName || ''}`}
88+
twClassName={`flex-1 ${tabsListContentTwClassName || ''}`}
8989
testID={testID ? `${testID}-content` : undefined}
9090
>
9191
{tabs.map((tab, index) => {

app/component-library/components-temp/Tabs/TabsIconTab/TabsIconAnimationContext.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { createContext } from 'react';
2-
import { Animated } from 'react-native';
2+
import type { SharedValue } from 'react-native-reanimated';
33

44
/**
5-
* Provides an optional RN Animated.Value (0 = icons expanded, 1 = icons collapsed)
6-
* to Tab components without threading props through TabsList / TabsBar.
7-
* Consumers that don't provide this context get the default (undefined), which
8-
* means icons render at full size — preserving existing behaviour.
5+
* Provides an optional Reanimated SharedValue (0 = icons expanded, 1 = icons collapsed)
6+
* to Tab components without threading props through TabsList / TabsBar. The value drives
7+
* the icon's height / marginBottom / opacity via useAnimatedStyle on the UI thread —
8+
* no per-frame JS work, no layout reflow on the JS thread.
9+
*
10+
* Consumers that don't provide this context get the default (undefined), which means
11+
* icons render at full size — preserving existing behaviour.
912
*/
1013
export interface TabIconAnimationContextValue {
11-
iconCollapseAnim?: Animated.Value;
14+
iconCollapseProgress?: SharedValue<number>;
1215
}
1316

1417
export const TabIconAnimationContext =

app/component-library/components-temp/Tabs/TabsIconTab/TabsIconTab.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { render, fireEvent } from '@testing-library/react-native';
44

55
// Internal dependencies.
66
import TabsIconTab from './TabsIconTab';
7+
import { TabIconAnimationContext } from './TabsIconAnimationContext';
78
import { IconName } from '../../../components/Icons/Icon/Icon.types';
89

910
describe('TabsIconTab', () => {
@@ -121,4 +122,39 @@ describe('TabsIconTab', () => {
121122
});
122123
});
123124
});
125+
126+
describe('Icon collapse animation context', () => {
127+
it('renders without throwing when iconCollapseProgress SharedValue is provided', () => {
128+
const iconCollapseProgress = { value: 0 } as never;
129+
expect(() =>
130+
render(
131+
<TabIconAnimationContext.Provider value={{ iconCollapseProgress }}>
132+
<TabsIconTab {...defaultProps} testID="tab" />
133+
</TabIconAnimationContext.Provider>,
134+
),
135+
).not.toThrow();
136+
});
137+
138+
it('renders without throwing when no SharedValue is provided (icons full size)', () => {
139+
expect(() =>
140+
render(
141+
<TabIconAnimationContext.Provider value={{}}>
142+
<TabsIconTab {...defaultProps} testID="tab" />
143+
</TabIconAnimationContext.Provider>,
144+
),
145+
).not.toThrow();
146+
});
147+
148+
it('renders the label text regardless of collapse progress value', () => {
149+
const fullyCollapsed = { value: 1 } as never;
150+
const { getByText } = render(
151+
<TabIconAnimationContext.Provider
152+
value={{ iconCollapseProgress: fullyCollapsed }}
153+
>
154+
<TabsIconTab {...defaultProps} />
155+
</TabIconAnimationContext.Provider>,
156+
);
157+
expect(getByText('Portfolio')).toBeOnTheScreen();
158+
});
159+
});
124160
});

app/component-library/components-temp/Tabs/TabsIconTab/TabsIconTab.tsx

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
// Third party dependencies.
22
import React, { useContext, useRef, useCallback } from 'react';
3-
import { Animated, Pressable, StyleProp, View, ViewStyle } from 'react-native';
3+
import { Pressable, StyleProp, View, ViewStyle } from 'react-native';
4+
import Reanimated, {
5+
useAnimatedStyle,
6+
interpolate,
7+
Extrapolation,
8+
} from 'react-native-reanimated';
49

510
// External dependencies.
611
import { useTailwind } from '@metamask/design-system-twrnc-preset';
@@ -37,27 +42,31 @@ const TabsIconTab: React.FC<TabsIconTabProps> = ({
3742
}) => {
3843
const tw = useTailwind();
3944
const viewRef = useRef<View>(null);
40-
const { iconCollapseAnim } = useContext(TabIconAnimationContext);
45+
const { iconCollapseProgress } = useContext(TabIconAnimationContext);
4146

42-
// translateY slides the icon upward out of the clipping boundary (overflow:hidden
43-
// on the outer View) without changing layout — keeps tab bar height fixed so
44-
// there is no layout cascade. Both transform and opacity run on the native thread.
45-
const iconAnimatedStyle = iconCollapseAnim
46-
? {
47-
opacity: iconCollapseAnim.interpolate({
48-
inputRange: [0, 1],
49-
outputRange: [1, 0],
50-
}),
51-
transform: [
52-
{
53-
translateY: iconCollapseAnim.interpolate({
54-
inputRange: [0, 1],
55-
outputRange: [0, -(ICON_SIZE_LG + ICON_MARGIN_BOTTOM)],
56-
}),
57-
},
58-
],
59-
}
60-
: undefined;
47+
// Drive icon's layout box (height + marginBottom) and opacity from a Reanimated
48+
// SharedValue so the entire transition runs on the UI thread — no per-frame JS work,
49+
// no layout reflow on the JS thread.
50+
const iconAnimatedStyle = useAnimatedStyle(() => {
51+
if (!iconCollapseProgress) {
52+
return {
53+
height: ICON_SIZE_LG,
54+
marginBottom: ICON_MARGIN_BOTTOM,
55+
opacity: 1,
56+
};
57+
}
58+
const p = iconCollapseProgress.value;
59+
return {
60+
height: interpolate(p, [0, 1], [ICON_SIZE_LG, 0], Extrapolation.CLAMP),
61+
marginBottom: interpolate(
62+
p,
63+
[0, 1],
64+
[ICON_MARGIN_BOTTOM, 0],
65+
Extrapolation.CLAMP,
66+
),
67+
opacity: interpolate(p, [0, 1], [1, 0], Extrapolation.CLAMP),
68+
};
69+
});
6170

6271
const handleOnLayout = useCallback(
6372
(layoutEvent: Parameters<NonNullable<typeof onLayout>>[0]) => {
@@ -92,12 +101,7 @@ const TabsIconTab: React.FC<TabsIconTabProps> = ({
92101
alignItems={BoxAlignItems.Center}
93102
justifyContent={BoxJustifyContent.Center}
94103
>
95-
<Animated.View
96-
style={[
97-
{ height: ICON_SIZE_LG, marginBottom: ICON_MARGIN_BOTTOM },
98-
iconAnimatedStyle,
99-
]}
100-
>
104+
<Reanimated.View style={iconAnimatedStyle}>
101105
<Icon
102106
name={iconName}
103107
size={IconSize.Lg}
@@ -110,7 +114,7 @@ const TabsIconTab: React.FC<TabsIconTabProps> = ({
110114
}
111115
{...iconProps}
112116
/>
113-
</Animated.View>
117+
</Reanimated.View>
114118
<Text
115119
variant={TextVariant.BodySm}
116120
fontWeight={

app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ interface PerpsHomeViewProps {
8282
tabEnterCallbackRef?: React.MutableRefObject<(() => void) | null>;
8383
/** Forwarded to useDiscoveryScrollManager to sync icon animations with header hide/show. */
8484
onHeaderHiddenChange?: (hidden: boolean) => void;
85+
/**
86+
* Top padding applied inside the scroll content container — used by HomepageDiscoveryTabs
87+
* (Hub Page Discovery Tabs feature flag treatment) so the perps gradient extends up
88+
* directly under the discovery tab bar instead of leaving a transparent gap.
89+
*/
90+
topInset?: number;
8591
}
8692

8793
const PerpsHomeView = ({
@@ -90,6 +96,7 @@ const PerpsHomeView = ({
9096
walletHeaderHeight = 0,
9197
tabEnterCallbackRef,
9298
onHeaderHiddenChange,
99+
topInset = 0,
93100
}: PerpsHomeViewProps) => {
94101
const { styles } = useStyles(styleSheet, {});
95102
const insets = useSafeAreaInsets();
@@ -483,7 +490,10 @@ const PerpsHomeView = ({
483490
{/* Main Content - ScrollView with all carousels */}
484491
<Reanimated.ScrollView
485492
style={styles.scrollView}
486-
contentContainerStyle={styles.scrollViewContent}
493+
contentContainerStyle={[
494+
styles.scrollViewContent,
495+
topInset > 0 ? { paddingTop: topInset } : null,
496+
]}
487497
showsVerticalScrollIndicator={false}
488498
onScroll={perpsScrollHandler}
489499
scrollEventThrottle={16}

app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const PerpsMarketListView = ({
6767
route.params?.showWatchlistOnly ?? propShowWatchlistOnly ?? false;
6868
const defaultMarketTypeFilter =
6969
route.params?.defaultMarketTypeFilter ?? 'all';
70+
const defaultSortOptionId = route.params?.defaultSortOptionId;
7071

7172
const fadeAnimation = useRef(new Animated.Value(0)).current;
7273
const [isSortFieldSheetVisible, setIsSortFieldSheetVisible] = useState(false);
@@ -84,6 +85,7 @@ const PerpsMarketListView = ({
8485
enablePolling: false,
8586
showWatchlistOnly,
8687
defaultMarketTypeFilter,
88+
defaultSortOptionId,
8789
showZeroVolume: __DEV__,
8890
});
8991

0 commit comments

Comments
 (0)