fix(router): respect target attribute on inner component#5115
Conversation
Signed-off-by: leesb971204 <leesb971204@gmail.com>
Signed-off-by: leesb971204 <leesb971204@gmail.com>
WalkthroughUpdates modify Link click interception in React and Solid routers to compute an effective target from either the prop or the rendered anchor’s target attribute. Clicks with effective target not set or set to "_self" are intercepted; others (e.g., "_blank") follow browser default. Tests added to validate custom createLink components. No API changes. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant L as Link Component
participant A as <a> Element
participant R as Router
participant B as Browser
U->>L: Click
L->>A: Read DOM target (elementTarget)
L->>L: effectiveTarget = propTarget ?? elementTarget
alt Intercept (effectiveTarget absent or "_self")
L->>U: preventDefault()
L->>R: navigate(to)
R-->>L: navigation complete
else Let browser handle (e.g., "_blank")
L-->>U: no preventDefault
L->>B: default navigation
Note over B: Opens per target (e.g., new tab)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
Comment |
|
View your CI Pipeline Execution ↗ for commit abf2149
☁️ Nx Cloud last updated this comment at |
| !disabled && | ||
| !isCtrlEvent(e) && | ||
| !e.defaultPrevented && | ||
| (!target || target === '_self') && |
There was a problem hiding this comment.
If the target prop is undefined, the condition treats it as _self, causing the router to intercept the click and prevent target="_blank" links from opening in a new tab.
| // Check actual element's target attribute as fallback | ||
| const elementTarget = (e.currentTarget as HTMLAnchorElement).target | ||
| const effectiveTarget = target !== undefined ? target : elementTarget | ||
|
|
There was a problem hiding this comment.
If no target prop is provided, check the rendered element’s target attribute to ensure correct behavior.
More templates
@tanstack/arktype-adapter
@tanstack/directive-functions-plugin
@tanstack/eslint-plugin-router
@tanstack/history
@tanstack/react-router
@tanstack/react-router-devtools
@tanstack/react-router-ssr-query
@tanstack/react-start
@tanstack/react-start-client
@tanstack/react-start-plugin
@tanstack/react-start-server
@tanstack/router-cli
@tanstack/router-core
@tanstack/router-devtools
@tanstack/router-devtools-core
@tanstack/router-generator
@tanstack/router-plugin
@tanstack/router-ssr-query-core
@tanstack/router-utils
@tanstack/router-vite-plugin
@tanstack/server-functions-plugin
@tanstack/solid-router
@tanstack/solid-router-devtools
@tanstack/solid-start
@tanstack/solid-start-client
@tanstack/solid-start-plugin
@tanstack/solid-start-server
@tanstack/start-client-core
@tanstack/start-plugin-core
@tanstack/start-server-core
@tanstack/start-server-functions-client
@tanstack/start-server-functions-fetcher
@tanstack/start-server-functions-server
@tanstack/start-storage-context
@tanstack/valibot-adapter
@tanstack/virtual-file-routes
@tanstack/zod-adapter
commit: |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
packages/react-router/src/link.tsx (1)
221-224: Make target handling robust (case-insensitive + generic Element) and don’t rely on anchor-only property.Reading
.targetassumes an anchor and is case-sensitive for_self. UsinggetAttribute('target')and normalizing improves resilience (e.g.,_SELF) and works with any element rendered viacreateLink.Apply within this hunk:
- // Check actual element's target attribute as fallback - const elementTarget = (e.currentTarget as HTMLAnchorElement).target - const effectiveTarget = target !== undefined ? target : elementTarget + // Check actual element's target attribute as fallback + const rawTarget = + (e.currentTarget as Element).getAttribute?.('target') ?? undefined + const effectiveTarget = target !== undefined ? target : rawTarget + const keywordTarget = + typeof effectiveTarget === 'string' + ? effectiveTarget.toLowerCase() + : undefined @@ - (!effectiveTarget || effectiveTarget === '_self') && + (!keywordTarget || keywordTarget === '_self') &&Additionally, to avoid clobbering a child component’s default
targetwhen the prop is undefined, consider returningtargetconditionally in the props spread later in this file:// ...inside the return object ...(target !== undefined && { target }),Also applies to: 229-229
packages/react-router/tests/link.test.tsx (1)
4686-4746: Remove unused window.open mock or assert on it.The test doesn’t assert
openMockcalls, so the manual override adds noise and potential flakiness. Either drop it or usevi.spyOn(window, 'open')and assert.Minimal cleanup within this hunk:
- const originalOpen = window.open - const openMock = vi.fn() - window.open = openMock @@ - window.open = originalOpenIf you prefer to assert:
+ const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null as any) @@ + await waitFor(() => expect(openSpy).toHaveBeenCalled()) + openSpy.mockRestore()
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/react-router/src/link.tsx(1 hunks)packages/react-router/tests/link.test.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/react-router/tests/link.test.tsx (2)
packages/react-router/src/index.tsx (6)
createLink(151-151)createRootRoute(262-262)createRoute(259-259)createRouter(277-277)createMemoryHistory(121-121)RouterProvider(287-287)packages/react-router/src/router.ts (1)
createRouter(80-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Test
- GitHub Check: Preview
🔇 Additional comments (1)
packages/react-router/tests/link.test.tsx (1)
4748-4796: LGTM: override semantics validated.This cleanly proves
targetprop overrides a custom component’s default, and the router intercepts navigation when_selfis effective.
|
other than making use of test-ids for the tests this looks good to me |
Signed-off-by: leesb971204 <leesb971204@gmail.com>
Thank you for the feedback. |
|
one last thing please apply the same tests and update to solid-router |
Signed-off-by: leesb971204 <leesb971204@gmail.com>
Signed-off-by: leesb971204 <leesb971204@gmail.com>
target attribute on inner componenttarget attribute on inner component
I’ve added the related logic and tests to solid-router. |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
packages/solid-router/src/link.tsx (1)
419-419: Style gating likely never appliesObject.keys is called on the accessor function, not its value, so the style prop is never added. Prefer checking keys of resolvedStyle() while still passing the accessor.
Apply this diff:
- ...(Object.keys(resolvedStyle).length && { style: resolvedStyle }), + ...(Object.keys(resolvedStyle()).length && { style: resolvedStyle }),packages/solid-router/tests/link.test.tsx (1)
4249-4307: Trim unused window.open stubThe test doesn’t assert on window.open; the stub can be removed to reduce globals churn.
- const originalOpen = window.open - const openMock = vi.fn() - window.open = openMock ... - window.open = originalOpen
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/solid-router/src/link.tsx(1 hunks)packages/solid-router/tests/link.test.tsx(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Preview
- GitHub Check: Test
🔇 Additional comments (2)
packages/solid-router/src/link.tsx (1)
267-271: Target fallback logic LGTMDeriving effectiveTarget from either prop or element attribute and gating interception on it fixes the custom link/new‑tab case.
Also applies to: 276-276
packages/solid-router/tests/link.test.tsx (1)
4310-4360: Override behavior test LGTMConfirms that an explicit target prop overrides a component’s default.
fixes #5011
Summary by CodeRabbit