mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 20:52:34 +08:00
Button/Link Focus: Use focus-visible for keyboard focus (#33066)
* outside react approach * fixed ts issues and updated radio button * css only solution * Removed a bit * Updated radio button design and fixed focus state on ToolbarButton * Fixes * Added missing fullWidth
This commit is contained in:
@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Story } from '@storybook/react';
|
import { Story } from '@storybook/react';
|
||||||
import { allButtonVariants, Button, ButtonProps } from './Button';
|
import { allButtonVariants, Button, ButtonProps } from './Button';
|
||||||
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
|
|
||||||
import { iconOptions } from '../../utils/storybook/knobs';
|
import { iconOptions } from '../../utils/storybook/knobs';
|
||||||
import mdx from './Button.mdx';
|
import mdx from './Button.mdx';
|
||||||
import { HorizontalGroup, VerticalGroup } from '../Layout/Layout';
|
import { HorizontalGroup, VerticalGroup } from '../Layout/Layout';
|
||||||
@ -12,7 +11,6 @@ import { Card } from '../Card/Card';
|
|||||||
export default {
|
export default {
|
||||||
title: 'Buttons/Button',
|
title: 'Buttons/Button',
|
||||||
component: Button,
|
component: Button,
|
||||||
decorators: [withCenteredStory, withHorizontallyCenteredStory],
|
|
||||||
argTypes: {
|
argTypes: {
|
||||||
variant: { control: { type: 'select', options: ['primary', 'secondary', 'destructive', 'link'] } },
|
variant: { control: { type: 'select', options: ['primary', 'secondary', 'destructive', 'link'] } },
|
||||||
size: { control: { type: 'select', options: ['sm', 'md', 'lg'] } },
|
size: { control: { type: 'select', options: ['sm', 'md', 'lg'] } },
|
||||||
|
@ -5,7 +5,7 @@ import { IconName } from '../../types/icon';
|
|||||||
import { getPropertiesForButtonSize } from '../Forms/commonStyles';
|
import { getPropertiesForButtonSize } from '../Forms/commonStyles';
|
||||||
import { colorManipulator, GrafanaTheme, GrafanaThemeV2, ThemePaletteColor } from '@grafana/data';
|
import { colorManipulator, GrafanaTheme, GrafanaThemeV2, ThemePaletteColor } from '@grafana/data';
|
||||||
import { ComponentSize } from '../../types/size';
|
import { ComponentSize } from '../../types/size';
|
||||||
import { getFocusStyles } from '../../themes/mixins';
|
import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins';
|
||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
|
|
||||||
export type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'link';
|
export type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'link';
|
||||||
@ -34,7 +34,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={cx(styles.button, className)} {...otherProps} ref={ref}>
|
<button className={cx(styles.button, className)} {...otherProps}>
|
||||||
{icon && <Icon name={icon} size={size} className={styles.icon} />}
|
{icon && <Icon name={icon} size={size} className={styles.icon} />}
|
||||||
{children && <span className={styles.content}>{children}</span>}
|
{children && <span className={styles.content}>{children}</span>}
|
||||||
</button>
|
</button>
|
||||||
@ -47,7 +47,21 @@ Button.displayName = 'Button';
|
|||||||
type ButtonLinkProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement> & AnchorHTMLAttributes<HTMLAnchorElement>;
|
type ButtonLinkProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement> & AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||||
|
|
||||||
export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
||||||
({ variant = 'primary', size = 'md', icon, fullWidth, children, className, disabled, ...otherProps }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
icon,
|
||||||
|
fullWidth,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
onBlur,
|
||||||
|
onFocus,
|
||||||
|
disabled,
|
||||||
|
...otherProps
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getButtonStyles({
|
const styles = getButtonStyles({
|
||||||
theme,
|
theme,
|
||||||
@ -60,7 +74,7 @@ export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
|||||||
const linkButtonStyles = cx(styles.button, { [styles.disabled]: disabled }, className);
|
const linkButtonStyles = cx(styles.button, { [styles.disabled]: disabled }, className);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a className={linkButtonStyles} {...otherProps} ref={ref} tabIndex={disabled ? -1 : 0}>
|
<a className={linkButtonStyles} {...otherProps} tabIndex={disabled ? -1 : 0}>
|
||||||
{icon && <Icon name={icon} size={size} className={styles.icon} />}
|
{icon && <Icon name={icon} size={size} className={styles.icon} />}
|
||||||
{children && <span className={styles.content}>{children}</span>}
|
{children && <span className={styles.content}>{children}</span>}
|
||||||
</a>
|
</a>
|
||||||
@ -99,6 +113,8 @@ export const getButtonStyles = (props: StyleProps) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const focusStyle = getFocusStyles(theme.v2);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
button: css({
|
button: css({
|
||||||
label: 'button',
|
label: 'button',
|
||||||
@ -114,6 +130,9 @@ export const getButtonStyles = (props: StyleProps) => {
|
|||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
borderRadius: theme.v2.shape.borderRadius(1),
|
borderRadius: theme.v2.shape.borderRadius(1),
|
||||||
|
'&:focus': focusStyle,
|
||||||
|
'&:focus-visible': focusStyle,
|
||||||
|
'&:focus:not(:focus-visible)': getMouseFocusStyles(theme.v2),
|
||||||
...(fullWidth && {
|
...(fullWidth && {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -155,10 +174,6 @@ function getButtonVariantStyles(theme: GrafanaThemeV2, color: ThemePaletteColor)
|
|||||||
color: color.contrastText,
|
color: color.contrastText,
|
||||||
boxShadow: theme.shadows.z2,
|
boxShadow: theme.shadows.z2,
|
||||||
},
|
},
|
||||||
|
|
||||||
'&:focus': {
|
|
||||||
...getFocusStyles(theme),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import { Icon } from '../Icon/Icon';
|
|||||||
import { getPropertiesForVariant } from './Button';
|
import { getPropertiesForVariant } from './Button';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { focusCss, getMouseFocusStyles } from '../../themes/mixins';
|
||||||
|
|
||||||
export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
/** Icon name */
|
/** Icon name */
|
||||||
@ -117,14 +118,20 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
border-radius: ${theme.v2.shape.borderRadius()};
|
border-radius: ${theme.v2.shape.borderRadius()};
|
||||||
line-height: ${theme.v2.components.height.md * theme.v2.spacing.gridSize - 2}px;
|
line-height: ${theme.v2.components.height.md * theme.v2.spacing.gridSize - 2}px;
|
||||||
font-weight: ${theme.v2.typography.fontWeightMedium};
|
font-weight: ${theme.v2.typography.fontWeightMedium};
|
||||||
border: 1px solid ${theme.v2.palette.border1};
|
border: 1px solid ${theme.v2.palette.border1};
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
transition: ${theme.v2.transitions.create(['background', 'box-shadow', 'border-color', 'color'], {
|
transition: ${theme.v2.transitions.create(['background', 'box-shadow', 'border-color', 'color'], {
|
||||||
duration: theme.v2.transitions.duration.short,
|
duration: theme.v2.transitions.duration.short,
|
||||||
})},
|
})};
|
||||||
|
|
||||||
&:focus {
|
&:focus,
|
||||||
outline: none;
|
&:focus-visible {
|
||||||
|
${focusCss(theme)}
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus:not(:focus-visible) {
|
||||||
|
${getMouseFocusStyles(theme.v2)}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -143,7 +150,7 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
background: ${theme.v2.palette.action.disabledBackground};
|
background: ${theme.v2.palette.action.disabledBackground};
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
default: css`
|
default: css`
|
||||||
color: ${theme.v2.palette.text.secondary};
|
color: ${theme.v2.palette.text.secondary};
|
||||||
|
@ -3,7 +3,7 @@ import { useTheme, stylesFactory } from '../../../themes';
|
|||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { getPropertiesForButtonSize } from '../commonStyles';
|
import { getPropertiesForButtonSize } from '../commonStyles';
|
||||||
import { focusCss } from '../../../themes/mixins';
|
import { getFocusStyles, getMouseFocusStyles } from '../../../themes/mixins';
|
||||||
|
|
||||||
export type RadioButtonSize = 'sm' | 'md';
|
export type RadioButtonSize = 'sm' | 'md';
|
||||||
|
|
||||||
@ -18,73 +18,6 @@ export interface RadioButtonProps {
|
|||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButtonSize, fullWidth?: boolean) => {
|
|
||||||
const { fontSize, height, padding } = getPropertiesForButtonSize(size, theme.v2);
|
|
||||||
|
|
||||||
const textColor = theme.v2.palette.text.secondary;
|
|
||||||
const textColorHover = theme.v2.palette.text.primary;
|
|
||||||
const textColorActive = theme.v2.palette.primary.text;
|
|
||||||
const borderColor = theme.v2.components.form.border;
|
|
||||||
const borderColorHover = theme.v2.components.form.borderHover;
|
|
||||||
const borderColorActive = theme.v2.components.form.border;
|
|
||||||
const bg = theme.colors.bodyBg;
|
|
||||||
const bgActive = theme.v2.palette.layer2;
|
|
||||||
const border = `1px solid ${borderColor}`;
|
|
||||||
const borderActive = `1px solid ${borderColorActive}`;
|
|
||||||
const borderHover = `1px solid ${borderColorHover}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
radio: css`
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
z-index: -1000;
|
|
||||||
|
|
||||||
&:checked + label {
|
|
||||||
border: ${borderActive};
|
|
||||||
color: ${textColorActive};
|
|
||||||
background: ${bgActive};
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus + label {
|
|
||||||
${focusCss(theme)};
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled + label {
|
|
||||||
cursor: default;
|
|
||||||
color: ${theme.v2.palette.text.disabled};
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
radioLabel: css`
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
font-size: ${fontSize};
|
|
||||||
height: ${theme.v2.spacing(height)};
|
|
||||||
// Deduct border from line-height for perfect vertical centering on windows and linux
|
|
||||||
line-height: ${theme.v2.spacing.gridSize * height - 2}px;
|
|
||||||
color: ${textColor};
|
|
||||||
padding: ${theme.v2.spacing(0, padding)};
|
|
||||||
margin-left: -1px;
|
|
||||||
border-radius: ${theme.border.radius.sm};
|
|
||||||
border: ${border};
|
|
||||||
background: ${bg};
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 1;
|
|
||||||
flex: ${fullWidth ? `1 0 0` : 'none'};
|
|
||||||
text-align: center;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: ${textColorHover};
|
|
||||||
border: ${borderHover};
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const RadioButton: React.FC<RadioButtonProps> = ({
|
export const RadioButton: React.FC<RadioButtonProps> = ({
|
||||||
children,
|
children,
|
||||||
active = false,
|
active = false,
|
||||||
@ -118,3 +51,62 @@ export const RadioButton: React.FC<RadioButtonProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
RadioButton.displayName = 'RadioButton';
|
RadioButton.displayName = 'RadioButton';
|
||||||
|
|
||||||
|
const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButtonSize, fullWidth?: boolean) => {
|
||||||
|
const { fontSize, height, padding } = getPropertiesForButtonSize(size, theme.v2);
|
||||||
|
|
||||||
|
const textColor = theme.v2.palette.text.secondary;
|
||||||
|
const textColorHover = theme.v2.palette.text.primary;
|
||||||
|
const bg = theme.colors.bodyBg;
|
||||||
|
|
||||||
|
return {
|
||||||
|
radio: css`
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: -1000;
|
||||||
|
|
||||||
|
&:checked + label {
|
||||||
|
color: ${theme.v2.palette.text.primary};
|
||||||
|
font-weight: ${theme.v2.typography.fontWeightMedium};
|
||||||
|
background: ${theme.v2.palette.action.selected};
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus + label,
|
||||||
|
&:focus-visible + label {
|
||||||
|
${getFocusStyles(theme.v2)};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus:not(:focus-visible) + label {
|
||||||
|
${getMouseFocusStyles(theme.v2)}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled + label {
|
||||||
|
cursor: default;
|
||||||
|
color: ${theme.v2.palette.text.disabled};
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
radioLabel: css`
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
font-size: ${fontSize};
|
||||||
|
height: ${theme.v2.spacing(height)};
|
||||||
|
// Deduct border from line-height for perfect vertical centering on windows and linux
|
||||||
|
line-height: ${theme.v2.spacing.gridSize * height - 2}px;
|
||||||
|
color: ${textColor};
|
||||||
|
padding: ${theme.v2.spacing(0, padding)};
|
||||||
|
border-radius: ${theme.v2.shape.borderRadius()};
|
||||||
|
background: ${bg};
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
flex: ${fullWidth ? `1 0 0` : 'none'};
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${textColorHover};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
@ -1,41 +1,11 @@
|
|||||||
import React, { useCallback, useRef } from 'react';
|
import React, { useCallback, useRef } from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import uniqueId from 'lodash/uniqueId';
|
import uniqueId from 'lodash/uniqueId';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||||
import { RadioButtonSize, RadioButton } from './RadioButton';
|
import { RadioButtonSize, RadioButton } from './RadioButton';
|
||||||
import { Icon } from '../../Icon/Icon';
|
import { Icon } from '../../Icon/Icon';
|
||||||
import { IconName } from '../../../types/icon';
|
import { IconName } from '../../../types/icon';
|
||||||
|
import { useStyles } from '../../../themes';
|
||||||
const getRadioButtonGroupStyles = () => {
|
|
||||||
return {
|
|
||||||
wrapper: css`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
position: relative;
|
|
||||||
`,
|
|
||||||
radioGroup: css`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
|
|
||||||
label {
|
|
||||||
border-radius: 0px;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
border-radius: 2px 0px 0px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
border-radius: 0px 2px 2px 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
icon: css`
|
|
||||||
margin-right: 6px;
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface RadioButtonGroupProps<T> {
|
export interface RadioButtonGroupProps<T> {
|
||||||
value?: T;
|
value?: T;
|
||||||
@ -70,10 +40,10 @@ export function RadioButtonGroup<T>({
|
|||||||
);
|
);
|
||||||
const id = uniqueId('radiogroup-');
|
const id = uniqueId('radiogroup-');
|
||||||
const groupName = useRef(id);
|
const groupName = useRef(id);
|
||||||
const styles = getRadioButtonGroupStyles();
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.radioGroup, className)}>
|
<div className={cx(styles.radioGroup, fullWidth && styles.fullWidth, className)}>
|
||||||
{options.map((o, i) => {
|
{options.map((o, i) => {
|
||||||
const isItemDisabled = disabledOptions && o.value && disabledOptions.includes(o.value);
|
const isItemDisabled = disabledOptions && o.value && disabledOptions.includes(o.value);
|
||||||
return (
|
return (
|
||||||
@ -85,8 +55,8 @@ export function RadioButtonGroup<T>({
|
|||||||
onChange={handleOnChange(o)}
|
onChange={handleOnChange(o)}
|
||||||
id={`option-${o.value}-${id}`}
|
id={`option-${o.value}-${id}`}
|
||||||
name={groupName.current}
|
name={groupName.current}
|
||||||
fullWidth={fullWidth}
|
|
||||||
description={o.description}
|
description={o.description}
|
||||||
|
fullWidth={fullWidth}
|
||||||
>
|
>
|
||||||
{o.icon && <Icon name={o.icon as IconName} className={styles.icon} />}
|
{o.icon && <Icon name={o.icon as IconName} className={styles.icon} />}
|
||||||
{o.label}
|
{o.label}
|
||||||
@ -98,3 +68,22 @@ export function RadioButtonGroup<T>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
RadioButtonGroup.displayName = 'RadioButtonGroup';
|
RadioButtonGroup.displayName = 'RadioButtonGroup';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => {
|
||||||
|
return {
|
||||||
|
radioGroup: css({
|
||||||
|
display: 'inline-flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
border: `1px solid ${theme.v2.components.form.border}`,
|
||||||
|
borderRadius: theme.v2.shape.borderRadius(),
|
||||||
|
padding: '2px',
|
||||||
|
}),
|
||||||
|
fullWidth: css({
|
||||||
|
display: 'flex',
|
||||||
|
}),
|
||||||
|
icon: css`
|
||||||
|
margin-right: 6px;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -7,7 +7,7 @@ import { useTheme } from '../../themes/ThemeContext';
|
|||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { Tooltip } from '../Tooltip/Tooltip';
|
import { Tooltip } from '../Tooltip/Tooltip';
|
||||||
import { TooltipPlacement } from '../Tooltip/PopoverController';
|
import { TooltipPlacement } from '../Tooltip/PopoverController';
|
||||||
import { focusCss } from '../../themes/mixins';
|
import { focusCss, getMouseFocusStyles } from '../../themes/mixins';
|
||||||
|
|
||||||
export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
/** Name of the icon **/
|
/** Name of the icon **/
|
||||||
@ -69,6 +69,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, size: IconSize) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
border-radius: ${theme.v2.shape.borderRadius()};
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
margin-right: ${theme.spacing.xs};
|
margin-right: ${theme.spacing.xs};
|
||||||
|
|
||||||
@ -98,10 +99,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme, size: IconSize) => {
|
|||||||
transition-property: transform, opacity;
|
transition-property: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
${focusCss(theme)}
|
${focusCss(theme)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus:not(:focus-visible) {
|
||||||
|
${getMouseFocusStyles(theme.v2)}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${theme.colors.linkHover};
|
color: ${theme.colors.linkHover};
|
||||||
|
|
||||||
|
@ -46,11 +46,19 @@ export const focusCss = (theme: GrafanaTheme) => `
|
|||||||
transition: all 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
transition: all 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export function getMouseFocusStyles(theme: GrafanaThemeV2): CSSObject {
|
||||||
|
return {
|
||||||
|
outline: 'none',
|
||||||
|
boxShadow: `${theme.shadows.z1}`,
|
||||||
|
transition: theme.transitions.create('box-shadow'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getFocusStyles(theme: GrafanaThemeV2): CSSObject {
|
export function getFocusStyles(theme: GrafanaThemeV2): CSSObject {
|
||||||
return {
|
return {
|
||||||
outline: '2px dotted transparent',
|
outline: '2px dotted transparent',
|
||||||
outlineOffset: '2px',
|
outlineOffset: '2px',
|
||||||
boxShadow: `0 0 0 2px ${theme.palette.layer0}, 0 0 0px 4px ${theme.palette.primary.border}`,
|
boxShadow: `0 0 0 2px ${theme.palette.layer0}, 0 0 0px 4px ${theme.palette.primary.main}`,
|
||||||
transition: `all 0.2s cubic-bezier(0.19, 1, 0.22, 1)`,
|
transition: `all 0.2s cubic-bezier(0.19, 1, 0.22, 1)`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
} from '../../copy/appNotification';
|
} from '../../copy/appNotification';
|
||||||
import { AppEvents } from '@grafana/data';
|
import { AppEvents } from '@grafana/data';
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
import { VerticalGroup } from '@grafana/ui';
|
||||||
|
|
||||||
export interface OwnProps {}
|
export interface OwnProps {}
|
||||||
|
|
||||||
@ -45,15 +46,17 @@ export class AppNotificationListUnConnected extends PureComponent<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-alert-list">
|
<div className="page-alert-list">
|
||||||
{appNotifications.map((appNotification, index) => {
|
<VerticalGroup>
|
||||||
return (
|
{appNotifications.map((appNotification, index) => {
|
||||||
<AppNotificationItem
|
return (
|
||||||
key={`${appNotification.id}-${index}`}
|
<AppNotificationItem
|
||||||
appNotification={appNotification}
|
key={`${appNotification.id}-${index}`}
|
||||||
onClearNotification={(id) => this.onClearAppNotification(id)}
|
appNotification={appNotification}
|
||||||
/>
|
onClearNotification={(id) => this.onClearAppNotification(id)}
|
||||||
);
|
/>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
|
</VerticalGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -130,6 +130,10 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
dragIcon: css`
|
dragIcon: css`
|
||||||
cursor: drag;
|
cursor: drag;
|
||||||
|
@ -11,7 +11,6 @@ import './jquery_extended';
|
|||||||
import './partials';
|
import './partials';
|
||||||
import './components/jsontree/jsontree';
|
import './components/jsontree/jsontree';
|
||||||
import './components/code_editor/code_editor';
|
import './components/code_editor/code_editor';
|
||||||
import './utils/outline';
|
|
||||||
import './components/colorpicker/spectrum_picker';
|
import './components/colorpicker/spectrum_picker';
|
||||||
import './services/search_srv';
|
import './services/search_srv';
|
||||||
import './services/ng_react';
|
import './services/ng_react';
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
// based on http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/
|
|
||||||
function outlineFixer() {
|
|
||||||
const d: any = document;
|
|
||||||
|
|
||||||
const styleElement = d.createElement('STYLE');
|
|
||||||
const domEvents = 'addEventListener' in d;
|
|
||||||
|
|
||||||
const addEventListener = (type: string, callback: { (): void; (): void }) => {
|
|
||||||
// Basic cross-browser event handling
|
|
||||||
if (domEvents) {
|
|
||||||
d.addEventListener(type, callback);
|
|
||||||
} else {
|
|
||||||
d.attachEvent('on' + type, callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCss = (cssText: string) => {
|
|
||||||
// Handle setting of <style> element contents in IE8
|
|
||||||
!!styleElement.styleSheet ? (styleElement.styleSheet.cssText = cssText) : (styleElement.innerHTML = cssText);
|
|
||||||
};
|
|
||||||
|
|
||||||
d.getElementsByTagName('HEAD')[0].appendChild(styleElement);
|
|
||||||
|
|
||||||
// Using mousedown instead of mouseover, so that previously focused elements don't lose focus ring on mouse move
|
|
||||||
addEventListener('mousedown', () => {
|
|
||||||
setCss(':focus{outline:0 !important}::-moz-focus-inner{border:0;}');
|
|
||||||
});
|
|
||||||
|
|
||||||
addEventListener('keydown', () => {
|
|
||||||
setCss('');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
outlineFixer();
|
|
Reference in New Issue
Block a user