refactor(IconButton): Refactor IconButton to use Ant Design 5 Card (#32890)

Co-authored-by: Maxime Beauchemin <maximebeauchemin@gmail.com>
Co-authored-by: Geido <60598000+geido@users.noreply.github.com>
This commit is contained in:
Sameer ali
2025-04-07 22:57:32 +05:00
committed by GitHub
parent c131205ff1
commit e1383d3821
5 changed files with 196 additions and 187 deletions

View File

@@ -16,42 +16,37 @@
* specific language governing permissions and limitations
* under the License.
*/
import IconButton, { IconButtonProps } from '.';
import { Meta, StoryObj } from '@storybook/react';
import { IconButton } from 'src/components/IconButton';
export default {
title: 'IconButton',
const meta: Meta<typeof IconButton> = {
title: 'Components/IconButton',
component: IconButton,
};
export const InteractiveIconButton = (args: IconButtonProps) => (
<IconButton
buttonText={args.buttonText}
altText={args.altText}
icon={args.icon}
href={args.href}
target={args.target}
htmlType={args.htmlType}
/>
);
InteractiveIconButton.args = {
buttonText: 'This is the IconButton text',
altText: 'This is an example of non-default alt text',
href: 'https://preset.io/',
target: '_blank',
};
InteractiveIconButton.argTypes = {
icon: {
defaultValue: '/images/icons/sql.svg',
control: {
type: 'select',
argTypes: {
onClick: { action: 'clicked' },
},
parameters: {
a11y: {
enabled: true,
},
options: [
'/images/icons/sql.svg',
'/images/icons/server.svg',
'/images/icons/image.svg',
'Click to see example alt text',
],
},
};
export default meta;
type Story = StoryObj<typeof IconButton>;
export const Default: Story = {
args: {
buttonText: 'Default IconButton',
altText: 'Default icon button alt text',
},
};
export const CustomIcon: Story = {
args: {
buttonText: 'Custom icon IconButton',
altText: 'Custom icon button alt text',
icon: '/images/sqlite.png',
},
};

View File

@@ -1,37 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 { render, screen } from 'spec/helpers/testing-library';
import IconButton from 'src/components/IconButton';
const defaultProps = {
buttonText: 'This is the IconButton text',
icon: '/images/icons/sql.svg',
};
describe('IconButton', () => {
it('renders an IconButton', () => {
render(<IconButton {...defaultProps} />);
const icon = screen.getByRole('img');
const buttonText = screen.getByText(/this is the iconbutton text/i);
expect(icon).toBeVisible();
expect(buttonText).toBeVisible();
});
});

View File

@@ -0,0 +1,90 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 { render, screen, fireEvent } from 'spec/helpers/testing-library';
import { IconButton } from 'src/components/IconButton';
const defaultProps = {
buttonText: 'This is the IconButton text',
icon: '/images/icons/sql.svg',
};
describe('IconButton', () => {
it('renders an IconButton with icon and text', () => {
render(<IconButton {...defaultProps} />);
const icon = screen.getByRole('img');
const buttonText = screen.getByText(/this is the iconbutton text/i);
expect(icon).toBeVisible();
expect(buttonText).toBeVisible();
});
it('is keyboard accessible and has correct aria attributes', () => {
render(<IconButton {...defaultProps} />);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('tabIndex', '0');
expect(button).toHaveAttribute('aria-label', defaultProps.buttonText);
});
it('handles Enter and Space key presses', () => {
const mockOnClick = jest.fn();
render(<IconButton {...defaultProps} onClick={mockOnClick} />);
const button = screen.getByRole('button');
fireEvent.keyDown(button, { key: 'Enter', code: 'Enter' });
expect(mockOnClick).toHaveBeenCalledTimes(1);
fireEvent.keyDown(button, { key: ' ', code: 'Space' });
expect(mockOnClick).toHaveBeenCalledTimes(2);
});
it('uses custom alt text when provided', () => {
const customAltText = 'Custom Alt Text';
render(
<IconButton
buttonText="Custom Alt Text Button"
icon="/images/icons/sql.svg"
altText={customAltText}
/>,
);
const icon = screen.getByAltText(customAltText);
expect(icon).toBeVisible();
});
it('displays tooltip with button text', () => {
render(<IconButton {...defaultProps} />);
const tooltipTrigger = screen.getByText(/this is the iconbutton text/i);
expect(tooltipTrigger).toBeVisible();
});
it('calls onClick handler when clicked', () => {
const mockOnClick = jest.fn();
render(<IconButton {...defaultProps} onClick={mockOnClick} />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(mockOnClick).toHaveBeenCalledTimes(1);
});
});

View File

@@ -16,129 +16,90 @@
* specific language governing permissions and limitations
* under the License.
*/
import { styled } from '@superset-ui/core';
import Button, { ButtonProps as AntdButtonProps } from 'src/components/Button';
import { Icons } from 'src/components/Icons';
import LinesEllipsis from 'react-lines-ellipsis';
export interface IconButtonProps extends AntdButtonProps {
// eslint-disable-next-line
import { Typography } from 'src/components';
import { Tooltip } from 'src/components/Tooltip';
import Card, { CardProps } from 'src/components/Card';
import { Icons } from 'src/components/Icons';
import { SupersetTheme, css } from '@superset-ui/core';
export interface IconButtonProps extends CardProps {
buttonText: string;
icon: string;
altText?: string;
}
const StyledButton = styled(Button)`
height: auto;
display: flex;
flex-direction: column;
padding: 0;
`;
const StyledImage = styled.div`
padding: ${({ theme }) => theme.gridUnit * 4}px;
height: ${({ theme }) => theme.gridUnit * 18}px;
margin: ${({ theme }) => theme.gridUnit * 3}px 0;
.default-db-icon {
font-size: 36px;
color: ${({ theme }) => theme.colors.grayscale.base};
margin-right: 0;
span:first-of-type {
margin-right: 0;
const IconButton: React.FC<IconButtonProps> = ({
buttonText,
icon,
altText,
...cardProps
}) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
if (cardProps.onClick) {
(cardProps.onClick as React.EventHandler<React.SyntheticEvent>)(e);
}
if (e.key === ' ') {
e.preventDefault();
}
}
}
cardProps.onKeyDown?.(e);
};
&:first-of-type {
margin-right: 0;
}
const renderIcon = () => {
const iconContent = icon ? (
<img
src={icon}
alt={altText || buttonText}
css={css`
width: 100%;
height: 120px;
object-fit: contain;
`}
/>
) : (
<div
css={css`
display: flex;
align-content: center;
align-items: center;
height: 120px;
`}
>
<Icons.DatabaseOutlined
css={css`
font-size: 48px;
`}
aria-label="default-icon"
/>
</div>
);
img {
width: ${({ theme }) => theme.gridUnit * 10}px;
height: ${({ theme }) => theme.gridUnit * 10}px;
margin: 0;
&:first-of-type {
margin-right: 0;
}
}
svg {
&:first-of-type {
margin-right: 0;
}
}
`;
return iconContent;
};
const StyledInner = styled.div`
max-height: calc(1.5em * 2);
white-space: break-spaces;
return (
<Card
hoverable
role="button"
tabIndex={0}
aria-label={buttonText}
onKeyDown={handleKeyDown}
cover={renderIcon()}
css={(theme: SupersetTheme) => ({
padding: theme.gridUnit * 3,
textAlign: 'center',
...cardProps.style,
})}
{...cardProps}
>
<Tooltip title={buttonText}>
<Typography.Text ellipsis>{buttonText}</Typography.Text>
</Tooltip>
</Card>
);
};
&:first-of-type {
margin-right: 0;
}
.LinesEllipsis {
&:first-of-type {
margin-right: 0;
}
}
`;
const StyledBottom = styled.div`
padding: ${({ theme }) => theme.gridUnit * 4}px 0;
border-radius: 0 0 ${({ theme }) => theme.borderRadius}px
${({ theme }) => theme.borderRadius}px;
background-color: ${({ theme }) => theme.colors.grayscale.light4};
width: 100%;
line-height: 1.5em;
overflow: hidden;
white-space: no-wrap;
text-overflow: ellipsis;
&:first-of-type {
margin-right: 0;
}
`;
const IconButton = styled(
({ icon, altText, buttonText, ...props }: IconButtonProps) => (
<StyledButton {...props}>
<StyledImage>
{icon && <img src={icon} alt={altText} />}
{!icon && (
<Icons.DatabaseOutlined
className="default-db-icon"
aria-label="default-icon"
/>
)}
</StyledImage>
<StyledBottom>
<StyledInner>
<LinesEllipsis
text={buttonText}
maxLine="2"
basedOn="words"
trimRight
/>
</StyledInner>
</StyledBottom>
</StyledButton>
),
)`
text-transform: none;
background-color: ${({ theme }) => theme.colors.grayscale.light5};
font-weight: ${({ theme }) => theme.typography.weights.normal};
color: ${({ theme }) => theme.colors.grayscale.dark2};
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
margin: 0;
width: 100%;
&:hover,
&:focus {
background-color: ${({ theme }) => theme.colors.grayscale.light5};
color: ${({ theme }) => theme.colors.grayscale.dark2};
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
box-shadow: 4px 4px 20px ${({ theme }) => theme.colors.grayscale.light2};
}
`;
export default IconButton;
export { IconButton };

View File

@@ -45,7 +45,7 @@ import { AntdSelect, Upload } from 'src/components';
import Alert from 'src/components/Alert';
import Modal from 'src/components/Modal';
import Button from 'src/components/Button';
import IconButton from 'src/components/IconButton';
import { IconButton } from 'src/components/IconButton';
import InfoTooltip from 'src/components/InfoTooltip';
import withToasts from 'src/components/MessageToasts/withToasts';
import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';