Skip to content

fix: Reset lastClicked in NavLink when route matches#3796

Open
doubleface wants to merge 2 commits intomasterfrom
fix/highlight-proper-section-in-sidebar-even-with-back-button
Open

fix: Reset lastClicked in NavLink when route matches#3796
doubleface wants to merge 2 commits intomasterfrom
fix/highlight-proper-section-in-sidebar-even-with-back-button

Conversation

@doubleface
Copy link
Copy Markdown
Contributor

@doubleface doubleface commented Apr 9, 2026

Keeps the optimistic highlight in the sidebar but also works when using the browser back button.

Also added unit tests for these cases

fixes #3793

Summary by CodeRabbit

  • Bug Fixes

    • Improved navigation link active-state logic to support an optimistic "pending click" state and reliably clear that pending state when navigation completes or when the route changes (including back/alternate navigation), ensuring accurate active indication.
  • Tests

    • Added a comprehensive test suite covering click handling, optimistic active-state behavior, and reset logic across route changes.

Also added unit tests for these cases
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c1cc48a-45c5-416e-845c-1ddc7422ed09

📥 Commits

Reviewing files that changed from the base of the PR and between bc57fde and d672a3a.

📒 Files selected for processing (2)
  • src/modules/navigation/NavLink.jsx
  • src/modules/navigation/NavLink.spec.jsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/modules/navigation/NavLink.jsx
  • src/modules/navigation/NavLink.spec.jsx

Walkthrough

The PR updates NavLink to use React hooks (useEffect, useRef) to track lastClicked and previous location.pathname, adding an effect that clears the pending click override when navigation reaches the clicked target or when the route changes away from the prior pathname. The change adjusts how pathname and isActive are computed to account for a pending click. A new Jest/RTL test file exercises click handling, optimistic active state, and the effect-driven reset across route-change scenarios.

Suggested reviewers

  • rezk2ll
  • zatteo
  • lethemanh
  • JF-Cozy
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: resetting lastClicked in NavLink when the route matches, which directly addresses the core issue of sidebar highlighting.
Linked Issues check ✅ Passed The PR fully addresses issue #3793 by implementing logic to reset lastClicked on route changes, fixing the sidebar highlighting behavior when using browser back button.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the NavLink component behavior and its tests, with no out-of-scope modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/highlight-proper-section-in-sidebar-even-with-back-button

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@bundlemon
Copy link
Copy Markdown

bundlemon bot commented Apr 9, 2026

BundleMon

Files updated (1)
Status Path Size Limits
static/js/main.(hash).js
168.23KB (+69B +0.04%) -
Unchanged files (20)
Status Path Size Limits
static/js/(chunkId).(hash).js
1.17MB -
public/static/js/(chunkId).(hash).js
1.1MB -
static/js/cozy.(hash).js
919.79KB -
public/static/js/cozy.(hash).js
782.19KB -
(hash).js
336.11KB -
public/(hash).js
336.11KB -
services/qualificationMigration.js
283.13KB -
services/dacc.js
262.86KB -
public/static/js/public.(hash).js
141.81KB -
public/static/js/lib-react.(hash).js
43.88KB -
static/js/lib-react.(hash).js
43.88KB -
public/static/css/cozy.(hash).css
31.56KB -
static/css/cozy.(hash).css
31.56KB -
public/static/js/lib-router.(hash).js
21.86KB -
static/js/lib-router.(hash).js
21.86KB -
static/css/main.(hash).css
17.04KB -
public/static/css/public.(hash).css
6.76KB -
manifest.webapp
3.04KB -
index.html
702B -
assets/manifest.json
185B -

Total files change +71B 0%

