Add new IconButton component

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-03-16 06:54:21 +00:00
parent 85df833fe3
commit 1e4dfdb0ae
10 changed files with 446 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': minor
---
We added a new IconButton component with fixed sizes showcasing a single icon.
+34
View File
@@ -589,6 +589,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';