Merge pull request #29239 from backstage/canon-icon-button

Canon - Add new IconButton component
This commit is contained in:
Charles de Dreuille
2025-03-16 18:43:32 +00:00
committed by GitHub
15 changed files with 627 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': minor
---
We added a new IconButton component with fixed sizes showcasing a single icon.
@@ -0,0 +1,111 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import {
IconButtonPreview,
IconButtonSizes,
IconButtonDisabled,
IconButtonResponsive,
} from '@/snippets/icon-button';
import { iconButtonVariants } from '@/snippets/_snippets';
import { iconButtonPropDefs } from '../../../../../../packages/canon/src/components/IconButton/IconButton.props';
# Icon Button
A button component with a single icon that can be used to trigger actions.
<Snippet
align="center"
py={4}
preview={<IconButtonPreview />}
code={`<Flex align="center">
<IconButton icon="cloud" variant="primary" />
<IconButton icon="cloud" variant="secondary" />
</Flex>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { IconButton } from '@backstage/canon';
<IconButton />
`}
/>
</Tabs.Panel>
<Tabs.Panel>
We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
your brand. For additional flexibility, you can use the provided class names for each element listed below.
<CodeBlock
code={`<IconButton className="canon-IconButton" />`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<PropsTable
data={{
...iconButtonPropDefs,
icon: { values: 'icon', responsive: false },
}}
/>
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<IconButtonPreview />}
code={iconButtonVariants}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<IconButtonSizes />}
code={`<Flex align="center">
<IconButton icon="cloud" size="small" />
<IconButton icon="cloud" size="medium" />
</Flex>`}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<IconButtonDisabled />}
code={`<IconButton icon="cloud" disabled />`}
/>
### Responsive
Here's a view when buttons are responsive.
<Snippet
align="center"
py={4}
open
preview={<IconButtonResponsive />}
code={`<IconButton icon="cloud" variant={{ initial: 'primary', lg: 'secondary' }} />`}
/>
@@ -19,10 +19,6 @@ export const PropsTable = <T extends Record<string, PropData>>({
}) => {
const completeData = {
...data,
children: {
values: 'ReactNode',
responsive: false,
},
className: {
values: 'string',
responsive: false,
+8 -2
View File
@@ -38,7 +38,7 @@ export const grid = `import { Grid } from '@backstage/canon';
</Grid>
`;
export const buttonVariants = `<Inline alignY="center">
export const buttonVariants = `<Flex align="center">
<Button iconStart="cloud" variant="primary">
Button
</Button>
@@ -48,7 +48,13 @@ export const buttonVariants = `<Inline alignY="center">
<Button iconStart="cloud" variant="tertiary">
Button
</Button>
</Inline>
</Flex>
`;
export const iconButtonVariants = `<Flex align="center">
<IconButton icon="cloud" variant="primary" />
<IconButton icon="cloud" variant="secondary" />
</Flex>
`;
export const flexFAQ1 = `<Grid columns={3} gap="4">
+57
View File
@@ -0,0 +1,57 @@
'use client';
import { IconButton, Flex, ButtonProps, Text } from '../../../packages/canon';
export const IconButtonPreview = () => {
return (
<Flex align="center">
<IconButton icon="cloud" variant="primary" />
<IconButton icon="cloud" variant="secondary" />
</Flex>
);
};
export const IconButtonSizes = () => {
return (
<Flex align="center">
<IconButton icon="cloud" size="medium" />
<IconButton icon="cloud" size="small" />
</Flex>
);
};
export const IconButtonDisabled = () => {
return <IconButton icon="cloud" disabled />;
};
export const IconButtonResponsive = () => {
return (
<IconButton
icon="cloud"
variant={{ initial: 'primary', lg: 'secondary' }}
/>
);
};
export const IconButtonPlayground = () => {
const variants: string[] = ['primary', 'secondary'];
return (
<Flex direction="column">
{variants.map(variant => (
<Flex direction="column" key={variant}>
<Text>{variant}</Text>
{['small', 'medium'].map(size => (
<Flex align="center" key={size}>
<IconButton
icon="cloud"
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
/>
</Flex>
))}
</Flex>
))}
</Flex>
);
};
+5
View File
@@ -87,6 +87,11 @@ export const components: Page[] = [
slug: 'icon',
status: 'alpha',
},
{
title: 'IconButton',
slug: 'icon-button',
status: 'alpha',
},
{
title: 'Input',
slug: 'input',
+34
View File
@@ -590,6 +590,40 @@ export type HeightProps = GetPropDefTypes<typeof heightPropDefs>;
// @public (undocumented)
export const Icon: (props: IconProps) => React_2.JSX.Element;
// @public (undocumented)
export const IconButton: React_2.ForwardRefExoticComponent<
IconButtonProps & React_2.RefAttributes<HTMLButtonElement>
>;
// @public (undocumented)
export type IconButtonOwnProps = GetPropDefTypes<typeof iconButtonPropDefs>;
// @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;
};
};
// @public
export interface IconButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
icon: IconNames;
size?: IconButtonOwnProps['size'];
variant?: IconButtonOwnProps['variant'];
}
// @public (undocumented)
export const IconContext: Context<IconContextProps>;
@@ -0,0 +1,41 @@
/*
* 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<typeof iconButtonPropDefs>;
@@ -0,0 +1,140 @@
/*
* 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 React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { IconButton } from './IconButton';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { IconButtonProps } from './types';
const meta = {
title: 'Components/IconButton',
component: IconButton,
argTypes: {
size: {
control: 'select',
options: ['small', 'medium'],
},
variant: {
control: 'select',
options: ['primary', 'secondary'],
},
},
args: {
size: 'medium',
variant: 'primary',
},
} satisfies Meta<typeof IconButton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Variants: Story = {
args: {
icon: 'cloud',
},
parameters: {
argTypes: {
variant: {
control: false,
},
},
},
render: args => (
<Flex align="center">
<IconButton {...args} variant="primary" />
<IconButton {...args} variant="secondary" />
</Flex>
),
};
export const Sizes: Story = {
args: {
icon: 'cloud',
},
render: args => (
<Flex align="center">
<IconButton {...args} size="medium" />
<IconButton {...args} size="small" />
</Flex>
),
};
export const Disabled: Story = {
args: {
icon: 'cloud',
disabled: true,
},
render: args => (
<Flex direction="row" gap="4">
<IconButton {...args} variant="primary" />
<IconButton {...args} variant="secondary" />
</Flex>
),
};
export const Responsive: Story = {
args: {
icon: 'cloud',
variant: {
initial: 'primary',
sm: 'secondary',
},
size: {
xs: 'small',
sm: 'medium',
},
},
};
const variants: string[] = ['primary', 'secondary'];
export const Playground: Story = {
args: {
icon: 'cloud',
},
render: args => (
<Flex direction="column">
{variants.map(variant => (
<Flex direction="column" key={variant}>
<Text>{variant}</Text>
{['small', 'medium'].map(size => (
<Flex align="center" key={size}>
<IconButton
{...args}
variant={variant as IconButtonProps['variant']}
size={size as IconButtonProps['size']}
/>
<IconButton
{...args}
icon="chevronRight"
variant={variant as IconButtonProps['variant']}
size={size as IconButtonProps['size']}
/>
<IconButton
{...args}
icon="chevronRight"
variant={variant as IconButtonProps['variant']}
size={size as IconButtonProps['size']}
/>
</Flex>
))}
</Flex>
))}
</Flex>
),
};
@@ -0,0 +1,57 @@
/*
* 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 React, { forwardRef } from 'react';
import { Icon } from '../Icon';
import clsx from 'clsx';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import type { IconButtonProps } from './types';
/** @public */
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
(props: IconButtonProps, ref) => {
const {
size = 'medium',
variant = 'primary',
icon,
className,
style,
...rest
} = props;
const responsiveSize = useResponsiveValue(size);
const responsiveVariant = useResponsiveValue(variant);
return (
<button
ref={ref}
className={clsx(
'canon-IconButton',
`canon-IconButton--size-${responsiveSize}`,
`canon-IconButton--variant-${responsiveVariant}`,
className,
)}
style={style}
{...rest}
>
<Icon name={icon} className="canon-IconButton--icon" />
</button>
);
},
);
export default IconButton;
@@ -0,0 +1,20 @@
/*
* 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 { IconButton } from './IconButton';
export type { IconButtonProps } from './types';
export { iconButtonPropDefs } from './IconButton.props';
export type { IconButtonOwnProps } from './IconButton.props';
@@ -0,0 +1,105 @@
/*
* 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--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--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--size-medium {
font-size: var(--canon-font-size-4);
height: 40px;
width: 40px;
}
.canon-IconButton--size-small {
font-size: var(--canon-font-size-3);
height: 32px;
width: 32px;
}
.canon-IconButton--size-small .canon-IconButton--icon {
width: 1rem;
height: 1rem;
}
.canon-IconButton--size-medium .canon-IconButton--icon {
width: 1.5rem;
height: 1.5rem;
}
@@ -0,0 +1,42 @@
/*
* 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 { IconNames } from '../Icon';
import type { IconButtonOwnProps } from './IconButton.props';
/**
* Properties for {@link IconButton}
*
* @public
*/
export interface IconButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, '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 at the start of the button
*/
icon: IconNames;
}
+1
View File
@@ -24,6 +24,7 @@
@import '../components/Table/styles.css';
@import '../components/Text/styles.css';
@import '../components/Heading/styles.css';
@import '../components/IconButton/styles.css';
@import '../components/Input/Input.styles.css';
@import '../components/Field/Field.styles.css';
@import '../components/Link/styles.css';
+1
View File
@@ -34,6 +34,7 @@ export * from './components/Heading';
// UI components
export * from './components/Button';
export * from './components/Icon';
export * from './components/IconButton';
export * from './components/Checkbox';
export * from './components/Table';
export * from './components/Input';