Groups updated (1)
Status Path Size Limits
**/*.js
7.61MB (+69B 0%) -
Unchanged groups (2)
Status Path Size Limits
**/*.{png,svg,ico}
2.19MB -
**/*.css
120.14KB -

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@Crash--
Copy link
Copy Markdown
Contributor

Crash-- commented Apr 9, 2026

@coderabbitai Does it fix also this use case? #3793 (comment)

I can't see a UT for this one

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me fetch the referenced issue comment and the test file to check coverage.


🏁 Script executed:

#!/bin/bash
# Fetch the specific issue comment
gh api repos/linagora/twake-drive/issues/comments/4211318636 --jq '.body'

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Read the NavLink spec file
cat src/modules/navigation/NavLink.spec.jsx

Length of output: 4726


The referenced comment is a video recording. Let me fetch the full issue thread to understand the specific scenario it demonstrates.


🏁 Script executed:

#!/bin/bash
# Get all comments on issue 3793
gh api repos/linagora/twake-drive/issues/3793/comments --jq '.[] | "---\nID: \(.id)\nAuthor: \(.user.login)\nBody: \(.body)\n"'

Length of output: 384


🏁 Script executed:

#!/bin/bash
# Also read the NavLink implementation
cat src/modules/navigation/NavLink.jsx

Length of output: 1359


@Crash-- Based on the code, the referenced scenario is not fully covered by this PR.

Here's the gap:

The useEffect in NavLink.jsx only resets lastClicked when the router arrives at the clicked destination:

useEffect(() => {
  if (!lastClicked) return
  if (navLinkMatch(rx, lastClicked, location.pathname)) {
    setLastClicked(null)  // ✅ route reached the destination → clear
  }
  // ❌ Missing: what if the route goes somewhere ELSE (e.g., back button)?
}, [location.pathname, rx, lastClicked, setLastClicked])

The uncovered scenario (likely what the video demonstrates):

  1. User is on / (Fichiers is active)
  2. User clicks "Partages" → lastClicked = '/shares' (optimistic highlight kicks in)
  3. User presses Back before the route settles (or the router navigates to a third section)
  4. Route returns to / (or goes elsewhere), but navLinkMatch(rx_shares, '/shares', '/') is falsesetLastClicked(null) is never called
  5. Sidebar is permanently stuck showing "Partages" as active even though the router is at /

The existing test even encodes this broken behavior as a success:

it('does not reset lastClicked when route changes to a different destination', async () => {
  // Route changed to /trash, not /folder → expects setLastClicked NOT called ✅ (but this is the bug)
  expect(setLastClicked).not.toHaveBeenCalledWith(null)
})

To fully fix this, the effect should also reset lastClicked whenever the route changes to a path that does not match the clicked destination:

useEffect(() => {
  if (!lastClicked) return
  if (navLinkMatch(rx, lastClicked, location.pathname)) {
    setLastClicked(null) // route arrived at destination
  } else if (!navLinkMatch(rx, to, location.pathname)) {
    setLastClicked(null) // route went somewhere else (e.g., back button)
  }
}, [location.pathname, rx, lastClicked, to, setLastClicked])

And a corresponding unit test would be:

it('resets lastClicked when route changes away from the clicked destination (e.g. back button)', async () => {
  const setLastClicked = jest.fn()
  useLocation.mockReturnValue({ pathname: '/recent' })
  const rx = /\/(folder|nextcloud)(\/.*)?/

  const { rerender } = render(
    <NavLink to="/folder" rx={rx} clickState={['/folder', setLastClicked]}>
      Drive
    </NavLink>
  )

  // Back button takes us somewhere that is NOT /folder
  useLocation.mockReturnValue({ pathname: '/recent' })
  await act(async () => {
    rerender(
      <NavLink to="/folder" rx={rx} clickState={['/folder', setLastClicked]}>
        Drive
      </NavLink>
    )
  })

  expect(setLastClicked).toHaveBeenCalledWith(null)
})

Could you confirm this matches what the video shows, @Crash--? And @doubleface — does the above approach cover this case correctly?


🧠 Learnings used
Learnt from: doubleface
Repo: linagora/twake-drive PR: 3778
File: src/modules/selection/RectangularSelection.jsx:165-173
Timestamp: 2026-04-01T10:01:13.983Z
Learning: In `src/modules/selection/RectangularSelection.jsx` (linagora/twake-drive, PR `#3778`), `wheelScrolledDuringDragRef.current` is intentionally kept `true` for the entire drag after the first wheel scroll and is only reset in `handleDragEnd`. Resetting it one-shot (immediately after `accumulateSelectedItemsDuringDrag` consumes it) reintroduces the original bug: after a wheel scroll, virtuoso does NOT remove boundary DOM elements synchronously, so the very next `onSelect` still sees those elements in `visibleFileIds`. With `preserveAll` already `false`, those items get dropped from the accumulator even though they have scrolled out of the visible area. Items virtuoso has already evicted survive through the `!visibleFileIds.has(fileId)` branch independently, but items still physically in the DOM do not. The `preserveAll` flag must remain `true` until drag end to cover this async cleanup window.

@doubleface
Copy link
Copy Markdown
Contributor Author

To fully fix this, the effect should also reset lastClicked whenever the route changes to a path that does not match the clicked destination:

useEffect(() => {
  if (!lastClicked) return
  if (navLinkMatch(rx, lastClicked, location.pathname)) {
    setLastClicked(null) // route arrived at destination
  } else if (!navLinkMatch(rx, to, location.pathname)) {
    setLastClicked(null) // route went somewhere else (e.g., back button)
  }
}, [location.pathname, rx, lastClicked, to, setLastClicked])

Good catch on the missing case. However the proposed fix has a subtle flaw: navLinkMatch ignores the to argument entirely when rx is provided (it just runs rx.test(pathname)), so the else if (!navLinkMatch(rx, to, location.pathname)) branch is always the logical complement of the first — meaning lastClicked would be cleared immediately on click, before any route change happens, defeating the whole optimistic-highlight purpose.

The fix I went with instead tracks the previous pathname in a useRef. On each effect run we compare the new location.pathname to the one from the last run/

This way:

  • Click fireslastClicked changes but pathname is still the same as prevPathnameRef.current → nothing happens, we wait for navigation
  • Route reaches destinationpathname !== prevPathname, first condition matches → clear
  • Back button / unexpected navigationpathname !== prevPathname, second condition matches → clear

Also updated the existing test that was encoding the bug (not.toHaveBeenCalledWith(null)) to assert the correct behaviour. All 8 tests pass.

Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates Failed
New code is healthy (1 new file with code health below 10.00)

Gates Passed
2 Quality Gates Passed

See analysis details in CodeScene

Reason for failure
New code is healthy Violations Code Health Impact
NavLink.spec.jsx 1 rule 9.39 Suppress

Quality Gate Profile: The Bare Minimum
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

Comment on lines +84 to +109
it('resets lastClicked once the route matches rx', async () => {
const setLastClicked = jest.fn()
useLocation.mockReturnValue({ pathname: '/recent' })
const rx = /\/(folder|nextcloud)(\/.*)?/

const { rerender } = render(
<NavLink to="/folder" rx={rx} clickState={['/folder', setLastClicked]}>
Drive
</NavLink>
)

useLocation.mockReturnValue({ pathname: '/folder' })
await act(async () => {
rerender(
<NavLink
to="/folder"
rx={rx}
clickState={['/folder', setLastClicked]}
>
Drive
</NavLink>
)
})

expect(setLastClicked).toHaveBeenCalledWith(null)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Code Duplication
The module contains 3 functions with similar structure: 'resets lastClicked once the route matches rx','resets lastClicked once the route matches without rx','resets lastClicked when route changes to a different destination (e.g. back button)'

Suppress

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sidebar: wrong selected section

2 participants