From 89ffac24f9ee6d702220591205ac585c49704fda Mon Sep 17 00:00:00 2001 From: Noah Waldner Date: Thu, 8 Oct 2026 10:24:35 +0200 Subject: [PATCH 1/3] respect targets in links --- .changeset/link-target-blank.md | 5 ++ .../src/components/Link/Link.stories.tsx | 14 ++++++ .../components/src/components/Link/Link.tsx | 17 ++++++- .../components/Link/__tests__/Link.spec.tsx | 47 ++++++++++++++++++- 4 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 .changeset/link-target-blank.md diff --git a/.changeset/link-target-blank.md b/.changeset/link-target-blank.md new file mode 100644 index 0000000000..25c0b4f448 --- /dev/null +++ b/.changeset/link-target-blank.md @@ -0,0 +1,5 @@ +--- +"@frontify/fondue-components": patch +--- + +fix(Link): respect `target="_blank"` and modifier clicks instead of always navigating through the router diff --git a/packages/components/src/components/Link/Link.stories.tsx b/packages/components/src/components/Link/Link.stories.tsx index 89f49d0bf1..f81d339be3 100644 --- a/packages/components/src/components/Link/Link.stories.tsx +++ b/packages/components/src/components/Link/Link.stories.tsx @@ -109,6 +109,20 @@ export const AllVariants: Story = { rel: 'noopener noreferrer', }, }; +export const External: Story = { + args: {}, + render: (args) => ( + + TEST + + ), +}; export const Truncated: Story = { args: { diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index 624211d880..fd08f16b42 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -116,6 +116,7 @@ export const Link = forwardRef( onContainer, onPress, disabled = false, + target, ...props }, ref, @@ -130,13 +131,24 @@ export const Link = forwardRef( return; } onPress?.(event); + if (event.defaultPrevented) { + return; + } - if (href) { + const useClientNavigation = !target || target === '_self'; + if ( + href && + useClientNavigation && + !event.metaKey && + !event.ctrlKey && + !event.shiftKey && + event.button === 0 + ) { event.preventDefault(); navigate(href); } }, - [href, navigate, onPress, disabled], + [href, navigate, onPress, disabled, target], ); const computedColor = disabled @@ -160,6 +172,7 @@ export const Link = forwardRef( .join(' ')} ref={ref} href={disabled ? undefined : resolvedHref} + target={target} onClick={handleClick} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : undefined} diff --git a/packages/components/src/components/Link/__tests__/Link.spec.tsx b/packages/components/src/components/Link/__tests__/Link.spec.tsx index 2714491610..87e8f57c71 100644 --- a/packages/components/src/components/Link/__tests__/Link.spec.tsx +++ b/packages/components/src/components/Link/__tests__/Link.spec.tsx @@ -1,7 +1,7 @@ /* (c) Copyright Frontify Ltd., all rights reserved. */ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; import { RouterProvider } from '../../RouterProvider/RouterProvider'; import { Link } from '../Link'; @@ -20,4 +20,47 @@ describe('Link Component', () => { const link = screen.getByText('Link Text'); expect(link).toHaveAttribute('href', '/resolved/foo'); }); + + it('should navigate client-side on a plain click', () => { + const navigate = vi.fn(); + + render( + path}> + Link Text + , + ); + + fireEvent.click(screen.getByText('Link Text')); + expect(navigate).toHaveBeenCalledWith('/foo'); + }); + + it('should let the browser handle links with target="_blank"', () => { + const navigate = vi.fn(); + + render( + path}> + + Link Text + + , + ); + + const link = screen.getByText('Link Text'); + expect(link).toHaveAttribute('target', '_blank'); + expect(fireEvent.click(link)).toBe(true); + expect(navigate).not.toHaveBeenCalled(); + }); + + it('should let the browser handle modifier clicks', () => { + const navigate = vi.fn(); + + render( + path}> + Link Text + , + ); + + expect(fireEvent.click(screen.getByText('Link Text'), { metaKey: true })).toBe(true); + expect(navigate).not.toHaveBeenCalled(); + }); }); From 76e9442c679ad0f2b74441b843f192a6a4897796 Mon Sep 17 00:00:00 2001 From: Noah Waldner Date: Thu, 8 Oct 2026 10:28:31 +0200 Subject: [PATCH 2/3] adjust story --- .../src/components/Link/Link.stories.tsx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/components/src/components/Link/Link.stories.tsx b/packages/components/src/components/Link/Link.stories.tsx index f81d339be3..45d2d19509 100644 --- a/packages/components/src/components/Link/Link.stories.tsx +++ b/packages/components/src/components/Link/Link.stories.tsx @@ -109,18 +109,20 @@ export const AllVariants: Story = { rel: 'noopener noreferrer', }, }; -export const External: Story = { - args: {}, - render: (args) => ( - - TEST - + +export const Targets: Story = { + render: () => ( + alert(`Navigate to: ${path}`)} useHref={(path: string) => path}> +
+ Default: navigates through the router + + target="_self": navigates through the router + + + target="_blank": opens a new tab + +
+
), }; From 82c03534ed683ff0ce606b264d0802b24bd3a74b Mon Sep 17 00:00:00 2001 From: Noah Waldner Date: Thu, 8 Oct 2026 10:33:10 +0200 Subject: [PATCH 3/3] rename --- packages/components/src/components/Link/Link.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/components/Link/Link.tsx b/packages/components/src/components/Link/Link.tsx index fd08f16b42..c4fb14414c 100644 --- a/packages/components/src/components/Link/Link.tsx +++ b/packages/components/src/components/Link/Link.tsx @@ -135,10 +135,10 @@ export const Link = forwardRef( return; } - const useClientNavigation = !target || target === '_self'; + const shouldUseClientSideNavigation = !target || target === '_self'; if ( href && - useClientNavigation && + shouldUseClientSideNavigation && !event.metaKey && !event.ctrlKey && !event.shiftKey &&