mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Enable no-unreduced-motion
and fix errors (#86572)
* enable `no-unreduced-motion` in betterer * move all animation calls inside handleReducedMotion * fix violations + enable rule * update rule README * remove unnecessary transition from <Collapse> * remove handleReducedMotion utility and add handleMotion to theme * update to use new theme value * handle Dropdown and IconButton * handle AppChromeMenu and update lint message * keep rotation at a reduced speed * handle DashboardLoading
This commit is contained in:
parent
8a1f43a65d
commit
c151a97110
@ -8,6 +8,7 @@
|
||||
},
|
||||
"rules": {
|
||||
"@grafana/no-border-radius-literal": "error",
|
||||
"@grafana/no-unreduced-motion": "error",
|
||||
"react/prop-types": "off",
|
||||
// need to ignore emotion's `css` prop, see https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unknown-property.md#rule-options
|
||||
"react/no-unknown-property": ["error", { "ignore": ["css"] }],
|
||||
|
@ -53,6 +53,12 @@ export function create(props: string | string[] = ['all'], options: CreateTransi
|
||||
.join(',');
|
||||
}
|
||||
|
||||
type ReducedMotionProps = 'no-preference' | 'reduce';
|
||||
|
||||
export function handleMotion(...props: ReducedMotionProps[]) {
|
||||
return props.map((prop) => `@media (prefers-reduced-motion: ${prop})`).join(',');
|
||||
}
|
||||
|
||||
export function getAutoHeightDuration(height: number) {
|
||||
if (!height) {
|
||||
return 0;
|
||||
@ -74,6 +80,7 @@ export interface ThemeTransitions {
|
||||
duration: typeof duration;
|
||||
easing: typeof easing;
|
||||
getAutoHeightDuration: typeof getAutoHeightDuration;
|
||||
handleMotion: typeof handleMotion;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@ -83,5 +90,6 @@ export function createTransitions(): ThemeTransitions {
|
||||
duration,
|
||||
easing,
|
||||
getAutoHeightDuration,
|
||||
handleMotion,
|
||||
};
|
||||
}
|
||||
|
@ -24,7 +24,89 @@ Avoid direct use of `animation*` or `transition*` properties.
|
||||
|
||||
To account for users with motion sensitivities, these should always be wrapped in a [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) media query.
|
||||
|
||||
`@grafana/ui` exposes a `handledReducedMotion` utility function that can be used to handle this.
|
||||
There is a `handleMotion` utility function exposed on the theme that can help with this.
|
||||
|
||||
#### Examples
|
||||
|
||||
```tsx
|
||||
// Bad ❌
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
loading: css({
|
||||
animationName: rotate,
|
||||
animationDuration: '2s',
|
||||
animationIterationCount: 'infinite',
|
||||
}),
|
||||
});
|
||||
|
||||
// Good ✅
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
loading: css({
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
animationName: rotate,
|
||||
animationDuration: '2s',
|
||||
animationIterationCount: 'infinite',
|
||||
},
|
||||
[theme.transitions.handleMotion('reduce')]: {
|
||||
animationName: pulse,
|
||||
animationDuration: '2s',
|
||||
animationIterationCount: 'infinite',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Good ✅
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
loading: css({
|
||||
'@media (prefers-reduced-motion: no-preference)': {
|
||||
animationName: rotate,
|
||||
animationDuration: '2s',
|
||||
animationIterationCount: 'infinite',
|
||||
},
|
||||
'@media (prefers-reduced-motion: reduce)': {
|
||||
animationName: pulse,
|
||||
animationDuration: '2s',
|
||||
animationIterationCount: 'infinite',
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
Note we've switched the potentially sensitive rotating animation to a less intense pulse animation when `prefers-reduced-motion` is set.
|
||||
|
||||
Animations that involve only non-moving properties, like opacity, color, and blurs, are unlikely to be problematic. In those cases, you still need to wrap the animation in a `prefers-reduced-motion` media query, but you can use the same animation for both cases:
|
||||
|
||||
```tsx
|
||||
// Bad ❌
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
card: css({
|
||||
transition: theme.transitions.create(['background-color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
// Good ✅
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
card: css({
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create(['background-color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Good ✅
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
card: css({
|
||||
'@media (prefers-reduced-motion: no-preference), @media (prefers-reduced-motion: reduce)': {
|
||||
transition: theme.transitions.create(['background-color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### `theme-token-usage`
|
||||
|
||||
|
@ -55,7 +55,7 @@ const rule = createRule({
|
||||
description: 'Check if animation or transition properties are used directly.',
|
||||
},
|
||||
messages: {
|
||||
noUnreducedMotion: 'Avoid direct use of `animation*` or `transition*` properties. Use the `handleReducedMotion` utility function or wrap in a `prefers-reduced-motion` media query.',
|
||||
noUnreducedMotion: 'Avoid direct use of `animation*` or `transition*` properties. Use the `handleMotion` utility function from theme.transitions or wrap in a `prefers-reduced-motion` media query.',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
|
@ -238,7 +238,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
borderRadius: theme.shape.radius.default,
|
||||
marginBottom: theme.spacing(1),
|
||||
position: 'relative',
|
||||
transition: 'all 0.5s ease-in 0s',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: 'all 0.5s ease-in 0s',
|
||||
},
|
||||
height: '100%',
|
||||
}),
|
||||
cardError: css({
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { css, keyframes } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
|
||||
export const EllipsisAnimated = React.memo(() => {
|
||||
@ -16,19 +18,25 @@ export const EllipsisAnimated = React.memo(() => {
|
||||
|
||||
EllipsisAnimated.displayName = 'EllipsisAnimated';
|
||||
|
||||
const getStyles = () => {
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
ellipsis: css({
|
||||
display: 'inline',
|
||||
}),
|
||||
firstDot: css({
|
||||
animation: `${firstDot} 2s linear infinite`,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
animation: `${firstDot} 2s linear infinite`,
|
||||
},
|
||||
}),
|
||||
secondDot: css({
|
||||
animation: `${secondDot} 2s linear infinite`,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
animation: `${secondDot} 2s linear infinite`,
|
||||
},
|
||||
}),
|
||||
thirdDot: css({
|
||||
animation: `${thirdDot} 2s linear infinite`,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
animation: `${thirdDot} 2s linear infinite`,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -120,7 +120,9 @@ const getLabelStyles = (theme: GrafanaTheme2) => ({
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
backgroundColor: theme.colors.primary.shade,
|
||||
color: theme.colors.text.primary,
|
||||
animation: 'pulse 3s ease-out 0s infinite normal forwards',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
animation: 'pulse 3s ease-out 0s infinite normal forwards',
|
||||
},
|
||||
'@keyframes pulse': {
|
||||
'0%': {
|
||||
color: theme.colors.text.primary,
|
||||
|
@ -94,9 +94,11 @@ export const getCardContainerStyles = (
|
||||
borderRadius: theme.shape.radius.default,
|
||||
marginBottom: '8px',
|
||||
pointerEvents: disabled ? 'none' : 'auto',
|
||||
transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
|
||||
...(!disableHover && {
|
||||
'&:hover': {
|
||||
@ -123,9 +125,11 @@ export const getCardContainerStyles = (
|
||||
position: 'relative',
|
||||
pointerEvents: disabled ? 'none' : 'auto',
|
||||
marginBottom: theme.spacing(1),
|
||||
transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
|
||||
...(!disableHover && {
|
||||
'&:hover': {
|
||||
|
@ -71,7 +71,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
label: 'collapse__header',
|
||||
padding: theme.spacing(1, 2, 1, 2),
|
||||
display: 'flex',
|
||||
transition: 'all 0.1s linear',
|
||||
}),
|
||||
headerCollapsed: css({
|
||||
label: 'collapse__header--collapsed',
|
||||
|
@ -80,9 +80,11 @@ const getStyles = (
|
||||
boxShadow: isSelected
|
||||
? `inset 0 0 0 2px ${color}, inset 0 0 0 4px ${theme.colors.getContrastText(color)}`
|
||||
: 'none',
|
||||
transition: theme.transitions.create(['transform'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: theme.transitions.create(['transform'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)',
|
||||
},
|
||||
|
@ -133,18 +133,22 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
}),
|
||||
mainButton: css({
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create(['opacity'], {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: theme.transitions.create(['opacity'], {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
}),
|
||||
},
|
||||
zIndex: 2,
|
||||
}),
|
||||
mainButtonHide: css({
|
||||
opacity: 0,
|
||||
transition: theme.transitions.create(['opacity', 'visibility'], {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
easing: theme.transitions.easing.easeIn,
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: theme.transitions.create(['opacity', 'visibility'], {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
easing: theme.transitions.easing.easeIn,
|
||||
}),
|
||||
},
|
||||
visibility: 'hidden',
|
||||
zIndex: 0,
|
||||
}),
|
||||
@ -164,19 +168,23 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
display: 'flex',
|
||||
opacity: 1,
|
||||
transform: 'translateX(0)',
|
||||
transition: theme.transitions.create(['opacity', 'transform'], {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: theme.transitions.create(['opacity', 'transform'], {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
}),
|
||||
},
|
||||
zIndex: 1,
|
||||
}),
|
||||
confirmButtonHide: css({
|
||||
opacity: 0,
|
||||
transform: 'translateX(100%)',
|
||||
transition: theme.transitions.create(['opacity', 'transform', 'visibility'], {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
easing: theme.transitions.easing.easeIn,
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: theme.transitions.create(['opacity', 'transform', 'visibility'], {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
easing: theme.transitions.easing.easeIn,
|
||||
}),
|
||||
},
|
||||
visibility: 'hidden',
|
||||
}),
|
||||
};
|
||||
|
@ -65,7 +65,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
transition: theme.transitions.create('opacity'),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create('opacity'),
|
||||
},
|
||||
zIndex: 1,
|
||||
}),
|
||||
scrollTopIndicator: css({
|
||||
|
@ -13,7 +13,10 @@ import {
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
import { ReactUtils, handleReducedMotion } from '../../utils';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { ReactUtils } from '../../utils';
|
||||
import { getPlacement } from '../../utils/tooltipUtils';
|
||||
import { Portal } from '../Portal/Portal';
|
||||
import { TooltipPlacement } from '../Tooltip/types';
|
||||
@ -63,7 +66,7 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
|
||||
|
||||
const animationDuration = 150;
|
||||
const animationStyles = getStyles(animationDuration);
|
||||
const animationStyles = useStyles2(getStyles, animationDuration);
|
||||
|
||||
const onOverlayClicked = () => {
|
||||
setShow(false);
|
||||
@ -109,22 +112,22 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi
|
||||
|
||||
Dropdown.displayName = 'Dropdown';
|
||||
|
||||
const getStyles = (duration: number) => {
|
||||
const getStyles = (theme: GrafanaTheme2, duration: number) => {
|
||||
return {
|
||||
appear: css({
|
||||
opacity: '0',
|
||||
position: 'relative',
|
||||
transformOrigin: 'top',
|
||||
...handleReducedMotion({
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transform: 'scaleY(0.5)',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
appearActive: css({
|
||||
opacity: '1',
|
||||
...handleReducedMotion({
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transform: 'scaleY(1)',
|
||||
transition: `transform ${duration}ms cubic-bezier(0.2, 0, 0.2, 1), opacity ${duration}ms cubic-bezier(0.2, 0, 0.2, 1)`,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -7,7 +7,6 @@ import { useStyles2 } from '../../themes';
|
||||
import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins';
|
||||
import { ComponentSize } from '../../types';
|
||||
import { IconName, IconSize, IconType } from '../../types/icon';
|
||||
import { handleReducedMotion } from '../../utils/handleReducedMotion';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { getSvgSize } from '../Icon/utils';
|
||||
import { TooltipPlacement, PopoverContent, Tooltip } from '../Tooltip';
|
||||
@ -144,11 +143,11 @@ const getStyles = (theme: GrafanaTheme2, size: IconSize, variant: IconButtonVari
|
||||
height: `${hoverSize}px`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
content: '""',
|
||||
...handleReducedMotion({
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transitionDuration: '0.2s',
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionProperty: 'opacity',
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
'&:focus, &:focus-visible': getFocusStyles(theme),
|
||||
|
@ -4,7 +4,6 @@ import React, { CSSProperties } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { handleReducedMotion } from '../../utils/handleReducedMotion';
|
||||
|
||||
export interface LoadingBarProps {
|
||||
width: number;
|
||||
@ -33,7 +32,7 @@ export function LoadingBar({ width, delay = DEFAULT_ANIMATION_DELAY, ariaLabel =
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (_theme: GrafanaTheme2, delay: number, duration: number) => {
|
||||
const getStyles = (theme: GrafanaTheme2, delay: number, duration: number) => {
|
||||
const animation = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(-100%)',
|
||||
@ -50,20 +49,23 @@ const getStyles = (_theme: GrafanaTheme2, delay: number, duration: number) => {
|
||||
height: 1,
|
||||
background: 'linear-gradient(90deg, rgba(110, 159, 255, 0) 0%, #6E9FFF 80.75%, rgba(110, 159, 255, 0) 100%)',
|
||||
transform: 'translateX(-100%)',
|
||||
animationName: animation,
|
||||
// an initial delay to prevent the loader from showing if the response is faster than the delay
|
||||
animationDelay: `${delay}ms`,
|
||||
animationTimingFunction: 'linear',
|
||||
animationIterationCount: 'infinite',
|
||||
willChange: 'transform',
|
||||
...handleReducedMotion(
|
||||
{
|
||||
animationDuration: `${duration}ms`,
|
||||
},
|
||||
{
|
||||
animationDuration: `${4 * duration}ms`,
|
||||
}
|
||||
),
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
animationName: animation,
|
||||
// an initial delay to prevent the loader from showing if the response is faster than the delay
|
||||
animationDelay: `${delay}ms`,
|
||||
animationTimingFunction: 'linear',
|
||||
animationIterationCount: 'infinite',
|
||||
animationDuration: `${duration}ms`,
|
||||
},
|
||||
[theme.transitions.handleMotion('reduce')]: {
|
||||
animationName: animation,
|
||||
// an initial delay to prevent the loader from showing if the response is faster than the delay
|
||||
animationDelay: `${delay}ms`,
|
||||
animationTimingFunction: 'linear',
|
||||
animationIterationCount: 'infinite',
|
||||
animationDuration: `${4 * duration}ms`,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -66,7 +66,9 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
container: css({
|
||||
label: 'hover-container-widget',
|
||||
transition: `all .1s linear`,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: `all .1s linear`,
|
||||
},
|
||||
display: 'flex',
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
|
@ -263,7 +263,9 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
|
||||
display: 'inline-block',
|
||||
background: resizerColor,
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
},
|
||||
width: '8px',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
|
@ -152,9 +152,11 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
border: `1px solid ${theme.colors.secondary.border}`,
|
||||
whiteSpace: 'nowrap',
|
||||
transition: theme.transitions.create(['background', 'box-shadow', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create(['background', 'box-shadow', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
|
||||
'&:focus, &:focus-visible': {
|
||||
...getFocusStyles(theme),
|
||||
|
@ -36,8 +36,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
display: 'block',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer',
|
||||
transition:
|
||||
'color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1)',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition:
|
||||
'color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1)',
|
||||
},
|
||||
}),
|
||||
|
||||
typeaheadItemSelected: css({
|
||||
|
@ -23,7 +23,7 @@ export function FadeTransition(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (_theme: GrafanaTheme2, duration: number) => ({
|
||||
const getStyles = (theme: GrafanaTheme2, duration: number) => ({
|
||||
enter: css({
|
||||
label: 'enter',
|
||||
opacity: 0,
|
||||
@ -31,7 +31,9 @@ const getStyles = (_theme: GrafanaTheme2, duration: number) => ({
|
||||
enterActive: css({
|
||||
label: 'enterActive',
|
||||
opacity: 1,
|
||||
transition: `opacity ${duration}ms ease-out`,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: `opacity ${duration}ms ease-out`,
|
||||
},
|
||||
}),
|
||||
exit: css({
|
||||
label: 'exit',
|
||||
@ -40,6 +42,8 @@ const getStyles = (_theme: GrafanaTheme2, duration: number) => ({
|
||||
exitActive: css({
|
||||
label: 'exitActive',
|
||||
opacity: 0,
|
||||
transition: `opacity ${duration}ms ease-out`,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: `opacity ${duration}ms ease-out`,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
@ -26,7 +26,7 @@ export function SlideOutTransition(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (_theme: GrafanaTheme2, duration: number, measurement: 'width' | 'height', size: number) => ({
|
||||
const getStyles = (theme: GrafanaTheme2, duration: number, measurement: 'width' | 'height', size: number) => ({
|
||||
enter: css({
|
||||
label: 'enter',
|
||||
[`${measurement}`]: 0,
|
||||
@ -36,7 +36,12 @@ const getStyles = (_theme: GrafanaTheme2, duration: number, measurement: 'width'
|
||||
label: 'enterActive',
|
||||
[`${measurement}`]: `${size}px`,
|
||||
opacity: 1,
|
||||
transition: `opacity ${duration}ms ease-out, ${measurement} ${duration}ms ease-out`,
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: `opacity ${duration}ms ease-out, ${measurement} ${duration}ms ease-out`,
|
||||
},
|
||||
[theme.transitions.handleMotion('reduce')]: {
|
||||
transition: `opacity ${duration}ms ease-out`,
|
||||
},
|
||||
}),
|
||||
exit: css({
|
||||
label: 'exit',
|
||||
@ -47,6 +52,11 @@ const getStyles = (_theme: GrafanaTheme2, duration: number, measurement: 'width'
|
||||
label: 'exitActive',
|
||||
opacity: 0,
|
||||
[`${measurement}`]: 0,
|
||||
transition: `opacity ${duration}ms ease-out, ${measurement} ${duration}ms ease-out`,
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: `opacity ${duration}ms ease-out, ${measurement} ${duration}ms ease-out`,
|
||||
},
|
||||
[theme.transitions.handleMotion('reduce')]: {
|
||||
transition: `opacity ${duration}ms ease-out`,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { CSSInterpolation } from '@emotion/css';
|
||||
|
||||
/**
|
||||
* @param styles - Styles to apply when no `prefers-reduced-motion` preference is set.
|
||||
* @param reducedMotionStyles - Styles to apply when `prefers-reduced-motion` is enabled.
|
||||
* Applies one of `styles` or `reducedMotionStyles` depending on a users `prefers-reduced-motion` setting. Omitting `reducedMotionStyles` entirely will result in no styles being applied when `prefers-reduced-motion` is enabled. In most cases this is a reasonable default.
|
||||
*/
|
||||
export const handleReducedMotion = (styles: CSSInterpolation, reducedMotionStyles?: CSSInterpolation) => {
|
||||
const result: Record<string, CSSInterpolation> = {
|
||||
'@media (prefers-reduced-motion: no-preference)': styles,
|
||||
};
|
||||
if (reducedMotionStyles) {
|
||||
result['@media (prefers-reduced-motion: reduce)'] = reducedMotionStyles;
|
||||
}
|
||||
return result;
|
||||
};
|
@ -19,6 +19,5 @@ export { createLogger } from './logger';
|
||||
export { attachDebugger } from './debug';
|
||||
export * from './nodeGraph';
|
||||
export { fuzzyMatch } from './fuzzy';
|
||||
export { handleReducedMotion } from './handleReducedMotion';
|
||||
|
||||
export { ReactUtils };
|
||||
|
@ -37,7 +37,9 @@ export function buildTooltipTheme(
|
||||
color: tooltipText,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
padding: theme.spacing(tooltipPadding.topBottom, tooltipPadding.rightLeft),
|
||||
transition: 'opacity 0.3s',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: 'opacity 0.3s',
|
||||
},
|
||||
zIndex: theme.zIndex.tooltip,
|
||||
maxWidth: '400px',
|
||||
overflowWrap: 'break-word',
|
||||
|
@ -6,7 +6,7 @@ import React, { useRef } from 'react';
|
||||
import CSSTransition from 'react-transition-group/CSSTransition';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { handleReducedMotion, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
import { KioskMode } from 'app/types';
|
||||
|
||||
@ -125,10 +125,10 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => {
|
||||
|
||||
const getAnimStyles = (theme: GrafanaTheme2, animationDuration: number) => {
|
||||
const commonTransition = {
|
||||
...handleReducedMotion({
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transitionDuration: `${animationDuration}ms`,
|
||||
transitionTimingFunction: theme.transitions.easing.easeInOut,
|
||||
}),
|
||||
},
|
||||
[theme.breakpoints.down('md')]: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
@ -33,6 +33,18 @@ const fadeIn = keyframes({
|
||||
},
|
||||
});
|
||||
|
||||
const pulse = keyframes({
|
||||
'0%': {
|
||||
opacity: 0,
|
||||
},
|
||||
'50%': {
|
||||
opacity: 1,
|
||||
},
|
||||
'100%': {
|
||||
opacity: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const bounce = keyframes({
|
||||
'from, to': {
|
||||
transform: 'translateY(0px)',
|
||||
@ -70,25 +82,37 @@ const squash = keyframes({
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
opacity: 0,
|
||||
animationName: fadeIn,
|
||||
animationIterationCount: 1,
|
||||
animationDuration: '0.9s',
|
||||
animationDelay: '0.5s',
|
||||
animationFillMode: 'forwards',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
animationName: fadeIn,
|
||||
animationIterationCount: 1,
|
||||
animationDuration: '0.9s',
|
||||
animationDelay: '0.5s',
|
||||
animationFillMode: 'forwards',
|
||||
},
|
||||
[theme.transitions.handleMotion('reduce')]: {
|
||||
animationName: pulse,
|
||||
animationIterationCount: 'infinite',
|
||||
animationDuration: '4s',
|
||||
animationDelay: '0.5s',
|
||||
},
|
||||
}),
|
||||
|
||||
bounce: css({
|
||||
textAlign: 'center',
|
||||
animationName: bounce,
|
||||
animationDuration: '0.9s',
|
||||
animationIterationCount: 'infinite',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
animationName: bounce,
|
||||
animationDuration: '0.9s',
|
||||
animationIterationCount: 'infinite',
|
||||
},
|
||||
}),
|
||||
|
||||
logo: css({
|
||||
display: 'inline-block',
|
||||
animationName: squash,
|
||||
animationDuration: '0.9s',
|
||||
animationIterationCount: 'infinite',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
animationName: squash,
|
||||
animationDuration: '0.9s',
|
||||
animationIterationCount: 'infinite',
|
||||
},
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
}),
|
||||
|
@ -2,7 +2,7 @@ import { cx, css, keyframes } from '@emotion/css';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { handleReducedMotion, useStyles2 } from '@grafana/ui';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Branding } from '../Branding/Branding';
|
||||
import { BrandingSettings } from '../Branding/types';
|
||||
@ -148,7 +148,9 @@ export const getLoginStyles = (theme: GrafanaTheme2) => {
|
||||
borderRadius: theme.shape.radius.default,
|
||||
padding: theme.spacing(2, 0),
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
},
|
||||
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
minHeight: theme.spacing(40),
|
||||
@ -171,12 +173,14 @@ export const getLoginStyles = (theme: GrafanaTheme2) => {
|
||||
maxWidth: 415,
|
||||
width: '100%',
|
||||
transform: 'translate(0px, 0px)',
|
||||
transition: '0.25s ease',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: '0.25s ease',
|
||||
},
|
||||
}),
|
||||
enterAnimation: css({
|
||||
...handleReducedMotion({
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
animation: `${flyInAnimation} ease-out 0.2s`,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css, cx, keyframes } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
|
||||
@ -149,6 +149,15 @@ function LinkToContactPoints() {
|
||||
);
|
||||
}
|
||||
|
||||
const rotation = keyframes({
|
||||
from: {
|
||||
transform: 'rotate(720deg)',
|
||||
},
|
||||
to: {
|
||||
transform: 'rotate(0deg)',
|
||||
},
|
||||
});
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
contactPointsSelector: css({
|
||||
display: 'flex',
|
||||
@ -172,14 +181,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
}),
|
||||
loading: css({
|
||||
pointerEvents: 'none',
|
||||
animation: 'rotation 2s infinite linear',
|
||||
'@keyframes rotation': {
|
||||
from: {
|
||||
transform: 'rotate(720deg)',
|
||||
},
|
||||
to: {
|
||||
transform: 'rotate(0deg)',
|
||||
},
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
animation: `${rotation} 2s infinite linear`,
|
||||
},
|
||||
[theme.transitions.handleMotion('reduce')]: {
|
||||
animation: `${rotation} 6s infinite linear`,
|
||||
},
|
||||
}),
|
||||
warn: css({
|
||||
|
@ -122,6 +122,8 @@ export const droneFrontItem: CanvasElementItem = {
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
droneFront: css({
|
||||
// TODO: figure out what styles to apply when prefers-reduced-motion is set
|
||||
// eslint-disable-next-line @grafana/no-unreduced-motion
|
||||
transition: 'transform 0.4s',
|
||||
}),
|
||||
});
|
||||
|
@ -121,6 +121,8 @@ export const droneSideItem: CanvasElementItem = {
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
droneSide: css({
|
||||
// TODO: figure out what styles to apply when prefers-reduced-motion is set
|
||||
// eslint-disable-next-line @grafana/no-unreduced-motion
|
||||
transition: 'transform 0.4s',
|
||||
}),
|
||||
});
|
||||
|
@ -180,9 +180,13 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
},
|
||||
}),
|
||||
propellerCW: css({
|
||||
// TODO: figure out what styles to apply when prefers-reduced-motion is set
|
||||
// eslint-disable-next-line @grafana/no-unreduced-motion
|
||||
animationDirection: 'normal',
|
||||
}),
|
||||
propellerCCW: css({
|
||||
// TODO: figure out what styles to apply when prefers-reduced-motion is set
|
||||
// eslint-disable-next-line @grafana/no-unreduced-motion
|
||||
animationDirection: 'reverse',
|
||||
}),
|
||||
});
|
||||
|
@ -173,7 +173,9 @@ export const getServerStyles = (data: ServerData | undefined) => (theme: Grafana
|
||||
fill: data?.statusColor ?? 'transparent',
|
||||
}),
|
||||
circle: css({
|
||||
animation: `blink ${data?.blinkRate ? 1 / data.blinkRate : 0}s infinite step-end`,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
animation: `blink ${data?.blinkRate ? 1 / data.blinkRate : 0}s infinite step-end`,
|
||||
},
|
||||
fill: data?.bulbColor,
|
||||
stroke: 'none',
|
||||
}),
|
||||
|
@ -90,7 +90,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
fontSize: theme.typography.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
paddingLeft: `${theme.spacing(1)}`,
|
||||
transition: 'background-color 0.1s ease-in-out',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: 'background-color 0.1s ease-in-out',
|
||||
},
|
||||
cursor: 'move',
|
||||
|
||||
'&:hover': {
|
||||
@ -102,7 +104,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
outline: '2px dotted transparent',
|
||||
outlineOffset: '2px',
|
||||
boxShadow: '0 0 0 2px black, 0 0 0px 4px #1f60c4',
|
||||
animation: `${pulsate} 2s ease infinite`,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
animation: `${pulsate} 2s ease infinite`,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -50,7 +50,9 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
opacity: '0%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
animation: `${invisibleToVisible} 0s step-end ${slowStartThreshold} 1 normal forwards`,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
animation: `${invisibleToVisible} 0s step-end ${slowStartThreshold} 1 normal forwards`,
|
||||
},
|
||||
}),
|
||||
dashboardLoadingText: css({
|
||||
fontSize: theme.typography.h4.fontSize,
|
||||
|
@ -68,6 +68,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
}),
|
||||
drawerActive: css({
|
||||
opacity: 1,
|
||||
animation: `0.5s ease-out ${drawerSlide(theme)}`,
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
animation: `0.5s ease-out ${drawerSlide(theme)}`,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
@ -567,7 +567,9 @@ function getStyles(theme: GrafanaTheme2, height: number, width: number) {
|
||||
}),
|
||||
rzHandle: css({
|
||||
background: theme.colors.secondary.main,
|
||||
transition: '0.3s background ease-in-out',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: '0.3s background ease-in-out',
|
||||
},
|
||||
position: 'relative',
|
||||
height: '50% !important',
|
||||
width: `${theme.spacing(1)} !important`,
|
||||
|
@ -138,9 +138,11 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
padding: theme.spacing(1),
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
transition: theme.transitions.create(['background'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create(['background'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
background: theme.colors.emphasize(theme.colors.background.secondary, 0.03),
|
||||
|
@ -100,9 +100,11 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
background: theme.colors.background.secondary,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
padding: theme.spacing(3),
|
||||
transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
background: theme.colors.emphasize(theme.colors.background.secondary, 0.03),
|
||||
|
@ -307,7 +307,9 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme2) => {
|
||||
marble: css({
|
||||
display: 'block',
|
||||
opacity: 0.5,
|
||||
transition: 'transform 0.15s ease-out',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: 'transform 0.15s ease-out',
|
||||
},
|
||||
}),
|
||||
activeMarble: css({
|
||||
transform: 'scale(1.3)',
|
||||
|
Loading…
Reference in New Issue
Block a user