diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 12375b5e909c..1a5c4e22c9a7 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -640,7 +640,7 @@ }, "ui/pages/settings/settings.tsx": { "react-hooks/preserve-manual-memoization": { - "count": 3 + "count": 1 } }, "ui/pages/shield/transaction-shield/shield-banner-animation.tsx": { @@ -653,4 +653,4 @@ "count": 1 } } -} +} \ No newline at end of file diff --git a/ui/pages/settings/settings.test.tsx b/ui/pages/settings/settings.test.tsx index 24957505d7bc..d18f3433b089 100644 --- a/ui/pages/settings/settings.test.tsx +++ b/ui/pages/settings/settings.test.tsx @@ -29,6 +29,10 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate, })); +jest.mock('../../hooks/useInAppBack', () => ({ + useInAppBack: () => () => mockNavigate(-1), +})); + jest.mock('../../../shared/lib/environment-type', () => ({ ...jest.requireActual('../../../shared/lib/environment-type'), getEnvironmentType: () => mockGetEnvironmentType(), @@ -148,7 +152,7 @@ describe('Settings', () => { }); }); - it('navigates to home with the global menu drawer open when back is clicked at settings root', async () => { + it('navigates back when back is clicked at settings root', async () => { renderSettings(mockStore); const backButton = await screen.findByTestId('page-header-back-button'); @@ -156,13 +160,11 @@ describe('Settings', () => { fireEvent.click(backButton); await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith( - `${DEFAULT_ROUTE}?drawerOpen=true`, - ); + expect(mockNavigate).toHaveBeenCalledWith(-1); }); }); - it('navigates to home with the drawer open when back is clicked at settings root regardless of settings URL query', async () => { + it('navigates back when back is clicked at settings root regardless of settings URL query', async () => { mockPathname = `${SETTINGS_ROUTE}?drawerOpen=true`; renderSettings(mockStore); @@ -171,9 +173,7 @@ describe('Settings', () => { fireEvent.click(backButton); await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith( - `${DEFAULT_ROUTE}?drawerOpen=true`, - ); + expect(mockNavigate).toHaveBeenCalledWith(-1); }); }); @@ -186,9 +186,7 @@ describe('Settings', () => { fireEvent.click(backButton); await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith( - PREFERENCES_AND_DISPLAY_ROUTE, - ); + expect(mockNavigate).toHaveBeenCalledWith(-1); }); }); @@ -201,7 +199,7 @@ describe('Settings', () => { fireEvent.click(backButton); await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith(NOTIFICATIONS_SETTINGS_ROUTE); + expect(mockNavigate).toHaveBeenCalledWith(-1); }); }); }); diff --git a/ui/pages/settings/settings.tsx b/ui/pages/settings/settings.tsx index 298d82ed9b4d..b9bbbbd0743a 100644 --- a/ui/pages/settings/settings.tsx +++ b/ui/pages/settings/settings.tsx @@ -30,6 +30,7 @@ import { } from '@metamask/design-system-react'; import classnames from 'clsx'; import { useSelector } from 'react-redux'; +import { useInAppBack } from '#ui/hooks/useInAppBack'; import { DEFAULT_ROUTE, SETTINGS_ROUTE, @@ -146,9 +147,7 @@ const SettingsLayout = ({ children }: { children: React.ReactNode }) => { environmentType === ENVIRONMENT_TYPE_POPUP || isCompactSidepanel; const isOnSettingsRoot = normalizedPathname === SETTINGS_ROUTE; const showRootLandingPage = isOnSettingsRoot && usesCompactSettingsLayout; - const backRoute = isOnSettingsRoot - ? `${DEFAULT_ROUTE}?drawerOpen=true` - : (meta?.parentPath ?? SETTINGS_ROUTE); + const navigateBack = useInAppBack(DEFAULT_ROUTE); const [isSearchOpen, setIsSearchOpen] = useState(false); const [searchValue, setSearchValue] = useState(''); @@ -222,12 +221,12 @@ const SettingsLayout = ({ children }: { children: React.ReactNode }) => { const handleClose = useCallback(() => { if (isOnSettingsRoot) { - runCloseTransition(() => navigate(backRoute)); + runCloseTransition(navigateBack); return; } - transitionBack(() => navigate(backRoute)); - }, [backRoute, isOnSettingsRoot, navigate, runCloseTransition]); + transitionBack(navigateBack); + }, [isOnSettingsRoot, navigateBack, runCloseTransition]); // Header: "Settings" on fullscreen; tab or sub-page name on popup/sidepanel const headerTitle = diff --git a/ui/pages/shield/transaction-shield/claims-list/claims-list.tsx b/ui/pages/shield/transaction-shield/claims-list/claims-list.tsx index 3723457a2b2b..7c0b831bc01f 100644 --- a/ui/pages/shield/transaction-shield/claims-list/claims-list.tsx +++ b/ui/pages/shield/transaction-shield/claims-list/claims-list.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback } from 'react'; import { Box, BoxBackgroundColor, @@ -39,24 +39,17 @@ const ClaimsList = () => { useClaims(); const { drafts } = useClaimDraft(); - const [activeTab, setActiveTab] = useState(() => { - const tabParam = searchParams.get('tab'); - if ( - tabParam === CLAIMS_TAB_KEYS.PENDING || - tabParam === CLAIMS_TAB_KEYS.HISTORY - ) { - return tabParam; - } - return CLAIMS_TAB_KEYS.PENDING; - }); + // The URL is the source of truth for the active tab so that history + // navigation (e.g. Back from a claim) restores the tab the user was on. + const activeTab = + searchParams.get('tab') === CLAIMS_TAB_KEYS.HISTORY + ? CLAIMS_TAB_KEYS.HISTORY + : CLAIMS_TAB_KEYS.PENDING; - // Clear the tab param from the URL after reading it - useEffect(() => { - if (searchParams.has('tab')) { - searchParams.delete('tab'); - setSearchParams(searchParams, { replace: true }); - } - }, [searchParams, setSearchParams]); + const handleTabClick = useCallback( + (tabKey: string) => setSearchParams({ tab: tabKey }, { replace: true }), + [setSearchParams], + ); const claimItem = useCallback( (claimData: Claim | ClaimDraft, tabKey: ClaimsTabKey, isDraft = false) => { @@ -234,7 +227,7 @@ const ClaimsList = () => {