62d44fcffc
* InfoTooltip: refactor component to be accessible * new: create ghostMode prop to allow turning on/off button styles * update infoToolTip component to use ghostMode prop * update story to show ghostMode state * fix condition to work properly * nit fix * revert changes to former infoTooltip state * InfoTooltip: use iconButton instead to achieve keyboard a11y * fix minor type nit
161 lines
4.3 KiB
TypeScript
161 lines
4.3 KiB
TypeScript
import React from 'react';
|
|
import { Icon, getSvgSize } from '../Icon/Icon';
|
|
import { IconName, IconSize, IconType } from '../../types/icon';
|
|
import { stylesFactory } from '../../themes/stylesFactory';
|
|
import { css, cx } from '@emotion/css';
|
|
import { useTheme2 } from '../../themes/ThemeContext';
|
|
import { GrafanaTheme2, colorManipulator } from '@grafana/data';
|
|
import { PopoverContent, Tooltip } from '../Tooltip/Tooltip';
|
|
import { TooltipPlacement } from '../Tooltip/PopoverController';
|
|
import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins';
|
|
|
|
export type IconButtonVariant = 'primary' | 'secondary' | 'destructive';
|
|
|
|
export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
/** Name of the icon **/
|
|
name: IconName;
|
|
/** Icon size */
|
|
size?: IconSize;
|
|
/** @deprecated */
|
|
surface?: SurfaceType;
|
|
/** Type od the icon - mono or default */
|
|
iconType?: IconType;
|
|
/** Tooltip content to display on hover */
|
|
tooltip?: PopoverContent;
|
|
/** Position of the tooltip */
|
|
tooltipPlacement?: TooltipPlacement;
|
|
/** Variant to change the color of the Icon */
|
|
variant?: IconButtonVariant;
|
|
/** Text avilable ony for screenscreen readers. Will use tooltip text as fallback. */
|
|
ariaLabel?: string;
|
|
}
|
|
|
|
type SurfaceType = 'dashboard' | 'panel' | 'header';
|
|
|
|
export const IconButton = React.forwardRef<HTMLButtonElement, Props>(
|
|
(
|
|
{
|
|
name,
|
|
size = 'md',
|
|
iconType,
|
|
tooltip,
|
|
tooltipPlacement,
|
|
ariaLabel,
|
|
className,
|
|
variant = 'secondary',
|
|
...restProps
|
|
},
|
|
ref
|
|
) => {
|
|
const theme = useTheme2();
|
|
const styles = getStyles(theme, size, variant);
|
|
const tooltipString = typeof tooltip === 'string' ? tooltip : '';
|
|
|
|
const button = (
|
|
<button ref={ref} aria-label={ariaLabel || tooltipString} {...restProps} className={cx(styles.button, className)}>
|
|
<Icon name={name} size={size} className={styles.icon} type={iconType} />
|
|
</button>
|
|
);
|
|
|
|
if (tooltip) {
|
|
return (
|
|
<Tooltip content={tooltip} placement={tooltipPlacement}>
|
|
{button}
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
return button;
|
|
}
|
|
);
|
|
|
|
IconButton.displayName = 'IconButton';
|
|
|
|
const getStyles = stylesFactory((theme: GrafanaTheme2, size: IconSize, variant: IconButtonVariant) => {
|
|
const pixelSize = getSvgSize(size);
|
|
const hoverSize = Math.max(pixelSize / 3, 8);
|
|
let iconColor = theme.colors.text.primary;
|
|
|
|
if (variant === 'primary') {
|
|
iconColor = theme.colors.primary.text;
|
|
} else if (variant === 'destructive') {
|
|
iconColor = theme.colors.error.text;
|
|
}
|
|
|
|
return {
|
|
button: css`
|
|
width: ${pixelSize}px;
|
|
height: ${pixelSize}px;
|
|
background: transparent;
|
|
border: none;
|
|
color: ${iconColor};
|
|
padding: 0;
|
|
margin: 0;
|
|
outline: none;
|
|
box-shadow: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
border-radius: ${theme.shape.borderRadius()};
|
|
z-index: 0;
|
|
margin-right: ${theme.spacing(0.5)};
|
|
|
|
&[disabled],
|
|
&:disabled {
|
|
cursor: not-allowed;
|
|
color: ${theme.colors.action.disabledText};
|
|
opacity: 0.65;
|
|
box-shadow: none;
|
|
}
|
|
|
|
&:before {
|
|
content: '';
|
|
display: block;
|
|
opacity: 1;
|
|
position: absolute;
|
|
transition-duration: 0.2s;
|
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
z-index: -1;
|
|
bottom: -${hoverSize}px;
|
|
left: -${hoverSize}px;
|
|
right: -${hoverSize}px;
|
|
top: -${hoverSize}px;
|
|
background: none;
|
|
border-radius: 50%;
|
|
box-sizing: border-box;
|
|
transform: scale(0);
|
|
transition-property: transform, opacity;
|
|
}
|
|
|
|
&:focus,
|
|
&:focus-visible {
|
|
${getFocusStyles(theme)}
|
|
}
|
|
|
|
&:focus:not(:focus-visible) {
|
|
${getMouseFocusStyles(theme)}
|
|
}
|
|
|
|
&:hover {
|
|
color: ${iconColor};
|
|
|
|
&:before {
|
|
background-color: ${variant === 'secondary'
|
|
? theme.colors.action.hover
|
|
: colorManipulator.alpha(iconColor, 0.12)};
|
|
border: none;
|
|
box-shadow: none;
|
|
opacity: 1;
|
|
transform: scale(0.8);
|
|
}
|
|
}
|
|
`,
|
|
icon: css`
|
|
margin-bottom: 0;
|
|
vertical-align: baseline;
|
|
display: flex;
|
|
`,
|
|
};
|
|
});
|