diff --git a/packages/website/components/breadcrumbs/breadcrumbs.js b/packages/website/components/breadcrumbs/breadcrumbs.js index 212a1daaa7..1d79f3680c 100644 --- a/packages/website/components/breadcrumbs/breadcrumbs.js +++ b/packages/website/components/breadcrumbs/breadcrumbs.js @@ -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} props.click */ export default function Breadcrumbs({ variant, click, items }) { return ( diff --git a/packages/website/components/footer/footer.js b/packages/website/components/footer/footer.js index defc1bd799..6d43c7294c 100644 --- a/packages/website/components/footer/footer.js +++ b/packages/website/components/footer/footer.js @@ -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'; @@ -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 => { @@ -77,7 +79,13 @@ export default function Footer({ isProductApp }) {
{resources.heading}
{resources.items.map(item => ( - + {item.text} ))} @@ -88,7 +96,13 @@ export default function Footer({ isProductApp }) {
{getStarted.heading}
{getStarted.items.map(item => ( - + {item.text} ))} diff --git a/packages/website/components/link/link.js b/packages/website/components/link/link.js index c243beee5b..eb4abae7a7 100644 --- a/packages/website/components/link/link.js +++ b/packages/website/components/link/link.js @@ -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 + * @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} [props.onClick] - the onClick handler for the link + */ +const WrappedLink = ({ tabIndex = 0, href, target, ...otherProps }) => ( {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}