Skip to content

Commit 73d15e7

Browse files
fix: perf(accessibility): reduce DOM nesting and fix iOS accessibility in … (#29120)
…Homepage core - WalletView: remove accessible={false} from root, fix scroll viewport tracking - Homepage sections (Cash, DeFi, NFTs, Tokens): View ref + Box gap → View ref style, –1 native node per section - SectionRow: add optional gap prop for direct children spacing - HomepageSectionUnrealizedPnlRow, ViewMoreCard: remove redundant Box wrappers - MainNavigator: simplify WalletTab navigation stack (flatten WalletModalFlow/WalletTabStackFlow) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **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: ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] 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** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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** > Primarily UI layout/accessibility tweaks (less view nesting, spacing changes, added `testID`s) with minimal business-logic impact; main risk is subtle layout or accessibility regressions across homepage sections and wallet tabs. > > **Overview** > Reduces native view nesting across Homepage sections by replacing `Box` wrapper gaps with `View` `style` gaps, flattening a few redundant wrappers (e.g., `HomepageSectionUnrealizedPnlRow`, `ViewMoreCard`), and tweaking the `PopularTokenRow` row layout styles. > > Improves accessibility/automation hooks by setting `accessible={false}` on the homepage container and legacy banner container, adding a `gap` prop to `SectionRow`, and introducing/propagating new `WalletViewSelectorsIDs` (homepage section header `testID`s plus wallet tabs-related IDs passed into `TabsList` and tab items). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ba0a738. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d506318 commit 73d15e7

11 files changed

Lines changed: 283 additions & 238 deletions

File tree

