Skip to content
Snippets Groups Projects
Unverified Commit e1383d38 authored by Sameer ali's avatar Sameer ali Committed by GitHub
Browse files

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

parent c131205f
No related branches found
No related tags found
No related merge requests found
...@@ -16,42 +16,37 @@ ...@@ -16,42 +16,37 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import IconButton, { IconButtonProps } from '.'; import { Meta, StoryObj } from '@storybook/react';
import { IconButton } from 'src/components/IconButton';
export default { const meta: Meta<typeof IconButton> = {
title: 'IconButton', title: 'Components/IconButton',
component: IconButton, component: IconButton,
argTypes: {
onClick: { action: 'clicked' },
},
parameters: {
a11y: {
enabled: true,
},
},
}; };
export const InteractiveIconButton = (args: IconButtonProps) => ( export default meta;
<IconButton
buttonText={args.buttonText}
altText={args.altText}
icon={args.icon}
href={args.href}
target={args.target}
htmlType={args.htmlType}
/>
);
InteractiveIconButton.args = { type Story = StoryObj<typeof IconButton>;
buttonText: 'This is the IconButton text',
altText: 'This is an example of non-default alt text',
href: 'https://preset.io/',
target: '_blank',
};
InteractiveIconButton.argTypes = { export const Default: Story = {
icon: { args: {
defaultValue: '/images/icons/sql.svg', buttonText: 'Default IconButton',
control: { altText: 'Default icon button alt text',
type: 'select',
}, },
options: [ };
'/images/icons/sql.svg',
'/images/icons/server.svg', export const CustomIcon: Story = {
'/images/icons/image.svg', args: {
'Click to see example alt text', buttonText: 'Custom icon IconButton',
], altText: 'Custom icon button alt text',
icon: '/images/sqlite.png',
}, },
}; };
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { render, screen } from 'spec/helpers/testing-library'; import { render, screen, fireEvent } from 'spec/helpers/testing-library';
import IconButton from 'src/components/IconButton'; import { IconButton } from 'src/components/IconButton';
const defaultProps = { const defaultProps = {
buttonText: 'This is the IconButton text', buttonText: 'This is the IconButton text',
...@@ -25,7 +25,7 @@ const defaultProps = { ...@@ -25,7 +25,7 @@ const defaultProps = {
}; };
describe('IconButton', () => { describe('IconButton', () => {
it('renders an IconButton', () => { it('renders an IconButton with icon and text', () => {
render(<IconButton {...defaultProps} />); render(<IconButton {...defaultProps} />);
const icon = screen.getByRole('img'); const icon = screen.getByRole('img');
...@@ -34,4 +34,57 @@ describe('IconButton', () => { ...@@ -34,4 +34,57 @@ describe('IconButton', () => {
expect(icon).toBeVisible(); expect(icon).toBeVisible();
expect(buttonText).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);
});
}); });
...@@ -16,129 +16,90 @@ ...@@ -16,129 +16,90 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { styled } from '@superset-ui/core';
import Button, { ButtonProps as AntdButtonProps } from 'src/components/Button'; // 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 { Icons } from 'src/components/Icons';
import LinesEllipsis from 'react-lines-ellipsis'; import { SupersetTheme, css } from '@superset-ui/core';
export interface IconButtonProps extends AntdButtonProps { export interface IconButtonProps extends CardProps {
buttonText: string; buttonText: string;
icon: string; icon: string;
altText?: string; altText?: string;
} }
const StyledButton = styled(Button)` const IconButton: React.FC<IconButtonProps> = ({
height: auto; buttonText,
display: flex; icon,
flex-direction: column; altText,
padding: 0; ...cardProps
`; }) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const StyledImage = styled.div` if (e.key === 'Enter' || e.key === ' ') {
padding: ${({ theme }) => theme.gridUnit * 4}px; if (cardProps.onClick) {
height: ${({ theme }) => theme.gridUnit * 18}px; (cardProps.onClick as React.EventHandler<React.SyntheticEvent>)(e);
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;
}
}
&:first-of-type {
margin-right: 0;
}
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;
}
}
`;
const StyledInner = styled.div`
max-height: calc(1.5em * 2);
white-space: break-spaces;
&:first-of-type {
margin-right: 0;
} }
if (e.key === ' ') {
.LinesEllipsis { e.preventDefault();
&:first-of-type {
margin-right: 0;
} }
} }
`; cardProps.onKeyDown?.(e);
};
const StyledBottom = styled.div` const renderIcon = () => {
padding: ${({ theme }) => theme.gridUnit * 4}px 0; const iconContent = icon ? (
border-radius: 0 0 ${({ theme }) => theme.borderRadius}px <img
${({ theme }) => theme.borderRadius}px; src={icon}
background-color: ${({ theme }) => theme.colors.grayscale.light4}; alt={altText || buttonText}
css={css`
width: 100%; width: 100%;
line-height: 1.5em; height: 120px;
overflow: hidden; object-fit: contain;
white-space: no-wrap; `}
text-overflow: ellipsis; />
) : (
&:first-of-type { <div
margin-right: 0; css={css`
} display: flex;
`; align-content: center;
align-items: center;
const IconButton = styled( height: 120px;
({ icon, altText, buttonText, ...props }: IconButtonProps) => ( `}
<StyledButton {...props}> >
<StyledImage>
{icon && <img src={icon} alt={altText} />}
{!icon && (
<Icons.DatabaseOutlined <Icons.DatabaseOutlined
className="default-db-icon" css={css`
font-size: 48px;
`}
aria-label="default-icon" aria-label="default-icon"
/> />
)} </div>
</StyledImage> );
<StyledBottom> return iconContent;
<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, return (
&:focus { <Card
background-color: ${({ theme }) => theme.colors.grayscale.light5}; hoverable
color: ${({ theme }) => theme.colors.grayscale.dark2}; role="button"
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; tabIndex={0}
box-shadow: 4px 4px 20px ${({ theme }) => theme.colors.grayscale.light2}; 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>
);
};
export default IconButton; export { IconButton };
...@@ -45,7 +45,7 @@ import { AntdSelect, Upload } from 'src/components'; ...@@ -45,7 +45,7 @@ import { AntdSelect, Upload } from 'src/components';
import Alert from 'src/components/Alert'; import Alert from 'src/components/Alert';
import Modal from 'src/components/Modal'; import Modal from 'src/components/Modal';
import Button from 'src/components/Button'; 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 InfoTooltip from 'src/components/InfoTooltip';
import withToasts from 'src/components/MessageToasts/withToasts'; import withToasts from 'src/components/MessageToasts/withToasts';
import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput'; import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment