Removed support for the as prop

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-06-19 15:37:33 +01:00
parent 346c839a82
commit 5c805d338e
23 changed files with 528 additions and 554 deletions
+4 -4
View File
@@ -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;
}
+14
View File
@@ -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;
}
+19 -94
View File
@@ -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);
+19 -94
View File
@@ -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);
+39 -68
View File
@@ -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<TAs extends ElementType> = {
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: {
<TAs extends ElementType = typeof Button_2>(
props: ButtonProps<TAs> & {
ref?: ComponentPropsWithRef<TAs>['ref'];
},
): ReactElement;
displayName: string;
};
export const Button: ForwardRefExoticComponent<
ButtonProps & RefAttributes<HTMLButtonElement>
>;
// @public (undocumented)
export const ButtonIcon: ForwardRefExoticComponent<
ButtonIconProps & RefAttributes<HTMLButtonElement>
>;
// @public
export type ButtonProps<TAs extends ElementType> = PolymorphicComponentProps<
TAs,
{
children?: ReactNode;
size?:
| 'small'
| 'medium'
| Partial<Record<Breakpoint_2, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint_2, 'primary' | 'secondary'>>;
iconStart?: ReactElement;
iconEnd?: ReactElement;
}
>;
export interface ButtonIconProps extends ButtonProps_2 {
// (undocumented)
icon?: ReactElement;
// (undocumented)
size?: 'small' | 'medium' | Partial<Record<Breakpoint_2, 'small' | 'medium'>>;
// (undocumented)
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint_2, 'primary' | 'secondary'>>;
}
// @public
export interface ButtonProps extends ButtonProps_2 {
// (undocumented)
children?: ReactNode;
// (undocumented)
iconEnd?: ReactElement;
// (undocumented)
iconStart?: ReactElement;
// (undocumented)
size?: 'small' | 'medium' | Partial<Record<Breakpoint_2, 'small' | 'medium'>>;
// (undocumented)
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint_2, 'primary' | 'secondary'>>;
}
// @public (undocumented)
export const Checkbox: ForwardRefExoticComponent<
@@ -692,33 +698,6 @@ export type HeightProps = GetPropDefTypes<typeof heightPropDefs>;
// @public (undocumented)
export const Icon: (props: IconProps) => JSX_2.Element | null;
// @public (undocumented)
export const IconButton: {
<TAs extends ElementType = typeof Button_2>(
props: IconButtonProps<TAs> & {
ref?: ComponentPropsWithRef<TAs>['ref'];
},
): ReactElement;
displayName: string;
};
// @public
export type IconButtonProps<TAs extends ElementType> =
PolymorphicComponentProps<
TAs,
{
size?:
| 'small'
| 'medium'
| Partial<Record<Breakpoint_2, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint_2, 'primary' | 'secondary'>>;
icon?: ReactElement;
}
>;
// @public (undocumented)
export const IconContext: Context<IconContextProps>;
@@ -1014,14 +993,6 @@ export const paddingPropDefs: (spacingValues: string[]) => {
// @public (undocumented)
export type PaddingProps = GetPropDefTypes<typeof paddingPropDefs>;
// @public
export type PolymorphicComponentProps<
TAs extends ElementType,
TProps = {},
> = TProps &
As<TAs> &
Omit<ComponentPropsWithoutRef<TAs>, keyof (As<TAs> & TProps)>;
// @public (undocumented)
export const positionPropDefs: {
position: {
@@ -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 }) => (
<a {...props} />
);
return (
<Button as={Link} to="/">
I am a using a custom component
</Button>
);
},
};
export const Responsive: Story = {
args: {
children: 'Button',
@@ -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<typeof RAButton>, ref: Ref<HTMLButtonElement>) => {
(props: ButtonProps, ref: Ref<HTMLButtonElement>) => {
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 (
<Component
<RAButton
className={clsx('canon-Button', className)}
data-variant={responsiveVariant}
data-size={responsiveSize}
ref={ref}
{...rest}
>
{iconStart && (
<span
className="canon-ButtonIcon"
aria-hidden="true"
data-size={responsiveSize}
>
{iconStart}
</span>
)}
{iconStart}
{children}
{iconEnd && (
<span
className="canon-ButtonIcon"
aria-hidden="true"
data-size={responsiveSize}
>
{iconEnd}
</span>
)}
</Component>
{iconEnd}
</RAButton>
);
},
) as {
<TAs extends ElementType = typeof RAButton>(
props: ButtonProps<TAs> & { ref?: ComponentPropsWithRef<TAs>['ref'] },
): ReactElement;
displayName: string;
};
);
Button.displayName = 'Button';
@@ -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;
}
+12 -15
View File
@@ -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<TAs extends ElementType> = PolymorphicComponentProps<
TAs,
{
children?: ReactNode;
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint, 'primary' | 'secondary'>>;
iconStart?: ReactElement;
iconEnd?: ReactElement;
}
>;
export interface ButtonProps extends RAButtonProps {
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint, 'primary' | 'secondary'>>;
iconStart?: ReactElement;
iconEnd?: ReactElement;
children?: ReactNode;
}
@@ -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<typeof IconButton>;
} satisfies Meta<typeof ButtonIcon>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <IconButton icon={<Icon name="cloud" />} />,
render: () => <ButtonIcon icon={<Icon name="cloud" />} />,
};
export const Variants: Story = {
render: () => (
<Flex align="center" gap="2">
<IconButton icon={<Icon name="cloud" />} variant="primary" />
<IconButton icon={<Icon name="cloud" />} variant="secondary" />
<ButtonIcon icon={<Icon name="cloud" />} variant="primary" />
<ButtonIcon icon={<Icon name="cloud" />} variant="secondary" />
</Flex>
),
};
@@ -54,8 +54,8 @@ export const Variants: Story = {
export const Sizes: Story = {
render: () => (
<Flex align="center" gap="2">
<IconButton icon={<Icon name="cloud" />} size="small" />
<IconButton icon={<Icon name="cloud" />} size="medium" />
<ButtonIcon icon={<Icon name="cloud" />} size="small" />
<ButtonIcon icon={<Icon name="cloud" />} size="medium" />
</Flex>
),
};
@@ -63,33 +63,12 @@ export const Sizes: Story = {
export const Disabled: Story = {
render: () => (
<Flex direction="row" gap="2">
<IconButton isDisabled icon={<Icon name="cloud" />} variant="primary" />
<IconButton isDisabled icon={<Icon name="cloud" />} variant="secondary" />
<ButtonIcon isDisabled icon={<Icon name="cloud" />} variant="primary" />
<ButtonIcon isDisabled icon={<Icon name="cloud" />} variant="secondary" />
</Flex>
),
};
export const AsLink: Story = {
render: () => (
<IconButton
as="a"
href="https://canon.backstage.io"
target="_blank"
icon={<Icon name="cloud" />}
/>
),
};
export const AsComponent: Story = {
render: () => {
const Link = (props: { children?: React.ReactNode; to: string }) => (
<a {...props} />
);
return <IconButton as={Link} to="/" icon={<Icon name="cloud" />} />;
},
};
export const Responsive: Story = {
args: {
variant: {
@@ -101,7 +80,7 @@ export const Responsive: Story = {
sm: 'medium',
},
},
render: args => <IconButton {...args} icon={<Icon name="cloud" />} />,
render: args => <ButtonIcon {...args} icon={<Icon name="cloud" />} />,
};
const variants = ['primary', 'secondary'] as const;
@@ -115,20 +94,20 @@ export const Playground: Story = {
<Text>{variant}</Text>
{sizes.map(size => (
<Flex align="center" key={size}>
<IconButton
<ButtonIcon
{...args}
variant={variant}
size={size}
icon={<Icon name="cloud" />}
/>
<IconButton
<ButtonIcon
{...args}
icon={<Icon name="chevron-right" />}
aria-label="Chevron right icon button"
variant={variant}
size={size}
/>
<IconButton
<ButtonIcon
{...args}
icon={<Icon name="chevron-right" />}
aria-label="Chevron right icon button"
@@ -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<typeof RAButton>, ref: Ref<HTMLButtonElement>) => {
export const ButtonIcon = forwardRef(
(props: ButtonIconProps, ref: Ref<HTMLButtonElement>) => {
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 (
<Component
className={clsx('canon-IconButton', className)}
<RAButton
className={clsx('canon-Button', 'canon-ButtonIcon', className)}
data-variant={responsiveVariant}
data-size={responsiveSize}
ref={ref}
{...rest}
>
<span
className="canon-IconButtonIcon"
aria-hidden="true"
data-size={responsiveSize}
>
{icon}
</span>
</Component>
{icon}
</RAButton>
);
},
) as {
<TAs extends ElementType = typeof RAButton>(
props: IconButtonProps<TAs> & { ref?: ComponentPropsWithRef<TAs>['ref'] },
): ReactElement;
displayName: string;
};
);
IconButton.displayName = 'IconButton';
ButtonIcon.displayName = 'ButtonIcon';
@@ -14,5 +14,5 @@
* limitations under the License.
*/
export * from './IconButton';
export * from './ButtonIcon';
export * from './types';
@@ -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;
}
@@ -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<TAs extends ElementType> =
PolymorphicComponentProps<
TAs,
{
size?:
| 'small'
| 'medium'
| Partial<Record<Breakpoint, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint, 'primary' | 'secondary'>>;
icon?: ReactElement;
}
>;
export interface ButtonIconProps extends RAButtonProps {
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint, 'primary' | 'secondary'>>;
icon?: ReactElement;
}
@@ -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<typeof ButtonLink>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Button',
},
};
export const Variants: Story = {
args: {
children: 'Button',
},
parameters: {
argTypes: {
variant: {
control: false,
},
},
},
render: () => (
<Flex align="center">
<ButtonLink iconStart={<Icon name="cloud" />} variant="primary">
Button
</ButtonLink>
<ButtonLink iconStart={<Icon name="cloud" />} variant="secondary">
Button
</ButtonLink>
</Flex>
),
};
export const Sizes: Story = {
args: {
children: 'Button',
},
render: () => (
<Flex align="center">
<ButtonLink size="small" iconStart={<Icon name="cloud" />}>
Small
</ButtonLink>
<ButtonLink size="medium" iconStart={<Icon name="cloud" />}>
Medium
</ButtonLink>
</Flex>
),
};
export const WithIcons: Story = {
args: {
children: 'Button',
},
render: args => (
<Flex align="center">
<ButtonLink {...args} iconStart={<Icon name="cloud" />} />
<ButtonLink {...args} iconEnd={<Icon name="chevron-right" />} />
<ButtonLink
{...args}
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevron-right" />}
/>
</Flex>
),
};
export const FullWidth: Story = {
args: {
children: 'Button',
},
render: args => (
<Flex direction="column" gap="4" style={{ width: '300px' }}>
<ButtonLink {...args} iconStart={<Icon name="cloud" />} />
<ButtonLink {...args} iconEnd={<Icon name="chevron-right" />} />
<ButtonLink
{...args}
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevron-right" />}
/>
</Flex>
),
};
export const Disabled: Story = {
render: () => (
<Flex direction="row" gap="4">
<ButtonLink variant="primary" isDisabled>
Primary
</ButtonLink>
<ButtonLink variant="secondary" isDisabled>
Secondary
</ButtonLink>
</Flex>
),
};
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: () => (
<Flex direction="column">
{variants.map(variant => (
<Flex direction="column" key={variant}>
<Text>{variant}</Text>
{sizes.map(size => (
<Flex align="center" key={size}>
<ButtonLink variant={variant} size={size}>
Button
</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
variant={variant}
size={size}
>
Button
</ButtonLink>
<ButtonLink
iconEnd={<Icon name="chevron-right" />}
variant={variant}
size={size}
>
Button
</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevron-right" />}
style={{ width: '200px' }}
variant={variant}
size={size}
>
Button
</ButtonLink>
<ButtonLink variant={variant} size={size} isDisabled>
Button
</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
variant={variant}
size={size}
isDisabled
>
Button
</ButtonLink>
<ButtonLink
iconEnd={<Icon name="chevron-right" />}
variant={variant}
size={size}
isDisabled
>
Button
</ButtonLink>
</Flex>
))}
</Flex>
))}
</Flex>
),
};
@@ -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<HTMLAnchorElement>) => {
const {
size = 'small',
variant = 'primary',
iconStart,
iconEnd,
children,
className,
...rest
} = props;
const responsiveSize = useResponsiveValue(size);
const responsiveVariant = useResponsiveValue(variant);
return (
<RALink
className={clsx('canon-Button', 'canon-ButtonLink', className)}
data-variant={responsiveVariant}
data-size={responsiveSize}
ref={ref}
{...rest}
>
{iconStart && (
<span
className="canon-ButtonIcon"
aria-hidden="true"
data-size={responsiveSize}
>
{iconStart}
</span>
)}
{children}
{iconEnd && (
<span
className="canon-ButtonIcon"
aria-hidden="true"
data-size={responsiveSize}
>
{iconEnd}
</span>
)}
</RALink>
);
},
);
ButtonLink.displayName = 'ButtonLink';
@@ -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';
@@ -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<Record<Breakpoint, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint, 'primary' | 'secondary'>>;
iconStart?: ReactElement;
iconEnd?: ReactElement;
children?: ReactNode;
}
@@ -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(
</div>
<div className="canon-DataTablePagination--right">
<Text variant="body">{`${fromCount} - ${toCount} of ${rowCount}`}</Text>
<IconButton
<ButtonIcon
variant="secondary"
size="small"
onClick={() => table?.previousPage()}
isDisabled={!table?.getCanPreviousPage()}
icon={<Icon name="chevron-left" />}
/>
<IconButton
<ButtonIcon
variant="secondary"
size="small"
onClick={() => table?.nextPage()}
@@ -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;
}
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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';
-18
View File
@@ -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<JustifyContent>;
rowSpan?: Responsive<Columns | 'full'>;
}
/** @public */
export type As<TAs extends ElementType> = {
as?: TAs;
};
/**
* This is the first reusable type utility we built
* @public
*/
export type PolymorphicComponentProps<
TAs extends ElementType,
TProps = {},
> = TProps &
As<TAs> &
Omit<ComponentPropsWithoutRef<TAs>, keyof (As<TAs> & TProps)>;