init
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import AppIconButton, { MUI_ICON_BUTTON_COLORS } from './AppIconButton';
|
||||
import { APP_ICON_SIZE } from '../../config';
|
||||
import { capitalize, randomColor, randomText } from '@/utils';
|
||||
import { ICONS } from '../AppIcon/config';
|
||||
|
||||
const ComponentToTest = AppIconButton;
|
||||
|
||||
function randomPropertyName(obj: object): string {
|
||||
const objectProperties = Object.keys(obj);
|
||||
const propertyName = objectProperties[Math.floor(Math.random() * objectProperties.length)];
|
||||
return propertyName;
|
||||
}
|
||||
|
||||
// function randomPropertyValue(obj: object): unknown {
|
||||
// const propertyName = randomPropertyName(obj);
|
||||
// return (obj as ObjectPropByName)[propertyName];
|
||||
// }
|
||||
|
||||
/**
|
||||
* Tests for <AppIconButton/> component
|
||||
*/
|
||||
describe('<AppIconButton/> component', () => {
|
||||
it('renders itself', () => {
|
||||
const testId = randomText(8);
|
||||
render(<ComponentToTest data-testid={testId} />);
|
||||
|
||||
// Button
|
||||
const button = screen.getByTestId(testId);
|
||||
expect(button).toBeDefined();
|
||||
expect(button).toHaveAttribute('role', 'button');
|
||||
expect(button).toHaveAttribute('type', 'button');
|
||||
|
||||
// Icon
|
||||
const svg = button.querySelector('svg');
|
||||
expect(svg).toBeDefined();
|
||||
expect(svg).toHaveAttribute('data-icon', 'default'); // default icon
|
||||
expect(svg).toHaveAttribute('size', String(APP_ICON_SIZE)); // default size
|
||||
expect(svg).toHaveAttribute('height', String(APP_ICON_SIZE)); // default size when .size is not set
|
||||
expect(svg).toHaveAttribute('width', String(APP_ICON_SIZE)); // default size when .size is not se
|
||||
});
|
||||
|
||||
it('supports .color property', () => {
|
||||
for (const color of [...MUI_ICON_BUTTON_COLORS, randomColor(), randomColor(), randomColor()]) {
|
||||
const testId = randomText(8);
|
||||
const icon = randomPropertyName(ICONS) as string;
|
||||
render(<ComponentToTest data-testid={testId} color={color} icon={icon} />);
|
||||
|
||||
// Button
|
||||
const button = screen.getByTestId(testId);
|
||||
expect(button).toBeDefined();
|
||||
|
||||
if (color == 'default') {
|
||||
return; // Nothing to test for default color
|
||||
}
|
||||
|
||||
if (MUI_ICON_BUTTON_COLORS.includes(color)) {
|
||||
expect(button).toHaveClass(`MuiIconButton-color${capitalize(color)}`);
|
||||
} else {
|
||||
expect(button).toHaveStyle({ color: color });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('supports .disable property', () => {
|
||||
const testId = randomText(8);
|
||||
const title = randomText(16);
|
||||
render(<ComponentToTest data-testid={testId} disabled />);
|
||||
|
||||
// Button
|
||||
const button = screen.getByTestId(testId);
|
||||
expect(button).toBeDefined();
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(button).toHaveClass('Mui-disabled');
|
||||
});
|
||||
|
||||
it('supports .icon property', () => {
|
||||
// Verify that all icons are supported
|
||||
for (const icon of Object.keys(ICONS)) {
|
||||
const testId = randomText(8);
|
||||
render(<ComponentToTest data-testid={testId} icon={icon} />);
|
||||
|
||||
// Button
|
||||
const button = screen.getByTestId(testId);
|
||||
expect(button).toBeDefined();
|
||||
|
||||
// Icon
|
||||
const svg = button.querySelector('svg');
|
||||
expect(button).toBeDefined();
|
||||
expect(svg).toHaveAttribute('data-icon', icon.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
it('supports .size property', () => {
|
||||
const sizes = ['small', 'medium', 'large'] as const; // as IconButtonProps['size'][];
|
||||
for (const size of sizes) {
|
||||
const testId = randomText(8);
|
||||
render(<ComponentToTest data-testid={testId} size={size} />);
|
||||
|
||||
// Button
|
||||
const button = screen.getByTestId(testId);
|
||||
expect(button).toBeDefined();
|
||||
expect(button).toHaveClass(`MuiIconButton-size${capitalize(size)}`); // MuiIconButton-sizeSmall | MuiIconButton-sizeMedium | MuiIconButton-sizeLarge
|
||||
}
|
||||
});
|
||||
|
||||
it('supports .title property', async () => {
|
||||
const testId = randomText(8);
|
||||
const title = randomText(16);
|
||||
render(<ComponentToTest data-testid={testId} title={title} />);
|
||||
|
||||
// Button
|
||||
const button = screen.getByTestId(testId);
|
||||
expect(button).toBeDefined();
|
||||
expect(button).toHaveAttribute('aria-label', title);
|
||||
|
||||
// Emulate mouseover event to show tooltip
|
||||
await fireEvent(button, new MouseEvent('mouseover', { bubbles: true }));
|
||||
|
||||
// Tooltip is rendered in a separate div, so we need to find it by role
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeDefined();
|
||||
expect(tooltip).toHaveTextContent(title);
|
||||
expect(tooltip).toHaveClass('MuiTooltip-popper');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { ElementType, FunctionComponent, useMemo } from 'react';
|
||||
import { Tooltip, IconButton, IconButtonProps, TooltipProps } from '@mui/material';
|
||||
import AppIcon from '../AppIcon';
|
||||
import AppLink from '../AppLink';
|
||||
import { alpha } from '@mui/material';
|
||||
import { Props } from '../AppIcon/AppIcon';
|
||||
import { IconName } from '../AppIcon/config';
|
||||
|
||||
export const MUI_ICON_BUTTON_COLORS = [
|
||||
'inherit',
|
||||
'default',
|
||||
'primary',
|
||||
'secondary',
|
||||
'success',
|
||||
'error',
|
||||
'info',
|
||||
'warning',
|
||||
];
|
||||
|
||||
export interface AppIconButtonProps extends Omit<IconButtonProps, 'color'> {
|
||||
color?: string; // Not only 'inherit' | 'default' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning',
|
||||
icon?: IconName | string;
|
||||
iconProps?: Partial<Props>;
|
||||
// Missing props
|
||||
component?: ElementType; // Could be RouterLink, AppLink, <a>, etc.
|
||||
to?: string; // Link prop
|
||||
href?: string; // Link prop
|
||||
openInNewTab?: boolean; // Link prop
|
||||
tooltipProps?: Partial<TooltipProps>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders MUI IconButton with SVG image by given Icon name
|
||||
* @param {string} [color] - color of background and hover effect. Non MUI values is also accepted.
|
||||
* @param {boolean} [disabled] - the IconButton is not active when true, also the Tooltip is not rendered.
|
||||
* @param {string} [href] - external link URI
|
||||
* @param {string} [icon] - name of Icon to render inside the IconButton
|
||||
* @param {object} [iconProps] - additional props to pass into the AppIcon component
|
||||
* @param {boolean} [openInNewTab] - link will be opened in new tab when true
|
||||
* @param {string} [size] - size of the button: 'small', 'medium' or 'large'
|
||||
* @param {Array<func| object| bool> | func | object} [sx] - additional CSS styles to apply to the button
|
||||
* @param {string} [title] - when set, the IconButton is rendered inside Tooltip with this text
|
||||
* @param {string} [to] - internal link URI
|
||||
* @param {object} [tooltipProps] - additional props to pass into the Tooltip component
|
||||
*/
|
||||
const AppIconButton: FunctionComponent<AppIconButtonProps> = ({
|
||||
color = 'default',
|
||||
component,
|
||||
children,
|
||||
disabled,
|
||||
icon,
|
||||
iconProps,
|
||||
sx,
|
||||
title,
|
||||
tooltipProps,
|
||||
...restOfProps
|
||||
}) => {
|
||||
const componentToRender = !component && (restOfProps?.href || restOfProps?.to) ? AppLink : component ?? IconButton;
|
||||
|
||||
const isMuiColor = useMemo(() => MUI_ICON_BUTTON_COLORS.includes(color), [color]);
|
||||
|
||||
const iconButtonToRender = useMemo(() => {
|
||||
const colorToRender = isMuiColor ? (color as IconButtonProps['color']) : 'default';
|
||||
const sxToRender = {
|
||||
...sx,
|
||||
...(!isMuiColor && {
|
||||
color: color,
|
||||
':hover': {
|
||||
backgroundColor: alpha(color, 0.04),
|
||||
},
|
||||
}),
|
||||
};
|
||||
return (
|
||||
<IconButton
|
||||
component={componentToRender}
|
||||
color={colorToRender}
|
||||
disabled={disabled}
|
||||
sx={sxToRender}
|
||||
{...restOfProps}
|
||||
>
|
||||
<AppIcon icon={icon} {...iconProps} />
|
||||
{children}
|
||||
</IconButton>
|
||||
);
|
||||
}, [color, componentToRender, children, disabled, icon, isMuiColor, sx, iconProps, restOfProps]);
|
||||
|
||||
// When title is set, wrap the IconButton with Tooltip.
|
||||
// Note: when IconButton is disabled the Tooltip is not working, so we don't need it
|
||||
return title && !disabled ? (
|
||||
<Tooltip title={title} {...tooltipProps}>
|
||||
{iconButtonToRender}
|
||||
</Tooltip>
|
||||
) : (
|
||||
iconButtonToRender
|
||||
);
|
||||
};
|
||||
|
||||
export default AppIconButton;
|
||||
@@ -0,0 +1,3 @@
|
||||
import AppIconButton from './AppIconButton';
|
||||
|
||||
export { AppIconButton as default, AppIconButton };
|
||||
Reference in New Issue
Block a user