diff --git a/superset-frontend/src/components/IconButton/IconButton.stories.tsx b/superset-frontend/src/components/IconButton/IconButton.stories.tsx
index 0cc10f32f82ba26a330bdbecc972a8497c18fe04..3b47e4a0c321b8d602ae81c897ad2ccfac5896ed 100644
--- a/superset-frontend/src/components/IconButton/IconButton.stories.tsx
+++ b/superset-frontend/src/components/IconButton/IconButton.stories.tsx
@@ -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,
+ argTypes: {
+ onClick: { action: 'clicked' },
+ },
+ parameters: {
+ a11y: {
+ enabled: true,
+ },
+ },
};
-export const InteractiveIconButton = (args: IconButtonProps) => (
- <IconButton
- buttonText={args.buttonText}
- altText={args.altText}
- icon={args.icon}
- href={args.href}
- target={args.target}
- htmlType={args.htmlType}
- />
-);
+export default meta;
-InteractiveIconButton.args = {
- buttonText: 'This is the IconButton text',
- altText: 'This is an example of non-default alt text',
- href: 'https://preset.io/',
- target: '_blank',
+type Story = StoryObj<typeof IconButton>;
+
+export const Default: Story = {
+ args: {
+ buttonText: 'Default IconButton',
+ altText: 'Default icon button alt text',
+ },
};
-InteractiveIconButton.argTypes = {
- icon: {
- defaultValue: '/images/icons/sql.svg',
- control: {
- type: 'select',
- },
- options: [
- '/images/icons/sql.svg',
- '/images/icons/server.svg',
- '/images/icons/image.svg',
- 'Click to see example alt text',
- ],
+export const CustomIcon: Story = {
+ args: {
+ buttonText: 'Custom icon IconButton',
+ altText: 'Custom icon button alt text',
+ icon: '/images/sqlite.png',
},
};
diff --git a/superset-frontend/src/components/IconButton/IconButton.test.jsx b/superset-frontend/src/components/IconButton/IconButton.test.jsx
deleted file mode 100644
index aa37d8949091d5d7ffe2e2c8074e14d127a38512..0000000000000000000000000000000000000000
--- a/superset-frontend/src/components/IconButton/IconButton.test.jsx
+++ /dev/null
@@ -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();
- });
-});
diff --git a/superset-frontend/src/components/IconButton/IconButton.test.tsx b/superset-frontend/src/components/IconButton/IconButton.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dc45fe003ecc6a8d886791e88f64e00a1eccaa43
--- /dev/null
+++ b/superset-frontend/src/components/IconButton/IconButton.test.tsx
@@ -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);
+ });
+});
diff --git a/superset-frontend/src/components/IconButton/index.tsx b/superset-frontend/src/components/IconButton/index.tsx
index f6b0650776f48eb63285dea46f80b83edf9089d3..b8ec447a5ea70b4ff701d118b2cb09b3c326182c 100644
--- a/superset-frontend/src/components/IconButton/index.tsx
+++ b/superset-frontend/src/components/IconButton/index.tsx
@@ -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';
+
+// 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 LinesEllipsis from 'react-lines-ellipsis';
+import { SupersetTheme, css } from '@superset-ui/core';
-export interface IconButtonProps extends AntdButtonProps {
+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;
- }
- }
-
- &: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 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();
+ }
}
- }
-`;
-
-const StyledInner = styled.div`
- max-height: calc(1.5em * 2);
- white-space: break-spaces;
-
- &: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;
- }
-`;
+ cardProps.onKeyDown?.(e);
+ };
-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>
+ 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>
+ );
- <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%;
+ return iconContent;
+ };
- &: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};
- }
-`;
+ 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>
+ );
+};
-export default IconButton;
+export { IconButton };
diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
index 6efe47bdd261507b027dae1c7634ff5e2366053b..8b359cc141b153827b73aaa2dafddaea7d915b6b 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
@@ -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';