Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Links] Show external-link icon where applicable #768

Merged
merged 21 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ce2d3b0
[Links] Show external-link icon where applicable
meissadia Jun 25, 2024
d0ab4da
Merge branch 'main' into external-link-icon
meissadia Oct 16, 2024
091e658
feat: [Link] External link - Apply "No wrap" to keep icon with link text
meissadia Oct 17, 2024
f2b44fe
Merge branch 'main' into external-link-icon
tanner-ricks Oct 17, 2024
e9e6860
[Bug] InstitutionHeading Pipe Spacing Fix (#1007)
tanner-ricks Oct 17, 2024
366c321
[Point of Contact] Phone Extension - Limit to 9 digits, 0-9 in string…
shindigira Oct 19, 2024
975cc68
fix: [BetaAndLegalNotice] Remove icon from external link border
meissadia Oct 18, 2024
e930993
feat: [Link] Programmatically determine `internal vs external` links
meissadia Oct 21, 2024
025ae81
task: Ensure all instances of <Link> use the SBL Link
meissadia Oct 21, 2024
4a94695
task: [Link] Remove unnecessary passages of the `isRouterLink` prop
meissadia Oct 21, 2024
0ceb20a
Merge branch 'main' into external-link-icon
meissadia Oct 21, 2024
925263e
task: cleanup
meissadia Oct 21, 2024
c913c7c
task: Link - Support forced rendering as an external link
meissadia Oct 21, 2024
b214cc9
Merge branch 'main' into external-link-icon
meissadia Oct 22, 2024
4626d0f
fix: [Links] Clarify logic for determining external links based on th…
meissadia Oct 23, 2024
ebaeda0
Merge branch 'external-link-icon' of https://github.com/cfpb/sbl-fron…
meissadia Oct 23, 2024
d7ec93c
task: [Link] Clarify comment
meissadia Oct 23, 2024
0a2eca9
task: [Link] Cleanup regex
meissadia Oct 23, 2024
50cdc01
fix: [Filing landing] Underline the `email our support staff` link
meissadia Oct 23, 2024
9f3b9ac
feat: [Link] Programmatically determine `internal vs external` links v2
meissadia Oct 24, 2024
6e0296f
Merge branch 'main' into external-link-icon
meissadia Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 35 additions & 56 deletions src/components/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,22 @@
import type { LinkProperties as DesignSystemReactLinkProperties } from 'design-system-react';
import {
Link as DesignSystemReactLink,
ListLink as DesignSystemReactListLink,
LinkText,
ListItem,
} from 'design-system-react';

const getIsLinkExternal = (url: string | undefined): boolean => {
if (url === undefined) {
return false;
}
const externalUriSchemes = ['http', 'mailto:', 'tel:', 'sms:', 'ftp:'];
let isExternal = false;
for (const uriScheme of externalUriSchemes) {
if (url.startsWith(uriScheme)) {
isExternal = true;
}
}
return isExternal;
};

const getIsRouterUsageInferred = (
href: string | undefined,
isRouterLink: boolean | undefined,
): boolean => isRouterLink === undefined && !getIsLinkExternal(href);

const getIsRouterLink = (
href: string | undefined,
isRouterLink: boolean | undefined,
): boolean => {
const isRouterUsageInferred = getIsRouterUsageInferred(href, isRouterLink);
if (isRouterUsageInferred) {
return true;
}
if (isRouterLink === undefined) {
return false;
}
return isRouterLink;
};
import {
IconExternalLink,
isExternalLinkImplied,
isNewTabImplied,
} from './Link.utils';

interface LinkProperties extends DesignSystemReactLinkProperties {
// design system react's Link component correctly allows undefined values without defaultProps
/* eslint-disable react/require-default-props */
href?: string | undefined;
isRouterLink?: boolean | undefined;
isExternalLink?: boolean | undefined;
isNewTab?: boolean | undefined;
target?: string | undefined;

/* eslint-enable react/require-default-props */
Expand All @@ -54,42 +29,46 @@ export function Link({
children,
href,
isRouterLink,
isNewTab,
className,
isExternalLink,
...others
}: LinkProperties): JSX.Element {
const isInternalLink = getIsRouterLink(href, isRouterLink);
const otherProperties: LinkProperties = { ...others };
let icon;

// Open link in new tab
const openInNewTab = isNewTab ?? isNewTabImplied(href);
if (openInNewTab) otherProperties.target = '_blank';

// Treat as an External link
const treatExternal = isExternalLink ?? isExternalLinkImplied(String(href));
if (treatExternal) {
otherProperties.hasIcon = true; // Underline text, not icon
icon = <IconExternalLink />; // Display icon
}

if (!isInternalLink) otherProperties.target = '_blank'; // Open link in new tab
const asInAppLink = isRouterLink ?? (!treatExternal && !openInNewTab);

return (
<DesignSystemReactLink
href={href}
isRouterLink={isInternalLink}
isRouterLink={asInAppLink}
className={className}
{...otherProperties}
>
{children}
<LinkText>{children}</LinkText>
{icon}
</DesignSystemReactLink>
);
}

export function ListLink({
href,
isRouterLink,
children,
...others
}: LinkProperties): JSX.Element {
const isInternalLink = getIsRouterLink(href, isRouterLink);
const otherProperties: LinkProperties = { ...others };

if (!isInternalLink) otherProperties.target = '_blank'; // Open link in new tab

export function ListLink({ children, ...others }: LinkProperties): JSX.Element {
return (
<DesignSystemReactListLink
href={href}
isRouterLink={getIsRouterLink(href, isRouterLink)}
{...others}
>
{children}
</DesignSystemReactListLink>
<ListItem>
<Link {...others} type='list'>
{children}
</Link>
</ListItem>
);
}
51 changes: 51 additions & 0 deletions src/components/Link.utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Icon } from 'design-system-react';
import type { ReactElement } from 'react';

// Link to specific regulation
// Ex: /rules-policy/regulations/1002/109/#a-1-ii
const regsLinkPattern = /\/rules-policy\/regulations\/\d+\/\d+\/#.+/;

// Link to specific FIG subsection
// Ex: /small-business-lending/filing-instructions-guide/2024-guide/#4.4.1
const figLinkPattern = /\/filing-instructions-guide\/\d{4}-guide\/#.+/;

/**
* Programmatically determine if a link is external to the CFPB sphere of websites
*/
export const isExternalLinkImplied = (targetUrl: string): boolean => {
let parsed;

try {
parsed = new URL(targetUrl);
} catch {
return false; // Relative targets will fail parsing (ex. '/home')
}

const externalProtocols = ['http', 'tel:', 'sms:', 'ftp:'];
if (externalProtocols.includes(parsed.protocol)) return true;

const internalProtocols = ['mailto:'];
if (internalProtocols.includes(parsed.protocol)) return false;

// Any subdomain of consumerfinance.gov or the current host
const isInternalDomain = new RegExp(
`([\\S]*\\.)?(consumerfinance\\.gov|${window.location.host})`,
).test(parsed.host);

return !isInternalDomain;
};

// External link icon w/ spacing
export function IconExternalLink(): ReactElement {
return (
<>
{' '}
<Icon name='external-link' className='link-icon-override-color' />
</>
);
}

export function isNewTabImplied(href: string | undefined): boolean {
if (!href) return false;
return regsLinkPattern.test(href) || figLinkPattern.test(href);
}
15 changes: 15 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,18 @@ td:last-child{
td {
background-color: white !important;
}

/* Design System overrides */

/* Alerts - all icons in DS Alerts are colored based on the Alert type */
a .link-icon-override-color .cf-icon-svg{
@apply fill-pacific;
}

a:visited .link-icon-override-color .cf-icon-svg {
@apply fill-teal;
}

a:hover .link-icon-override-color .cf-icon-svg {
@apply !fill-pacificDark;
}
6 changes: 3 additions & 3 deletions src/pages/AuthenticatedLanding/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Divider, Hero, Layout, ListLink } from 'design-system-react';
import './Landing.less';

import AdditionalResources from 'components/AdditionalResources';
import BetaAndLegalNotice from 'components/BetaAndLegalNotice';
import { ListLink } from 'components/Link';
import { Divider, Hero, Layout } from 'design-system-react';
import type { ReactElement } from 'react';
import { LoadingContent } from '../../components/Loading';
import { useAssociatedInstitutions } from '../../utils/useAssociatedInstitutions';
import { FileSbl } from './FileSbl';
import './Landing.less';
import { ReviewInstitutions } from './ReviewInstitutions';

function Landing(): ReactElement | null {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Error/Error500.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function Error500({
We have encountered an error. Visit the platform homepage for
additional resources or contact our support staff.
</span>
<LinkVisitHomepage isRouterLink={false} />
<LinkVisitHomepage />
<br />
<br />
<span className='contact-us'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Alert, Link, Paragraph } from 'design-system-react';
import { Link } from 'components/Link';
import { Alert, Paragraph } from 'design-system-react';
import { ValidationInitialFetchFailAlert } from 'pages/Filing/FilingApp/FileSubmission.data';
import { dataValidationLink } from 'utils/common';

Expand Down
4 changes: 1 addition & 3 deletions src/pages/Filing/FilingApp/FilingOverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ export default function FilingOverview(): ReactElement {
<div className='mx-auto max-w-[48.125rem]'>
<Head title='File your small business lending data' />
<CrumbTrail>
<Link isRouterLink href='/landing'>
Home
</Link>
<Link href='/landing'>Home</Link>
</CrumbTrail>
<main id='main' className='u-mt30 u-mb60'>
<div className='max-w-[41.875rem]'>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Filing/FilingApp/FilingSubmit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
// @ts-nocheck
import WrapperPageContent from 'WrapperPageContent';
import Links from 'components/CommonLinks';
import { Link } from 'components/Link';
import { LoadingContent } from 'components/Loading';
import {
Alert,
Checkbox,
Grid,
Link,
Paragraph,
TextIntroduction,
} from 'design-system-react';
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Filing/UpdateFinancialProfile/UfpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default function UFPForm({
Home
</Link>
{lei ? (
<Link isRouterLink href={`/institution/${lei}`} key='view-instition'>
<Link href={`/institution/${lei}`} key='view-instition'>
View your financial institution profile
</Link>
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable react/require-default-props */
import Links from 'components/CommonLinks';
import { Link } from 'components/Link';
import SectionIntro from 'components/SectionIntro';
import { Link, WellContainer } from 'design-system-react';
import { WellContainer } from 'design-system-react';
import type { ReactNode } from 'react';
import type {
DomainType as Domain,
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Filing/ViewInstitutionProfile/PageIntro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function PageIntro(): JSX.Element {
}
callToAction={
<List isLinks>
<ListLink isRouterLink href={`/institution/${lei}/update`}>
<ListLink href={`/institution/${lei}/update`}>
Update your financial institution profile
</ListLink>
</List>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FormParagraph from 'components/FormParagraph';
import InputErrorMessage from 'components/InputErrorMessage';
import { Checkbox, Link, Paragraph } from 'design-system-react';
import { Link } from 'components/Link';
import { Checkbox, Paragraph } from 'design-system-react';
import type { FieldErrors } from 'react-hook-form';
import { Element } from 'react-scroll';

Expand Down
3 changes: 2 additions & 1 deletion src/pages/ProfileForm/Step1Form/NoDatabaseResultError.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AlertFieldLevel, Link, Paragraph } from 'design-system-react';
import { Link } from 'components/Link';
import { AlertFieldLevel, Paragraph } from 'design-system-react';

function NoDatabaseResultError(): JSX.Element {
return (
Expand Down