diff --git a/.changeset/silent-dots-run.md b/.changeset/silent-dots-run.md new file mode 100644 index 00000000000..0fce0295c4a --- /dev/null +++ b/.changeset/silent-dots-run.md @@ -0,0 +1,5 @@ +--- +"@salt-ds/core": minor +--- + +Added `render` prop to `Link`. diff --git a/packages/core/src/__tests__/__e2e__/link/Link.cy.tsx b/packages/core/src/__tests__/__e2e__/link/Link.cy.tsx index d200510d44d..6d8711794bb 100644 --- a/packages/core/src/__tests__/__e2e__/link/Link.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/link/Link.cy.tsx @@ -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( + + Action + , + ); + + cy.mount(); + + 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 = ( + + Action + + ); + + cy.mount(); + + cy.findByTestId(testId).should("exist"); + }); }); diff --git a/packages/core/src/link/Link.tsx b/packages/core/src/link/Link.tsx index 0a86426e100..f2e3a27cef5 100644 --- a/packages/core/src/link/Link.tsx +++ b/packages/core/src/link/Link.tsx @@ -4,9 +4,11 @@ import { useWindow } from "@salt-ds/window"; import { clsx } from "clsx"; import { type ComponentType, type ReactElement, forwardRef } from "react"; import { useIcon } from "../semantic-icon-provider"; -import { Text, type TextProps } from "../text"; +import type { TextProps } from "../text"; import { makePrefixer } from "../utils"; +import type { RenderPropsType } from "../utils"; import linkCss from "./Link.css"; +import { LinkAction } from "./LinkAction"; const withBaseName = makePrefixer("saltLink"); @@ -18,6 +20,10 @@ const withBaseName = makePrefixer("saltLink"); */ export interface LinkProps extends Omit, "as" | "disabled"> { IconComponent?: ComponentType | null; + /** + * Render prop to enable customisation of link element. + */ + render?: RenderPropsType["render"]; } export const Link = forwardRef(function Link( @@ -29,6 +35,7 @@ export const Link = forwardRef(function Link( color: colorProp, variant, target = "_self", + render, ...rest }, ref, @@ -46,13 +53,14 @@ export const Link = forwardRef(function Link( IconComponent === undefined ? ExternalIcon : IconComponent; return ( - {children} @@ -64,6 +72,6 @@ export const Link = forwardRef(function Link( External )} - + ); }); diff --git a/packages/core/src/link/LinkAction.tsx b/packages/core/src/link/LinkAction.tsx new file mode 100644 index 00000000000..15698db5045 --- /dev/null +++ b/packages/core/src/link/LinkAction.tsx @@ -0,0 +1,15 @@ +import type { ComponentPropsWithoutRef } from "react"; +import { Text } from "../text"; +import { renderProps } from "../utils"; + +interface LinkActionProps extends ComponentPropsWithoutRef {} + +export function LinkAction(props: LinkActionProps) { + const { render, ...rest } = props; + + if (render) { + return renderProps("a", props); + } + + return ; +} diff --git a/packages/core/stories/link/link.stories.tsx b/packages/core/stories/link/link.stories.tsx index b61b8c54666..903c69fcf47 100644 --- a/packages/core/stories/link/link.stories.tsx +++ b/packages/core/stories/link/link.stories.tsx @@ -101,3 +101,22 @@ export const Truncation: StoryFn = () => { // // ); // }; + +const CustomLinkImplementation = (props: any) => ( + + Your own Link implementation + +); + +export const RenderElement: StoryFn = () => { + return } />; +}; + +export const RenderProp: StoryFn = () => { + return ( + } + /> + ); +}; diff --git a/site/docs/components/link/examples.mdx b/site/docs/components/link/examples.mdx index a02f7c3c6e6..62c228db9c7 100644 --- a/site/docs/components/link/examples.mdx +++ b/site/docs/components/link/examples.mdx @@ -66,4 +66,20 @@ The default variant is `primary`. + + +## 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. + + + + + +## 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. + + + diff --git a/site/src/examples/link/RenderElement.tsx b/site/src/examples/link/RenderElement.tsx new file mode 100644 index 00000000000..790d610ff3d --- /dev/null +++ b/site/src/examples/link/RenderElement.tsx @@ -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) => ( + + Your own Link implementation + +); + +export const RenderElement = (): ReactElement => { + return ( + } + /> + ); +}; diff --git a/site/src/examples/link/RenderProp.tsx b/site/src/examples/link/RenderProp.tsx new file mode 100644 index 00000000000..48d68017c80 --- /dev/null +++ b/site/src/examples/link/RenderProp.tsx @@ -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) => ( + + Your own Link implementation + +); + +export const RenderProp = (): ReactElement => { + return ( + } + /> + ); +}; diff --git a/site/src/examples/link/index.ts b/site/src/examples/link/index.ts index 8a3cedda73b..cdf3700890c 100644 --- a/site/src/examples/link/index.ts +++ b/site/src/examples/link/index.ts @@ -3,3 +3,5 @@ export * from "./OpenInANewTab"; export * from "./Variant"; export * from "./Color"; export * from "./Visited"; +export * from "./RenderElement"; +export * from "./RenderProp";