mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
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:
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user