;
const DefaultArguments = {
args: {
- href: '#',
+ to: '#',
children: 'Link Text',
},
};
@@ -23,19 +24,19 @@ const DefaultArguments = {
export const Inline: Story = {
render: () => (
- Here's the default style.
+ Here's the default style.
),
};
export const Standalone: Story = {
render: (arguments_) => (
-
+
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const link = canvas.getByRole('link', { name: /standalone link/i });
- await expect(link).toHaveAttribute('href', '/#');
+ await expect(link).toHaveAttribute('to', '/#');
},
};
@@ -50,13 +51,13 @@ export const WithIcon: Story = {
The document icon should emphasize a link that contains a{' '}
. The external link icon is used to emphasize a link to a{' '}
@@ -66,7 +67,7 @@ export const WithIcon: Story = {
@@ -74,7 +75,7 @@ export const WithIcon: Story = {
@@ -82,7 +83,7 @@ export const WithIcon: Story = {
@@ -90,7 +91,7 @@ export const WithIcon: Story = {
@@ -106,9 +107,9 @@ export const Listlink: Story = {
},
render: () => (
-
-
-
+
+
+
),
};
@@ -117,22 +118,45 @@ export const Destructive: Story = {
args: {
...DefaultArguments.args,
},
- render: () => ,
+ render: () => ,
};
-export const LinkWithReactRouterLink: Story = {
- name: 'Link using React Router Link',
- parameters: {
- docs: {
- description: {
- story:
- 'See [React Router Link docs](https://reactrouter.com/api/components/Link) for usage information',
- },
- },
- },
+const CustomLinkComponent = ({
+ to,
+ children,
+ ...others
+}: BaseLinkProperties): JSXElement | null => {
+ return (
+
+ {children}
+
+ );
+};
+
+/**
+ * You can configure the DSR to use a router library's link component by wrapping your app
+ * in the DSRContext provider and setting a `LinkComponent` value.
+ * Your custom link component will be output instead of the default anchor element
+ * everywhere the DSR's Link component is used.
+ *
+ * Example usage:
+ *
+ * \
+ * App content
+ * \
+ */
+export const LinkWithCustomLinkComponent: Story = {
+ name: 'Link using custom component',
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
render: () => (
-
-
-
+
),
};
+
+
diff --git a/src/components/Link/link.test.tsx b/src/components/Link/link.test.tsx
index dacb77da9e..f9205927fb 100644
--- a/src/components/Link/link.test.tsx
+++ b/src/components/Link/link.test.tsx
@@ -1,16 +1,35 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
-import { MemoryRouter } from 'react-router';
import Link, { LinkText, ListLink } from './link';
+import { DSRContext } from '../../context/dsr-context';
+import { ReactNode } from 'react';
+import type { JSXElement } from "../../types/jsx-element";
describe('', () => {
const linkBaseProperties = {
- href: '/foo/bar',
+ to: '/foo/bar',
'data-testid': 'link-test-id',
label: 'some link',
};
const testId = linkBaseProperties['data-testid'];
+ interface CustomLinkProperties {
+ to: string | undefined;
+ children: ReactNode;
+ }
+
+ const CustomLinkComponent = ({
+ to,
+ children,
+ ...others
+ }: CustomLinkProperties): JSXElement => {
+ return (
+
+ {children}
+
+ );
+ };
+
it('Type: "default"', () => {
render();
const link = screen.getByTestId(testId);
@@ -79,43 +98,28 @@ describe('', () => {
expect(link).toHaveAttribute('target', '_blank');
});
- it('Option: isRouterLink - it renders a router link', () => {
+ it('Context: uses link component configured in context', () => {
render(
-
-
- ,
+
+
+ Child
+
+ ,
);
-
- const link = screen.getByRole('link', { name: /some link/i });
- expect(link).toHaveAttribute('href', '/foo/bar');
+ expect(screen.getByTestId('link-test-id')).toBeInTheDocument();
+ expect(screen.getByTestId('link-test-id')).toHaveClass('link-component-from-context');
});
- it('Option: isRouterLink - it renders children and icons', async () => {
+ it('Context: uses base link component by default', () => {
render(
-
-
+
Child
-
- ,
+ ,
);
-
- const link = screen.getByRole('link', { name: /some link/i });
- expect(link).toHaveClass('a-link');
- expect(screen.getByTestId('link-child')).toBeInTheDocument();
- expect(screen.getByText('some link')).toHaveClass('a-link__text');
- expect(await screen.findByTestId('link-icon-left')).toBeInTheDocument();
+ expect(screen.getByTestId('link-test-id')).toBeInTheDocument();
+ expect(screen.getByTestId('link-test-id')).not.toHaveClass('link-component-from-context');
});
- it('Option: isRouterLink - it requires href', () => {
- const brokenProperties = {
- ...linkBaseProperties,
- href: undefined as unknown as string,
- };
-
- expect(() => render()).toThrow(
- 'Link component: href is a required attribute when isRouterLink is true',
- );
- });
});
describe('', () => {
@@ -131,7 +135,7 @@ describe('', () => {
const testId = 'list-link';
it('includes all expected elements', () => {
- render();
+ render();
// ListItem
const listItem = screen.getByRole('listitem');
expect(listItem).toBeInTheDocument();
diff --git a/src/components/Link/link.tsx b/src/components/Link/link.tsx
index fa86b4f17f..5a55bd70e7 100644
--- a/src/components/Link/link.tsx
+++ b/src/components/Link/link.tsx
@@ -1,9 +1,8 @@
import { JSX } from 'react';
-import { Link as RouterLink } from 'react-router';
import type { JSXElement } from '../../types/jsx-element';
-
import classnames from 'classnames';
import type { HTMLProps, ReactNode, Ref } from 'react';
+import { useDSRContext } from '../../context/dsr-context';
import { Icon } from '../Icon/icon';
import ListItem from '../List/list-item';
import './link.scss';
@@ -20,7 +19,7 @@ export interface LinkProperties extends HTMLProps {
/**
* The link's destination URL.
*/
- href: string;
+ to: string | undefined;
/**
* Name of icon to display left of link text
*/
@@ -33,10 +32,6 @@ export interface LinkProperties extends HTMLProps {
* Whether the link is a standalone link
*/
isJump?: boolean;
- /**
- * Whether the link is a react router link
- */
- isRouterLink?: boolean;
/**
* The link's text content, not required if children are provided
*/
@@ -57,11 +52,10 @@ export interface LinkProperties extends HTMLProps {
export default function Link({
isButton = false,
children,
- href,
+ to,
iconLeft,
iconRight,
isJump = false,
- isRouterLink = false,
label,
type = 'default',
...others
@@ -80,39 +74,18 @@ export default function Link({
'a-link': shouldUseLinkStyles,
});
+ const { LinkComponent } = useDSRContext();
+
if (hasLeftIcon && hasRightIcon) {
throw new Error(
'Link component: only one of iconLeft or iconRight can be provided',
);
}
- if (isRouterLink) {
- if (!href) {
- throw new Error(
- 'Link component: href is a required attribute when isRouterLink is true',
- );
- }
- return (
-
- {children}
- {!!iconLeft && (
-
- )}
- {labelNode}
- {!!iconRight && (
-
- )}
-
- );
- }
-
return (
-
+
{children}
+
{!!iconLeft && (
)}
@@ -120,7 +93,7 @@ export default function Link({
{!!iconRight && (
)}
-
+
);
}
diff --git a/src/components/SecondaryNav/secondary-nav.test.tsx b/src/components/SecondaryNav/secondary-nav.test.tsx
index 3d3b1b5704..31eb1bcfef 100644
--- a/src/components/SecondaryNav/secondary-nav.test.tsx
+++ b/src/components/SecondaryNav/secondary-nav.test.tsx
@@ -5,9 +5,9 @@ import type { SecondaryNavItem } from './secondary-nav';
describe('', () => {
const defaultItems: SecondaryNavItem[] = [
- { href: '/a', label: 'Link A' },
- { href: '/b', label: 'Link B', isActive: true },
- { href: '/c', label: 'Link C' },
+ { to: '/a', label: 'Link A' },
+ { to: '/b', label: 'Link B', isActive: true },
+ { to: '/c', label: 'Link C' },
];
it('renders a nav with the default aria-label', () => {
@@ -62,8 +62,8 @@ describe('', () => {
label: 'Parent',
isActive: true,
children: [
- { href: '/child-a', label: 'Child A', isActive: true },
- { href: '/child-b', label: 'Child B' },
+ { to: '/child-a', label: 'Child A', isActive: true },
+ { to: '/child-b', label: 'Child B' },
],
},
];
diff --git a/src/components/SecondaryNav/secondary-nav.tsx b/src/components/SecondaryNav/secondary-nav.tsx
index ba9e1903f2..64d1a12f20 100644
--- a/src/components/SecondaryNav/secondary-nav.tsx
+++ b/src/components/SecondaryNav/secondary-nav.tsx
@@ -5,13 +5,13 @@ import Link from '../Link/link';
import './secondary-nav.scss';
export interface SecondaryNavChildItem {
- href: string;
+ to: string;
label: string;
isActive?: boolean;
}
export interface SecondaryNavItem {
- href?: string;
+ to?: string;
label: string;
/**
* Whether this item is the current page. Ignored when the item has children;
@@ -61,13 +61,13 @@ export const SecondaryNav = ({
return (
- {item.href ? (
+ {item.to ? (
0 ? (
{item.children.map((child) => (
- -
+
-
()({
+ * component: (): JSX.Element => (
+ *
+ *
+ *
+ * )
+ * })
+ *
+ */
+
+export interface DSRContextType {
+ LinkComponent: ComponentType;
+}
+
+export const DSRContext = createContext({
+ LinkComponent: BaseLink,
+});
+
+export const useDSRContext = () => use(DSRContext);
+
+export interface DSRProviderProperties {
+ LinkComponent: ComponentType;
+ children?: ReactNode;
+};
+
+export const DSRProvider = ({ children, LinkComponent }: {
+ children: ReactNode;
+ LinkComponent: ComponentType;
+}) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/index.ts b/src/index.ts
index 3a0810699f..60e9bda147 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,6 +6,7 @@ export { AlertFieldLevel } from './components/Alert/alert-field-level';
export { Banner } from './components/Banner/banner';
export { LanguageLink } from './components/Banner/banner-language-link';
export { Breadcrumb } from './components/Breadcrumb/breadcrumb';
+export type { BreadcrumbCrumb } from './components/Breadcrumb/breadcrumb';
export { Button } from './components/Buttons/button';
export { ButtonGroup } from './components/Buttons/button-group';
export { Checkbox } from './components/Checkbox/checkbox';
@@ -22,6 +23,8 @@ export { default as Hero } from './components/Hero/hero';
export { Icon } from './components/Icon/icon';
export { Label } from './components/Label/label';
export { default as Layout } from './components/Layout/layout';
+export { BaseLink } from './components/Link/base-link';
+export type { BaseLinkProperties } from './components/Link/base-link';
export { default as Link, LinkText, ListLink } from './components/Link/link';
export type { LinkProperties } from './components/Link/link';
export { default as List } from './components/List/list';
@@ -55,3 +58,12 @@ export {
TextIntroductionSubheading,
} from './components/TextIntroduction/text-introduction';
export { WellContainer, WellContent } from './components/Well/well';
+export {
+ useDSRContext,
+ DSRProvider,
+ DSRContext,
+} from './context/dsr-context';
+export type {
+ DSRContextType,
+ DSRProviderProperties}
+from './context/dsr-context';
diff --git a/yarn.lock b/yarn.lock
index a02b223390..d39ce0b4b7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1305,7 +1305,6 @@ __metadata:
"@cfpb/cfpb-design-system": 5.3.2
react: ^19.2.4
react-dom: ^19.2.4
- react-router: ^7.14.0
languageName: unknown
linkType: soft