diff --git a/.changeset/silent-dots-run.md b/.changeset/silent-dots-run.md new file mode 100644 index 00000000000..c1b9bf76464 --- /dev/null +++ b/.changeset/silent-dots-run.md @@ -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. 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..274e16f65ec 100644 --- a/packages/core/src/link/Link.tsx +++ b/packages/core/src/link/Link.tsx @@ -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"); @@ -16,8 +22,14 @@ const withBaseName = makePrefixer("saltLink"); * @example * Action */ -export interface LinkProps extends Omit, "as" | "disabled"> { +export interface LinkProps + extends Omit, "color">, + Pick, "maxRows" | "styleAs" | "color" | "variant"> { IconComponent?: ComponentType | null; + /** + * Render prop to enable customisation of anchor element. + */ + render?: RenderPropsType["render"]; } export const Link = forwardRef(function Link( @@ -29,6 +41,8 @@ export const Link = forwardRef(function Link( color: colorProp, variant, target = "_self", + styleAs, + maxRows, ...rest }, ref, @@ -47,12 +61,14 @@ export const Link = forwardRef(function Link( return ( {children} diff --git a/packages/core/src/link/LinkAction.tsx b/packages/core/src/link/LinkAction.tsx new file mode 100644 index 00000000000..3eae4eaf1d6 --- /dev/null +++ b/packages/core/src/link/LinkAction.tsx @@ -0,0 +1,10 @@ +import { type ComponentPropsWithoutRef, forwardRef } from "react"; +import { renderProps } from "../utils"; + +interface LinkActionProps extends ComponentPropsWithoutRef<"a"> {} + +export const LinkAction = forwardRef( + function LinkAction(props, ref) { + return renderProps("a", { ...props, ref }); + }, +); 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";