Skip to content

Commit 42a758f

Browse files
feat: set NetworkManager bottom sheet to 75% height (#24493)
## **Description** Modified the NetworkManager component to display the Networks bottom sheet at 75% of screen height instead of expanding based on content. This provides a more consistent and predictable UI experience when users open the Networks modal. **What is the reason for the change?** The Networks bottom sheet was expanding to fit all content, there is a design decision that there should be no BottomSheets that open 100% height see ticket. **What is the improvement/solution?** Constrained the NetworkManager bottom sheet to exactly 75% of the screen height with proper scrolling for content that exceeds this space. Also added a close button (X icon) in the header for improved UX. ## **Changelog** CHANGELOG entry: Updated Networks bottom sheet to open at 50% screen height with improved header and close button ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MDP-237 ## **Manual testing steps** ```gherkin Feature: Networks bottom sheet height constraint Scenario: user opens Networks bottom sheet Given the user is on the wallet home screen When user taps on the network selector button Then the Networks bottom sheet should open to exactly 75% of screen height And the sheet should display a close button (X) in the top-right corner And the content should be scrollable Scenario: user interacts with Networks bottom sheet Given the Networks bottom sheet is open at 75% height When user scrolls through the network list Then the sheet should remain at 75% height And only the content should scroll Scenario: user closes Networks bottom sheet Given the Networks bottom sheet is open When user taps the close button (X) Then the bottom sheet should close with animation And user should return to the home screen Scenario: user swipes to dismiss bottom sheet Given the Networks bottom sheet is open When user swipes down on the sheet Then the bottom sheet should dismiss ``` ## **Screenshots/Recordings** ### **Before** Networks bottom sheet expanded to fit all content (100% height) and no close button https://github.com/user-attachments/assets/da2023a4-bdd9-4980-8ce5-d634ba524119 ### **After** Networks bottom sheet constrained to 75% of screen height with scrollable content and close button https://github.com/user-attachments/assets/7f8c2ab5-94c5-4997-adf8-fdf45c0159df ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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. ## **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. --- ## Technical Details **Changes made:** 1. **NetworkManager** (Primary Fix): - Wrapped BottomSheet content in a View with fixed height constraint - Added `BottomSheetHeader` with close button - Created `scrollableTabView` style with `flex: 1` - Removed unused imports and variables 2. **NetworkSelector** (Bonus Migration): - Migrated from deprecated `ReusableModal` to `BottomSheet` component - Added close button and 75% height constraint - Cleaned up deprecated styles **Key insight:** The BottomSheetDialog uses `onLayout` to measure content height for animation. By placing the height constraint on a wrapper View inside the BottomSheet (rather than on the BottomSheet component itself), we ensure the measured height is exactly 75%. **Files changed:** - `app/components/UI/NetworkManager/index.tsx` - `app/components/UI/NetworkManager/index.styles.ts` - `app/components/Views/NetworkSelector/NetworkSelector.tsx` - `app/components/Views/NetworkSelector/NetworkSelector.styles.ts` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Imposes a fixed-height Networks bottom sheet and modernizes header/metrics usage. > > - Wraps BottomSheet content in a container with `height`/`maxHeight` at `75%` of device height; removes safe-area-based sizing and legacy sheet/title styles > - Replaces inline title `Text` with `BottomSheetHeader` providing a close action; adjusts layout to place `ScrollableTabView` directly within the container > - Refactors metrics: replaces `MetaMetrics.getInstance().addTraitsToUser` with `useMetrics().addTraitsToUser`; updates delete flow to use this and keeps analytics tab tracking > - Adds RPC selection modal wiring and EVM-only config mapping for `RpcSelectionModal` > - Renames `setNetworkMenuModal` -> `setShowNetworkMenuModal` and minor cleanup of unused imports > - Updates tests/mocks for new header and button icon components, removes safe-area style assertion, and adapts deletion confirmation assertions > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6d5c0dd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 78b8f5a commit 42a758f

File tree

3 files changed

+72
-79
lines changed

3 files changed

+72
-79
lines changed

app/components/UI/NetworkManager/index.styles.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ import {
55
} from '../../../component-library/components/Texts/Text';
66
import { Theme } from '../../../util/theme/models';
77

8-
const SHEET_BORDER_RADIUS = 20;
9-
const TITLE_PADDING_TOP = 16;
10-
const TITLE_MARGIN_TOP = 4;
118
const UNDERLINE_HEIGHT = 2;
129
const TAB_PADDING_BOTTOM = 8;
1310
const TAB_PADDING_VERTICAL = 8;
@@ -20,27 +17,11 @@ const createStyles = (params: { theme: Theme }) => {
2017
const { theme } = params;
2118
const { colors, typography } = theme;
2219

23-
const backgroundDefault = colors.background.default;
2420
const borderMuted = colors.border.muted;
2521
const textDefault = colors.text.default;
2622
const textAlternative = colors.text.alternative;
2723

2824
return StyleSheet.create({
29-
// reusable modal
30-
sheet: {
31-
backgroundColor: backgroundDefault,
32-
borderTopLeftRadius: SHEET_BORDER_RADIUS,
33-
borderTopRightRadius: SHEET_BORDER_RADIUS,
34-
},
35-
// network tabs selectors
36-
networkTabsSelectorWrapper: {
37-
height: '100%',
38-
},
39-
networkTabsSelectorTitle: {
40-
alignSelf: 'center',
41-
paddingTop: TITLE_PADDING_TOP,
42-
marginTop: TITLE_MARGIN_TOP,
43-
},
4425
// tab
4526
tabUnderlineStyle: {
4627
height: UNDERLINE_HEIGHT,

app/components/UI/NetworkManager/index.test.tsx

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ jest.mock('../../hooks/useMetrics', () => ({
132132
useMetrics: () => ({
133133
trackEvent: mockTrackEvent,
134134
createEventBuilder: mockCreateEventBuilder,
135+
addTraitsToUser: mockAddTraitsToUser,
135136
}),
136137
MetaMetricsEvents: {
137138
ASSET_FILTER_SELECTED: 'asset_filter_selected',
@@ -565,6 +566,36 @@ jest.mock(
565566
}),
566567
);
567568

569+
jest.mock('../../../component-library/components/Buttons/ButtonIcon', () => {
570+
const { TouchableOpacity } = jest.requireActual('react-native');
571+
return {
572+
__esModule: true,
573+
default: ({ onPress }: { onPress?: () => void }) => (
574+
<TouchableOpacity testID="button-icon" onPress={onPress} />
575+
),
576+
ButtonIconSizes: {
577+
Sm: 'sm',
578+
Md: 'md',
579+
Lg: 'lg',
580+
},
581+
};
582+
});
583+
584+
jest.mock(
585+
'../../../component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader',
586+
() => {
587+
const { View: RNView, Text: RNText } = jest.requireActual('react-native');
588+
return {
589+
__esModule: true,
590+
default: ({ children }: { children: React.ReactNode }) => (
591+
<RNView testID="header">
592+
<RNText>{children}</RNText>
593+
</RNView>
594+
),
595+
};
596+
},
597+
);
598+
568599
jest.mock('../../../component-library/components/Buttons/Button', () => ({
569600
ButtonVariants: {
570601
Primary: 'primary',
@@ -734,18 +765,6 @@ describe('NetworkManager Component', () => {
734765
expect(getByTestId('custom-network-selector')).toBeOnTheScreen();
735766
});
736767

737-
it('should apply correct container styles with safe area insets', () => {
738-
const { getByTestId } = renderComponent();
739-
const modal = getByTestId('main-bottom-sheet');
740-
741-
expect(modal.props.style).toEqual([
742-
{
743-
paddingTop: 44 + 800 * 0.02, // safeAreaInsets.top + Device.getDeviceHeight() * 0.02
744-
paddingBottom: 34, // safeAreaInsets.bottom
745-
},
746-
]);
747-
});
748-
749768
it('should set initial tab to popular networks when selectedCount > 0', () => {
750769
(useNetworksByNamespace as jest.Mock).mockReturnValue({
751770
selectedCount: 3,
@@ -859,7 +878,7 @@ describe('NetworkManager Component', () => {
859878

860879
describe('Network Deletion Workflow', () => {
861880
it('should show confirmation modal when delete is pressed', async () => {
862-
const { getByTestId } = renderComponent();
881+
const { getAllByTestId, getByTestId } = renderComponent();
863882

864883
const openModalButton = getByTestId('open-modal-button');
865884
fireEvent.press(openModalButton);
@@ -871,13 +890,15 @@ describe('NetworkManager Component', () => {
871890

872891
expect(mockOnCloseBottomSheet).toHaveBeenCalled();
873892
await waitFor(() => {
874-
expect(getByTestId('header')).toBeOnTheScreen();
893+
// There are now multiple headers (main sheet + delete modal)
894+
const headers = getAllByTestId('header');
895+
expect(headers.length).toBeGreaterThan(0);
875896
expect(getByTestId('bottom-sheet-footer')).toBeOnTheScreen();
876897
});
877898
});
878899

879900
it('should display correct network name in delete confirmation', async () => {
880-
const { getByTestId, getByText } = renderComponent();
901+
const { getAllByTestId, getByTestId, getByText } = renderComponent();
881902

882903
const openModalButton = getByTestId('open-modal-button');
883904
fireEvent.press(openModalButton);
@@ -888,7 +909,9 @@ describe('NetworkManager Component', () => {
888909
});
889910

890911
await waitFor(() => {
891-
expect(getByTestId('header')).toBeOnTheScreen();
912+
// There are now multiple headers (main sheet + delete modal)
913+
const headers = getAllByTestId('header');
914+
expect(headers.length).toBeGreaterThan(0);
892915
// The network name appears as part of a larger text string, use partial match
893916
expect(getByText(/Ethereum Mainnet/)).toBeOnTheScreen();
894917
expect(getByText(/app_settings\.network_delete/)).toBeOnTheScreen();

app/components/UI/NetworkManager/index.tsx

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { View } from 'react-native';
33
import React, { useRef, useMemo, useCallback, useState } from 'react';
44
import { useSelector } from 'react-redux';
5-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
65
import { useNavigation } from '@react-navigation/native';
76
import ScrollableTabView, {
87
ChangeTabProperties,
@@ -14,7 +13,6 @@ import { toHex } from '@metamask/controller-utils';
1413
// external dependencies
1514
import Engine from '../../../core/Engine';
1615
import { removeItemFromChainIdList } from '../../../util/metrics/MultichainAPI/networkMetricUtils';
17-
import { MetaMetrics } from '../../../core/Analytics';
1816
import { useTheme } from '../../../util/theme';
1917
import { MetaMetricsEvents, useMetrics } from '../../hooks/useMetrics';
2018
import { strings } from '../../../../locales/i18n';
@@ -30,9 +28,7 @@ import { useStyles } from '../../../component-library/hooks/useStyles';
3028
import BottomSheet, {
3129
BottomSheetRef,
3230
} from '../../../component-library/components/BottomSheets/BottomSheet';
33-
import Text, {
34-
TextVariant,
35-
} from '../../../component-library/components/Texts/Text';
31+
import Text from '../../../component-library/components/Texts/Text';
3632
import { IconName } from '../../../component-library/components/Icons/Icon';
3733
import AccountAction from '../../Views/AccountAction';
3834
import NetworkMultiSelector from '../NetworkMultiSelector/NetworkMultiSelector';
@@ -90,8 +86,7 @@ const NetworkManager = () => {
9086
const navigation = useNavigation();
9187
const { colors } = useTheme();
9288
const { styles } = useStyles(createStyles, { colors });
93-
const { trackEvent, createEventBuilder } = useMetrics();
94-
const safeAreaInsets = useSafeAreaInsets();
89+
const { trackEvent, createEventBuilder, addTraitsToUser } = useMetrics();
9590
const { selectedCount } = useNetworksByNamespace({
9691
networkType: NetworkType.Popular,
9792
});
@@ -123,7 +118,7 @@ const NetworkManager = () => {
123118
return getEnabledNetworks(enabledNetworksByNamespace);
124119
}, [enabledNetworksByNamespace]);
125120

126-
const [showNetworkMenuModal, setNetworkMenuModal] =
121+
const [showNetworkMenuModal, setShowNetworkMenuModal] =
127122
useState<NetworkMenuModalState>(initialNetworkMenuModal);
128123
const [showConfirmDeleteModal, setShowConfirmDeleteModal] =
129124
useState<ShowConfirmDeleteModalState>(initialShowConfirmDeleteModal);
@@ -160,11 +155,11 @@ const NetworkManager = () => {
160155
const containerStyle = useMemo(
161156
() => [
162157
{
163-
paddingTop: safeAreaInsets.top + Device.getDeviceHeight() * 0.02,
164-
paddingBottom: safeAreaInsets.bottom,
158+
height: Device.getDeviceHeight() * 0.75,
159+
maxHeight: Device.getDeviceHeight() * 0.75,
165160
},
166161
],
167-
[safeAreaInsets.top, safeAreaInsets.bottom],
162+
[],
168163
);
169164

170165
const defaultTabProps = useMemo(
@@ -231,7 +226,7 @@ const NetworkManager = () => {
231226
);
232227

233228
const openModal = useCallback((networkMenuModal: NetworkMenuModalState) => {
234-
setNetworkMenuModal((prev) => ({
229+
setShowNetworkMenuModal((prev) => ({
235230
...prev,
236231
...networkMenuModal,
237232
isVisible: true,
@@ -240,7 +235,7 @@ const NetworkManager = () => {
240235
}, []);
241236

242237
const closeModal = useCallback(() => {
243-
setNetworkMenuModal(initialNetworkMenuModal);
238+
setShowNetworkMenuModal(initialNetworkMenuModal);
244239
networkMenuSheetRef.current?.onCloseBottomSheet();
245240
}, []);
246241

@@ -311,13 +306,11 @@ const NetworkManager = () => {
311306
NetworkController.removeNetwork(chainId);
312307
disableNetwork(showConfirmDeleteModal.caipChainId);
313308

314-
MetaMetrics.getInstance().addTraitsToUser(
315-
removeItemFromChainIdList(chainId),
316-
);
309+
addTraitsToUser(removeItemFromChainIdList(chainId));
317310

318311
setShowConfirmDeleteModal(initialShowConfirmDeleteModal);
319312
}
320-
}, [showConfirmDeleteModal, disableNetwork]);
313+
}, [showConfirmDeleteModal, disableNetwork, addTraitsToUser]);
321314

322315
const cancelButtonProps: ButtonProps = useMemo(
323316
() => ({
@@ -368,37 +361,33 @@ const NetworkManager = () => {
368361
<BottomSheet
369362
testID={NETWORK_MULTI_SELECTOR_TEST_IDS.NETWORK_MANAGER_BOTTOM_SHEET}
370363
ref={sheetRef}
371-
style={containerStyle}
372364
shouldNavigateBack
373365
>
374-
<View style={styles.sheet}>
375-
<Text
376-
variant={TextVariant.HeadingMD}
377-
style={styles.networkTabsSelectorTitle}
366+
<View style={containerStyle}>
367+
<BottomSheetHeader
368+
onClose={() => sheetRef.current?.onCloseBottomSheet()}
378369
>
379370
{strings('wallet.networks')}
380-
</Text>
381-
382-
<View style={styles.networkTabsSelectorWrapper}>
383-
<ScrollableTabView
384-
renderTabBar={renderTabBar}
385-
onChangeTab={onChangeTab}
386-
initialPage={initialTabIndexRef.current ?? 0}
387-
>
388-
<NetworkMultiSelector
389-
{...defaultTabProps}
390-
openModal={openModal}
391-
dismissModal={dismissModal}
392-
openRpcModal={openRpcModal}
393-
/>
394-
<CustomNetworkSelector
395-
{...customTabProps}
396-
openModal={openModal}
397-
dismissModal={dismissModal}
398-
openRpcModal={openRpcModal}
399-
/>
400-
</ScrollableTabView>
401-
</View>
371+
</BottomSheetHeader>
372+
373+
<ScrollableTabView
374+
renderTabBar={renderTabBar}
375+
onChangeTab={onChangeTab}
376+
initialPage={initialTabIndexRef.current ?? 0}
377+
>
378+
<NetworkMultiSelector
379+
{...defaultTabProps}
380+
openModal={openModal}
381+
dismissModal={dismissModal}
382+
openRpcModal={openRpcModal}
383+
/>
384+
<CustomNetworkSelector
385+
{...customTabProps}
386+
openModal={openModal}
387+
dismissModal={dismissModal}
388+
openRpcModal={openRpcModal}
389+
/>
390+
</ScrollableTabView>
402391
</View>
403392

404393
{showNetworkMenuModal.isVisible && (

0 commit comments

Comments
 (0)