fix(ui): open external Table row hrefs in a new tab

Automatically apply `target="_blank"` and `rel="noopener noreferrer"` on
`Row` when `href` is an external link, so external links in default BUI
Table rows open in a new tab instead of navigating the current page.

`rel` tokens are merged rather than replaced, so consumer-provided tokens
(e.g. `rel="nofollow"`) are preserved while `noopener noreferrer` is
always enforced whenever `target="_blank"` is in effect.

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-03-14 10:00:41 +00:00
parent 6f01c33a0e
commit 612c217c1f
2 changed files with 28 additions and 2 deletions
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Fixed `Table` rows with external `href` values to open in a new tab by automatically applying `target="_blank"` and `rel="noopener noreferrer"`.
**Affected components**: Table
@@ -36,9 +36,26 @@ export function Row<T extends object>(props: RowProps<T>) {
props,
);
const { classes, columns, children, href } = ownProps;
const hasInternalHref = !!href && !isExternalLink(href);
const isExternal = isExternalLink(href);
const hasInternalHref = !!href && !isExternal;
const hasExternalHref = !!href && isExternal;
const hasInteraction = !!restProps.onAction || !!href;
// Derive the effective target, defaulting to _blank for external links.
const effectiveTarget = hasExternalHref ? '_blank' : restProps.target;
// Always include noopener noreferrer when target=_blank, merging any
// consumer-provided rel tokens to avoid reverse-tabnabbing risk.
const effectiveRel =
effectiveTarget === '_blank'
? [
...new Set([
'noopener',
'noreferrer',
...(restProps.rel?.split(/\s+/).filter(Boolean) ?? []),
]),
].join(' ')
: restProps.rel;
const handlePress = hasInteraction
? () => {
restProps.onAction?.();
@@ -71,9 +88,11 @@ export function Row<T extends object>(props: RowProps<T>) {
<InternalLinkProvider href={href}>
<ReactAriaRow
href={href}
{...restProps}
target={effectiveTarget}
rel={effectiveRel}
className={classes.root}
data-react-aria-pressable={hasInternalHref ? 'true' : undefined}
{...restProps}
onAction={handlePress}
>
{content}