Skip to content

Commit cc0dafa

Browse files
authored
chore: Refactor Predict tabs for consistency (#29010)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR refactors the tab components to use design system components. It polishes the UI of the feature. ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: Predictions market details tabs (design system tab bar) Scenario: user switches tabs on a prediction market details screen Given Predictions is available and I am on a prediction market or game details screen that shows the details tab bar When user taps a different tab in the details tab bar Then the newly selected tab is highlighted with the shared tab styling (including the animated underline) and the details content updates to match that tab ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** ### **After** <img width="780" height="780" alt="Screenshot 2026-04-18 at 5 37 42 PM" src="https://github.com/user-attachments/assets/00883700-e9dd-463f-bbba-6450909eec01" /> ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI refactor: switches to a shared design-system `TabsBar` and removes a styling prop; main risk is minor layout/selection regressions (including active index clamping) on the market details screen. > > **Overview** > Refactors `PredictMarketDetailsTabBar` to use the shared design-system `TabsBar` (animated underline/scroll behavior) instead of custom `Pressable` tabs, and removes the `tabTwStyle` customization hook from callers. > > Adds active-tab index clamping to avoid out-of-range/null values and updates tests to account for `TabsBar` rendering details (duplicate labels) and to trigger presses via tab `testID`s. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b0cee08. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 60a0da0 commit cc0dafa

3 files changed

Lines changed: 67 additions & 90 deletions

File tree

app/components/UI/Predict/components/PredictGameDetailsContent/PredictGameDetailsContent.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ const PredictGameDetailsContent: React.FC<PredictGameDetailsContentProps> = ({
227227
tabs={tabs}
228228
activeTab={activeTab}
229229
onTabPress={handleTabPress}
230-
tabTwStyle="flex-1"
231230
/>
232231
)}
233232
{showChips && (

app/components/UI/Predict/views/PredictMarketDetails/components/PredictMarketDetailsTabBar/PredictMarketDetailsTabBar.test.tsx

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,18 @@ describe('PredictMarketDetailsTabBar', () => {
4141
});
4242

4343
it('renders all tab labels', () => {
44-
const { getByText } = render(
44+
const { getAllByText } = render(
4545
<PredictMarketDetailsTabBar
4646
tabs={threeTabs}
4747
activeTab={0}
4848
onTabPress={mockOnTabPress}
4949
/>,
5050
);
5151

52-
expect(getByText('Positions')).toBeOnTheScreen();
53-
expect(getByText('Outcomes')).toBeOnTheScreen();
54-
expect(getByText('About')).toBeOnTheScreen();
52+
// Tab renders a hidden + visible label copy for layout; allow duplicates per label.
53+
expect(getAllByText('Positions').length).toBeGreaterThanOrEqual(1);
54+
expect(getAllByText('Outcomes').length).toBeGreaterThanOrEqual(1);
55+
expect(getAllByText('About').length).toBeGreaterThanOrEqual(1);
5556
});
5657

5758
it('renders correct testID for each tab', () => {
@@ -72,15 +73,17 @@ describe('PredictMarketDetailsTabBar', () => {
7273
});
7374

7475
it('calls onTabPress with correct index when tab is pressed', () => {
75-
const { getByText } = render(
76+
const { getByTestId } = render(
7677
<PredictMarketDetailsTabBar
7778
tabs={twoTabs}
7879
activeTab={0}
7980
onTabPress={mockOnTabPress}
8081
/>,
8182
);
8283

83-
fireEvent.press(getByText('Outcomes'));
84+
fireEvent.press(
85+
getByTestId(getPredictMarketDetailsSelector.tabBarTab('outcomes')),
86+
);
8487

8588
expect(mockOnTabPress).toHaveBeenCalledWith(1);
8689
});
@@ -99,34 +102,4 @@ describe('PredictMarketDetailsTabBar', () => {
99102
).toBeOnTheScreen();
100103
expect(queryByText('Positions')).not.toBeOnTheScreen();
101104
});
102-
103-
describe('tabTwStyle prop', () => {
104-
it('produces different tab styles when tabTwStyle is provided', () => {
105-
const { getByTestId: getWithStyle } = render(
106-
<PredictMarketDetailsTabBar
107-
tabs={twoTabs}
108-
activeTab={0}
109-
onTabPress={mockOnTabPress}
110-
tabTwStyle="flex-1"
111-
/>,
112-
);
113-
114-
const { getByTestId: getWithoutStyle } = render(
115-
<PredictMarketDetailsTabBar
116-
tabs={twoTabs}
117-
activeTab={0}
118-
onTabPress={mockOnTabPress}
119-
/>,
120-
);
121-
122-
const tabWithStyle = getWithStyle(
123-
getPredictMarketDetailsSelector.tabBarTab('positions'),
124-
);
125-
const tabWithoutStyle = getWithoutStyle(
126-
getPredictMarketDetailsSelector.tabBarTab('positions'),
127-
);
128-
129-
expect(tabWithStyle.props.style).not.toEqual(tabWithoutStyle.props.style);
130-
});
131-
});
132105
});

app/components/UI/Predict/views/PredictMarketDetails/components/PredictMarketDetailsTabBar/PredictMarketDetailsTabBar.tsx

Lines changed: 58 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import React, { memo } from 'react';
2-
import { Pressable } from 'react-native';
1+
import React, { memo, useMemo } from 'react';
2+
import { Box } from '@metamask/design-system-react-native';
33
import {
4-
Box,
5-
BoxAlignItems,
6-
BoxFlexDirection,
7-
Text,
8-
TextColor,
9-
TextVariant,
10-
} from '@metamask/design-system-react-native';
11-
import { useTailwind } from '@metamask/design-system-twrnc-preset';
4+
TabsBar,
5+
type TabItem,
6+
} from '../../../../../../../component-library/components-temp/Tabs';
127
import {
138
getPredictMarketDetailsSelector,
149
PredictMarketDetailsSelectorsIDs,
@@ -19,54 +14,64 @@ export interface PredictMarketDetailsTabBarProps {
1914
tabs: { label: string; key: PredictMarketDetailsTabKey }[];
2015
activeTab: number | null;
2116
onTabPress: (tabIndex: number) => void;
22-
tabTwStyle?: string;
2317
}
2418

19+
const clampActiveIndex = (
20+
activeTab: number | null,
21+
tabCount: number,
22+
): number => {
23+
if (tabCount === 0) {
24+
return 0;
25+
}
26+
if (activeTab === null || activeTab < 0) {
27+
return 0;
28+
}
29+
if (activeTab >= tabCount) {
30+
return tabCount - 1;
31+
}
32+
return activeTab;
33+
};
34+
35+
/**
36+
* Market / game details tabs — same `TabsBar` + `Tab` stack as {@link PredictFeedTabBar}
37+
* (body md typography, animated underline, horizontal scroll when needed).
38+
*/
2539
const PredictMarketDetailsTabBar = memo(
26-
({
27-
tabs,
28-
activeTab,
29-
tabTwStyle,
30-
onTabPress,
31-
}: PredictMarketDetailsTabBarProps) => {
32-
const tw = useTailwind();
40+
({ tabs, activeTab, onTabPress }: PredictMarketDetailsTabBarProps) => {
41+
const tabItems: TabItem[] = useMemo(
42+
() =>
43+
tabs.map((tab) => ({
44+
key: tab.key,
45+
label: tab.label,
46+
content: null,
47+
testID: getPredictMarketDetailsSelector.tabBarTab(tab.key),
48+
})),
49+
[tabs],
50+
);
3351

34-
return (
35-
<Box
36-
twClassName="bg-default border-b border-muted pt-4"
37-
testID={PredictMarketDetailsSelectorsIDs.TAB_BAR}
38-
>
52+
const activeIndex = useMemo(
53+
() => clampActiveIndex(activeTab, tabs.length),
54+
[activeTab, tabs.length],
55+
);
56+
57+
if (tabs.length === 0) {
58+
return (
3959
<Box
40-
flexDirection={BoxFlexDirection.Row}
41-
alignItems={BoxAlignItems.Center}
42-
twClassName="px-3"
43-
>
44-
{tabs.map((tab, index) => (
45-
<Pressable
46-
key={tab.key}
47-
onPress={() => onTabPress(index)}
48-
style={tw.style(
49-
'w-1/3 py-3',
50-
activeTab === index ? 'border-b-2 border-default' : '',
51-
tabTwStyle,
52-
)}
53-
testID={getPredictMarketDetailsSelector.tabBarTab(tab.key)}
54-
>
55-
<Text
56-
variant={TextVariant.BodyMd}
57-
twClassName="font-medium"
58-
color={
59-
activeTab === index
60-
? TextColor.TextDefault
61-
: TextColor.TextAlternative
62-
}
63-
style={tw.style('text-center')}
64-
>
65-
{tab.label}
66-
</Text>
67-
</Pressable>
68-
))}
69-
</Box>
60+
twClassName="bg-default pt-4"
61+
testID={PredictMarketDetailsSelectorsIDs.TAB_BAR}
62+
/>
63+
);
64+
}
65+
66+
return (
67+
<Box twClassName="bg-default pt-4">
68+
<TabsBar
69+
tabs={tabItems}
70+
activeIndex={activeIndex}
71+
onTabPress={onTabPress}
72+
testID={PredictMarketDetailsSelectorsIDs.TAB_BAR}
73+
twClassName="!px-3"
74+
/>
7075
</Box>
7176
);
7277
},

0 commit comments

Comments
 (0)