diff --git a/.changeset/fix-table-row-external-href.md b/.changeset/fix-table-row-external-href.md new file mode 100644 index 0000000000..c3e5b1f494 --- /dev/null +++ b/.changeset/fix-table-row-external-href.md @@ -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 diff --git a/packages/ui/src/components/Table/components/Row.tsx b/packages/ui/src/components/Table/components/Row.tsx index 3da46b3eaa..34b38c784e 100644 --- a/packages/ui/src/components/Table/components/Row.tsx +++ b/packages/ui/src/components/Table/components/Row.tsx @@ -36,9 +36,26 @@ export function Row(props: RowProps) { 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(props: RowProps) { {content}