From 4c6d891e5aad36d6e2b415e69602c90addd25826 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 17 Jun 2025 14:22:54 +0100 Subject: [PATCH 01/19] Improve support for button links Signed-off-by: Charles de Dreuille --- .changeset/public-nails-melt.md | 5 + canon-docs/src/content/components/button.mdx | 13 ++ .../src/content/components/button.props.ts | 4 + packages/canon/report.api.md | 47 +++--- .../src/components/Button/Button.props.ts | 41 ------ .../src/components/Button/Button.stories.tsx | 8 +- .../canon/src/components/Button/Button.tsx | 134 ++++++++++-------- .../components/Button/{index.tsx => index.ts} | 7 +- packages/canon/src/components/Button/types.ts | 57 ++++---- 9 files changed, 160 insertions(+), 156 deletions(-) create mode 100644 .changeset/public-nails-melt.md delete mode 100644 packages/canon/src/components/Button/Button.props.ts rename packages/canon/src/components/Button/{index.tsx => index.ts} (76%) diff --git a/.changeset/public-nails-melt.md b/.changeset/public-nails-melt.md new file mode 100644 index 0000000000..a8d916950a --- /dev/null +++ b/.changeset/public-nails-melt.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': minor +--- + +**Breaking** We are removing the render prop on the Button component. If you want to use Button as a link, simply add the href prop and we will automaticlly use it as an anchor tag underneath. diff --git a/canon-docs/src/content/components/button.mdx b/canon-docs/src/content/components/button.mdx index aff74fe60b..dceabde154 100644 --- a/canon-docs/src/content/components/button.mdx +++ b/canon-docs/src/content/components/button.mdx @@ -11,6 +11,7 @@ import { buttonFullWidthSnippet, buttonDisabledSnippet, buttonResponsiveSnippet, + buttonAsLinkSnippet, } from './button.props'; import { ComponentInfos } from '@/components/ComponentInfos'; @@ -102,3 +103,15 @@ Here's a view when buttons are disabled. Here's a view when buttons are responsive. + +### As Link + +Use the `href` prop to make a button act as a link. + +} + code={buttonAsLinkSnippet} +/> diff --git a/canon-docs/src/content/components/button.props.ts b/canon-docs/src/content/components/button.props.ts index fc833a5f73..9cbfc4a80c 100644 --- a/canon-docs/src/content/components/button.props.ts +++ b/canon-docs/src/content/components/button.props.ts @@ -61,3 +61,7 @@ export const buttonDisabledSnippet = ` export const buttonResponsiveSnippet = ``; + +export const buttonAsLinkSnippet = ``; diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 4db3a81bbc..df15e5c20d 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -3,8 +3,10 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnchorHTMLAttributes } from 'react'; import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; +import { ButtonHTMLAttributes } from 'react'; import { ChangeEvent } from 'react'; import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible'; import { ComponentProps } from 'react'; @@ -152,37 +154,36 @@ export const breakpoints: Breakpoint[]; // @public (undocumented) export const Button: ForwardRefExoticComponent< - Omit & RefAttributes + ButtonProps & RefAttributes >; // @public (undocumented) -export type ButtonOwnProps = GetPropDefTypes; +export type ButtonAnchorProps = ButtonCommonProps & + Omit, 'type' | 'onClick'> & { + href: string; + onClick?: React.MouseEventHandler; + }; // @public (undocumented) -export const buttonPropDefs: { - variant: { - type: 'enum'; - values: ('primary' | 'secondary')[]; - className: string; - default: 'primary'; - responsive: true; - }; - size: { - type: 'enum'; - values: ('small' | 'medium')[]; - className: string; - default: 'medium'; - responsive: true; - }; +export type ButtonCommonProps = { + size?: 'small' | 'medium'; + variant?: 'primary' | 'secondary'; + iconStart?: ReactElement; + iconEnd?: ReactElement; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; }; +// @public (undocumented) +export type ButtonNativeProps = ButtonCommonProps & + Omit, 'href'> & { + href?: undefined; + onClick?: React.MouseEventHandler; + }; + // @public -export interface ButtonProps extends useRender.ComponentProps<'button'> { - iconEnd?: ReactElement; - iconStart?: ReactElement; - size?: ButtonOwnProps['size']; - variant?: ButtonOwnProps['variant']; -} +export type ButtonProps = ButtonAnchorProps | ButtonNativeProps; // @public (undocumented) export const Checkbox: ForwardRefExoticComponent< diff --git a/packages/canon/src/components/Button/Button.props.ts b/packages/canon/src/components/Button/Button.props.ts deleted file mode 100644 index e363360afa..0000000000 --- a/packages/canon/src/components/Button/Button.props.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { PropDef, GetPropDefTypes } from '../../props/prop-def'; - -/** @public */ -export const buttonPropDefs = { - variant: { - type: 'enum', - values: ['primary', 'secondary'], - className: 'canon-Button--variant', - default: 'primary', - responsive: true, - }, - size: { - type: 'enum', - values: ['small', 'medium'], - className: 'canon-Button--size', - default: 'medium', - responsive: true, - }, -} satisfies { - variant: PropDef<'primary' | 'secondary'>; - size: PropDef<'small' | 'medium'>; -}; - -/** @public */ -export type ButtonOwnProps = GetPropDefTypes; diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index 2d79da7e29..8c9cc521f0 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -134,13 +134,9 @@ export const Disabled: Story = { export const AsLink: Story = { args: { children: 'I am a link', + href: 'https://canon.backstage.io', + target: '_blank', }, - render: args => ( - + ); + } +}); export default Button; diff --git a/packages/canon/src/components/Button/index.tsx b/packages/canon/src/components/Button/index.ts similarity index 76% rename from packages/canon/src/components/Button/index.tsx rename to packages/canon/src/components/Button/index.ts index 9051f1145e..b9c497ae1f 100644 --- a/packages/canon/src/components/Button/index.tsx +++ b/packages/canon/src/components/Button/index.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Button } from './Button'; -export type { ButtonProps } from './types'; -export { buttonPropDefs } from './Button.props'; -export type { ButtonOwnProps } from './Button.props'; + +export * from './Button'; +export * from './types'; diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts index e91538299e..c7ac2b88b7 100644 --- a/packages/canon/src/components/Button/types.ts +++ b/packages/canon/src/components/Button/types.ts @@ -14,35 +14,40 @@ * limitations under the License. */ -import type { ButtonOwnProps } from './Button.props'; -import { ReactElement } from 'react'; -import type { useRender } from '@base-ui-components/react/use-render'; +import { + ReactElement, + AnchorHTMLAttributes, + ButtonHTMLAttributes, +} from 'react'; + +/** @public */ +export type ButtonCommonProps = { + size?: 'small' | 'medium'; + variant?: 'primary' | 'secondary'; + iconStart?: ReactElement; + iconEnd?: ReactElement; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; +}; + +/** @public */ +export type ButtonAnchorProps = ButtonCommonProps & + Omit, 'type' | 'onClick'> & { + href: string; + onClick?: React.MouseEventHandler; + }; + +/** @public */ +export type ButtonNativeProps = ButtonCommonProps & + Omit, 'href'> & { + href?: undefined; + onClick?: React.MouseEventHandler; + }; /** * Properties for {@link Button} * * @public */ -export interface ButtonProps extends useRender.ComponentProps<'button'> { - /** - * The size of the button - * @defaultValue 'medium' - */ - size?: ButtonOwnProps['size']; - - /** - * The visual variant of the button - * @defaultValue 'primary' - */ - variant?: ButtonOwnProps['variant']; - - /** - * Optional icon to display at the start of the button - */ - iconStart?: ReactElement; - - /** - * Optional icon to display at the end of the button - */ - iconEnd?: ReactElement; -} +export type ButtonProps = ButtonAnchorProps | ButtonNativeProps; From 0a5d96e7bbd1c9c0fd2925c4bc5db48a4af37d9b Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 17 Jun 2025 14:37:57 +0100 Subject: [PATCH 02/19] Fix types Signed-off-by: Charles de Dreuille --- packages/canon/report.api.md | 7 +++++-- packages/canon/src/components/Button/types.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index df15e5c20d..5361e8ef2f 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -166,8 +166,11 @@ export type ButtonAnchorProps = ButtonCommonProps & // @public (undocumented) export type ButtonCommonProps = { - size?: 'small' | 'medium'; - variant?: 'primary' | 'secondary'; + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; iconStart?: ReactElement; iconEnd?: ReactElement; className?: string; diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts index c7ac2b88b7..b4f1df2fad 100644 --- a/packages/canon/src/components/Button/types.ts +++ b/packages/canon/src/components/Button/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Breakpoint } from '@backstage/canon'; import { ReactElement, AnchorHTMLAttributes, @@ -22,8 +23,11 @@ import { /** @public */ export type ButtonCommonProps = { - size?: 'small' | 'medium'; - variant?: 'primary' | 'secondary'; + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; iconStart?: ReactElement; iconEnd?: ReactElement; className?: string; From 078a3f64e5233c19e0c9a0e8b2b8032947853119 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 17 Jun 2025 18:51:10 +0100 Subject: [PATCH 03/19] Update IconButton Signed-off-by: Charles de Dreuille --- packages/canon/report.api.md | 47 ++++++------- .../canon/src/components/Button/Button.tsx | 6 +- .../components/IconButton/IconButton.props.ts | 41 ----------- .../IconButton/IconButton.stories.tsx | 9 +++ .../src/components/IconButton/IconButton.tsx | 68 +++++++++++++------ .../canon/src/components/IconButton/index.tsx | 6 +- .../canon/src/components/IconButton/types.ts | 55 +++++++++------ 7 files changed, 119 insertions(+), 113 deletions(-) delete mode 100644 packages/canon/src/components/IconButton/IconButton.props.ts diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 5361e8ef2f..872719f8c8 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -697,37 +697,38 @@ export const Icon: (props: IconProps) => JSX_2.Element | null; // @public (undocumented) export const IconButton: ForwardRefExoticComponent< - IconButtonProps & RefAttributes + IconButtonProps & RefAttributes >; // @public (undocumented) -export type IconButtonOwnProps = GetPropDefTypes; +export type IconButtonAnchorProps = IconButtonCommonProps & + Omit, 'type' | 'onClick'> & { + href: string; + onClick?: React.MouseEventHandler; + }; // @public (undocumented) -export const iconButtonPropDefs: { - variant: { - type: 'enum'; - values: ('primary' | 'secondary')[]; - className: string; - default: 'primary'; - responsive: true; - }; - size: { - type: 'enum'; - values: ('small' | 'medium')[]; - className: string; - default: 'medium'; - responsive: true; - }; +export type IconButtonCommonProps = { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; }; +// @public (undocumented) +export type IconButtonNativeProps = IconButtonCommonProps & + Omit, 'href'> & { + href?: undefined; + onClick?: React.MouseEventHandler; + }; + // @public -export interface IconButtonProps - extends Omit, 'children'> { - icon: ReactElement; - size?: IconButtonOwnProps['size']; - variant?: IconButtonOwnProps['variant']; -} +export type IconButtonProps = IconButtonAnchorProps | IconButtonNativeProps; // @public (undocumented) export const IconContext: Context; diff --git a/packages/canon/src/components/Button/Button.tsx b/packages/canon/src/components/Button/Button.tsx index aae20c74e4..8693c1e016 100644 --- a/packages/canon/src/components/Button/Button.tsx +++ b/packages/canon/src/components/Button/Button.tsx @@ -33,9 +33,7 @@ export const Button = forwardRef((props, ref) => { ...rest } = props; - function isAnchor(props: { href?: unknown }): props is { href: string } { - return typeof props.href === 'string'; - } + const isAnchor = typeof props.href === 'string'; const responsiveSize = useResponsiveValue(size); const responsiveVariant = useResponsiveValue(variant); @@ -64,7 +62,7 @@ export const Button = forwardRef((props, ref) => { ); - if (isAnchor(props)) { + if (isAnchor) { const { onClick, ...anchorRest } = rest as AnchorHTMLAttributes; return ( diff --git a/packages/canon/src/components/IconButton/IconButton.props.ts b/packages/canon/src/components/IconButton/IconButton.props.ts deleted file mode 100644 index e584997ef5..0000000000 --- a/packages/canon/src/components/IconButton/IconButton.props.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { PropDef, GetPropDefTypes } from '../../props/prop-def'; - -/** @public */ -export const iconButtonPropDefs = { - variant: { - type: 'enum', - values: ['primary', 'secondary'], - className: 'canon-Button--variant', - default: 'primary', - responsive: true, - }, - size: { - type: 'enum', - values: ['small', 'medium'], - className: 'canon-Button--size', - default: 'medium', - responsive: true, - }, -} satisfies { - variant: PropDef<'primary' | 'secondary'>; - size: PropDef<'small' | 'medium'>; -}; - -/** @public */ -export type IconButtonOwnProps = GetPropDefTypes; diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx index 7a7272ad05..97778e56a2 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -83,6 +83,15 @@ export const Disabled: Story = { ), }; +export const AsLink: Story = { + args: { + icon: , + href: 'https://canon.backstage.io', + target: '_blank', + 'aria-label': 'Cloud icon button', + }, +}; + export const Responsive: Story = { args: { icon: , diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/IconButton/IconButton.tsx index fd80c91ddd..e84599874c 100644 --- a/packages/canon/src/components/IconButton/IconButton.tsx +++ b/packages/canon/src/components/IconButton/IconButton.tsx @@ -14,46 +14,74 @@ * limitations under the License. */ -import { forwardRef } from 'react'; +import { AnchorHTMLAttributes, ButtonHTMLAttributes, forwardRef } from 'react'; import clsx from 'clsx'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; - import type { IconButtonProps } from './types'; /** @public */ -export const IconButton = forwardRef( - (props: IconButtonProps, ref) => { +export const IconButton = forwardRef( + (props, ref) => { const { size = 'small', variant = 'primary', icon, className, + href, style, ...rest } = props; + const isAnchor = typeof props.href === 'string'; const responsiveSize = useResponsiveValue(size); const responsiveVariant = useResponsiveValue(variant); - return ( - + {icon} + ); + + if (isAnchor) { + const { onClick, ...anchorRest } = + rest as AnchorHTMLAttributes; + return ( + } + onClick={onClick} + {...anchorRest} + > + {content} + + ); + } else { + const { onClick, ...buttonRest } = + rest as ButtonHTMLAttributes; + return ( + + ); + } }, ); -export default IconButton; +IconButton.displayName = 'IconButton'; diff --git a/packages/canon/src/components/IconButton/index.tsx b/packages/canon/src/components/IconButton/index.tsx index 5f9943557e..1d30c871a4 100644 --- a/packages/canon/src/components/IconButton/index.tsx +++ b/packages/canon/src/components/IconButton/index.tsx @@ -14,7 +14,5 @@ * limitations under the License. */ -export { IconButton } from './IconButton'; -export type { IconButtonProps } from './types'; -export { iconButtonPropDefs } from './IconButton.props'; -export type { IconButtonOwnProps } from './IconButton.props'; +export * from './IconButton'; +export * from './types'; diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/IconButton/types.ts index ece1950d1b..145893991e 100644 --- a/packages/canon/src/components/IconButton/types.ts +++ b/packages/canon/src/components/IconButton/types.ts @@ -14,30 +14,43 @@ * limitations under the License. */ -import type { IconButtonOwnProps } from './IconButton.props'; -import { ReactElement } from 'react'; +import { + AnchorHTMLAttributes, + ButtonHTMLAttributes, + ReactElement, +} from 'react'; +import { Breakpoint } from '@backstage/canon'; + +/** @public */ +export type IconButtonCommonProps = { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; +}; + +/** @public */ +export type IconButtonAnchorProps = IconButtonCommonProps & + Omit, 'type' | 'onClick'> & { + href: string; + onClick?: React.MouseEventHandler; + }; + +/** @public */ +export type IconButtonNativeProps = IconButtonCommonProps & + Omit, 'href'> & { + href?: undefined; + onClick?: React.MouseEventHandler; + }; /** * Properties for {@link IconButton} * * @public */ -export interface IconButtonProps - extends Omit, 'children'> { - /** - * The size of the button - * @defaultValue 'medium' - */ - size?: IconButtonOwnProps['size']; - - /** - * The visual variant of the button - * @defaultValue 'primary' - */ - variant?: IconButtonOwnProps['variant']; - - /** - * Icon to display in the button - */ - icon: ReactElement; -} +export type IconButtonProps = IconButtonAnchorProps | IconButtonNativeProps; From 3c1752b83935110114a8b6abaa913ff6455bc9fc Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 17 Jun 2025 20:24:45 +0100 Subject: [PATCH 04/19] Update Button.tsx Signed-off-by: Charles de Dreuille --- packages/canon/src/components/Button/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/canon/src/components/Button/Button.tsx b/packages/canon/src/components/Button/Button.tsx index 8693c1e016..e293eeb0f9 100644 --- a/packages/canon/src/components/Button/Button.tsx +++ b/packages/canon/src/components/Button/Button.tsx @@ -99,4 +99,4 @@ export const Button = forwardRef((props, ref) => { } }); -export default Button; +Button.displayName = 'Button'; From be519bc5ff0ee2efeb51a38fe8c87ff62489212f Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 18 Jun 2025 12:16:21 +0100 Subject: [PATCH 05/19] Improve types for both Button and IconButton Signed-off-by: Charles de Dreuille --- canon-docs/src/content/components/button.mdx | 2 +- .../src/content/components/button.props.ts | 8 +- packages/canon/report.api.md | 116 ++++++++---------- .../src/components/Button/Button.stories.tsx | 44 ++++--- .../canon/src/components/Button/Button.tsx | 62 +++------- packages/canon/src/components/Button/types.ts | 48 +++----- .../IconButton/IconButton.stories.tsx | 34 +++-- .../src/components/IconButton/IconButton.tsx | 83 ++++--------- .../canon/src/components/IconButton/types.ts | 50 +++----- packages/canon/src/types.ts | 18 +++ 10 files changed, 200 insertions(+), 265 deletions(-) diff --git a/canon-docs/src/content/components/button.mdx b/canon-docs/src/content/components/button.mdx index dceabde154..6690a90e23 100644 --- a/canon-docs/src/content/components/button.mdx +++ b/canon-docs/src/content/components/button.mdx @@ -106,7 +106,7 @@ Here's a view when buttons are responsive. ### As Link -Use the `href` prop to make a button act as a link. +You can use the `as` prop to make a button act as a link. `; -export const buttonAsLinkSnippet = ` + +// Using a custom component +`; diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 872719f8c8..b648025251 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -3,10 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AnchorHTMLAttributes } from 'react'; import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; -import { ButtonHTMLAttributes } from 'react'; import { ChangeEvent } from 'react'; import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible'; import { ComponentProps } from 'react'; @@ -44,6 +42,11 @@ export type ArbitraryStylingPropDef = { parseValue?: (value: string) => string | undefined; }; +// @public (undocumented) +export type AsProp = { + as?: C; +}; + // @public (undocumented) export type AsProps = | 'div' @@ -153,40 +156,26 @@ export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export const breakpoints: Breakpoint[]; // @public (undocumented) -export const Button: ForwardRefExoticComponent< - ButtonProps & RefAttributes ->; - -// @public (undocumented) -export type ButtonAnchorProps = ButtonCommonProps & - Omit, 'type' | 'onClick'> & { - href: string; - onClick?: React.MouseEventHandler; - }; - -// @public (undocumented) -export type ButtonCommonProps = { - size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - iconStart?: ReactElement; - iconEnd?: ReactElement; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; -}; - -// @public (undocumented) -export type ButtonNativeProps = ButtonCommonProps & - Omit, 'href'> & { - href?: undefined; - onClick?: React.MouseEventHandler; - }; +export const Button: ( + props: ButtonProps, +) => JSX_2.Element; // @public -export type ButtonProps = ButtonAnchorProps | ButtonNativeProps; +export type ButtonProps = PolymorphicComponentProp< + C, + { + size?: + | 'small' + | 'medium' + | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + } +>; // @public (undocumented) export const Checkbox: ForwardRefExoticComponent< @@ -696,39 +685,26 @@ export type HeightProps = GetPropDefTypes; export const Icon: (props: IconProps) => JSX_2.Element | null; // @public (undocumented) -export const IconButton: ForwardRefExoticComponent< - IconButtonProps & RefAttributes ->; - -// @public (undocumented) -export type IconButtonAnchorProps = IconButtonCommonProps & - Omit, 'type' | 'onClick'> & { - href: string; - onClick?: React.MouseEventHandler; - }; - -// @public (undocumented) -export type IconButtonCommonProps = { - size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - icon?: ReactElement; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; -}; - -// @public (undocumented) -export type IconButtonNativeProps = IconButtonCommonProps & - Omit, 'href'> & { - href?: undefined; - onClick?: React.MouseEventHandler; - }; +export const IconButton: ( + props: IconButtonProps, +) => JSX_2.Element; // @public -export type IconButtonProps = IconButtonAnchorProps | IconButtonNativeProps; +export type IconButtonProps = + PolymorphicComponentProp< + C, + { + size?: + | 'small' + | 'medium' + | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; + } + >; // @public (undocumented) export const IconContext: Context; @@ -1025,6 +1001,13 @@ export const paddingPropDefs: (spacingValues: string[]) => { // @public (undocumented) export type PaddingProps = GetPropDefTypes; +// @public +export type PolymorphicComponentProp< + C extends React.ElementType, + Props = {}, +> = React.PropsWithChildren> & + Omit, PropsToOmit>; + // @public (undocumented) export const positionPropDefs: { position: { @@ -1041,6 +1024,9 @@ export type PositionProps = GetPropDefTypes; // @public (undocumented) export type PropDef = RegularPropDef | ResponsivePropDef; +// @public (undocumented) +export type PropsToOmit = keyof (AsProp & P); + // @public (undocumented) export type ReactNodePropDef = { type: 'ReactNode'; diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index 8c9cc521f0..30559d2698 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -133,12 +133,26 @@ export const Disabled: Story = { export const AsLink: Story = { args: { - children: 'I am a link', + children: 'Button', href: 'https://canon.backstage.io', target: '_blank', }, }; +export const AsComponent: Story = { + render: () => { + const Link = (props: { children: React.ReactNode; to: string }) => ( + + ); + + return ( + + ); + }, +}; + export const Responsive: Story = { args: { children: 'Button', @@ -167,22 +181,22 @@ export const Playground: Story = { {['small', 'medium'].map(size => ( @@ -190,30 +204,30 @@ export const Playground: Story = { iconStart={} iconEnd={} style={{ width: '200px' }} - variant={variant as ButtonProps['variant']} - size={size as ButtonProps['size']} + variant={variant as ButtonProps['variant']} + size={size as ButtonProps['size']} > Button - ); - } -}); - -Button.displayName = 'Button'; +}; diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts index b4f1df2fad..1ff4e19cb0 100644 --- a/packages/canon/src/components/Button/types.ts +++ b/packages/canon/src/components/Button/types.ts @@ -15,43 +15,23 @@ */ import { Breakpoint } from '@backstage/canon'; -import { - ReactElement, - AnchorHTMLAttributes, - ButtonHTMLAttributes, -} from 'react'; - -/** @public */ -export type ButtonCommonProps = { - size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - iconStart?: ReactElement; - iconEnd?: ReactElement; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; -}; - -/** @public */ -export type ButtonAnchorProps = ButtonCommonProps & - Omit, 'type' | 'onClick'> & { - href: string; - onClick?: React.MouseEventHandler; - }; - -/** @public */ -export type ButtonNativeProps = ButtonCommonProps & - Omit, 'href'> & { - href?: undefined; - onClick?: React.MouseEventHandler; - }; +import { ReactElement } from 'react'; +import { PolymorphicComponentProp } from '../../types'; /** * Properties for {@link Button} * * @public */ -export type ButtonProps = ButtonAnchorProps | ButtonNativeProps; +export type ButtonProps = PolymorphicComponentProp< + C, + { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + } +>; diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx index 97778e56a2..d74e7ebf1a 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -84,11 +84,23 @@ export const Disabled: Story = { }; export const AsLink: Story = { - args: { - icon: , - href: 'https://canon.backstage.io', - target: '_blank', - 'aria-label': 'Cloud icon button', + render: () => ( + } + /> + ), +}; + +export const AsComponent: Story = { + render: () => { + const Link = (props: { children?: React.ReactNode; to: string }) => ( + + ); + + return } />; }, }; @@ -123,22 +135,22 @@ export const Playground: Story = { ['variant']} + size={size as IconButtonProps['size']} /> } aria-label="Chevron right icon button" - variant={variant as IconButtonProps['variant']} - size={size as IconButtonProps['size']} + variant={variant as IconButtonProps['variant']} + size={size as IconButtonProps['size']} /> } aria-label="Chevron right icon button" - variant={variant as IconButtonProps['variant']} - size={size as IconButtonProps['size']} + variant={variant as IconButtonProps['variant']} + size={size as IconButtonProps['size']} /> ))} diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/IconButton/IconButton.tsx index e84599874c..079aa2e6f4 100644 --- a/packages/canon/src/components/IconButton/IconButton.tsx +++ b/packages/canon/src/components/IconButton/IconButton.tsx @@ -14,29 +14,36 @@ * limitations under the License. */ -import { AnchorHTMLAttributes, ButtonHTMLAttributes, forwardRef } from 'react'; import clsx from 'clsx'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import type { IconButtonProps } from './types'; /** @public */ -export const IconButton = forwardRef( - (props, ref) => { - const { - size = 'small', - variant = 'primary', - icon, - className, - href, - style, - ...rest - } = props; +export const IconButton = ( + props: IconButtonProps, +) => { + const { + as, + size = 'small', + variant = 'primary', + icon, + className, + href, + style, + ...rest + } = props; - const isAnchor = typeof props.href === 'string'; - const responsiveSize = useResponsiveValue(size); - const responsiveVariant = useResponsiveValue(variant); + const Component = as || 'button'; + const responsiveSize = useResponsiveValue(size); + const responsiveVariant = useResponsiveValue(variant); - const content = ( + return ( + - ); - - if (isAnchor) { - const { onClick, ...anchorRest } = - rest as AnchorHTMLAttributes; - return ( - } - onClick={onClick} - {...anchorRest} - > - {content} - - ); - } else { - const { onClick, ...buttonRest } = - rest as ButtonHTMLAttributes; - return ( - - ); - } - }, -); - -IconButton.displayName = 'IconButton'; + + ); +}; diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/IconButton/types.ts index 145893991e..f15831e4a4 100644 --- a/packages/canon/src/components/IconButton/types.ts +++ b/packages/canon/src/components/IconButton/types.ts @@ -14,43 +14,27 @@ * limitations under the License. */ -import { - AnchorHTMLAttributes, - ButtonHTMLAttributes, - ReactElement, -} from 'react'; import { Breakpoint } from '@backstage/canon'; - -/** @public */ -export type IconButtonCommonProps = { - size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - icon?: ReactElement; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; -}; - -/** @public */ -export type IconButtonAnchorProps = IconButtonCommonProps & - Omit, 'type' | 'onClick'> & { - href: string; - onClick?: React.MouseEventHandler; - }; - -/** @public */ -export type IconButtonNativeProps = IconButtonCommonProps & - Omit, 'href'> & { - href?: undefined; - onClick?: React.MouseEventHandler; - }; +import { ReactElement } from 'react'; +import { PolymorphicComponentProp } from '../../types'; /** * Properties for {@link IconButton} * * @public */ -export type IconButtonProps = IconButtonAnchorProps | IconButtonNativeProps; +export type IconButtonProps = + PolymorphicComponentProp< + C, + { + size?: + | 'small' + | 'medium' + | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; + } + >; diff --git a/packages/canon/src/types.ts b/packages/canon/src/types.ts index ee5d954576..df3959e368 100644 --- a/packages/canon/src/types.ts +++ b/packages/canon/src/types.ts @@ -131,3 +131,21 @@ export interface UtilityProps extends SpaceProps { justifyContent?: Responsive; rowSpan?: Responsive; } + +/** @public */ +export type AsProp = { + as?: C; +}; + +/** @public */ +export type PropsToOmit = keyof (AsProp & P); + +/** + * This is the first reusable type utility we built + * @public + */ +export type PolymorphicComponentProp< + C extends React.ElementType, + Props = {}, +> = React.PropsWithChildren> & + Omit, PropsToOmit>; From ae661604970c8472a8d6cd06ddf63dab1ac41446 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 18 Jun 2025 12:41:08 +0100 Subject: [PATCH 06/19] Improve docs Signed-off-by: Charles de Dreuille --- .../src/content/components/button.props.ts | 3 ++ .../src/content/components/icon-button.mdx | 13 +++++ .../content/components/icon-button.props.ts | 29 +++++++---- .../IconButton/IconButton.stories.tsx | 50 ++++++------------- 4 files changed, 52 insertions(+), 43 deletions(-) diff --git a/canon-docs/src/content/components/button.props.ts b/canon-docs/src/content/components/button.props.ts index 02ed9e9134..ef72a70596 100644 --- a/canon-docs/src/content/components/button.props.ts +++ b/canon-docs/src/content/components/button.props.ts @@ -14,6 +14,9 @@ export const buttonPropDefs: Record = { default: 'medium', responsive: true, }, + iconStart: { type: 'enum', values: ['ReactNode'], responsive: false }, + iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false }, + children: { type: 'enum', values: ['ReactNode'], responsive: false }, ...classNamePropDefs, ...stylePropDefs, }; diff --git a/canon-docs/src/content/components/icon-button.mdx b/canon-docs/src/content/components/icon-button.mdx index 80e7e5a46d..d8eb3beab2 100644 --- a/canon-docs/src/content/components/icon-button.mdx +++ b/canon-docs/src/content/components/icon-button.mdx @@ -10,6 +10,7 @@ import { iconButtonSizesSnippet, iconButtonDisabledSnippet, iconButtonResponsiveSnippet, + iconButtonAsLinkSnippet, } from './icon-button.props'; import { ComponentInfos } from '@/components/ComponentInfos'; @@ -77,3 +78,15 @@ Here's a view when buttons are disabled. Here's a view when buttons are responsive. + +### As Link + +You can use the `as` prop to make a button act as a link. + +} + code={iconButtonAsLinkSnippet} +/> diff --git a/canon-docs/src/content/components/icon-button.props.ts b/canon-docs/src/content/components/icon-button.props.ts index 8fdfaba6b9..9e3a2818fe 100644 --- a/canon-docs/src/content/components/icon-button.props.ts +++ b/canon-docs/src/content/components/icon-button.props.ts @@ -17,7 +17,7 @@ export const iconButtonPropDefs: Record = { default: 'medium', responsive: true, }, - icon: { type: 'enum', values: 'icon', responsive: false }, + icon: { type: 'enum', values: ['ReactNode'], responsive: false }, ...classNamePropDefs, ...stylePropDefs, }; @@ -27,20 +27,31 @@ export const iconButtonUsageSnippet = `import { IconButton } from '@backstage/ca `; export const iconButtonDefaultSnippet = ` - - + } variant="primary" /> + } variant="secondary" /> `; export const iconButtonVariantsSnippet = ` - - + } variant="primary" /> + } variant="secondary" /> `; export const iconButtonSizesSnippet = ` - - + } size="small" /> + } size="medium" /> `; -export const iconButtonDisabledSnippet = ``; +export const iconButtonDisabledSnippet = `} disabled />`; -export const iconButtonResponsiveSnippet = ``; +export const iconButtonResponsiveSnippet = `} variant={{ initial: 'primary', lg: 'secondary' }} />`; + +export const iconButtonAsLinkSnippet = `// Using the \`as\` prop +} +/> + +// Using a custom component +} />`; diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx index d74e7ebf1a..110db0aa89 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -40,58 +40,44 @@ export default meta; type Story = StoryObj; export const Default: Story = { - args: { - icon: , - }, + render: args => } />, }; export const Variants: Story = { - args: { - ...Default.args, - }, render: args => ( - - + } variant="primary" /> + } variant="secondary" /> ), }; export const Sizes: Story = { - args: { - icon: , - }, render: args => ( - - + } size="small" /> + } size="medium" /> ), }; export const Disabled: Story = { - args: { - icon: , - disabled: true, - 'aria-label': 'Cloud icon button', - }, + args: { disabled: true }, render: args => ( - - + } variant="primary" /> + } variant="secondary" /> ), }; export const AsLink: Story = { - render: () => ( - } - /> - ), + args: { + as: 'a', + href: 'https://canon.backstage.io', + target: '_blank', + }, + render: args => } />, }; export const AsComponent: Story = { @@ -106,8 +92,6 @@ export const AsComponent: Story = { export const Responsive: Story = { args: { - icon: , - 'aria-label': 'Cloud icon button', variant: { initial: 'primary', sm: 'secondary', @@ -117,15 +101,12 @@ export const Responsive: Story = { sm: 'medium', }, }, + render: args => } />, }; const variants: string[] = ['primary', 'secondary']; export const Playground: Story = { - args: { - icon: , - 'aria-label': 'Cloud icon button', - }, render: args => ( {variants.map(variant => ( @@ -137,6 +118,7 @@ export const Playground: Story = { {...args} variant={variant as IconButtonProps['variant']} size={size as IconButtonProps['size']} + icon={} /> Date: Wed, 18 Jun 2025 12:46:12 +0100 Subject: [PATCH 07/19] Update public-nails-melt.md Signed-off-by: Charles de Dreuille --- .changeset/public-nails-melt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/public-nails-melt.md b/.changeset/public-nails-melt.md index a8d916950a..169bf7fbfb 100644 --- a/.changeset/public-nails-melt.md +++ b/.changeset/public-nails-melt.md @@ -2,4 +2,4 @@ '@backstage/canon': minor --- -**Breaking** We are removing the render prop on the Button component. If you want to use Button as a link, simply add the href prop and we will automaticlly use it as an anchor tag underneath. +**Breaking** We are transforming the way we handle custom tags for both Button and IconButton. We are introducing a new as prop for both of these components and we are removing support for the render prop. From 6f9b502232161193be9c6556bdaf46cd18fd4225 Mon Sep 17 00:00:00 2001 From: James Brooks Date: Wed, 18 Jun 2025 17:49:05 +0100 Subject: [PATCH 08/19] Attempting to add forwardRef Signed-off-by: James Brooks --- .../canon/src/components/Button/Button.tsx | 104 ++++++++++-------- packages/canon/src/components/Button/types.ts | 9 +- packages/canon/src/types.ts | 20 ++-- 3 files changed, 75 insertions(+), 58 deletions(-) diff --git a/packages/canon/src/components/Button/Button.tsx b/packages/canon/src/components/Button/Button.tsx index 19e89171e7..905e81111b 100644 --- a/packages/canon/src/components/Button/Button.tsx +++ b/packages/canon/src/components/Button/Button.tsx @@ -15,54 +15,70 @@ */ import clsx from 'clsx'; +import { + ComponentPropsWithRef, + ElementType, + forwardRef, + ReactElement, + Ref, +} from 'react'; +import { Button as RAButton } from 'react-aria-components'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import type { ButtonProps } from './types'; /** @public */ -export const Button = ( - props: ButtonProps, -) => { - const { - as, - size = 'small', - variant = 'primary', - iconStart, - iconEnd, - children, - className, - ...rest - } = props; +export const Button = forwardRef( + (props: ButtonProps, ref: Ref) => { + const { + as, + size = 'small', + variant = 'primary', + iconStart, + iconEnd, + children, + className, + ...rest + } = props; - const Component = as || 'button'; - const responsiveSize = useResponsiveValue(size); - const responsiveVariant = useResponsiveValue(variant); + const Component = as || RAButton; + const responsiveSize = useResponsiveValue(size); + const responsiveVariant = useResponsiveValue(variant); - return ( - - {iconStart && ( - - )} - {children} - {iconEnd && ( - - )} - - ); + return ( + + {iconStart && ( + + )} + {children} + {iconEnd && ( + + )} + + ); + }, +) as { + ( + props: ButtonProps & { ref?: ComponentPropsWithRef['ref'] }, + ): ReactElement; + displayName: string; }; + +Button.displayName = 'Button'; diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts index 1ff4e19cb0..327fa27bb6 100644 --- a/packages/canon/src/components/Button/types.ts +++ b/packages/canon/src/components/Button/types.ts @@ -15,17 +15,18 @@ */ import { Breakpoint } from '@backstage/canon'; -import { ReactElement } from 'react'; -import { PolymorphicComponentProp } from '../../types'; +import { ElementType, ReactElement, ReactNode } from 'react'; +import { PolymorphicComponentProps } from '../../types'; /** * Properties for {@link Button} * * @public */ -export type ButtonProps = PolymorphicComponentProp< - C, +export type ButtonProps = PolymorphicComponentProps< + TAs, { + children?: ReactNode; size?: 'small' | 'medium' | Partial>; variant?: | 'primary' diff --git a/packages/canon/src/types.ts b/packages/canon/src/types.ts index df3959e368..7e542f3635 100644 --- a/packages/canon/src/types.ts +++ b/packages/canon/src/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { ComponentPropsWithoutRef, ElementType } from 'react'; + /** @public */ export type AsProps = | 'div' @@ -133,19 +135,17 @@ export interface UtilityProps extends SpaceProps { } /** @public */ -export type AsProp = { - as?: C; +export type As = { + as?: TAs; }; -/** @public */ -export type PropsToOmit = keyof (AsProp & P); - /** * This is the first reusable type utility we built * @public */ -export type PolymorphicComponentProp< - C extends React.ElementType, - Props = {}, -> = React.PropsWithChildren> & - Omit, PropsToOmit>; +export type PolymorphicComponentProps< + TAs extends ElementType, + TProps = {}, +> = TProps & + As & + Omit, keyof (As & TProps)>; From 13dbfce65e2bb2c7eb95b6c5a2a6586df5999440 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 18 Jun 2025 18:18:27 +0100 Subject: [PATCH 09/19] Fixed IconButton Signed-off-by: Charles de Dreuille --- packages/canon/report.api.md | 57 ++++++++------ .../src/components/Button/Button.stories.tsx | 6 +- .../IconButton/IconButton.stories.tsx | 35 ++++----- .../src/components/IconButton/IconButton.tsx | 75 +++++++++++-------- .../canon/src/components/IconButton/types.ts | 10 +-- 5 files changed, 105 insertions(+), 78 deletions(-) diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index b648025251..96e9e31235 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -5,13 +5,15 @@ ```ts import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; +import { Button as Button_2 } from 'react-aria-components'; import { ChangeEvent } from 'react'; import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible'; import { ComponentProps } from 'react'; -import type { ComponentPropsWithRef } from 'react'; +import { ComponentPropsWithoutRef } from 'react'; +import { ComponentPropsWithRef } from 'react'; import { Context } from 'react'; import type { CSSProperties } from 'react'; -import type { ElementType } from 'react'; +import { ElementType } from 'react'; import { FC } from 'react'; import { FocusEvent as FocusEvent_2 } from 'react'; import { ForwardRefExoticComponent } from 'react'; @@ -43,8 +45,8 @@ export type ArbitraryStylingPropDef = { }; // @public (undocumented) -export type AsProp = { - as?: C; +export type As = { + as?: TAs; }; // @public (undocumented) @@ -156,14 +158,20 @@ export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export const breakpoints: Breakpoint[]; // @public (undocumented) -export const Button: ( - props: ButtonProps, -) => JSX_2.Element; +export const Button: { + ( + props: ButtonProps & { + ref?: ComponentPropsWithRef['ref']; + }, + ): ReactElement; + displayName: string; +}; // @public -export type ButtonProps = PolymorphicComponentProp< - C, +export type ButtonProps = PolymorphicComponentProps< + TAs, { + children?: ReactNode; size?: | 'small' | 'medium' @@ -685,14 +693,19 @@ export type HeightProps = GetPropDefTypes; export const Icon: (props: IconProps) => JSX_2.Element | null; // @public (undocumented) -export const IconButton: ( - props: IconButtonProps, -) => JSX_2.Element; +export const IconButton: { + ( + props: IconButtonProps & { + ref?: ComponentPropsWithRef['ref']; + }, + ): ReactElement; + displayName: string; +}; // @public -export type IconButtonProps = - PolymorphicComponentProp< - C, +export type IconButtonProps = + PolymorphicComponentProps< + TAs, { size?: | 'small' @@ -1002,11 +1015,12 @@ export const paddingPropDefs: (spacingValues: string[]) => { export type PaddingProps = GetPropDefTypes; // @public -export type PolymorphicComponentProp< - C extends React.ElementType, - Props = {}, -> = React.PropsWithChildren> & - Omit, PropsToOmit>; +export type PolymorphicComponentProps< + TAs extends ElementType, + TProps = {}, +> = TProps & + As & + Omit, keyof (As & TProps)>; // @public (undocumented) export const positionPropDefs: { @@ -1024,9 +1038,6 @@ export type PositionProps = GetPropDefTypes; // @public (undocumented) export type PropDef = RegularPropDef | ResponsivePropDef; -// @public (undocumented) -export type PropsToOmit = keyof (AsProp & P); - // @public (undocumented) export type ReactNodePropDef = { type: 'ReactNode'; diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index 30559d2698..7baff061b3 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -212,7 +212,7 @@ export const Playground: Story = { @@ -220,7 +220,7 @@ export const Playground: Story = { iconStart={} variant={variant as ButtonProps['variant']} size={size as ButtonProps['size']} - disabled + isDisabled > Button @@ -228,7 +228,7 @@ export const Playground: Story = { iconEnd={} variant={variant as ButtonProps['variant']} size={size as ButtonProps['size']} - disabled + isDisabled > Button diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx index 110db0aa89..4065b61cb2 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -40,44 +40,45 @@ export default meta; type Story = StoryObj; export const Default: Story = { - render: args => } />, + render: () => } />, }; export const Variants: Story = { - render: args => ( + render: () => ( - } variant="primary" /> - } variant="secondary" /> + } variant="primary" /> + } variant="secondary" /> ), }; export const Sizes: Story = { - render: args => ( + render: () => ( - } size="small" /> - } size="medium" /> + } size="small" /> + } size="medium" /> ), }; export const Disabled: Story = { - args: { disabled: true }, - render: args => ( + render: () => ( - } variant="primary" /> - } variant="secondary" /> + } variant="primary" /> + } variant="secondary" /> ), }; export const AsLink: Story = { - args: { - as: 'a', - href: 'https://canon.backstage.io', - target: '_blank', - }, - render: args => } />, + render: () => ( + } + /> + ), }; export const AsComponent: Story = { diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/IconButton/IconButton.tsx index 079aa2e6f4..dfa06cb612 100644 --- a/packages/canon/src/components/IconButton/IconButton.tsx +++ b/packages/canon/src/components/IconButton/IconButton.tsx @@ -15,42 +15,57 @@ */ import clsx from 'clsx'; +import { + ComponentPropsWithRef, + ElementType, + forwardRef, + ReactElement, + Ref, +} from 'react'; +import { Button as RAButton } from 'react-aria-components'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import type { IconButtonProps } from './types'; /** @public */ -export const IconButton = ( - props: IconButtonProps, -) => { - const { - as, - size = 'small', - variant = 'primary', - icon, - className, - href, - style, - ...rest - } = props; +export const IconButton = forwardRef( + (props: IconButtonProps, ref: Ref) => { + const { + as, + size = 'small', + variant = 'primary', + icon, + className, + style, + ...rest + } = props; - const Component = as || 'button'; - const responsiveSize = useResponsiveValue(size); - const responsiveVariant = useResponsiveValue(variant); + const Component = as || RAButton; + const responsiveSize = useResponsiveValue(size); + const responsiveVariant = useResponsiveValue(variant); - return ( - - - - ); + + + ); + }, +) as { + ( + props: IconButtonProps & { ref?: ComponentPropsWithRef['ref'] }, + ): ReactElement; + displayName: string; }; + +IconButton.displayName = 'IconButton'; diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/IconButton/types.ts index f15831e4a4..1d3deb94c2 100644 --- a/packages/canon/src/components/IconButton/types.ts +++ b/packages/canon/src/components/IconButton/types.ts @@ -15,17 +15,17 @@ */ import { Breakpoint } from '@backstage/canon'; -import { ReactElement } from 'react'; -import { PolymorphicComponentProp } from '../../types'; +import { ElementType, ReactElement } from 'react'; +import { PolymorphicComponentProps } from '../../types'; /** * Properties for {@link IconButton} * * @public */ -export type IconButtonProps = - PolymorphicComponentProp< - C, +export type IconButtonProps = + PolymorphicComponentProps< + TAs, { size?: | 'small' From 67737f222138099a070d8aa31e6e3b57968ff70e Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 18 Jun 2025 18:25:44 +0100 Subject: [PATCH 10/19] Update DataTablePagination.tsx Signed-off-by: Charles de Dreuille --- .../components/DataTable/Pagination/DataTablePagination.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx index 2efa73bf14..452e0c34fd 100644 --- a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx +++ b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx @@ -75,14 +75,14 @@ const DataTablePagination = forwardRef( variant="secondary" size="small" onClick={() => table?.previousPage()} - disabled={!table?.getCanPreviousPage()} + isDisabled={!table?.getCanPreviousPage()} icon={} /> table?.nextPage()} - disabled={!table?.getCanNextPage()} + isDisabled={!table?.getCanNextPage()} icon={} /> From b4db4d190d3bebf0ad999a8051975fc8b213ad8f Mon Sep 17 00:00:00 2001 From: James Brooks Date: Thu, 19 Jun 2025 09:07:55 +0100 Subject: [PATCH 11/19] Improve story types Signed-off-by: James Brooks --- .../IconButton/IconButton.stories.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx index 4065b61cb2..c32ddb878a 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -18,7 +18,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { IconButton } from './IconButton'; import { Flex } from '../Flex'; import { Text } from '../Text'; -import { IconButtonProps } from './types'; import { Icon } from '../Icon'; const meta = { @@ -105,7 +104,8 @@ export const Responsive: Story = { render: args => } />, }; -const variants: string[] = ['primary', 'secondary']; +const variants = ['primary', 'secondary'] as const; +const sizes = ['small', 'medium'] as const; export const Playground: Story = { render: args => ( @@ -113,27 +113,27 @@ export const Playground: Story = { {variants.map(variant => ( {variant} - {['small', 'medium'].map(size => ( + {sizes.map(size => ( ['variant']} - size={size as IconButtonProps['size']} + variant={variant} + size={size} icon={} /> } aria-label="Chevron right icon button" - variant={variant as IconButtonProps['variant']} - size={size as IconButtonProps['size']} + variant={variant} + size={size} /> } aria-label="Chevron right icon button" - variant={variant as IconButtonProps['variant']} - size={size as IconButtonProps['size']} + variant={variant} + size={size} /> ))} From aae98c9747662997d34fa9b9095ab8993ab67cfa Mon Sep 17 00:00:00 2001 From: James Brooks Date: Thu, 19 Jun 2025 09:10:45 +0100 Subject: [PATCH 12/19] Fix class name Signed-off-by: James Brooks --- packages/canon/src/components/IconButton/IconButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/IconButton/IconButton.tsx index dfa06cb612..73c95ee131 100644 --- a/packages/canon/src/components/IconButton/IconButton.tsx +++ b/packages/canon/src/components/IconButton/IconButton.tsx @@ -45,7 +45,7 @@ export const IconButton = forwardRef( return ( Date: Thu, 19 Jun 2025 09:12:33 +0100 Subject: [PATCH 13/19] Improve more story types Signed-off-by: James Brooks --- .../src/components/Button/Button.stories.tsx | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index 7baff061b3..82709f1577 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -18,7 +18,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Button } from './Button'; import { Flex } from '../Flex'; import { Text } from '../Text'; -import { ButtonProps } from './types'; import { Icon } from '../Icon'; const meta = { @@ -167,7 +166,8 @@ export const Responsive: Story = { }, }; -const variants: string[] = ['primary', 'secondary']; +const variants = ['primary', 'secondary'] as const; +const sizes = ['small', 'medium'] as const; export const Playground: Story = { args: { @@ -178,25 +178,22 @@ export const Playground: Story = { {variants.map(variant => ( {variant} - {['small', 'medium'].map(size => ( + {sizes.map(size => ( - @@ -204,30 +201,26 @@ export const Playground: Story = { iconStart={} iconEnd={} style={{ width: '200px' }} - variant={variant as ButtonProps['variant']} - size={size as ButtonProps['size']} + variant={variant} + size={size} > Button - + ), }; export const AsLink: Story = { args: { - children: 'Button', + as: 'a', + children: 'I am a link', href: 'https://canon.backstage.io', target: '_blank', }, From 5c805d338e9ef327e9df4873f1a1278879e20b02 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 15:37:33 +0100 Subject: [PATCH 15/19] Removed support for the as prop Signed-off-by: Charles de Dreuille --- packages/canon/css/button.css | 8 +- packages/canon/css/buttonicon.css | 14 ++ packages/canon/css/components.css | 113 ++-------- packages/canon/css/styles.css | 113 ++-------- packages/canon/report.api.md | 107 ++++----- .../src/components/Button/Button.stories.tsx | 23 -- .../canon/src/components/Button/Button.tsx | 43 +--- .../canon/src/components/Button/styles.css | 10 +- packages/canon/src/components/Button/types.ts | 27 +-- .../ButtonIcon.stories.tsx} | 51 ++--- .../ButtonIcon.tsx} | 41 +--- .../{IconButton => ButtonIcon}/index.tsx | 2 +- .../src/components/ButtonIcon/styles.css | 30 +++ .../{IconButton => ButtonIcon}/types.ts | 29 +-- .../ButtonLink/ButtonLink.stories.tsx | 212 ++++++++++++++++++ .../src/components/ButtonLink/ButtonLink.tsx | 71 ++++++ .../canon/src/components/ButtonLink/index.ts | 18 ++ .../canon/src/components/ButtonLink/types.ts | 35 +++ .../Pagination/DataTablePagination.tsx | 6 +- .../src/components/IconButton/styles.css | 107 --------- packages/canon/src/css/components.css | 2 +- packages/canon/src/index.ts | 2 +- packages/canon/src/types.ts | 18 -- 23 files changed, 528 insertions(+), 554 deletions(-) create mode 100644 packages/canon/css/buttonicon.css rename packages/canon/src/components/{IconButton/IconButton.stories.tsx => ButtonIcon/ButtonIcon.stories.tsx} (69%) rename packages/canon/src/components/{IconButton/IconButton.tsx => ButtonIcon/ButtonIcon.tsx} (59%) rename packages/canon/src/components/{IconButton => ButtonIcon}/index.tsx (95%) create mode 100644 packages/canon/src/components/ButtonIcon/styles.css rename packages/canon/src/components/{IconButton => ButtonIcon}/types.ts (56%) create mode 100644 packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx create mode 100644 packages/canon/src/components/ButtonLink/ButtonLink.tsx create mode 100644 packages/canon/src/components/ButtonLink/index.ts create mode 100644 packages/canon/src/components/ButtonLink/types.ts delete mode 100644 packages/canon/src/components/IconButton/styles.css diff --git a/packages/canon/css/button.css b/packages/canon/css/button.css index 7aaad3ec48..7f15f4a541 100644 --- a/packages/canon/css/button.css +++ b/packages/canon/css/button.css @@ -69,21 +69,21 @@ .canon-Button[data-size="medium"] { font-size: var(--canon-font-size-4); padding: 0 var(--canon-space-3); - height: 40px; + height: 2.5rem; } .canon-Button[data-size="small"] { font-size: var(--canon-font-size-3); padding: 0 var(--canon-space-2); - height: 32px; + height: 2rem; } -.canon-ButtonIcon[data-size="small"], .canon-ButtonIcon[data-size="small"] svg { +.canon-Button[data-size="small"] svg { width: 1rem; height: 1rem; } -.canon-ButtonIcon[data-size="medium"], .canon-ButtonIcon[data-size="medium"] svg { +.canon-Button[data-size="medium"] svg { width: 1.25rem; height: 1.25rem; } diff --git a/packages/canon/css/buttonicon.css b/packages/canon/css/buttonicon.css new file mode 100644 index 0000000000..1a791b481f --- /dev/null +++ b/packages/canon/css/buttonicon.css @@ -0,0 +1,14 @@ +.canon-ButtonIcon { + justify-content: center; + align-items: center; +} + +.canon-ButtonIcon[data-size="small"] { + width: 2rem; + padding: 0; +} + +.canon-ButtonIcon[data-size="medium"] { + width: 2.5rem; + padding: 0; +} diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index d420031ada..327f168696 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -125,25 +125,40 @@ .canon-Button[data-size="medium"] { font-size: var(--canon-font-size-4); padding: 0 var(--canon-space-3); - height: 40px; + height: 2.5rem; } .canon-Button[data-size="small"] { font-size: var(--canon-font-size-3); padding: 0 var(--canon-space-2); - height: 32px; + height: 2rem; } -.canon-ButtonIcon[data-size="small"], .canon-ButtonIcon[data-size="small"] svg { +.canon-Button[data-size="small"] svg { width: 1rem; height: 1rem; } -.canon-ButtonIcon[data-size="medium"], .canon-ButtonIcon[data-size="medium"] svg { +.canon-Button[data-size="medium"] svg { width: 1.25rem; height: 1.25rem; } +.canon-ButtonIcon { + justify-content: center; + align-items: center; +} + +.canon-ButtonIcon[data-size="small"] { + width: 2rem; + padding: 0; +} + +.canon-ButtonIcon[data-size="medium"] { + width: 2.5rem; + padding: 0; +} + .canon-CheckboxRoot { width: 1rem; height: 1rem; @@ -351,96 +366,6 @@ height: 1rem; } -.canon-IconButton { - user-select: none; - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-bold); - cursor: pointer; - border-radius: var(--canon-radius-2); - justify-content: center; - align-items: center; - gap: var(--canon-space-1_5); - border: none; - padding: 0; - display: inline-flex; - - &:disabled { - cursor: not-allowed; - } -} - -.canon-IconButton[data-variant="primary"] { - background-color: var(--canon-bg-solid); - color: var(--canon-fg-solid); - transition: background-color .15s, box-shadow .15s; - - &:hover { - background-color: var(--canon-bg-solid-hover); - } - - &:active { - background-color: var(--canon-bg-solid-pressed); - } - - &:focus-visible { - outline: 2px solid var(--canon-ring); - outline-offset: 2px; - } - - &:disabled { - background-color: var(--canon-bg-solid-disabled); - color: var(--canon-fg-solid-disabled); - } -} - -.canon-IconButton[data-variant="secondary"] { - background-color: var(--canon-bg-surface-1); - box-shadow: inset 0 0 0 1px var(--canon-border); - color: var(--canon-fg-primary); - transition: box-shadow .15s; - - &:hover { - box-shadow: inset 0 0 0 1px var(--canon-border-hover); - } - - &:active { - box-shadow: inset 0 0 0 1px var(--canon-border-pressed); - } - - &:focus-visible { - box-shadow: inset 0 0 0 2px var(--canon-ring); - outline: none; - transition: none; - } - - &:disabled { - box-shadow: inset 0 0 0 1px var(--canon-border-disabled); - color: var(--canon-fg-disabled); - } -} - -.canon-IconButton[data-size="medium"] { - font-size: var(--canon-font-size-4); - width: 40px; - height: 40px; -} - -.canon-IconButton[data-size="small"] { - font-size: var(--canon-font-size-3); - width: 32px; - height: 32px; -} - -.canon-IconButtonIcon[data-size="small"], .canon-IconButtonIcon[data-size="small"] svg { - width: 1rem; - height: 1rem; -} - -.canon-IconButtonIcon[data-size="medium"], .canon-IconButtonIcon[data-size="medium"] svg { - width: 1.25rem; - height: 1.25rem; -} - .canon-Link { font-family: var(--canon-font-regular); color: var(--canon-fg-link); diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index b2d111b185..9f32da5d66 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9349,25 +9349,40 @@ .canon-Button[data-size="medium"] { font-size: var(--canon-font-size-4); padding: 0 var(--canon-space-3); - height: 40px; + height: 2.5rem; } .canon-Button[data-size="small"] { font-size: var(--canon-font-size-3); padding: 0 var(--canon-space-2); - height: 32px; + height: 2rem; } -.canon-ButtonIcon[data-size="small"], .canon-ButtonIcon[data-size="small"] svg { +.canon-Button[data-size="small"] svg { width: 1rem; height: 1rem; } -.canon-ButtonIcon[data-size="medium"], .canon-ButtonIcon[data-size="medium"] svg { +.canon-Button[data-size="medium"] svg { width: 1.25rem; height: 1.25rem; } +.canon-ButtonIcon { + justify-content: center; + align-items: center; +} + +.canon-ButtonIcon[data-size="small"] { + width: 2rem; + padding: 0; +} + +.canon-ButtonIcon[data-size="medium"] { + width: 2.5rem; + padding: 0; +} + .canon-CheckboxRoot { width: 1rem; height: 1rem; @@ -9575,96 +9590,6 @@ height: 1rem; } -.canon-IconButton { - user-select: none; - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-bold); - cursor: pointer; - border-radius: var(--canon-radius-2); - justify-content: center; - align-items: center; - gap: var(--canon-space-1_5); - border: none; - padding: 0; - display: inline-flex; - - &:disabled { - cursor: not-allowed; - } -} - -.canon-IconButton[data-variant="primary"] { - background-color: var(--canon-bg-solid); - color: var(--canon-fg-solid); - transition: background-color .15s, box-shadow .15s; - - &:hover { - background-color: var(--canon-bg-solid-hover); - } - - &:active { - background-color: var(--canon-bg-solid-pressed); - } - - &:focus-visible { - outline: 2px solid var(--canon-ring); - outline-offset: 2px; - } - - &:disabled { - background-color: var(--canon-bg-solid-disabled); - color: var(--canon-fg-solid-disabled); - } -} - -.canon-IconButton[data-variant="secondary"] { - background-color: var(--canon-bg-surface-1); - box-shadow: inset 0 0 0 1px var(--canon-border); - color: var(--canon-fg-primary); - transition: box-shadow .15s; - - &:hover { - box-shadow: inset 0 0 0 1px var(--canon-border-hover); - } - - &:active { - box-shadow: inset 0 0 0 1px var(--canon-border-pressed); - } - - &:focus-visible { - box-shadow: inset 0 0 0 2px var(--canon-ring); - outline: none; - transition: none; - } - - &:disabled { - box-shadow: inset 0 0 0 1px var(--canon-border-disabled); - color: var(--canon-fg-disabled); - } -} - -.canon-IconButton[data-size="medium"] { - font-size: var(--canon-font-size-4); - width: 40px; - height: 40px; -} - -.canon-IconButton[data-size="small"] { - font-size: var(--canon-font-size-3); - width: 32px; - height: 32px; -} - -.canon-IconButtonIcon[data-size="small"], .canon-IconButtonIcon[data-size="small"] svg { - width: 1rem; - height: 1rem; -} - -.canon-IconButtonIcon[data-size="medium"], .canon-IconButtonIcon[data-size="medium"] svg { - width: 1.25rem; - height: 1.25rem; -} - .canon-Link { font-family: var(--canon-font-regular); color: var(--canon-fg-link); diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 96e9e31235..bd0b9cbee0 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -5,15 +5,14 @@ ```ts import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; -import { Button as Button_2 } from 'react-aria-components'; +import { ButtonProps as ButtonProps_2 } from 'react-aria-components'; import { ChangeEvent } from 'react'; import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible'; import { ComponentProps } from 'react'; -import { ComponentPropsWithoutRef } from 'react'; -import { ComponentPropsWithRef } from 'react'; +import type { ComponentPropsWithRef } from 'react'; import { Context } from 'react'; import type { CSSProperties } from 'react'; -import { ElementType } from 'react'; +import type { ElementType } from 'react'; import { FC } from 'react'; import { FocusEvent as FocusEvent_2 } from 'react'; import { ForwardRefExoticComponent } from 'react'; @@ -44,11 +43,6 @@ export type ArbitraryStylingPropDef = { parseValue?: (value: string) => string | undefined; }; -// @public (undocumented) -export type As = { - as?: TAs; -}; - // @public (undocumented) export type AsProps = | 'div' @@ -158,32 +152,44 @@ export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export const breakpoints: Breakpoint[]; // @public (undocumented) -export const Button: { - ( - props: ButtonProps & { - ref?: ComponentPropsWithRef['ref']; - }, - ): ReactElement; - displayName: string; -}; +export const Button: ForwardRefExoticComponent< + ButtonProps & RefAttributes +>; + +// @public (undocumented) +export const ButtonIcon: ForwardRefExoticComponent< + ButtonIconProps & RefAttributes +>; // @public -export type ButtonProps = PolymorphicComponentProps< - TAs, - { - children?: ReactNode; - size?: - | 'small' - | 'medium' - | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - iconStart?: ReactElement; - iconEnd?: ReactElement; - } ->; +export interface ButtonIconProps extends ButtonProps_2 { + // (undocumented) + icon?: ReactElement; + // (undocumented) + size?: 'small' | 'medium' | Partial>; + // (undocumented) + variant?: + | 'primary' + | 'secondary' + | Partial>; +} + +// @public +export interface ButtonProps extends ButtonProps_2 { + // (undocumented) + children?: ReactNode; + // (undocumented) + iconEnd?: ReactElement; + // (undocumented) + iconStart?: ReactElement; + // (undocumented) + size?: 'small' | 'medium' | Partial>; + // (undocumented) + variant?: + | 'primary' + | 'secondary' + | Partial>; +} // @public (undocumented) export const Checkbox: ForwardRefExoticComponent< @@ -692,33 +698,6 @@ export type HeightProps = GetPropDefTypes; // @public (undocumented) export const Icon: (props: IconProps) => JSX_2.Element | null; -// @public (undocumented) -export const IconButton: { - ( - props: IconButtonProps & { - ref?: ComponentPropsWithRef['ref']; - }, - ): ReactElement; - displayName: string; -}; - -// @public -export type IconButtonProps = - PolymorphicComponentProps< - TAs, - { - size?: - | 'small' - | 'medium' - | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - icon?: ReactElement; - } - >; - // @public (undocumented) export const IconContext: Context; @@ -1014,14 +993,6 @@ export const paddingPropDefs: (spacingValues: string[]) => { // @public (undocumented) export type PaddingProps = GetPropDefTypes; -// @public -export type PolymorphicComponentProps< - TAs extends ElementType, - TProps = {}, -> = TProps & - As & - Omit, keyof (As & TProps)>; - // @public (undocumented) export const positionPropDefs: { position: { diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index f28ebccc28..e020ebb64d 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -130,29 +130,6 @@ export const Disabled: Story = { ), }; -export const AsLink: Story = { - args: { - as: 'a', - children: 'I am a link', - href: 'https://canon.backstage.io', - target: '_blank', - }, -}; - -export const AsComponent: Story = { - render: () => { - const Link = (props: { children: React.ReactNode; to: string }) => ( - - ); - - return ( - - ); - }, -}; - export const Responsive: Story = { args: { children: 'Button', diff --git a/packages/canon/src/components/Button/Button.tsx b/packages/canon/src/components/Button/Button.tsx index 905e81111b..9d256a5815 100644 --- a/packages/canon/src/components/Button/Button.tsx +++ b/packages/canon/src/components/Button/Button.tsx @@ -15,22 +15,15 @@ */ import clsx from 'clsx'; -import { - ComponentPropsWithRef, - ElementType, - forwardRef, - ReactElement, - Ref, -} from 'react'; +import { forwardRef, Ref } from 'react'; import { Button as RAButton } from 'react-aria-components'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import type { ButtonProps } from './types'; /** @public */ export const Button = forwardRef( - (props: ButtonProps, ref: Ref) => { + (props: ButtonProps, ref: Ref) => { const { - as, size = 'small', variant = 'primary', iconStart, @@ -40,45 +33,23 @@ export const Button = forwardRef( ...rest } = props; - const Component = as || RAButton; const responsiveSize = useResponsiveValue(size); const responsiveVariant = useResponsiveValue(variant); return ( - - {iconStart && ( - - )} + {iconStart} {children} - {iconEnd && ( - - )} - + {iconEnd} + ); }, -) as { - ( - props: ButtonProps & { ref?: ComponentPropsWithRef['ref'] }, - ): ReactElement; - displayName: string; -}; +); Button.displayName = 'Button'; diff --git a/packages/canon/src/components/Button/styles.css b/packages/canon/src/components/Button/styles.css index a4afd85c9b..a2274fd907 100644 --- a/packages/canon/src/components/Button/styles.css +++ b/packages/canon/src/components/Button/styles.css @@ -85,23 +85,21 @@ .canon-Button[data-size='medium'] { font-size: var(--canon-font-size-4); padding: 0 var(--canon-space-3); - height: 40px; + height: 2.5rem; } .canon-Button[data-size='small'] { font-size: var(--canon-font-size-3); padding: 0 var(--canon-space-2); - height: 32px; + height: 2rem; } -.canon-ButtonIcon[data-size='small'], -.canon-ButtonIcon[data-size='small'] svg { +.canon-Button[data-size='small'] svg { width: 1rem; height: 1rem; } -.canon-ButtonIcon[data-size='medium'], -.canon-ButtonIcon[data-size='medium'] svg { +.canon-Button[data-size='medium'] svg { width: 1.25rem; height: 1.25rem; } diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts index 327fa27bb6..ad89dbfe50 100644 --- a/packages/canon/src/components/Button/types.ts +++ b/packages/canon/src/components/Button/types.ts @@ -15,24 +15,21 @@ */ import { Breakpoint } from '@backstage/canon'; -import { ElementType, ReactElement, ReactNode } from 'react'; -import { PolymorphicComponentProps } from '../../types'; +import { ReactElement, ReactNode } from 'react'; +import { ButtonProps as RAButtonProps } from 'react-aria-components'; /** * Properties for {@link Button} * * @public */ -export type ButtonProps = PolymorphicComponentProps< - TAs, - { - children?: ReactNode; - size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - iconStart?: ReactElement; - iconEnd?: ReactElement; - } ->; +export interface ButtonProps extends RAButtonProps { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + children?: ReactNode; +} diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/ButtonIcon/ButtonIcon.stories.tsx similarity index 69% rename from packages/canon/src/components/IconButton/IconButton.stories.tsx rename to packages/canon/src/components/ButtonIcon/ButtonIcon.stories.tsx index c32ddb878a..c4c811eb92 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/ButtonIcon/ButtonIcon.stories.tsx @@ -15,14 +15,14 @@ */ import type { Meta, StoryObj } from '@storybook/react'; -import { IconButton } from './IconButton'; +import { ButtonIcon } from './ButtonIcon'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { Icon } from '../Icon'; const meta = { - title: 'Components/IconButton', - component: IconButton, + title: 'Components/ButtonIcon', + component: ButtonIcon, argTypes: { size: { control: 'select', @@ -33,20 +33,20 @@ const meta = { options: ['primary', 'secondary'], }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; export const Default: Story = { - render: () => } />, + render: () => } />, }; export const Variants: Story = { render: () => ( - } variant="primary" /> - } variant="secondary" /> + } variant="primary" /> + } variant="secondary" /> ), }; @@ -54,8 +54,8 @@ export const Variants: Story = { export const Sizes: Story = { render: () => ( - } size="small" /> - } size="medium" /> + } size="small" /> + } size="medium" /> ), }; @@ -63,33 +63,12 @@ export const Sizes: Story = { export const Disabled: Story = { render: () => ( - } variant="primary" /> - } variant="secondary" /> + } variant="primary" /> + } variant="secondary" /> ), }; -export const AsLink: Story = { - render: () => ( - } - /> - ), -}; - -export const AsComponent: Story = { - render: () => { - const Link = (props: { children?: React.ReactNode; to: string }) => ( - - ); - - return } />; - }, -}; - export const Responsive: Story = { args: { variant: { @@ -101,7 +80,7 @@ export const Responsive: Story = { sm: 'medium', }, }, - render: args => } />, + render: args => } />, }; const variants = ['primary', 'secondary'] as const; @@ -115,20 +94,20 @@ export const Playground: Story = { {variant} {sizes.map(size => ( - } /> - } aria-label="Chevron right icon button" variant={variant} size={size} /> - } aria-label="Chevron right icon button" diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx similarity index 59% rename from packages/canon/src/components/IconButton/IconButton.tsx rename to packages/canon/src/components/ButtonIcon/ButtonIcon.tsx index 73c95ee131..0fc61acaf7 100644 --- a/packages/canon/src/components/IconButton/IconButton.tsx +++ b/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx @@ -15,22 +15,15 @@ */ import clsx from 'clsx'; -import { - ComponentPropsWithRef, - ElementType, - forwardRef, - ReactElement, - Ref, -} from 'react'; +import { forwardRef, Ref } from 'react'; import { Button as RAButton } from 'react-aria-components'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; -import type { IconButtonProps } from './types'; +import type { ButtonIconProps } from './types'; /** @public */ -export const IconButton = forwardRef( - (props: IconButtonProps, ref: Ref) => { +export const ButtonIcon = forwardRef( + (props: ButtonIconProps, ref: Ref) => { const { - as, size = 'small', variant = 'primary', icon, @@ -39,33 +32,23 @@ export const IconButton = forwardRef( ...rest } = props; - const Component = as || RAButton; const responsiveSize = useResponsiveValue(size); const responsiveVariant = useResponsiveValue(variant); + console.log(clsx('canon-Button', 'canon-ButtonIcon', className)); + return ( - - - + {icon} + ); }, -) as { - ( - props: IconButtonProps & { ref?: ComponentPropsWithRef['ref'] }, - ): ReactElement; - displayName: string; -}; +); -IconButton.displayName = 'IconButton'; +ButtonIcon.displayName = 'ButtonIcon'; diff --git a/packages/canon/src/components/IconButton/index.tsx b/packages/canon/src/components/ButtonIcon/index.tsx similarity index 95% rename from packages/canon/src/components/IconButton/index.tsx rename to packages/canon/src/components/ButtonIcon/index.tsx index 1d30c871a4..686fd844e8 100644 --- a/packages/canon/src/components/IconButton/index.tsx +++ b/packages/canon/src/components/ButtonIcon/index.tsx @@ -14,5 +14,5 @@ * limitations under the License. */ -export * from './IconButton'; +export * from './ButtonIcon'; export * from './types'; diff --git a/packages/canon/src/components/ButtonIcon/styles.css b/packages/canon/src/components/ButtonIcon/styles.css new file mode 100644 index 0000000000..27c96e696e --- /dev/null +++ b/packages/canon/src/components/ButtonIcon/styles.css @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.canon-ButtonIcon { + align-items: center; + justify-content: center; +} + +.canon-ButtonIcon[data-size='small'] { + padding: 0; + width: 2rem; +} + +.canon-ButtonIcon[data-size='medium'] { + padding: 0; + width: 2.5rem; +} diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/ButtonIcon/types.ts similarity index 56% rename from packages/canon/src/components/IconButton/types.ts rename to packages/canon/src/components/ButtonIcon/types.ts index 1d3deb94c2..7f8b845de4 100644 --- a/packages/canon/src/components/IconButton/types.ts +++ b/packages/canon/src/components/ButtonIcon/types.ts @@ -15,26 +15,19 @@ */ import { Breakpoint } from '@backstage/canon'; -import { ElementType, ReactElement } from 'react'; -import { PolymorphicComponentProps } from '../../types'; +import { ReactElement } from 'react'; +import { ButtonProps as RAButtonProps } from 'react-aria-components'; /** - * Properties for {@link IconButton} + * Properties for {@link ButtonIcon} * * @public */ -export type IconButtonProps = - PolymorphicComponentProps< - TAs, - { - size?: - | 'small' - | 'medium' - | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - icon?: ReactElement; - } - >; +export interface ButtonIconProps extends RAButtonProps { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; +} diff --git a/packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx b/packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx new file mode 100644 index 0000000000..db2883c984 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx @@ -0,0 +1,212 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react'; +import { ButtonLink } from './ButtonLink'; +import { Flex } from '../Flex'; +import { Text } from '../Text'; +import { Icon } from '../Icon'; + +const meta = { + title: 'Components/ButtonLink', + component: ButtonLink, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium'], + }, + variant: { + control: 'select', + options: ['primary', 'secondary'], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'Button', + }, +}; + +export const Variants: Story = { + args: { + children: 'Button', + }, + parameters: { + argTypes: { + variant: { + control: false, + }, + }, + }, + render: () => ( + + } variant="primary"> + Button + + } variant="secondary"> + Button + + + ), +}; + +export const Sizes: Story = { + args: { + children: 'Button', + }, + render: () => ( + + }> + Small + + }> + Medium + + + ), +}; + +export const WithIcons: Story = { + args: { + children: 'Button', + }, + render: args => ( + + } /> + } /> + } + iconEnd={} + /> + + ), +}; + +export const FullWidth: Story = { + args: { + children: 'Button', + }, + render: args => ( + + } /> + } /> + } + iconEnd={} + /> + + ), +}; + +export const Disabled: Story = { + render: () => ( + + + Primary + + + Secondary + + + ), +}; + +export const Responsive: Story = { + args: { + children: 'Button', + variant: { + initial: 'primary', + sm: 'secondary', + }, + size: { + xs: 'small', + sm: 'medium', + }, + }, +}; + +const variants = ['primary', 'secondary'] as const; +const sizes = ['small', 'medium'] as const; + +export const Playground: Story = { + args: { + children: 'Button', + }, + render: () => ( + + {variants.map(variant => ( + + {variant} + {sizes.map(size => ( + + + Button + + } + variant={variant} + size={size} + > + Button + + } + variant={variant} + size={size} + > + Button + + } + iconEnd={} + style={{ width: '200px' }} + variant={variant} + size={size} + > + Button + + + Button + + } + variant={variant} + size={size} + isDisabled + > + Button + + } + variant={variant} + size={size} + isDisabled + > + Button + + + ))} + + ))} + + ), +}; diff --git a/packages/canon/src/components/ButtonLink/ButtonLink.tsx b/packages/canon/src/components/ButtonLink/ButtonLink.tsx new file mode 100644 index 0000000000..72eb78efe3 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/ButtonLink.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import clsx from 'clsx'; +import { forwardRef, Ref } from 'react'; +import { Link as RALink } from 'react-aria-components'; +import { useResponsiveValue } from '../../hooks/useResponsiveValue'; +import type { ButtonLinkProps } from './types'; + +/** @public */ +export const ButtonLink = forwardRef( + (props: ButtonLinkProps, ref: Ref) => { + const { + size = 'small', + variant = 'primary', + iconStart, + iconEnd, + children, + className, + ...rest + } = props; + + const responsiveSize = useResponsiveValue(size); + const responsiveVariant = useResponsiveValue(variant); + + return ( + + {iconStart && ( + + )} + {children} + {iconEnd && ( + + )} + + ); + }, +); + +ButtonLink.displayName = 'ButtonLink'; diff --git a/packages/canon/src/components/ButtonLink/index.ts b/packages/canon/src/components/ButtonLink/index.ts new file mode 100644 index 0000000000..bbfc696c67 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './ButtonLink'; +export * from './types'; diff --git a/packages/canon/src/components/ButtonLink/types.ts b/packages/canon/src/components/ButtonLink/types.ts new file mode 100644 index 0000000000..f6524fbe62 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Breakpoint } from '@backstage/canon'; +import { ReactElement, ReactNode } from 'react'; +import { LinkProps as RALinkProps } from 'react-aria-components'; + +/** + * Properties for {@link ButtonLink} + * + * @public + */ +export interface ButtonLinkProps extends RALinkProps { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + children?: ReactNode; +} diff --git a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx index 452e0c34fd..ee9dda7ab5 100644 --- a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx +++ b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx @@ -17,7 +17,7 @@ import { forwardRef } from 'react'; import { Text } from '../../Text'; import { DataTablePaginationProps } from './types'; -import { IconButton } from '../../IconButton'; +import { ButtonIcon } from '../../ButtonIcon'; import clsx from 'clsx'; import { Select } from '../../Select'; import { useDataTable } from '../Root/DataTableRoot'; @@ -71,14 +71,14 @@ const DataTablePagination = forwardRef(
{`${fromCount} - ${toCount} of ${rowCount}`} - table?.previousPage()} isDisabled={!table?.getCanPreviousPage()} icon={} /> - table?.nextPage()} diff --git a/packages/canon/src/components/IconButton/styles.css b/packages/canon/src/components/IconButton/styles.css deleted file mode 100644 index 066b0fbad5..0000000000 --- a/packages/canon/src/components/IconButton/styles.css +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -.canon-IconButton { - border: none; - display: inline-flex; - align-items: center; - justify-content: center; - user-select: none; - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-bold); - padding: 0; - cursor: pointer; - border-radius: var(--canon-radius-2); - gap: var(--canon-space-1_5); - - &:disabled { - cursor: not-allowed; - } -} - -.canon-IconButton[data-variant='primary'] { - background-color: var(--canon-bg-solid); - color: var(--canon-fg-solid); - transition: background-color 150ms ease, box-shadow 150ms ease; - - &:hover { - background-color: var(--canon-bg-solid-hover); - } - - &:active { - background-color: var(--canon-bg-solid-pressed); - } - - &:focus-visible { - outline: 2px solid var(--canon-ring); - outline-offset: 2px; - } - - &:disabled { - background-color: var(--canon-bg-solid-disabled); - color: var(--canon-fg-solid-disabled); - } -} - -.canon-IconButton[data-variant='secondary'] { - background-color: var(--canon-bg-surface-1); - box-shadow: inset 0 0 0 1px var(--canon-border); - color: var(--canon-fg-primary); - transition: box-shadow 150ms ease; - - &:hover { - box-shadow: inset 0 0 0 1px var(--canon-border-hover); - } - - &:active { - box-shadow: inset 0 0 0 1px var(--canon-border-pressed); - } - - &:focus-visible { - outline: none; - transition: none; - box-shadow: inset 0 0 0 2px var(--canon-ring); - } - - &:disabled { - box-shadow: inset 0 0 0 1px var(--canon-border-disabled); - color: var(--canon-fg-disabled); - } -} - -.canon-IconButton[data-size='medium'] { - font-size: var(--canon-font-size-4); - height: 40px; - width: 40px; -} - -.canon-IconButton[data-size='small'] { - font-size: var(--canon-font-size-3); - height: 32px; - width: 32px; -} - -.canon-IconButtonIcon[data-size='small'], -.canon-IconButtonIcon[data-size='small'] svg { - width: 1rem; - height: 1rem; -} - -.canon-IconButtonIcon[data-size='medium'], -.canon-IconButtonIcon[data-size='medium'] svg { - width: 1.25rem; - height: 1.25rem; -} diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index c85c1bbdd9..aa453f7314 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -17,6 +17,7 @@ @import '../components/Avatar/Avatar.styles.css'; @import '../components/Box/styles.css'; @import '../components/Button/styles.css'; +@import '../components/ButtonIcon/styles.css'; @import '../components/Checkbox/styles.css'; @import '../components/Collapsible/Collapsible.styles.css'; @import '../components/Container/styles.css'; @@ -27,7 +28,6 @@ @import '../components/Grid/styles.css'; @import '../components/Heading/styles.css'; @import '../components/Icon/styles.css'; -@import '../components/IconButton/styles.css'; @import '../components/Link/styles.css'; @import '../components/Menu/Menu.styles.css'; @import '../components/Table/styles.css'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index 9b5a454254..69c302bdc5 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -38,7 +38,7 @@ export * from './components/Collapsible'; export * from './components/DataTable'; export * from './components/FieldLabel'; export * from './components/Icon'; -export * from './components/IconButton'; +export * from './components/ButtonIcon'; export * from './components/Checkbox'; export * from './components/Table'; export * from './components/Tabs'; diff --git a/packages/canon/src/types.ts b/packages/canon/src/types.ts index 7e542f3635..ee5d954576 100644 --- a/packages/canon/src/types.ts +++ b/packages/canon/src/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { ComponentPropsWithoutRef, ElementType } from 'react'; - /** @public */ export type AsProps = | 'div' @@ -133,19 +131,3 @@ export interface UtilityProps extends SpaceProps { justifyContent?: Responsive; rowSpan?: Responsive; } - -/** @public */ -export type As = { - as?: TAs; -}; - -/** - * This is the first reusable type utility we built - * @public - */ -export type PolymorphicComponentProps< - TAs extends ElementType, - TProps = {}, -> = TProps & - As & - Omit, keyof (As & TProps)>; From 65993b0a74ed110e4a5456645ae40e40b1f14803 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 17:17:38 +0100 Subject: [PATCH 16/19] Improve docs Signed-off-by: Charles de Dreuille --- .../src/components/Sidebar/Sidebar.module.css | 4 - .../src/components/Sidebar/playground.tsx | 5 +- .../src/content/components/button-icon.mdx | 80 ++++++++++++++++ .../content/components/button-icon.props.ts | 59 ++++++++++++ .../src/content/components/button-link.mdx | 91 ++++++++++++++++++ .../content/components/button-link.props.ts | 73 +++++++++++++++ canon-docs/src/content/components/button.mdx | 19 +--- .../src/content/components/button.props.ts | 36 ++++---- .../src/content/components/icon-button.mdx | 92 ------------------- .../content/components/icon-button.props.ts | 57 ------------ canon-docs/src/snippets/stories-snippets.tsx | 36 +++++--- canon-docs/src/utils/data.ts | 15 ++- .../src/components/ButtonIcon/ButtonIcon.tsx | 2 - .../ButtonLink/ButtonLink.stories.tsx | 24 ++--- .../src/components/ButtonLink/ButtonLink.tsx | 20 +--- 15 files changed, 372 insertions(+), 241 deletions(-) create mode 100644 canon-docs/src/content/components/button-icon.mdx create mode 100644 canon-docs/src/content/components/button-icon.props.ts create mode 100644 canon-docs/src/content/components/button-link.mdx create mode 100644 canon-docs/src/content/components/button-link.props.ts delete mode 100644 canon-docs/src/content/components/icon-button.mdx delete mode 100644 canon-docs/src/content/components/icon-button.props.ts diff --git a/canon-docs/src/components/Sidebar/Sidebar.module.css b/canon-docs/src/components/Sidebar/Sidebar.module.css index e558621670..4eb49682df 100644 --- a/canon-docs/src/components/Sidebar/Sidebar.module.css +++ b/canon-docs/src/components/Sidebar/Sidebar.module.css @@ -55,10 +55,6 @@ gap: 2px; } -.playgroundSection { - position: absolute; -} - .sectionTitle { font-size: var(--canon-font-size-3); font-weight: var(--canon-font-weight-bold); diff --git a/canon-docs/src/components/Sidebar/playground.tsx b/canon-docs/src/components/Sidebar/playground.tsx index eb1c3213cc..2aaa4fe2c6 100644 --- a/canon-docs/src/components/Sidebar/playground.tsx +++ b/canon-docs/src/components/Sidebar/playground.tsx @@ -38,9 +38,7 @@ export const Playground = () => { return ( { visibility: isPlayground ? 'visible' : 'hidden', }} transition={{ duration: 0.2 }} + style={{ position: 'absolute' }} >
Components
{components.map(({ slug, title }) => ( diff --git a/canon-docs/src/content/components/button-icon.mdx b/canon-docs/src/content/components/button-icon.mdx new file mode 100644 index 0000000000..2281814a89 --- /dev/null +++ b/canon-docs/src/content/components/button-icon.mdx @@ -0,0 +1,80 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { CodeBlock } from '@/components/CodeBlock'; +import { ButtonIconSnippet } from '@/snippets/stories-snippets'; +import { + buttonIconPropDefs, + buttonIconUsageSnippet, + buttonIconDefaultSnippet, + buttonIconVariantsSnippet, + buttonIconSizesSnippet, + buttonIconDisabledSnippet, + buttonIconResponsiveSnippet, + buttonIconAsLinkSnippet, +} from './button-icon.props'; +import { ComponentInfos } from '@/components/ComponentInfos'; + +# Icon Button + +A button component with a single icon that can be used to trigger actions. + +} + code={buttonIconDefaultSnippet} +/> + + + +## API reference + + + +## Examples + +### Variants + +Here's a view when buttons have different variants. + +} + code={buttonIconVariantsSnippet} +/> + +### Sizes + +Here's a view when buttons have different sizes. + +} + code={buttonIconSizesSnippet} +/> + +### Disabled + +Here's a view when buttons are disabled. + +} + code={buttonIconDisabledSnippet} +/> + +### Responsive + +Here's a view when buttons are responsive. + + diff --git a/canon-docs/src/content/components/button-icon.props.ts b/canon-docs/src/content/components/button-icon.props.ts new file mode 100644 index 0000000000..160690a29e --- /dev/null +++ b/canon-docs/src/content/components/button-icon.props.ts @@ -0,0 +1,59 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const buttonIconPropDefs: Record = { + variant: { + type: 'enum', + values: ['primary', 'secondary'], + default: 'primary', + responsive: true, + }, + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'medium', + responsive: true, + }, + icon: { type: 'enum', values: ['ReactNode'], responsive: false }, + isDisabled: { type: 'boolean', default: 'false', responsive: false }, + type: { + type: 'enum', + values: ['button', 'submit', 'reset'], + default: 'button', + responsive: false, + }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/canon'; + +`; + +export const buttonIconDefaultSnippet = ` + } variant="primary" /> + } variant="secondary" /> +`; + +export const buttonIconVariantsSnippet = ` + } variant="primary" /> + } variant="secondary" /> +`; + +export const buttonIconSizesSnippet = ` + } size="small" /> + } size="medium" /> +`; + +export const buttonIconDisabledSnippet = `} disabled />`; + +export const buttonIconResponsiveSnippet = `} variant={{ initial: 'primary', lg: 'secondary' }} />`; + +export const buttonIconAsLinkSnippet = `import { ButtonLink } from '@backstage/canon'; + + + Button +`; diff --git a/canon-docs/src/content/components/button-link.mdx b/canon-docs/src/content/components/button-link.mdx new file mode 100644 index 0000000000..3f24874220 --- /dev/null +++ b/canon-docs/src/content/components/button-link.mdx @@ -0,0 +1,91 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { CodeBlock } from '@/components/CodeBlock'; +import { ButtonLinkSnippet } from '@/snippets/stories-snippets'; +import { + buttonLinkPropDefs, + buttonLinkSnippetUsage, + buttonLinkVariantsSnippet, + buttonLinkSizesSnippet, + buttonLinkIconsSnippet, + buttonLinkDisabledSnippet, + buttonLinkResponsiveSnippet, +} from './button-link.props'; +import { ComponentInfos } from '@/components/ComponentInfos'; + +# ButtonLink + +A button component that can be used as a link. + +} + code={buttonLinkVariantsSnippet} +/> + + + +## API reference + + + +## Examples + +### Variants + +Here's a view when buttons have different variants. + +} + code={buttonLinkVariantsSnippet} +/> + +### Sizes + +Here's a view when buttons have different sizes. + +} + code={buttonLinkSizesSnippet} +/> + +### With Icons + +Here's a view when buttons have icons. + +} + code={buttonLinkIconsSnippet} +/> + +### Disabled + +Here's a view when buttons are disabled. + +} + code={buttonLinkDisabledSnippet} +/> + +### Responsive + +Here's a view when buttons are responsive. + + diff --git a/canon-docs/src/content/components/button-link.props.ts b/canon-docs/src/content/components/button-link.props.ts new file mode 100644 index 0000000000..94d696b736 --- /dev/null +++ b/canon-docs/src/content/components/button-link.props.ts @@ -0,0 +1,73 @@ +import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs'; +import type { PropDef } from '../../utils/propDefs'; + +export const buttonLinkPropDefs: Record = { + variant: { + type: 'enum', + values: ['primary', 'secondary'], + default: 'primary', + responsive: true, + }, + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'medium', + responsive: true, + }, + iconStart: { type: 'enum', values: ['ReactNode'], responsive: false }, + iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false }, + isDisabled: { type: 'boolean', default: 'false', responsive: false }, + href: { type: 'string', responsive: false }, + hrefLang: { type: 'string', responsive: false }, + target: { + type: 'enum', + values: ['HTMLAttributeAnchorTarget'], + default: '_self', + responsive: false, + }, + rel: { type: 'string', responsive: false }, + children: { type: 'enum', values: ['ReactNode'], responsive: false }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const buttonLinkSnippetUsage = `import { ButtonLink } from '@backstage/canon'; + +`; + +export const buttonLinkVariantsSnippet = ` + + Button + + + Button + +`; + +export const buttonLinkSizesSnippet = ` + Small + Medium +`; + +export const buttonLinkIconsSnippet = ` + }>Button + }>Button + } + iconEnd={}> + Button + +`; + +export const buttonLinkDisabledSnippet = ` + + Primary + + + Secondary + +`; + +export const buttonLinkResponsiveSnippet = ` + Responsive Button +`; diff --git a/canon-docs/src/content/components/button.mdx b/canon-docs/src/content/components/button.mdx index 6690a90e23..89d3c3f612 100644 --- a/canon-docs/src/content/components/button.mdx +++ b/canon-docs/src/content/components/button.mdx @@ -1,14 +1,13 @@ import { PropsTable } from '@/components/PropsTable'; import { Snippet } from '@/components/Snippet'; import { CodeBlock } from '@/components/CodeBlock'; -import { ButtonSnippet } from '@/snippets/stories-snippets'; +import { ButtonSnippet, ButtonLinkSnippet } from '@/snippets/stories-snippets'; import { buttonPropDefs, buttonSnippetUsage, buttonVariantsSnippet, buttonSizesSnippet, buttonIconsSnippet, - buttonFullWidthSnippet, buttonDisabledSnippet, buttonResponsiveSnippet, buttonAsLinkSnippet, @@ -74,18 +73,6 @@ Here's a view when buttons have icons. code={buttonIconsSnippet} /> -### Full width - -Here's a view when buttons are full width. - -} - code={buttonFullWidthSnippet} -/> - ### Disabled Here's a view when buttons are disabled. @@ -106,12 +93,12 @@ Here's a view when buttons are responsive. ### As Link -You can use the `as` prop to make a button act as a link. +If you want to use a button as a link, please use the `ButtonLink` component. } + preview={} code={buttonAsLinkSnippet} /> diff --git a/canon-docs/src/content/components/button.props.ts b/canon-docs/src/content/components/button.props.ts index ef72a70596..b32cba4725 100644 --- a/canon-docs/src/content/components/button.props.ts +++ b/canon-docs/src/content/components/button.props.ts @@ -16,7 +16,14 @@ export const buttonPropDefs: Record = { }, iconStart: { type: 'enum', values: ['ReactNode'], responsive: false }, iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false }, + isDisabled: { type: 'boolean', default: 'false', responsive: false }, children: { type: 'enum', values: ['ReactNode'], responsive: false }, + type: { + type: 'enum', + values: ['button', 'submit', 'reset'], + default: 'button', + responsive: false, + }, ...classNamePropDefs, ...stylePropDefs, }; @@ -32,9 +39,6 @@ export const buttonVariantsSnippet = ` - `; export const buttonSizesSnippet = ` @@ -43,20 +47,16 @@ export const buttonSizesSnippet = ` `; export const buttonIconsSnippet = ` - - - -`; - -export const buttonFullWidthSnippet = ` - + + + `; export const buttonDisabledSnippet = ` - - `; @@ -65,12 +65,8 @@ export const buttonResponsiveSnippet = ` +export const buttonAsLinkSnippet = `import { ButtonLink } from '@backstage/canon'; -// Using a custom component -`; + + Button +`; diff --git a/canon-docs/src/content/components/icon-button.mdx b/canon-docs/src/content/components/icon-button.mdx deleted file mode 100644 index d8eb3beab2..0000000000 --- a/canon-docs/src/content/components/icon-button.mdx +++ /dev/null @@ -1,92 +0,0 @@ -import { PropsTable } from '@/components/PropsTable'; -import { Snippet } from '@/components/Snippet'; -import { CodeBlock } from '@/components/CodeBlock'; -import { IconButtonSnippet } from '@/snippets/stories-snippets'; -import { - iconButtonPropDefs, - iconButtonUsageSnippet, - iconButtonDefaultSnippet, - iconButtonVariantsSnippet, - iconButtonSizesSnippet, - iconButtonDisabledSnippet, - iconButtonResponsiveSnippet, - iconButtonAsLinkSnippet, -} from './icon-button.props'; -import { ComponentInfos } from '@/components/ComponentInfos'; - -# Icon Button - -A button component with a single icon that can be used to trigger actions. - -} - code={iconButtonDefaultSnippet} -/> - - - -## API reference - - - -## Examples - -### Variants - -Here's a view when buttons have different variants. - -} - code={iconButtonVariantsSnippet} -/> - -### Sizes - -Here's a view when buttons have different sizes. - -} - code={iconButtonSizesSnippet} -/> - -### Disabled - -Here's a view when buttons are disabled. - -} - code={iconButtonDisabledSnippet} -/> - -### Responsive - -Here's a view when buttons are responsive. - - - -### As Link - -You can use the `as` prop to make a button act as a link. - -} - code={iconButtonAsLinkSnippet} -/> diff --git a/canon-docs/src/content/components/icon-button.props.ts b/canon-docs/src/content/components/icon-button.props.ts deleted file mode 100644 index 9e3a2818fe..0000000000 --- a/canon-docs/src/content/components/icon-button.props.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const iconButtonPropDefs: Record = { - variant: { - type: 'enum', - values: ['primary', 'secondary'], - default: 'primary', - responsive: true, - }, - size: { - type: 'enum', - values: ['small', 'medium'], - default: 'medium', - responsive: true, - }, - icon: { type: 'enum', values: ['ReactNode'], responsive: false }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const iconButtonUsageSnippet = `import { IconButton } from '@backstage/canon'; - -`; - -export const iconButtonDefaultSnippet = ` - } variant="primary" /> - } variant="secondary" /> -`; - -export const iconButtonVariantsSnippet = ` - } variant="primary" /> - } variant="secondary" /> -`; - -export const iconButtonSizesSnippet = ` - } size="small" /> - } size="medium" /> -`; - -export const iconButtonDisabledSnippet = `} disabled />`; - -export const iconButtonResponsiveSnippet = `} variant={{ initial: 'primary', lg: 'secondary' }} />`; - -export const iconButtonAsLinkSnippet = `// Using the \`as\` prop -} -/> - -// Using a custom component -} />`; diff --git a/canon-docs/src/snippets/stories-snippets.tsx b/canon-docs/src/snippets/stories-snippets.tsx index 95738a6330..e5a7464b43 100644 --- a/canon-docs/src/snippets/stories-snippets.tsx +++ b/canon-docs/src/snippets/stories-snippets.tsx @@ -3,11 +3,12 @@ import { composeStories } from '@storybook/react'; import * as BoxStories from '../../../packages/canon/src/components/Box/Box.stories'; import * as ButtonStories from '../../../packages/canon/src/components/Button/Button.stories'; +import * as ButtonIconStories from '../../../packages/canon/src/components/ButtonIcon/ButtonIcon.stories'; +import * as ButtonLinkStories from '../../../packages/canon/src/components/ButtonLink/ButtonLink.stories'; import * as CheckboxStories from '../../../packages/canon/src/components/Checkbox/Checkbox.stories'; import * as ContainerStories from '../../../packages/canon/src/components/Container/Container.stories'; import * as GridStories from '../../../packages/canon/src/components/Grid/Grid.stories'; import * as HeadingStories from '../../../packages/canon/src/components/Heading/Heading.stories'; -import * as IconButtonStories from '../../../packages/canon/src/components/IconButton/IconButton.stories'; import * as IconStories from '../../../packages/canon/src/components/Icon/Icon.stories'; import * as TextFieldStories from '../../../packages/canon/src/components/TextField/TextField.stories'; import * as TextStories from '../../../packages/canon/src/components/Text/Text.stories'; @@ -38,6 +39,28 @@ export const ButtonSnippet = ({ return StoryComponent ? : null; }; +export const ButtonIconSnippet = ({ + story, +}: { + story: keyof typeof ButtonIconStories; +}) => { + const stories = composeStories(ButtonIconStories); + const StoryComponent = stories[story as keyof typeof stories]; + + return StoryComponent ? : null; +}; + +export const ButtonLinkSnippet = ({ + story, +}: { + story: keyof typeof ButtonLinkStories; +}) => { + const stories = composeStories(ButtonLinkStories); + const StoryComponent = stories[story as keyof typeof stories]; + + return StoryComponent ? : null; +}; + export const CheckboxSnippet = ({ story, }: { @@ -96,17 +119,6 @@ export const HeadingSnippet = ({ return StoryComponent ? : null; }; -export const IconButtonSnippet = ({ - story, -}: { - story: keyof typeof IconButtonStories; -}) => { - const stories = composeStories(IconButtonStories); - const StoryComponent = stories[story as keyof typeof stories]; - - return StoryComponent ? : null; -}; - export const IconSnippet = ({ story }: { story: keyof typeof IconStories }) => { const stories = composeStories(IconStories); const StoryComponent = stories[story as keyof typeof stories]; diff --git a/canon-docs/src/utils/data.ts b/canon-docs/src/utils/data.ts index 20ff9146a7..a741ad5160 100644 --- a/canon-docs/src/utils/data.ts +++ b/canon-docs/src/utils/data.ts @@ -76,6 +76,16 @@ export const components: Page[] = [ slug: 'button', status: 'alpha', }, + { + title: 'ButtonIcon', + slug: 'button-icon', + status: 'alpha', + }, + { + title: 'ButtonLink', + slug: 'button-link', + status: 'alpha', + }, { title: 'Checkbox', slug: 'checkbox', @@ -96,11 +106,6 @@ export const components: Page[] = [ slug: 'icon', status: 'alpha', }, - { - title: 'IconButton', - slug: 'icon-button', - status: 'alpha', - }, { title: 'Link', slug: 'link', diff --git a/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx b/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx index 0fc61acaf7..43c2f598ac 100644 --- a/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx +++ b/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx @@ -35,8 +35,6 @@ export const ButtonIcon = forwardRef( const responsiveSize = useResponsiveValue(size); const responsiveVariant = useResponsiveValue(variant); - console.log(clsx('canon-Button', 'canon-ButtonIcon', className)); - return ( ( - } variant="primary"> + } + variant="primary" + href="https://canon.backstage.io" + target="_blank" + > Button - } variant="secondary"> + } + variant="secondary" + href="https://canon.backstage.io" + target="_blank" + > Button diff --git a/packages/canon/src/components/ButtonLink/ButtonLink.tsx b/packages/canon/src/components/ButtonLink/ButtonLink.tsx index 72eb78efe3..cb68401160 100644 --- a/packages/canon/src/components/ButtonLink/ButtonLink.tsx +++ b/packages/canon/src/components/ButtonLink/ButtonLink.tsx @@ -44,25 +44,9 @@ export const ButtonLink = forwardRef( ref={ref} {...rest} > - {iconStart && ( - - )} + {iconStart} {children} - {iconEnd && ( - - )} + {iconEnd} ); }, From 71105b9bc4e93b04ba8fe4f1860e857fe3d89e33 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 17:22:02 +0100 Subject: [PATCH 17/19] Fix some props Signed-off-by: Charles de Dreuille --- canon-docs/src/content/components/button-icon.mdx | 4 ++-- canon-docs/src/content/components/button-icon.props.ts | 2 +- canon-docs/src/content/components/button-link.mdx | 2 +- canon-docs/src/content/components/button-link.props.ts | 4 ++-- canon-docs/src/content/components/button.mdx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/canon-docs/src/content/components/button-icon.mdx b/canon-docs/src/content/components/button-icon.mdx index 2281814a89..b020b19f33 100644 --- a/canon-docs/src/content/components/button-icon.mdx +++ b/canon-docs/src/content/components/button-icon.mdx @@ -14,7 +14,7 @@ import { } from './button-icon.props'; import { ComponentInfos } from '@/components/ComponentInfos'; -# Icon Button +# ButtonIcon A button component with a single icon that can be used to trigger actions. @@ -27,7 +27,7 @@ A button component with a single icon that can be used to trigger actions. diff --git a/canon-docs/src/content/components/button-icon.props.ts b/canon-docs/src/content/components/button-icon.props.ts index 160690a29e..a21bf3cd57 100644 --- a/canon-docs/src/content/components/button-icon.props.ts +++ b/canon-docs/src/content/components/button-icon.props.ts @@ -48,7 +48,7 @@ export const buttonIconSizesSnippet = ` } size="medium" /> `; -export const buttonIconDisabledSnippet = `} disabled />`; +export const buttonIconDisabledSnippet = `} isDisabled />`; export const buttonIconResponsiveSnippet = `} variant={{ initial: 'primary', lg: 'secondary' }} />`; diff --git a/canon-docs/src/content/components/button-link.mdx b/canon-docs/src/content/components/button-link.mdx index 3f24874220..f6da987941 100644 --- a/canon-docs/src/content/components/button-link.mdx +++ b/canon-docs/src/content/components/button-link.mdx @@ -26,7 +26,7 @@ A button component that can be used as a link. diff --git a/canon-docs/src/content/components/button-link.props.ts b/canon-docs/src/content/components/button-link.props.ts index 94d696b736..b92b3154e5 100644 --- a/canon-docs/src/content/components/button-link.props.ts +++ b/canon-docs/src/content/components/button-link.props.ts @@ -36,10 +36,10 @@ export const buttonLinkSnippetUsage = `import { ButtonLink } from '@backstage/ca `; export const buttonLinkVariantsSnippet = ` - + } variant="primary"> Button - + } variant="secondary"> Button `; diff --git a/canon-docs/src/content/components/button.mdx b/canon-docs/src/content/components/button.mdx index 89d3c3f612..c0053d0f5e 100644 --- a/canon-docs/src/content/components/button.mdx +++ b/canon-docs/src/content/components/button.mdx @@ -27,7 +27,7 @@ A button component that can be used to trigger actions. From afb728d8b7bce848fcd9103dc7c3e00de1ec5eed Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 17:30:20 +0100 Subject: [PATCH 18/19] Update public-nails-melt.md Signed-off-by: Charles de Dreuille --- .changeset/public-nails-melt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/public-nails-melt.md b/.changeset/public-nails-melt.md index 169bf7fbfb..b4c77e764e 100644 --- a/.changeset/public-nails-melt.md +++ b/.changeset/public-nails-melt.md @@ -2,4 +2,4 @@ '@backstage/canon': minor --- -**Breaking** We are transforming the way we handle custom tags for both Button and IconButton. We are introducing a new as prop for both of these components and we are removing support for the render prop. +**Breaking** We are transforming our Button component to allow for beatter support for button links. Instead of using the render prop, we are introducing the ButtonLin component and renaming IconButton to ButtonIcon to make it consistent. We are also removing the render prop on all button components. From 072c25dd51bc7b835483ff8171a3272b29ee23cb Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 17:37:49 +0100 Subject: [PATCH 19/19] Update public-nails-melt.md Signed-off-by: Charles de Dreuille --- .changeset/public-nails-melt.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.changeset/public-nails-melt.md b/.changeset/public-nails-melt.md index b4c77e764e..1529ee94d0 100644 --- a/.changeset/public-nails-melt.md +++ b/.changeset/public-nails-melt.md @@ -2,4 +2,12 @@ '@backstage/canon': minor --- -**Breaking** We are transforming our Button component to allow for beatter support for button links. Instead of using the render prop, we are introducing the ButtonLin component and renaming IconButton to ButtonIcon to make it consistent. We are also removing the render prop on all button components. +**BREAKING CHANGES** + +We’re updating our Button component to provide better support for button links. + +- We’re introducing a new `ButtonLink` component, which replaces the previous render prop pattern. +- To maintain naming consistency across components, `IconButton` is being renamed to `ButtonIcon`. +- Additionally, the render prop will be removed from all button-related components. + +These changes aim to simplify usage and improve clarity in our component API.