app/components/Views/Homepage/Homepage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ const Homepage = forwardRef<SectionRefreshHandle, HomepageProps>(
306306
marginBottom={8}
307307
paddingTop={6}
308308
testID={WalletViewSelectorsIDs.HOMEPAGE_CONTAINER}
309+
accessible={false}
309310
>
310311
{/* Cash — always first */}
311312
<CashSection
@@ -359,6 +360,7 @@ const Homepage = forwardRef<SectionRefreshHandle, HomepageProps>(
359360
marginBottom={8}
360361
paddingTop={6}
361362
testID={WalletViewSelectorsIDs.HOMEPAGE_CONTAINER}
363+
accessible={false}
362364
>
363365
<CashSection
364366
ref={cashSectionRef}

app/components/Views/Homepage/Sections/Cash/CashSection.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ import useHomeViewedEvent, {
1515
HomeSectionNames,
1616
} from '../../hooks/useHomeViewedEvent';
1717
import { useSectionPerformance } from '../../hooks/useSectionPerformance';
18+
import { WalletViewSelectorsIDs } from '../../../Wallet/WalletView.testIds';
1819
import { selectIsMusdConversionFlowEnabledFlag } from '../../../../UI/Earn/selectors/featureFlags';
20+
import { selectMoneyHomeScreenEnabledFlag } from '../../../../UI/Money/selectors/featureFlags';
1921
import { useMusdConversionEligibility } from '../../../../UI/Earn/hooks/useMusdConversionEligibility';
2022
import { useMusdBalance } from '../../../../UI/Earn/hooks/useMusdBalance';
21-
import { selectMoneyHomeScreenEnabledFlag } from '../../../../UI/Money/selectors/featureFlags';
2223
import MusdAggregatedRow from './MusdAggregatedRow';
2324
import { useCashNavigation } from './useCashNavigation';
2425

@@ -91,7 +92,11 @@ const CashSection = forwardRef<SectionRefreshHandle, CashSectionProps>(
9192
return (
9293
<View ref={sectionViewRef} onLayout={onLayout}>
9394
<Box gap={3}>
94-
<SectionHeader title={title} onPress={navigateToCash} />
95+
<SectionHeader
96+
title={title}
97+
onPress={navigateToCash}
98+
testID={WalletViewSelectorsIDs.HOMEPAGE_SECTION_TITLE('cash')}
99+
/>
95100
{!hasMusdBalanceOnAnyChain ? (
96101
<SectionRow>
97102
<CashGetMusdEmptyState key={`cash-empty-${refreshVersion}`} />

app/components/Views/Homepage/Sections/DeFi/DeFiSection.tsx

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import React, {
44
useImperativeHandle,
55
useRef,
66
} from 'react';
7-
import { View } from 'react-native';
7+
import { StyleSheet, View } from 'react-native';
88
import { useFocusEffect, useNavigation } from '@react-navigation/native';
99
import { useSelector } from 'react-redux';
1010
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
11-
import { Box } from '@metamask/design-system-react-native';
1211
import { useTailwind } from '@metamask/design-system-twrnc-preset';
1312
import { useTheme } from '../../../../../util/theme';
1413
import SectionHeader from '../../../../../component-library/components-temp/SectionHeader';
@@ -26,9 +25,14 @@ import useHomeViewedEvent, {
2625
HomeSectionNames,
2726
} from '../../hooks/useHomeViewedEvent';
2827
import { useSectionPerformance } from '../../hooks/useSectionPerformance';
28+
import { WalletViewSelectorsIDs } from '../../../Wallet/WalletView.testIds';
2929

3030
const MAX_POSITIONS_DISPLAYED = 5;
3131

32+
const styles = StyleSheet.create({
33+
sectionGap: { gap: 12 },
34+
});
35+
3236
interface DeFiSectionProps {
3337
sectionIndex: number;
3438
totalSectionsLoaded: number;
@@ -146,42 +150,48 @@ const DeFiSection = forwardRef<SectionRefreshHandle, DeFiSectionProps>(
146150
// Show retry UI on error
147151
if (!isLoading && hasError) {
148152
return (
149-
<View ref={sectionViewRef} onLayout={onLayout}>
150-
<Box gap={3}>
151-
<SectionHeader title={title} onPress={handleViewAllDeFi} />
152-
<ErrorState
153-
title={strings('homepage.error.unable_to_load', {
154-
section: title.toLowerCase(),
155-
})}
156-
onRetry={refresh}
157-
/>
158-
</Box>
153+
<View
154+
ref={sectionViewRef}
155+
onLayout={onLayout}
156+
style={styles.sectionGap}
157+
>
158+
<SectionHeader
159+
title={title}
160+
onPress={handleViewAllDeFi}
161+
testID={WalletViewSelectorsIDs.HOMEPAGE_SECTION_TITLE('defi')}
162+
/>
163+
<ErrorState
164+
title={strings('homepage.error.unable_to_load', {
165+
section: title.toLowerCase(),
166+
})}
167+
onRetry={refresh}
168+
/>
159169
</View>
160170
);
161171
}
162172

163173
return (
164-
<View ref={sectionViewRef} onLayout={onLayout}>
165-
<Box gap={3}>
166-
<SectionHeader title={title} onPress={handleViewAllDeFi} />
167-
<SectionRow>
168-
<Box>
169-
{isLoading ? (
170-
<DeFiPositionsSkeleton />
171-
) : (
172-
positions.map((position: DeFiPositionEntry) => (
173-
<DeFiPositionsListItem
174-
key={`${position.chainId}-${position.protocolAggregate.protocolDetails.name}`}
175-
chainId={position.chainId}
176-
protocolId={position.protocolId}
177-
protocolAggregate={position.protocolAggregate}
178-
privacyMode={privacyMode}
179-
/>
180-
))
181-
)}
182-
</Box>
183-
</SectionRow>
184-
</Box>
174+
<View ref={sectionViewRef} onLayout={onLayout} style={styles.sectionGap}>
175+
<SectionHeader
176+
title={title}
177+
onPress={handleViewAllDeFi}
178+
testID={WalletViewSelectorsIDs.HOMEPAGE_SECTION_TITLE('defi')}
179+
/>
180+
<SectionRow>
181+
{isLoading ? (
182+
<DeFiPositionsSkeleton />
183+
) : (
184+
positions.map((position: DeFiPositionEntry) => (
185+
<DeFiPositionsListItem
186+
key={`${position.chainId}-${position.protocolAggregate.protocolDetails.name}`}
187+
chainId={position.chainId}
188+
protocolId={position.protocolId}
189+
protocolAggregate={position.protocolAggregate}
190+
privacyMode={privacyMode}
191+
/>
192+
))
193+
)}
194+
</SectionRow>
185195
</View>
186196
);
187197
},

app/components/Views/Homepage/Sections/NFTs/NFTsSection.tsx

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import React, {
66
useRef,
77
useState,
88
} from 'react';
9-
import { View } from 'react-native';
9+
import { StyleSheet, View } from 'react-native';
1010
import { useSelector } from 'react-redux';
1111
import { useFocusEffect, useNavigation } from '@react-navigation/native';
1212
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
@@ -29,13 +29,19 @@ import useHomeViewedEvent, {
2929
HomeSectionNames,
3030
} from '../../hooks/useHomeViewedEvent';
3131
import { useSectionPerformance } from '../../hooks/useSectionPerformance';
32+
import { WalletViewSelectorsIDs } from '../../../Wallet/WalletView.testIds';
3233
import { Nft } from '@metamask/assets-controllers';
3334
import { MetaMetricsEvents } from '../../../../../core/Analytics';
3435
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
3536

3637
const MAX_NFTS_DISPLAYED = 6;
3738
const NFTS_PER_ROW = 3;
3839

40+
const styles = StyleSheet.create({
41+
sectionGap: { gap: 12 },
42+
flex1: { flex: 1 },
43+
});
44+
3945
const NftSkeletonRow = () => {
4046
const { colors } = useTheme();
4147
const tw = useTailwind();
@@ -49,10 +55,8 @@ const NftSkeletonRow = () => {
4955
{Array.from({ length: NFTS_PER_ROW }, (_, index) => (
5056
<View key={index} style={tw.style('flex-1')}>
5157
<View style={tw.style('w-full aspect-square rounded-xl mb-3')} />
52-
<View>
53-
<View style={tw.style('h-4 rounded-lg mb-1 w-[60%]')} />
54-
<View style={tw.style('h-3.5 rounded-md w-full')} />
55-
</View>
58+
<View style={tw.style('h-4 rounded-lg mb-1 w-[60%]')} />
59+
<View style={tw.style('h-3.5 rounded-md w-full')} />
5660
</View>
5761
))}
5862
</View>
@@ -164,56 +168,56 @@ const NFTsSection = forwardRef<SectionRefreshHandle, NFTsSectionProps>(
164168
});
165169

166170
return (
167-
<View ref={sectionViewRef} onLayout={onLayout}>
168-
<Box gap={3}>
169-
<SectionHeader title={title} onPress={handleViewAllNfts} />
170-
{hasNfts ? (
171-
<SectionRow>
172-
<Box gap={3}>
173-
{nftRows.map((row, rowIndex) => (
174-
<Box
175-
key={`nft-row-${rowIndex}`}
176-
flexDirection={BoxFlexDirection.Row}
177-
gap={3}
171+
<View ref={sectionViewRef} onLayout={onLayout} style={styles.sectionGap}>
172+
<SectionHeader
173+
title={title}
174+
onPress={handleViewAllNfts}
175+
testID={WalletViewSelectorsIDs.HOMEPAGE_SECTION_TITLE('nfts')}
176+
/>
177+
{hasNfts ? (
178+
<SectionRow gap={3}>
179+
{nftRows.map((row, rowIndex) => (
180+
<Box
181+
key={`nft-row-${rowIndex}`}
182+
flexDirection={BoxFlexDirection.Row}
183+
gap={3}
184+
>
185+
{row.map((nft) => (
186+
<View
187+
key={`${nft.address}-${nft.tokenId}`}
188+
style={styles.flex1}
178189
>
179-
{row.map((nft) => (
190+
<NftGridItem
191+
item={nft}
192+
onLongPress={handleLongPress}
193+
source="mobile-nft-list"
194+
/>
195+
</View>
196+
))}
197+
{/* Add empty boxes to maintain grid alignment for incomplete rows */}
198+
{row.length < NFTS_PER_ROW &&
199+
Array.from({ length: NFTS_PER_ROW - row.length }).map(
200+
(__, i) => (
180201
<Box
181-
key={`${nft.address}-${nft.tokenId}`}
202+
key={`empty-${rowIndex}-${i}`}
182203
twClassName="flex-1"
183-
>
184-
<NftGridItem
185-
item={nft}
186-
onLongPress={handleLongPress}
187-
source="mobile-nft-list"
188-
/>
189-
</Box>
190-
))}
191-
{/* Add empty boxes to maintain grid alignment for incomplete rows */}
192-
{row.length < NFTS_PER_ROW &&
193-
Array.from({ length: NFTS_PER_ROW - row.length }).map(
194-
(__, i) => (
195-
<Box
196-
key={`empty-${rowIndex}-${i}`}
197-
twClassName="flex-1"
198-
/>
199-
),
200-
)}
201-
</Box>
202-
))}
204+
/>
205+
),
206+
)}
203207
</Box>
204-
</SectionRow>
205-
) : showSkeleton ? (
206-
<SectionRow>
207-
<NftSkeletonRow />
208-
</SectionRow>
209-
) : (
210-
<CollectiblesEmptyState
211-
onAction={handleImportNfts}
212-
actionButtonProps={{ isDisabled: !isAddNFTEnabled }}
213-
twClassName="mx-auto mt-2"
214-
/>
215-
)}
216-
</Box>
208+
))}
209+
</SectionRow>
210+
) : showSkeleton ? (
211+
<SectionRow>
212+
<NftSkeletonRow />
213+
</SectionRow>
214+
) : (
215+
<CollectiblesEmptyState
216+
onAction={handleImportNfts}
217+
actionButtonProps={{ isDisabled: !isAddNFTEnabled }}
218+
twClassName="mx-auto mt-2"
219+
/>
220+
)}
217221
<NftGridItemBottomSheet
218222
isVisible={longPressedNft !== null}
219223
onClose={() => setLongPressedNft(null)}

0 commit comments

Comments
 (0)