import React from "react"; import PropTypes from "prop-types"; import { Link as GatsbyLink } from "gatsby"; import styled, { css } from "styled-components"; import urlLib from "url"; import { useLocation } from "@reach/router"; import { IS_PRODUCTION } from "constants/env"; import { LINK_DESTINATIONS } from "constants/routes"; import { FONT_WEIGHT } from "constants/styles"; import { resolveRelativePath, getLinkTarget, isRelativeUrl, isHashUrl, } from "helpers/routes"; import { Context as ScrollRouterContext } from "components/ApiReference/ScrollRouter"; const basicLinkStyles = css` text-decoration: none; color: inherit; font-weight: ${FONT_WEIGHT.bold}; line-height: 1.5; `; const StyledLink = styled(GatsbyLink)` ${basicLinkStyles}; `; export const ExternalLink = styled.a` ${basicLinkStyles}; `; export const BasicLink = ({ href, newTab, ...props }) => { const location = useLocation(); const finalProps = { ...props, ...(newTab && { rel: "noreferrer", target: "_blank" }), }; if (!href && !IS_PRODUCTION) { // dev-only warning so links don't asplode // eslint-disable-next-line no-console console.warn(`A link was made with no href. Children is ${props.children}`); return ; } const { host } = urlLib.parse(href, location.origin); // If a host is defined, it's external. We also want the browser to // handle hash links. if (host || href[0] === "#") { return ; } return ; }; BasicLink.propTypes = { href: PropTypes.string.isRequired, children: PropTypes.node, newTab: PropTypes.bool, }; export const OriginalFileContext = React.createContext(""); /** * Links, unfortunately, are complex due to the variety of page types. * * We have some external links, which we don't want to pass to Gatsby Link * components. /docs subpages are all regular Gatsby pages, so those need to use * Gatsby Link components. / api pages are different positions on the page, so we * have to handle clicks ourselves, in ScrollRouter. For SEO/UX reasons, each / * api subpage has an equivalent that is displayed if JS is disabled, * communicated via a ?javascript=false querystring. * * Additionally, all of the links need to work correctly both within GitHub and * on the production site. Because of the constraints of GitHub, that means * they're authored as relative paths. * * This means that we need to know 3 things in order to properly construct a link: * * - What page the browser is on * - Where the link is destined for * - What file created the link * * With the original file and relative path to the destination, we can resolve * that into a complete pathname. With the current page and the destination, we * can determine what type of component is needed. * * Coming from → * | | api | docs | no-js | external | * Going to | ----- | ------------ | ------- | ------- | -------- | * ↓ | api | ScrollRouter | | | | * | docs | | | | | * | no-js | -ish | -ish | -ish | | * * To ensure that /no-js pages display as intended if JS is disabled, we need to * detect if the current page is being accessed with javascript=false, and pass * it through if so. */ export const Link = ({ href, newTab, ...props }) => { const location = useLocation(); const originalFilePath = React.useContext(OriginalFileContext); const { url, destinationType } = React.useMemo(() => { if (isHashUrl(href)) { return { url: href, destinationType: LINK_DESTINATIONS.hash, }; } const destination = urlLib.parse(href); if (isRelativeUrl(href)) { destination.pathname = resolveRelativePath( originalFilePath, destination.pathname || "", ); } // If the page is being generated with a /no-js url, or if we're currently on // an /api page and `javascript=false` is in the query string, then we need // to make sure the links render with the right path and querystring so they // load correctly when clicked. const fromNoJs = destination.path?.startsWith("/no-js"); const fromApiNoJs = location.pathname.startsWith("/api") && location.search.includes("javascript=false"); if (fromNoJs) { destination.pathname = destination.path.split("/no-js")[1]; } if (fromNoJs || fromApiNoJs) { destination.search = destination.search ? `${destination.query}&javascript=false` : "javascript=false"; } const finalUrl = urlLib.format(destination); return { url: finalUrl, destinationType: getLinkTarget(finalUrl), }; }, [href, originalFilePath, location.pathname, location.search]); switch (destinationType) { case LINK_DESTINATIONS.api: if (location.pathname.startsWith("/api")) { return ( // We can't do `useContext(ScrollRouterContext)` because the provider // won't always be present. Can't conditionally use hooks. {({ onLinkClick }) => ( { // Only use the scroll router logic if the visitor hasn't // opted-out of JS. This is only expected for search crawlers. if (!url.includes("javascript=false")) { e.preventDefault(); e.stopPropagation(); onLinkClick(url); } }} href={url} {...props} /> )} ); } return ; case LINK_DESTINATIONS.docs: case LINK_DESTINATIONS.hash: return ; case LINK_DESTINATIONS.external: default: return ; } }; Link.propTypes = { href: PropTypes.string.isRequired, children: PropTypes.node, newTab: PropTypes.bool, };