Skip to content

Commit

Permalink
feat: website link component defaults target to _blank when the href …
Browse files Browse the repository at this point in the history
…is external (#2038)

Motivation:
* CIC - this was at top of CIC backlog for the repo
* #2006

How?
* made it so the go-to website `<Link />` component will enforce the
logic @jchris requested

Co-authored-by: Yusef Napora <yusef@napora.org>
  • Loading branch information
gobengo and yusefnapora committed Oct 17, 2022
1 parent 6d40979 commit 947e764
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 5 deletions.
2 changes: 1 addition & 1 deletion packages/website/components/breadcrumbs/breadcrumbs.js
Expand Up @@ -10,7 +10,7 @@ import Link from '../link/link';
* @param {Object} props
* @param {String} props.variant
* @param {Object} [props.items]
* @param {Function} props.click
* @param {React.MouseEventHandler<HTMLAnchorElement>} props.click
*/
export default function Breadcrumbs({ variant, click, items }) {
return (
Expand Down
20 changes: 17 additions & 3 deletions packages/website/components/footer/footer.js
Expand Up @@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import clsx from 'clsx';

import { trackCustomLinkClick, events } from '../../lib/countly';
import Link from '../link/link';
import Link, { useIsExternalHref } from '../link/link';
import SiteLogo from '../../assets/icons/w3storage-logo.js';
import Button from '../button/button';
import Img from '../cloudflareImage.js';
Expand All @@ -25,6 +25,8 @@ export default function Footer({ isProductApp }) {
const resources = GeneralPageData.footer.resources;
const getStarted = GeneralPageData.footer.get_started;
const copyright = GeneralPageData.footer.copyright;
const isExternalHref = useIsExternalHref();
const getLinkTarget = useCallback(href => (isExternalHref(href) ? '_blank' : undefined), [isExternalHref]);

// ================================================================= Functions
const onLinkClick = useCallback(e => {
Expand Down Expand Up @@ -77,7 +79,13 @@ export default function Footer({ isProductApp }) {
<div className="footer_resources">
<div className="label">{resources.heading}</div>
{resources.items.map(item => (
<Link href={item.url} key={item.text} className="footer-link" onClick={onLinkClick}>
<Link
href={item.url}
key={item.text}
className="footer-link"
onClick={onLinkClick}
target={getLinkTarget(item.url)}
>
{item.text}
</Link>
))}
Expand All @@ -88,7 +96,13 @@ export default function Footer({ isProductApp }) {
<div className="footer_get-started">
<div className="label">{getStarted.heading}</div>
{getStarted.items.map(item => (
<Link className="footer-link" href={item.url} key={item.text} onClick={onLinkClick}>
<Link
className="footer-link"
href={item.url}
key={item.text}
onClick={onLinkClick}
target={getLinkTarget(item.url)}
>
{item.text}
</Link>
))}
Expand Down
53 changes: 52 additions & 1 deletion packages/website/components/link/link.js
Expand Up @@ -2,7 +2,58 @@ import React from 'react';
import PropTypes from 'prop-types';
import Link from 'next/link';

const WrappedLink = ({ tabIndex = 0, href, target = '_self', ...otherProps }) => (
/**
* Return whether a url is 'external' relative to another URL.
* The url is considered external if it has a different hostname.
* @param {URL} url
* @param {URL} relativeTo - url to compare for externality
*/
function isExternalLink(url, relativeTo) {
return url.hostname !== relativeTo.hostname;
}

/**
* React hook that provides an isExternalHref function.
* @returns {(href: string) => boolean} - fn that determines whether the provided href
* is known to be external from the current document
*/
export function useIsExternalHref() {
const [document, setDocument] = React.useState(/** @type {Document|undefined} */ (undefined));
// useEffect because next ssr wont have a document
React.useEffect(() => {
if (typeof globalThis.document !== 'undefined') {
setDocument(globalThis.document);
}
}, []);
const isExternalHref = React.useCallback(
/**
*
* @param {string} href - href attribute of <a>
* @returns {boolean} whether the provided href is external to the current document
*/
href => {
if (!document) {
return false;
}
const documentURL = new URL(document.URL);
const isExternal = isExternalLink(new URL(href, documentURL), documentURL);
return isExternal;
},
[document]
);
return isExternalHref;
}

/**
* A generic hyperlink component.
* @param {object} props
* @param {string} props.href - the href attribute for the link
* @param {number} [props.tabIndex] - the tabIndex attribute for the link
* @param {string} [props.target] - the target attribute for the link
* @param {React.ReactNode} [props.children]
* @param {React.MouseEventHandler<HTMLAnchorElement>} [props.onClick] - the onClick handler for the link
*/
const WrappedLink = ({ tabIndex = 0, href, target, ...otherProps }) => (
<Link href={href} {...otherProps}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a target={target} {...otherProps} tabIndex={tabIndex} onClick={otherProps.onClick}>
Expand Down

0 comments on commit 947e764

Please sign in to comment.