From d2ffa7a775f5c30d8608f7d71509607735846958 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 12 Aug 2026 21:50:50 +1000 Subject: [PATCH] PM-5852: Open support description links in new tabs What was broken Links rendered from a support ticket Description navigated the current Support tab instead of opening a new tab. Root cause The shared Support Markdown renderer used ReactMarkdown's default anchor output, which does not set a new-tab target. What was changed Added a scoped Markdown anchor renderer that preserves the link destination while applying target="_blank" and rel="noopener noreferrer". Updated the renderer documentation to describe the behavior. Any added/updated tests Added regression coverage for the bare GFM challenge URL shown in the ticket recording, including href, target, and rel assertions. All 9 Support test suites, lint, and the production build pass. The repository-wide suite still has unrelated failures reproduced on clean origin/dev. --- .../MarkdownContent/MarkdownContent.spec.tsx | 26 ++++++++++++++++++ .../MarkdownContent/MarkdownContent.tsx | 27 ++++++++++++++++--- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.spec.tsx b/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.spec.tsx index f47188cf3..79f398813 100644 --- a/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.spec.tsx +++ b/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.spec.tsx @@ -1,5 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */ import '@testing-library/jest-dom' +import type { ElementType } from 'react' import { readFileSync } from 'fs' import { render, screen } from '@testing-library/react' import remarkBreaks from 'remark-breaks' @@ -9,6 +10,9 @@ import { MarkdownContent } from './MarkdownContent' interface MarkdownRendererProps { children: string + components: { + a: ElementType + } remarkPlugins: unknown[] skipHtml: boolean } @@ -65,6 +69,28 @@ describe('MarkdownContent', () => { })) }) + it('opens GFM links in a safe new tab', () => { + const markdown = 'https://www.topcoder-dev.com/challenges' + + render() + const markdownProps = mockReactMarkdown.mock.calls[0][0] as MarkdownRendererProps + const MarkdownLink = markdownProps.components.a + + render( + + {markdown} + , + ) + const link = screen.getByRole('link', { name: markdown }) + + expect(link) + .toHaveAttribute('href', markdown) + expect(link) + .toHaveAttribute('target', '_blank') + expect(link) + .toHaveAttribute('rel', 'noopener noreferrer') + }) + it('keeps Markdown formatting visible after the platform style reset', () => { expect(markdownStyles) .toMatch(/a,[\s\S]*a:hover \{[\s\S]*color: \$link-blue-dark;[\s\S]*text-decoration: underline;/) diff --git a/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.tsx b/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.tsx index 8a0d1fb31..ce0c367d7 100644 --- a/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.tsx +++ b/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.tsx @@ -1,6 +1,6 @@ /** Safe Markdown renderer for user-authored support content. */ -import { FC } from 'react' -import ReactMarkdown from 'react-markdown' +import type { FC } from 'react' +import ReactMarkdown, { type Components } from 'react-markdown' import remarkBreaks from 'remark-breaks' import remarkGfm from 'remark-gfm' @@ -10,8 +10,28 @@ export interface MarkdownContentProps { markdown: string } +const markdownComponents: Components = { + /** + * Renders a Markdown link in a new tab without exposing the opener page. + * + * @param props anchor attributes and AST metadata produced by ReactMarkdown. + * @returns a safely targeted anchor used for links in support conversations. + * @throws Does not throw. + */ + a: props => ( + + {props.children} + + ), +} + /** - * Renders GFM and line breaks while dropping raw HTML. + * Renders GFM and line breaks with links opening in new tabs while dropping raw HTML. * * @param props untrusted Markdown source. * @returns safely rendered Markdown. @@ -20,6 +40,7 @@ export interface MarkdownContentProps { export const MarkdownContent: FC = props => (