diff --git a/src/modules/navigation/NavLink.jsx b/src/modules/navigation/NavLink.jsx
index cddcf792b9..a95b20bf86 100644
--- a/src/modules/navigation/NavLink.jsx
+++ b/src/modules/navigation/NavLink.jsx
@@ -1,6 +1,6 @@
import cx from 'classnames'
import PropTypes from 'prop-types'
-import React from 'react'
+import React, { useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import { NavLink as UINavLink } from 'cozy-ui/transpiled/react/Nav'
@@ -19,6 +19,21 @@ const NavLink = ({
clickState: [lastClicked, setLastClicked]
}) => {
const location = useLocation()
+ const prevPathnameRef = useRef(location.pathname)
+
+ useEffect(() => {
+ const prevPathname = prevPathnameRef.current
+ prevPathnameRef.current = location.pathname
+
+ if (!lastClicked) return
+
+ if (navLinkMatch(rx, lastClicked, location.pathname)) {
+ setLastClicked(null) // route arrived at destination
+ } else if (location.pathname !== prevPathname) {
+ setLastClicked(null) // route changed but went elsewhere (e.g. back button)
+ }
+ }, [location.pathname, rx, lastClicked, setLastClicked])
+
const pathname = lastClicked ? lastClicked : location.pathname
const isActive = navLinkMatch(rx, to, pathname)
return (
diff --git a/src/modules/navigation/NavLink.spec.jsx b/src/modules/navigation/NavLink.spec.jsx
new file mode 100644
index 0000000000..38035e6327
--- /dev/null
+++ b/src/modules/navigation/NavLink.spec.jsx
@@ -0,0 +1,161 @@
+import { render, fireEvent, act } from '@testing-library/react'
+import React from 'react'
+import { useLocation } from 'react-router-dom'
+
+import { NavLink } from './NavLink'
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useLocation: jest.fn()
+}))
+
+jest.mock('cozy-ui/transpiled/react/Nav', () => ({
+ NavLink: { className: 'nav-link', activeClassName: 'active' }
+}))
+
+describe('NavLink', () => {
+ beforeEach(() => {
+ useLocation.mockReturnValue({ pathname: '/recent' })
+ })
+
+ describe('click handler', () => {
+ it('calls setLastClicked with to on click', () => {
+ const setLastClicked = jest.fn()
+ const { getByRole } = render(
+
+ Drive
+
+ )
+ fireEvent.click(getByRole('link'))
+ expect(setLastClicked).toHaveBeenCalledWith('/folder')
+ })
+
+ it('prevents default when no to prop', () => {
+ const setLastClicked = jest.fn()
+ const { getByRole } = render(
+ Drive
+ )
+ const event = new MouseEvent('click', { bubbles: true, cancelable: true })
+ getByRole('link').dispatchEvent(event)
+ expect(event.defaultPrevented).toBe(true)
+ })
+ })
+
+ describe('optimistic active state', () => {
+ it('shows as active when lastClicked matches rx', () => {
+ const rx = /\/(folder|nextcloud)(\/.*)?/
+ const { getByRole } = render(
+
+ Drive
+
+ )
+ expect(getByRole('link').className).toContain('active')
+ })
+
+ it('is not active when lastClicked does not match rx', () => {
+ const rx = /\/(folder|nextcloud)(\/.*)?/
+ const { getByRole } = render(
+
+ Drive
+
+ )
+ expect(getByRole('link').className).not.toContain('active')
+ })
+ })
+
+ describe('useEffect: reset lastClicked when route arrives', () => {
+ it('does not reset lastClicked while the route has not yet changed', () => {
+ const setLastClicked = jest.fn()
+ useLocation.mockReturnValue({ pathname: '/recent' })
+
+ render(
+
+ Drive
+
+ )
+
+ expect(setLastClicked).not.toHaveBeenCalledWith(null)
+ })
+
+ it('resets lastClicked once the route matches rx', async () => {
+ const setLastClicked = jest.fn()
+ useLocation.mockReturnValue({ pathname: '/recent' })
+ const rx = /\/(folder|nextcloud)(\/.*)?/
+
+ const { rerender } = render(
+
+ Drive
+
+ )
+
+ useLocation.mockReturnValue({ pathname: '/folder' })
+ await act(async () => {
+ rerender(
+
+ Drive
+
+ )
+ })
+
+ expect(setLastClicked).toHaveBeenCalledWith(null)
+ })
+
+ it('resets lastClicked once the route matches without rx', async () => {
+ const setLastClicked = jest.fn()
+ useLocation.mockReturnValue({ pathname: '/recent' })
+
+ const { rerender } = render(
+
+ Drive
+
+ )
+
+ useLocation.mockReturnValue({ pathname: '/folder' })
+ await act(async () => {
+ rerender(
+
+ Drive
+
+ )
+ })
+
+ expect(setLastClicked).toHaveBeenCalledWith(null)
+ })
+
+ it('resets lastClicked when route changes to a different destination (e.g. back button)', async () => {
+ const setLastClicked = jest.fn()
+ useLocation.mockReturnValue({ pathname: '/recent' })
+ const rx = /\/(folder|nextcloud)(\/.*)?/
+
+ const { rerender } = render(
+
+ Drive
+
+ )
+
+ // Route changed but to /trash, not /folder (e.g. back button or unexpected navigation)
+ useLocation.mockReturnValue({ pathname: '/trash' })
+ await act(async () => {
+ rerender(
+
+ Drive
+
+ )
+ })
+
+ expect(setLastClicked).toHaveBeenCalledWith(null)
+ })
+ })
+})