From ce2d3b091c6202a25f125420a25f8fe6ac642e06 Mon Sep 17 00:00:00 2001 From: Meis Date: Tue, 25 Jun 2024 14:53:12 -0600 Subject: [PATCH 01/15] [Links] Show external-link icon where applicable --- src/components/Link.tsx | 16 ++++++++++------ src/index.css | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/components/Link.tsx b/src/components/Link.tsx index e1a5e729c..819971dbb 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,11 +1,7 @@ // this component could potentially be lifted with others into the DSR in a later refactor // see: https://github.com/cfpb/sbl-frontend/issues/200 -import type { LinkProperties as DesignSystemReactLinkProperties } from 'design-system-react'; -import { - Link as DesignSystemReactLink, - ListLink as DesignSystemReactListLink, -} from 'design-system-react'; +import { Link as DesignSystemReactLink, LinkProperties as DesignSystemReactLinkProperties, ListLink as DesignSystemReactListLink, Icon } from 'design-system-react'; const getIsLinkExternal = (url: string | undefined): boolean => { if (url === undefined) { @@ -59,15 +55,23 @@ export function Link({ const isInternalLink = getIsRouterLink(href, isRouterLink); const otherProperties: LinkProperties = { ...others }; - if (!isInternalLink) otherProperties.target = '_blank'; // Open link in new tab + let icon = null + if (!isInternalLink) { + otherProperties.target = '_blank'; // Open link in new tab + icon =<> + {' '} + + } return ( {children} + {icon} ); } diff --git a/src/index.css b/src/index.css index 82a18b58c..3a19c3f95 100644 --- a/src/index.css +++ b/src/index.css @@ -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; +} From 091e65897462ba1ea7d6fcad78df3a3d5ebe113c Mon Sep 17 00:00:00 2001 From: Meis Date: Thu, 17 Oct 2024 12:45:04 -0600 Subject: [PATCH 02/15] feat: [Link] External link - Apply "No wrap" to keep icon with link text --- src/components/Link.tsx | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 819971dbb..c5d3502ad 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,7 +1,12 @@ // this component could potentially be lifted with others into the DSR in a later refactor // see: https://github.com/cfpb/sbl-frontend/issues/200 -import { Link as DesignSystemReactLink, LinkProperties as DesignSystemReactLinkProperties, ListLink as DesignSystemReactListLink, Icon } from 'design-system-react'; +import type { LinkProperties as DesignSystemReactLinkProperties } from 'design-system-react'; +import { + Link as DesignSystemReactLink, + ListLink as DesignSystemReactListLink, + Icon, +} from 'design-system-react'; const getIsLinkExternal = (url: string | undefined): boolean => { if (url === undefined) { @@ -50,17 +55,23 @@ export function Link({ children, href, isRouterLink, + className, ...others }: LinkProperties): JSX.Element { const isInternalLink = getIsRouterLink(href, isRouterLink); const otherProperties: LinkProperties = { ...others }; + let cname = className ?? ''; - let icon = null + let icon = null; if (!isInternalLink) { + cname += ' whitespace-nowrap'; otherProperties.target = '_blank'; // Open link in new tab - icon =<> - {' '} - + icon = ( + <> + {' '} + + + ); } return ( @@ -68,6 +79,7 @@ export function Link({ href={href} isRouterLink={isInternalLink} hasIcon={!isInternalLink} + className={cname} {...otherProperties} > {children} From e9e68600486e1f3f7d685d8d499b349f7fd116fe Mon Sep 17 00:00:00 2001 From: Tanner Ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:09:10 -0500 Subject: [PATCH 03/15] [Bug] InstitutionHeading Pipe Spacing Fix (#1007) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed a bug that was causing one space to appear instead of the intended two. ## Changes - src - pages - Filing - FilingApp - InsitutionHeading.tsx: Replaced double spaces with double unicode non-breaking spaces. ## How to test this PR 1. Pull the branch 2. Relaunch whatever is necessary 3. Navigate to a page in the app that contains the Filing Steps and the Institution Heading 4. Verify that the pipe character is surrounded by two spaces in the Institution Heading. The attached screenshot has a red box which surrounds the area to check. ## Screenshots ![Screenshot 2024-10-17 at 12 27 18 PM](https://github.com/user-attachments/assets/06b50ca5-76fb-4920-849c-47e67e5b1537) --- src/pages/Filing/FilingApp/InstitutionHeading.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/Filing/FilingApp/InstitutionHeading.tsx b/src/pages/Filing/FilingApp/InstitutionHeading.tsx index c8d9014db..6c6de0b22 100644 --- a/src/pages/Filing/FilingApp/InstitutionHeading.tsx +++ b/src/pages/Filing/FilingApp/InstitutionHeading.tsx @@ -17,7 +17,9 @@ function InstitutionHeading({ content.push(item); } } - const contentUsed = content.filter(Boolean).join(`${' '}|${' '}`); + const contentUsed = content + .filter(Boolean) + .join(`${'\u00A0\u00A0'}|${'\u00A0\u00A0'}`); return {contentUsed}; } export default InstitutionHeading; From 366c321b47d2f4fcaeb04aa33823e1e7fa5fca57 Mon Sep 17 00:00:00 2001 From: S T Date: Sat, 19 Oct 2024 08:15:41 -0700 Subject: [PATCH 04/15] [Point of Contact] Phone Extension - Limit to 9 digits, 0-9 in string (#1001) closes #997 ## Changes - style(Point of Contact): Phone Extension always gets its own line - content(Point of Contact): Change the helper and error text to match the figma - feat(Point of Contact): Phone Extension validation ## How to test this PR 1. Navigate to Point of Contact 2. Enter valid phone extension - ex. 00757 3. Enter invalid phone extension - ex ukhi778ih89h98h9h9h89h ## Screenshots Screenshot 2024-10-15 at 3 08 40 PM --------- Co-authored-by: Tanner Ricks <182143365+tanner-ricks@users.noreply.github.com> --- src/components/FormErrorHeader.data.ts | 2 + src/pages/PointOfContact/index.tsx | 53 ++++++++++++++++---------- src/types/formTypes.ts | 14 +++++-- src/utils/constants.ts | 1 + src/utils/processNumbersOnlyString.ts | 9 +++++ 5 files changed, 56 insertions(+), 23 deletions(-) create mode 100644 src/utils/processNumbersOnlyString.ts diff --git a/src/components/FormErrorHeader.data.ts b/src/components/FormErrorHeader.data.ts index 9cea9a43e..1419ab10e 100644 --- a/src/components/FormErrorHeader.data.ts +++ b/src/components/FormErrorHeader.data.ts @@ -181,6 +181,7 @@ export const PocZodSchemaErrors = { phoneMin: 'You must enter the phone number of the point of contact for your filing.', phoneRegex: 'You must enter a valid phone number.', + phoneExtension: 'You must enter a valid phone extension.', emailMin: 'You must enter the email address of the point of contact for your filing.', emailRegex: 'You must enter a valid email address.', @@ -214,6 +215,7 @@ export const PocFormHeaderErrors: PocFormHeaderErrorsType = { [PocZodSchemaErrors.phoneMin]: 'Enter the phone number of the point of contact', [PocZodSchemaErrors.phoneRegex]: 'Enter a valid phone number', + [PocZodSchemaErrors.phoneExtension]: 'Enter a valid phone extension', [PocZodSchemaErrors.emailMin]: 'Enter the email address of the point of contact', [PocZodSchemaErrors.emailRegex]: 'Enter a valid email address', diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index dc51a995a..5a589701d 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -31,6 +31,7 @@ import { formatPointOfContactObject, scrollToElement, } from 'pages/ProfileForm/ProfileFormUtils'; +import type React from 'react'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate, useParams } from 'react-router-dom'; @@ -155,6 +156,13 @@ function PointOfContact(): JSX.Element { const navigateSignSubmit = (): void => navigate(`/filing/${year}/${lei}/submit`); + // Note: Design Choice to be made: ignore non-number input or just rely on error handling + // const handlePhoneExtensionInput = ( + // event: React.ChangeEvent, + // ): void => { + // setValue('phoneExtension', processNumbersOnlyString(event.target.value)); + // }; + const { mutateAsync: mutateSubmitPointOfContact } = useSubmitPointOfContact({ // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 lei, @@ -276,26 +284,31 @@ function PointOfContact(): JSX.Element { errorMessage={formErrors.lastName?.message} showError /> -
- - -
+ {/* Note: Phone and Phone Extension styling saved till a final decision */} + {/*
*/} + + + {/*
*/} Date: Fri, 18 Oct 2024 12:59:48 -0600 Subject: [PATCH 05/15] fix: [BetaAndLegalNotice] Remove icon from external link border --- src/components/BetaAndLegalNotice.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/BetaAndLegalNotice.tsx b/src/components/BetaAndLegalNotice.tsx index 0f2e55762..5a67e3e5b 100644 --- a/src/components/BetaAndLegalNotice.tsx +++ b/src/components/BetaAndLegalNotice.tsx @@ -16,10 +16,7 @@ export default function BetaAndLegalNotice(): ReactElement { Thank you for participating. The beta platform is available to upload, test, and validate data. All uploaded data is for testing purposes only and may be removed at any time. For questions or feedback,{' '} - + email our support staff . From e9309938fa6ea563dc2e96e2eaf7d3f32ee950f9 Mon Sep 17 00:00:00 2001 From: Meis Date: Mon, 21 Oct 2024 11:45:24 -0600 Subject: [PATCH 06/15] feat: [Link] Programmatically determine `internal vs external` links task: [Link] Refactor to simplify implied usage of RouterLink --- src/components/Link.tsx | 86 ++++++++--------------------------- src/components/Link.utils.tsx | 36 +++++++++++++++ 2 files changed, 54 insertions(+), 68 deletions(-) create mode 100644 src/components/Link.utils.tsx diff --git a/src/components/Link.tsx b/src/components/Link.tsx index c5d3502ad..c9244c242 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -4,42 +4,10 @@ import type { LinkProperties as DesignSystemReactLinkProperties } from 'design-system-react'; import { Link as DesignSystemReactLink, - ListLink as DesignSystemReactListLink, - Icon, + 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, isExternalLink } from './Link.utils'; interface LinkProperties extends DesignSystemReactLinkProperties { // design system react's Link component correctly allows undefined values without defaultProps @@ -58,54 +26,36 @@ export function Link({ className, ...others }: LinkProperties): JSX.Element { - const isInternalLink = getIsRouterLink(href, isRouterLink); + const hrefString = String(href); + const isExternal = isExternalLink(hrefString); const otherProperties: LinkProperties = { ...others }; - let cname = className ?? ''; - let icon = null; - if (!isInternalLink) { - cname += ' whitespace-nowrap'; + + if (isExternal) { otherProperties.target = '_blank'; // Open link in new tab - icon = ( - <> - {' '} - - - ); + otherProperties.hasIcon = true; // Underline text, not icon + icon = ; // Display icon } return ( - {children} + {children} {icon} ); } -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 ( - - {children} - + + + {children} + + ); } diff --git a/src/components/Link.utils.tsx b/src/components/Link.utils.tsx new file mode 100644 index 000000000..3e8fb89c2 --- /dev/null +++ b/src/components/Link.utils.tsx @@ -0,0 +1,36 @@ +import { Icon } from 'design-system-react'; +import type { ReactElement } from 'react'; + +/** + * Programmatically determine if a link is external to the CFPB sphere of websites + */ +export const isExternalLink = (targetUrl: string): boolean => { + let parsed; + + try { + parsed = new URL(targetUrl); + } catch { + return false; // Internal targets will fail parsing (ex. '/home') + } + + const externalProtocols = ['http', 'mailto:', 'tel:', 'sms:', 'ftp:']; + if (externalProtocols.includes(parsed.protocol)) return true; + + // Any subdomain of consumerfinance.gov or the current host + const isInternal = new RegExp( + `([\\S]*\\.)?(((consumerfinance|cf)\\.gov)|(${window.location.host}))`, + ); + if (!isInternal.test(parsed.host)) return true; + + return false; +}; + +// External link icon w/ spacing +export function IconExternalLink(): ReactElement { + return ( + <> + {' '} + + + ); +} From 025ae8126749a64618a042b29eac07ba0c59290e Mon Sep 17 00:00:00 2001 From: Meis Date: Mon, 21 Oct 2024 13:02:47 -0600 Subject: [PATCH 07/15] task: Ensure all instances of use the SBL Link --- src/pages/AuthenticatedLanding/index.tsx | 6 +++--- .../Filing/FilingApp/FilingErrors/FilingErrorsAlerts.tsx | 3 ++- src/pages/Filing/FilingApp/FilingSubmit.tsx | 2 +- .../ViewInstitutionProfile/FinancialInstitutionDetails.tsx | 3 ++- .../Step1Form/AssociatedFinancialInstitutions.tsx | 3 ++- src/pages/ProfileForm/Step1Form/NoDatabaseResultError.tsx | 3 ++- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pages/AuthenticatedLanding/index.tsx b/src/pages/AuthenticatedLanding/index.tsx index 730212e55..e1fd25216 100644 --- a/src/pages/AuthenticatedLanding/index.tsx +++ b/src/pages/AuthenticatedLanding/index.tsx @@ -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 { diff --git a/src/pages/Filing/FilingApp/FilingErrors/FilingErrorsAlerts.tsx b/src/pages/Filing/FilingApp/FilingErrors/FilingErrorsAlerts.tsx index dbc3fdbd8..96fbff854 100644 --- a/src/pages/Filing/FilingApp/FilingErrors/FilingErrorsAlerts.tsx +++ b/src/pages/Filing/FilingApp/FilingErrors/FilingErrorsAlerts.tsx @@ -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'; diff --git a/src/pages/Filing/FilingApp/FilingSubmit.tsx b/src/pages/Filing/FilingApp/FilingSubmit.tsx index a77ab0d63..1a8b1f01d 100644 --- a/src/pages/Filing/FilingApp/FilingSubmit.tsx +++ b/src/pages/Filing/FilingApp/FilingSubmit.tsx @@ -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'; diff --git a/src/pages/Filing/ViewInstitutionProfile/FinancialInstitutionDetails.tsx b/src/pages/Filing/ViewInstitutionProfile/FinancialInstitutionDetails.tsx index cba63fbbf..c7312eb24 100644 --- a/src/pages/Filing/ViewInstitutionProfile/FinancialInstitutionDetails.tsx +++ b/src/pages/Filing/ViewInstitutionProfile/FinancialInstitutionDetails.tsx @@ -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, diff --git a/src/pages/ProfileForm/Step1Form/AssociatedFinancialInstitutions.tsx b/src/pages/ProfileForm/Step1Form/AssociatedFinancialInstitutions.tsx index fd783bf19..54156e503 100644 --- a/src/pages/ProfileForm/Step1Form/AssociatedFinancialInstitutions.tsx +++ b/src/pages/ProfileForm/Step1Form/AssociatedFinancialInstitutions.tsx @@ -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'; diff --git a/src/pages/ProfileForm/Step1Form/NoDatabaseResultError.tsx b/src/pages/ProfileForm/Step1Form/NoDatabaseResultError.tsx index e61df322a..795a156a7 100644 --- a/src/pages/ProfileForm/Step1Form/NoDatabaseResultError.tsx +++ b/src/pages/ProfileForm/Step1Form/NoDatabaseResultError.tsx @@ -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 ( From 4a94695ba697a606bba2f73c7fe588d9e8944dfe Mon Sep 17 00:00:00 2001 From: Meis Date: Mon, 21 Oct 2024 13:06:08 -0600 Subject: [PATCH 08/15] task: [Link] Remove unnecessary passages of the `isRouterLink` prop --- src/pages/Error/Error500.tsx | 2 +- src/pages/Filing/FilingApp/FilingOverviewPage.tsx | 4 +--- src/pages/Filing/UpdateFinancialProfile/UfpForm.tsx | 2 +- src/pages/Filing/ViewInstitutionProfile/PageIntro.tsx | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pages/Error/Error500.tsx b/src/pages/Error/Error500.tsx index caa5a780f..314d8de08 100644 --- a/src/pages/Error/Error500.tsx +++ b/src/pages/Error/Error500.tsx @@ -63,7 +63,7 @@ export function Error500({ We have encountered an error. Visit the platform homepage for additional resources or contact our support staff. - +

diff --git a/src/pages/Filing/FilingApp/FilingOverviewPage.tsx b/src/pages/Filing/FilingApp/FilingOverviewPage.tsx index 6a5cc7f10..71639ba0e 100644 --- a/src/pages/Filing/FilingApp/FilingOverviewPage.tsx +++ b/src/pages/Filing/FilingApp/FilingOverviewPage.tsx @@ -55,9 +55,7 @@ export default function FilingOverview(): ReactElement {
- - Home - + Home
diff --git a/src/pages/Filing/UpdateFinancialProfile/UfpForm.tsx b/src/pages/Filing/UpdateFinancialProfile/UfpForm.tsx index 65331b892..cc7625013 100644 --- a/src/pages/Filing/UpdateFinancialProfile/UfpForm.tsx +++ b/src/pages/Filing/UpdateFinancialProfile/UfpForm.tsx @@ -115,7 +115,7 @@ export default function UFPForm({ Home {lei ? ( - + View your financial institution profile ) : null} diff --git a/src/pages/Filing/ViewInstitutionProfile/PageIntro.tsx b/src/pages/Filing/ViewInstitutionProfile/PageIntro.tsx index e07e944bd..c6aa85aab 100644 --- a/src/pages/Filing/ViewInstitutionProfile/PageIntro.tsx +++ b/src/pages/Filing/ViewInstitutionProfile/PageIntro.tsx @@ -18,7 +18,7 @@ export function PageIntro(): JSX.Element { } callToAction={ - + Update your financial institution profile From 925263e5301a0d0935350bd8a60b9770c587edc9 Mon Sep 17 00:00:00 2001 From: Meis Date: Mon, 21 Oct 2024 13:32:43 -0600 Subject: [PATCH 09/15] task: cleanup --- src/components/Link.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Link.tsx b/src/components/Link.tsx index c9244c242..9d69cafe1 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -26,10 +26,9 @@ export function Link({ className, ...others }: LinkProperties): JSX.Element { - const hrefString = String(href); - const isExternal = isExternalLink(hrefString); + const isExternal = isExternalLink(String(href)); const otherProperties: LinkProperties = { ...others }; - let icon = null; + let icon; if (isExternal) { otherProperties.target = '_blank'; // Open link in new tab From c913c7cd194ce8495aaa38fbde9388d41fc6aedd Mon Sep 17 00:00:00 2001 From: Meis Date: Mon, 21 Oct 2024 15:19:17 -0600 Subject: [PATCH 10/15] task: Link - Support forced rendering as an external link task: Link - Consider `mailto:` links as internal --- src/components/Link.tsx | 3 ++- src/components/Link.utils.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 9d69cafe1..9d39a93f5 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -14,6 +14,7 @@ interface LinkProperties extends DesignSystemReactLinkProperties { /* eslint-disable react/require-default-props */ href?: string | undefined; isRouterLink?: boolean | undefined; + isExternalLink?: boolean | undefined; target?: string | undefined; /* eslint-enable react/require-default-props */ @@ -26,7 +27,7 @@ export function Link({ className, ...others }: LinkProperties): JSX.Element { - const isExternal = isExternalLink(String(href)); + const isExternal = others.isExternalLink ?? isExternalLink(String(href)); const otherProperties: LinkProperties = { ...others }; let icon; diff --git a/src/components/Link.utils.tsx b/src/components/Link.utils.tsx index 3e8fb89c2..e19b97522 100644 --- a/src/components/Link.utils.tsx +++ b/src/components/Link.utils.tsx @@ -13,7 +13,7 @@ export const isExternalLink = (targetUrl: string): boolean => { return false; // Internal targets will fail parsing (ex. '/home') } - const externalProtocols = ['http', 'mailto:', 'tel:', 'sms:', 'ftp:']; + const externalProtocols = ['http', 'tel:', 'sms:', 'ftp:']; if (externalProtocols.includes(parsed.protocol)) return true; // Any subdomain of consumerfinance.gov or the current host From 4626d0fef364d91b4d35f363ccd149b3cee3704d Mon Sep 17 00:00:00 2001 From: Meis Date: Wed, 23 Oct 2024 09:46:49 -0600 Subject: [PATCH 11/15] fix: [Links] Clarify logic for determining external links based on the link domain. --- src/components/Link.utils.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/Link.utils.tsx b/src/components/Link.utils.tsx index e19b97522..47b243d52 100644 --- a/src/components/Link.utils.tsx +++ b/src/components/Link.utils.tsx @@ -16,13 +16,15 @@ export const isExternalLink = (targetUrl: string): boolean => { 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 isInternal = new RegExp( - `([\\S]*\\.)?(((consumerfinance|cf)\\.gov)|(${window.location.host}))`, - ); - if (!isInternal.test(parsed.host)) return true; + const isInternalDomain = new RegExp( + `([\\S]*\\.)?(consumerfinance\\.gov|${window.location.host})`, + ).test(parsed.host); - return false; + return !isInternalDomain; }; // External link icon w/ spacing From d7ec93c5b070a19d400ace34cc9c83e19e96f18d Mon Sep 17 00:00:00 2001 From: Meis Date: Wed, 23 Oct 2024 09:49:43 -0600 Subject: [PATCH 12/15] task: [Link] Clarify comment --- src/components/Link.utils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Link.utils.tsx b/src/components/Link.utils.tsx index 47b243d52..ec2c8a7a3 100644 --- a/src/components/Link.utils.tsx +++ b/src/components/Link.utils.tsx @@ -10,7 +10,7 @@ export const isExternalLink = (targetUrl: string): boolean => { try { parsed = new URL(targetUrl); } catch { - return false; // Internal targets will fail parsing (ex. '/home') + return false; // Relative targets will fail parsing (ex. '/home') } const externalProtocols = ['http', 'tel:', 'sms:', 'ftp:']; From 0a2eca9cb1f3dbff7da6830c2d6817168d7adb92 Mon Sep 17 00:00:00 2001 From: Meis Date: Wed, 23 Oct 2024 09:51:29 -0600 Subject: [PATCH 13/15] task: [Link] Cleanup regex --- src/components/Link.utils.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Link.utils.tsx b/src/components/Link.utils.tsx index ec2c8a7a3..769d7843f 100644 --- a/src/components/Link.utils.tsx +++ b/src/components/Link.utils.tsx @@ -20,9 +20,8 @@ export const isExternalLink = (targetUrl: string): boolean => { 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); + const internalPattern = `([\\S]*\\.)?(consumerfinance\\.gov|${window.location.host})`; + const isInternalDomain = new RegExp(internalPattern).test(parsed.host); return !isInternalDomain; }; From 50cdc01dffeea3e53428c571fc0421ac9c7814d9 Mon Sep 17 00:00:00 2001 From: Meis Date: Wed, 23 Oct 2024 12:57:23 -0600 Subject: [PATCH 14/15] fix: [Filing landing] Underline the `email our support staff` link --- src/components/BetaAndLegalNotice.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/BetaAndLegalNotice.tsx b/src/components/BetaAndLegalNotice.tsx index 5a67e3e5b..0f2e55762 100644 --- a/src/components/BetaAndLegalNotice.tsx +++ b/src/components/BetaAndLegalNotice.tsx @@ -16,7 +16,10 @@ export default function BetaAndLegalNotice(): ReactElement { Thank you for participating. The beta platform is available to upload, test, and validate data. All uploaded data is for testing purposes only and may be removed at any time. For questions or feedback,{' '} - + email our support staff . From 9f3b9acc55850d8cfb558e3997b776661d2f2137 Mon Sep 17 00:00:00 2001 From: Meis Date: Thu, 24 Oct 2024 15:37:46 -0600 Subject: [PATCH 15/15] feat: [Link] Programmatically determine `internal vs external` links v2 --- src/components/Link.tsx | 23 ++++++++++++++++++----- src/components/Link.utils.tsx | 20 +++++++++++++++++--- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 9d39a93f5..4ee9fbf69 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -7,7 +7,11 @@ import { LinkText, ListItem, } from 'design-system-react'; -import { IconExternalLink, isExternalLink } from './Link.utils'; +import { + IconExternalLink, + isExternalLinkImplied, + isNewTabImplied, +} from './Link.utils'; interface LinkProperties extends DesignSystemReactLinkProperties { // design system react's Link component correctly allows undefined values without defaultProps @@ -15,6 +19,7 @@ interface LinkProperties extends DesignSystemReactLinkProperties { href?: string | undefined; isRouterLink?: boolean | undefined; isExternalLink?: boolean | undefined; + isNewTab?: boolean | undefined; target?: string | undefined; /* eslint-enable react/require-default-props */ @@ -24,23 +29,31 @@ export function Link({ children, href, isRouterLink, + isNewTab, className, + isExternalLink, ...others }: LinkProperties): JSX.Element { - const isExternal = others.isExternalLink ?? isExternalLink(String(href)); const otherProperties: LinkProperties = { ...others }; let icon; - if (isExternal) { - otherProperties.target = '_blank'; // Open link in new tab + // 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 = ; // Display icon } + const asInAppLink = isRouterLink ?? (!treatExternal && !openInNewTab); + return ( diff --git a/src/components/Link.utils.tsx b/src/components/Link.utils.tsx index 769d7843f..176d5f469 100644 --- a/src/components/Link.utils.tsx +++ b/src/components/Link.utils.tsx @@ -1,10 +1,18 @@ 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 isExternalLink = (targetUrl: string): boolean => { +export const isExternalLinkImplied = (targetUrl: string): boolean => { let parsed; try { @@ -20,8 +28,9 @@ export const isExternalLink = (targetUrl: string): boolean => { if (internalProtocols.includes(parsed.protocol)) return false; // Any subdomain of consumerfinance.gov or the current host - const internalPattern = `([\\S]*\\.)?(consumerfinance\\.gov|${window.location.host})`; - const isInternalDomain = new RegExp(internalPattern).test(parsed.host); + const isInternalDomain = new RegExp( + `([\\S]*\\.)?(consumerfinance\\.gov|${window.location.host})`, + ).test(parsed.host); return !isInternalDomain; }; @@ -35,3 +44,8 @@ export function IconExternalLink(): ReactElement { ); } + +export function isNewTabImplied(href: string | undefined): boolean { + if (!href) return false; + return regsLinkPattern.test(href) || figLinkPattern.test(href); +}