From 7909300e75dd2072a76339bab2969e953f8aef96 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 17 Mar 2025 06:38:50 +0000 Subject: [PATCH 1/5] Add render prop to Link Signed-off-by: Charles de Dreuille --- packages/canon/src/components/Link/Link.tsx | 35 +++++++++++---------- packages/canon/src/components/Link/types.ts | 1 + 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/canon/src/components/Link/Link.tsx b/packages/canon/src/components/Link/Link.tsx index 8763575bf9..8b88009a86 100644 --- a/packages/canon/src/components/Link/Link.tsx +++ b/packages/canon/src/components/Link/Link.tsx @@ -28,28 +28,31 @@ export const Link = forwardRef((props, ref) => { weight = 'regular', style, className, + render, ...restProps } = props; - // Get the responsive values for the variant and weight const responsiveVariant = useResponsiveValue(variant); const responsiveWeight = useResponsiveValue(weight); - return ( - - {children} - - ); + const linkProps = { + ref, + className: clsx( + 'canon-Link', + responsiveVariant && `canon-Link--variant-${responsiveVariant}`, + responsiveWeight && `canon-Link--weight-${responsiveWeight}`, + className, + ), + style, + children, + ...restProps, + }; + + if (render) { + return render(linkProps); + } + + return ; }); Link.displayName = 'Link'; diff --git a/packages/canon/src/components/Link/types.ts b/packages/canon/src/components/Link/types.ts index 4c5b34e14e..7bde893de2 100644 --- a/packages/canon/src/components/Link/types.ts +++ b/packages/canon/src/components/Link/types.ts @@ -30,4 +30,5 @@ export interface LinkProps weight?: 'regular' | 'bold' | Partial>; className?: string; style?: CSSProperties; + render?: (props: Omit) => ReactNode; } From 4fe5b08db69de12bc690cd013047917658bf1298 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 17 Mar 2025 06:50:25 +0000 Subject: [PATCH 2/5] Create ten-dodos-lead.md Signed-off-by: Charles de Dreuille --- .changeset/ten-dodos-lead.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ten-dodos-lead.md diff --git a/.changeset/ten-dodos-lead.md b/.changeset/ten-dodos-lead.md new file mode 100644 index 0000000000..340c5170cf --- /dev/null +++ b/.changeset/ten-dodos-lead.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': patch +--- + +We added a render prop to the Link component to make sure it can work with React Router. From 02b86027ef03e3e1f90e1d7405384c78ffa2370c Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 17 Mar 2025 15:12:25 +0000 Subject: [PATCH 3/5] Update Link render prop mechanism Signed-off-by: Charles de Dreuille --- .../src/components/Link/Link.stories.tsx | 3 +- packages/canon/src/components/Link/Link.tsx | 78 +++++++++++-------- packages/canon/src/components/Link/types.ts | 12 ++- packages/canon/src/index.ts | 1 + 4 files changed, 58 insertions(+), 36 deletions(-) diff --git a/packages/canon/src/components/Link/Link.stories.tsx b/packages/canon/src/components/Link/Link.stories.tsx index 38e71c310a..856fe2532d 100644 --- a/packages/canon/src/components/Link/Link.stories.tsx +++ b/packages/canon/src/components/Link/Link.stories.tsx @@ -33,7 +33,7 @@ type Story = StoryObj; export const Default: Story = { args: { - href: 'https://canon.backstage.io', + to: 'https://canon.backstage.io', children: 'Sign up for Backstage', }, }; @@ -66,6 +66,7 @@ export const AllWeights: Story = { export const Responsive: Story = { args: { + ...Default.args, variant: { xs: 'label', md: 'body', diff --git a/packages/canon/src/components/Link/Link.tsx b/packages/canon/src/components/Link/Link.tsx index 8b88009a86..e3f636ea06 100644 --- a/packages/canon/src/components/Link/Link.tsx +++ b/packages/canon/src/components/Link/Link.tsx @@ -14,45 +14,59 @@ * limitations under the License. */ -import React, { forwardRef } from 'react'; +import React, { forwardRef, memo, ComponentType } from 'react'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import clsx from 'clsx'; -import type { LinkProps } from './types'; +import type { LinkProps, LinkRenderProps } from './types'; /** @public */ -export const Link = forwardRef((props, ref) => { - const { - children, - variant = 'body', - weight = 'regular', - style, - className, - render, - ...restProps - } = props; - - const responsiveVariant = useResponsiveValue(variant); - const responsiveWeight = useResponsiveValue(weight); - - const linkProps = { - ref, - className: clsx( - 'canon-Link', - responsiveVariant && `canon-Link--variant-${responsiveVariant}`, - responsiveWeight && `canon-Link--weight-${responsiveWeight}`, +export const Link = memo( + forwardRef((props, ref) => { + const { + children, + variant = 'body', + weight = 'regular', + style, className, - ), - style, - children, - ...restProps, - }; + render, + to, + ...restProps + } = props; - if (render) { - return render(linkProps); - } + const responsiveVariant = useResponsiveValue(variant); + const responsiveWeight = useResponsiveValue(weight); - return ; -}); + const linkProps: LinkRenderProps = { + className: clsx( + 'canon-Link', + responsiveVariant && `canon-Link--variant-${responsiveVariant}`, + responsiveWeight && `canon-Link--weight-${responsiveWeight}`, + className, + ), + style, + children, + to, + ...restProps, + }; + + if (render) { + // If render is a component type, wrap it in memo to prevent unnecessary re-renders + if (typeof render === 'function' && !render.length) { + const MemoizedComponent = memo( + render as ComponentType, + ); + return ; + } + // If it's a render function, call it directly + const RenderComponent = render as ( + props: LinkRenderProps, + ) => React.ReactNode; + return ; + } + + return ; + }), +); Link.displayName = 'Link'; diff --git a/packages/canon/src/components/Link/types.ts b/packages/canon/src/components/Link/types.ts index 7bde893de2..134509563a 100644 --- a/packages/canon/src/components/Link/types.ts +++ b/packages/canon/src/components/Link/types.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import type { CSSProperties, ReactNode } from 'react'; +import type { CSSProperties, ReactNode, ComponentType } from 'react'; import type { Breakpoint } from '../../types'; /** @public */ export interface LinkProps - extends React.AnchorHTMLAttributes { + extends Omit, 'to'> { children: ReactNode; + to: string; variant?: | 'subtitle' | 'body' @@ -30,5 +31,10 @@ export interface LinkProps weight?: 'regular' | 'bold' | Partial>; className?: string; style?: CSSProperties; - render?: (props: Omit) => ReactNode; + render?: + | ((props: Omit) => ReactNode) + | ComponentType>; } + +/** @public */ +export type LinkRenderProps = Omit; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index 3c9a28b44d..f30b825c8c 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -42,6 +42,7 @@ export * from './components/Field'; export * from './components/Tooltip'; export * from './components/Menu'; export * from './components/ScrollArea'; +export * from './components/Link'; // Types export * from './types'; From e91f071535069188569161e4a4f36349906ea622 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 17 Mar 2025 15:21:34 +0000 Subject: [PATCH 4/5] Update report.api.md Signed-off-by: Charles de Dreuille --- packages/canon/report.api.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index e993df054b..345907e44c 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import type { ComponentType } from 'react'; import { Context } from 'react'; import type { CSSProperties } from 'react'; import { Field as Field_2 } from '@base-ui-components/react/field'; @@ -703,6 +704,39 @@ export type JustifyContent = | 'around' | 'between'; +// @public (undocumented) +export const Link: React_2.MemoExoticComponent< + React_2.ForwardRefExoticComponent< + LinkProps & React_2.RefAttributes + > +>; + +// @public (undocumented) +export interface LinkProps + extends Omit, 'to'> { + // (undocumented) + children: ReactNode; + // (undocumented) + className?: string; + // (undocumented) + render?: + | ((props: Omit) => ReactNode) + | ComponentType>; + // (undocumented) + style?: CSSProperties; + // (undocumented) + to: string; + // (undocumented) + variant?: + | 'subtitle' + | 'body' + | 'caption' + | 'label' + | Partial>; + // (undocumented) + weight?: 'regular' | 'bold' | Partial>; +} + // @public (undocumented) export const marginPropDefs: (spacingValues: string[]) => { m: { From 8c2a4e95491d678fea4a82955219d8cc06b9702a Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 4 Apr 2025 08:58:07 +0100 Subject: [PATCH 5/5] Move to useRender from BaseUI Signed-off-by: Charles de Dreuille --- packages/canon/report.api.md | 27 ++++++++ .../src/components/Link/Link.stories.tsx | 18 ++++++ packages/canon/src/components/Link/Link.tsx | 63 +++++++------------ packages/canon/src/components/Link/types.ts | 14 ++--- 4 files changed, 73 insertions(+), 49 deletions(-) diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 766b40febd..7335ee93f3 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -15,6 +15,7 @@ import { RefAttributes } from 'react'; import type { RemixiconComponentType } from '@remixicon/react'; import { ScrollArea as ScrollArea_2 } from '@base-ui-components/react/scroll-area'; import { Tooltip as Tooltip_2 } from '@base-ui-components/react/tooltip'; +import type { useRender } from '@base-ui-components/react/use-render'; // @public (undocumented) export type AlignItems = 'stretch' | 'start' | 'center' | 'end'; @@ -696,6 +697,32 @@ export type JustifyContent = | 'around' | 'between'; +// @public (undocumented) +export const Link: React_2.ForwardRefExoticComponent< + Omit & React_2.RefAttributes +>; + +// @public (undocumented) +export interface LinkProps extends useRender.ComponentProps<'a'> { + // (undocumented) + children: ReactNode; + // (undocumented) + className?: string; + // (undocumented) + style?: CSSProperties; + // (undocumented) + to?: string; + // (undocumented) + variant?: + | 'subtitle' + | 'body' + | 'caption' + | 'label' + | Partial>; + // (undocumented) + weight?: 'regular' | 'bold' | Partial>; +} + // @public (undocumented) export const marginPropDefs: (spacingValues: string[]) => { m: { diff --git a/packages/canon/src/components/Link/Link.stories.tsx b/packages/canon/src/components/Link/Link.stories.tsx index 856fe2532d..bdaa4006ae 100644 --- a/packages/canon/src/components/Link/Link.stories.tsx +++ b/packages/canon/src/components/Link/Link.stories.tsx @@ -19,6 +19,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Link } from './Link'; import { Flex } from '../Flex'; import { Text } from '../Text'; +import { Link as RouterLink, MemoryRouter } from 'react-router-dom'; const meta = { title: 'Components/Link', @@ -74,6 +75,23 @@ export const Responsive: Story = { }, }; +export const CustomRender: Story = { + render: () => { + return ( + + }>Go to Catalog + + ); + }, + decorators: [ + Story => ( + + + + ), + ], +}; + export const Playground: Story = { args: { ...Default.args, diff --git a/packages/canon/src/components/Link/Link.tsx b/packages/canon/src/components/Link/Link.tsx index e3f636ea06..e9d0d6aadf 100644 --- a/packages/canon/src/components/Link/Link.tsx +++ b/packages/canon/src/components/Link/Link.tsx @@ -14,59 +14,44 @@ * limitations under the License. */ -import React, { forwardRef, memo, ComponentType } from 'react'; +import React, { forwardRef } from 'react'; +import { useRender } from '@base-ui-components/react/use-render'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import clsx from 'clsx'; -import type { LinkProps, LinkRenderProps } from './types'; +import type { LinkProps } from './types'; /** @public */ -export const Link = memo( - forwardRef((props, ref) => { - const { - children, - variant = 'body', - weight = 'regular', - style, - className, - render, - to, - ...restProps - } = props; +export const Link = forwardRef((props, ref) => { + const { + className, + variant = 'body', + weight = 'regular', + render = , + ...restProps + } = props; - const responsiveVariant = useResponsiveValue(variant); - const responsiveWeight = useResponsiveValue(weight); + const responsiveVariant = useResponsiveValue(variant); + const responsiveWeight = useResponsiveValue(weight); + const internalRef = React.useRef(null); - const linkProps: LinkRenderProps = { + const { renderElement } = useRender({ + render, + props: { className: clsx( 'canon-Link', responsiveVariant && `canon-Link--variant-${responsiveVariant}`, responsiveWeight && `canon-Link--weight-${responsiveWeight}`, className, ), - style, - children, - to, + responsiveVariant, + responsiveWeight, ...restProps, - }; + }, + refs: [ref, internalRef], + }); - if (render) { - // If render is a component type, wrap it in memo to prevent unnecessary re-renders - if (typeof render === 'function' && !render.length) { - const MemoizedComponent = memo( - render as ComponentType, - ); - return ; - } - // If it's a render function, call it directly - const RenderComponent = render as ( - props: LinkRenderProps, - ) => React.ReactNode; - return ; - } - - return ; - }), -); + return renderElement(); +}); Link.displayName = 'Link'; diff --git a/packages/canon/src/components/Link/types.ts b/packages/canon/src/components/Link/types.ts index 134509563a..20fa4a2f31 100644 --- a/packages/canon/src/components/Link/types.ts +++ b/packages/canon/src/components/Link/types.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import type { CSSProperties, ReactNode, ComponentType } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; import type { Breakpoint } from '../../types'; +import type { useRender } from '@base-ui-components/react/use-render'; /** @public */ -export interface LinkProps - extends Omit, 'to'> { +export interface LinkProps extends useRender.ComponentProps<'a'> { children: ReactNode; - to: string; + to?: string; variant?: | 'subtitle' | 'body' @@ -31,10 +31,4 @@ export interface LinkProps weight?: 'regular' | 'bold' | Partial>; className?: string; style?: CSSProperties; - render?: - | ((props: Omit) => ReactNode) - | ComponentType>; } - -/** @public */ -export type LinkRenderProps = Omit;