From a0562c02c09a677222e6b0b58beda89b75dd3308 Mon Sep 17 00:00:00 2001 From: Liam Smith Date: Sun, 20 Oct 2024 17:37:41 +0100 Subject: [PATCH 1/5] Add render prop to Link # Conflicts: # packages/core/src/link/Link.tsx --- .../src/__tests__/__e2e__/link/Link.cy.tsx | 36 +++++++++++++++++++ packages/core/src/link/Link.tsx | 14 ++++++-- packages/core/src/link/LinkAction.tsx | 15 ++++++++ packages/core/stories/link/link.stories.tsx | 19 ++++++++++ site/docs/components/link/examples.mdx | 16 +++++++++ site/src/examples/link/RenderElement.tsx | 19 ++++++++++ site/src/examples/link/RenderProp.tsx | 19 ++++++++++ site/src/examples/link/index.ts | 2 ++ 8 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/link/LinkAction.tsx create mode 100644 site/src/examples/link/RenderElement.tsx create mode 100644 site/src/examples/link/RenderProp.tsx 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..97911341445 --- /dev/null +++ b/site/src/examples/link/RenderElement.tsx @@ -0,0 +1,19 @@ +import { Link, Text } from "@salt-ds/core"; +import React, { 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..f51185e88b3 --- /dev/null +++ b/site/src/examples/link/RenderProp.tsx @@ -0,0 +1,19 @@ +import { Link, Text } from "@salt-ds/core"; +import React, { 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"; From c6ff605723f4e6065b4ed0f311aa42f7aa7277d1 Mon Sep 17 00:00:00 2001 From: Liam Smith Date: Mon, 21 Oct 2024 11:23:35 +0100 Subject: [PATCH 2/5] Update changeset --- .changeset/silent-dots-run.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/silent-dots-run.md diff --git a/.changeset/silent-dots-run.md b/.changeset/silent-dots-run.md new file mode 100644 index 00000000000..ed1bb8c6b71 --- /dev/null +++ b/.changeset/silent-dots-run.md @@ -0,0 +1,9 @@ +--- +"@salt-ds/core": minor +--- + +Added `render` prop to `Link`. +```tsx +} /> + } /> +``` From 6ae094fc895f0e8d1738eaff7fa1e9de06203bb6 Mon Sep 17 00:00:00 2001 From: Liam Smith Date: Mon, 21 Oct 2024 11:37:20 +0100 Subject: [PATCH 3/5] Fix lint --- .changeset/silent-dots-run.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.changeset/silent-dots-run.md b/.changeset/silent-dots-run.md index ed1bb8c6b71..0fce0295c4a 100644 --- a/.changeset/silent-dots-run.md +++ b/.changeset/silent-dots-run.md @@ -3,7 +3,3 @@ --- Added `render` prop to `Link`. -```tsx -} /> - } /> -``` From 650c3d82301c0c2a51edff4db1d21ce4579d15db Mon Sep 17 00:00:00 2001 From: Liam Smith Date: Mon, 11 Nov 2024 21:45:53 +0000 Subject: [PATCH 4/5] Fix lint --- site/src/examples/link/RenderElement.tsx | 2 +- site/src/examples/link/RenderProp.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/examples/link/RenderElement.tsx b/site/src/examples/link/RenderElement.tsx index 97911341445..790d610ff3d 100644 --- a/site/src/examples/link/RenderElement.tsx +++ b/site/src/examples/link/RenderElement.tsx @@ -1,5 +1,5 @@ import { Link, Text } from "@salt-ds/core"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import styles from "./index.module.css"; const CustomLinkImplementation = (props: any) => ( diff --git a/site/src/examples/link/RenderProp.tsx b/site/src/examples/link/RenderProp.tsx index f51185e88b3..48d68017c80 100644 --- a/site/src/examples/link/RenderProp.tsx +++ b/site/src/examples/link/RenderProp.tsx @@ -1,5 +1,5 @@ import { Link, Text } from "@salt-ds/core"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import styles from "./index.module.css"; const CustomLinkImplementation = (props: any) => ( From ae72662f04dd203bf3c31f8dd0dec4447ab924ae Mon Sep 17 00:00:00 2001 From: Josh Wooding <12938082+joshwooding@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:15:32 +0000 Subject: [PATCH 5/5] Updates --- .changeset/silent-dots-run.md | 2 +- packages/core/src/link/Link.tsx | 30 +++++++++++++++++---------- packages/core/src/link/LinkAction.tsx | 19 +++++++---------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.changeset/silent-dots-run.md b/.changeset/silent-dots-run.md index 0fce0295c4a..c1b9bf76464 100644 --- a/.changeset/silent-dots-run.md +++ b/.changeset/silent-dots-run.md @@ -2,4 +2,4 @@ "@salt-ds/core": minor --- -Added `render` prop to `Link`. +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/link/Link.tsx b/packages/core/src/link/Link.tsx index f2e3a27cef5..274e16f65ec 100644 --- a/packages/core/src/link/Link.tsx +++ b/packages/core/src/link/Link.tsx @@ -2,11 +2,15 @@ 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 type { TextProps } from "../text"; -import { makePrefixer } from "../utils"; -import type { RenderPropsType } from "../utils"; +import { Text, type TextProps } from "../text"; +import { type RenderPropsType, makePrefixer } from "../utils"; import linkCss from "./Link.css"; import { LinkAction } from "./LinkAction"; @@ -18,10 +22,12 @@ 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 link element. + * Render prop to enable customisation of anchor element. */ render?: RenderPropsType["render"]; } @@ -35,7 +41,8 @@ export const Link = forwardRef(function Link( color: colorProp, variant, target = "_self", - render, + styleAs, + maxRows, ...rest }, ref, @@ -53,14 +60,15 @@ export const Link = forwardRef(function Link( IconComponent === undefined ? ExternalIcon : IconComponent; return ( - {children} @@ -72,6 +80,6 @@ export const Link = forwardRef(function Link( External )} - + ); }); diff --git a/packages/core/src/link/LinkAction.tsx b/packages/core/src/link/LinkAction.tsx index 15698db5045..3eae4eaf1d6 100644 --- a/packages/core/src/link/LinkAction.tsx +++ b/packages/core/src/link/LinkAction.tsx @@ -1,15 +1,10 @@ -import type { ComponentPropsWithoutRef } from "react"; -import { Text } from "../text"; +import { type ComponentPropsWithoutRef, forwardRef } from "react"; import { renderProps } from "../utils"; -interface LinkActionProps extends ComponentPropsWithoutRef {} +interface LinkActionProps extends ComponentPropsWithoutRef<"a"> {} -export function LinkAction(props: LinkActionProps) { - const { render, ...rest } = props; - - if (render) { - return renderProps("a", props); - } - - return ; -} +export const LinkAction = forwardRef( + function LinkAction(props, ref) { + return renderProps("a", { ...props, ref }); + }, +);