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

Add render prop to Link #4325

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/silent-dots-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@salt-ds/core": minor
---

Added `render` prop to `Link`. The `render` prop enables the substitution of the default anchor tag with an alternate link, such as React Router, facilitating integration with routing libraries.
36 changes: 36 additions & 0 deletions packages/core/src/__tests__/__e2e__/link/Link.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,40 @@ describe("GIVEN a link", () => {

cy.findByTestId(/TearOutIcon/i).should("not.exist");
});

it("WHEN `render` is passed a render function, THEN should call `render` to create the element", () => {
const testId = "link-testid";

const mockRender = cy
.stub()
.as("render")
.returns(
<a href="#root" data-testid={testId}>
Action
</a>,
);

cy.mount(<Link href="#root" render={mockRender} />);

cy.findByTestId(testId).should("exist");

cy.get("@render").should("have.been.calledWithMatch", {
className: Cypress.sinon.match.string,
children: Cypress.sinon.match.any,
});
});

it("WHEN `render` is given a JSX element, THEN should merge the props and render the JSX element", () => {
const testId = "link-testid";

const mockRender = (
<a href="#root" data-testid={testId}>
Action
</a>
);

cy.mount(<Link href="#root" render={mockRender} />);

cy.findByTestId(testId).should("exist");
});
});
24 changes: 20 additions & 4 deletions packages/core/src/link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import type { IconProps } from "@salt-ds/icons";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { clsx } from "clsx";
import { type ComponentType, type ReactElement, forwardRef } from "react";
import {
type ComponentPropsWithoutRef,
type ComponentType,
type ReactElement,
forwardRef,
} from "react";
import { useIcon } from "../semantic-icon-provider";
import { Text, type TextProps } from "../text";
import { makePrefixer } from "../utils";
import { type RenderPropsType, makePrefixer } from "../utils";
import linkCss from "./Link.css";
import { LinkAction } from "./LinkAction";

const withBaseName = makePrefixer("saltLink");

Expand All @@ -16,8 +22,14 @@ const withBaseName = makePrefixer("saltLink");
* @example
* <LinkExample to="#link">Action</LinkExample>
*/
export interface LinkProps extends Omit<TextProps<"a">, "as" | "disabled"> {
export interface LinkProps
extends Omit<ComponentPropsWithoutRef<"a">, "color">,
Pick<TextProps<"a">, "maxRows" | "styleAs" | "color" | "variant"> {
IconComponent?: ComponentType<IconProps> | null;
/**
* Render prop to enable customisation of anchor element.
*/
render?: RenderPropsType["render"];
}

export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
Expand All @@ -29,6 +41,8 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
color: colorProp,
variant,
target = "_self",
styleAs,
maxRows,
...rest
},
ref,
Expand All @@ -47,12 +61,14 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(

return (
<Text
as="a"
as={LinkAction}
className={clsx(withBaseName(), className)}
href={href}
ref={ref}
target={target}
color={color}
styleAs={styleAs}
maxRows={maxRows}
{...rest}
>
{children}
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/link/LinkAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { type ComponentPropsWithoutRef, forwardRef } from "react";
import { renderProps } from "../utils";

interface LinkActionProps extends ComponentPropsWithoutRef<"a"> {}

export const LinkAction = forwardRef<HTMLAnchorElement, LinkActionProps>(
function LinkAction(props, ref) {
return renderProps("a", { ...props, ref });
},
);
19 changes: 19 additions & 0 deletions packages/core/stories/link/link.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,22 @@ export const Truncation: StoryFn<typeof Link> = () => {
// </div>
// );
// };

const CustomLinkImplementation = (props: any) => (
<a href="#root" aria-label={"overridden-label"} {...props}>
Your own Link implementation
</a>
);

export const RenderElement: StoryFn<typeof Link> = () => {
return <Link href="#root" render={<CustomLinkImplementation />} />;
};

export const RenderProp: StoryFn<typeof Link> = () => {
return (
<Link
href="#root"
render={(props) => <CustomLinkImplementation {...props} />}
/>
);
};
16 changes: 16 additions & 0 deletions site/docs/components/link/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,20 @@ The default variant is `primary`.

</LivePreview>

<LivePreview componentName="link" exampleName="RenderElement" displayName="Render prop - element">

## Render prop - element

Using the `render` prop, you can customize the element rendered by the Link. Props defined on the JSX element will be merged with props from the Link.

</LivePreview>

<LivePreview componentName="link" exampleName="RenderProp" displayName="Render prop - callback">

## Render prop - callback

The `render` prop can also accept a function. This approach allows more control over how props are merged, allowing for more precise customization of the component's behavior.

</LivePreview>

</LivePreviewControls>
19 changes: 19 additions & 0 deletions site/src/examples/link/RenderElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Link, Text } from "@salt-ds/core";
import type { ReactElement } from "react";
import styles from "./index.module.css";

const CustomLinkImplementation = (props: any) => (
<a aria-label={"overridden-label"} {...props}>
<Text>Your own Link implementation</Text>
</a>
);

export const RenderElement = (): ReactElement => {
return (
<Link
href="#"
className={styles.linkExample}
render={<CustomLinkImplementation />}
/>
);
};
19 changes: 19 additions & 0 deletions site/src/examples/link/RenderProp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Link, Text } from "@salt-ds/core";
import type { ReactElement } from "react";
import styles from "./index.module.css";

const CustomLinkImplementation = (props: any) => (
<a aria-label={"overridden-label"} {...props}>
<Text>Your own Link implementation</Text>
</a>
);

export const RenderProp = (): ReactElement => {
return (
<Link
href="#"
className={styles.linkExample}
render={(props) => <CustomLinkImplementation {...props} />}
/>
);
};
2 changes: 2 additions & 0 deletions site/src/examples/link/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export * from "./OpenInANewTab";
export * from "./Variant";
export * from "./Color";
export * from "./Visited";
export * from "./RenderElement";
export * from "./RenderProp";
Loading