From 5c805d338e9ef327e9df4873f1a1278879e20b02 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 15:37:33 +0100 Subject: [PATCH] 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)>;