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
pull/87060/head
Ashley Harrison 1 year ago committed by GitHub
parent 8a1f43a65d
commit c151a97110
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      .eslintrc
  2. 8
      packages/grafana-data/src/themes/createTransitions.ts
  3. 84
      packages/grafana-eslint-rules/README.md
  4. 2
      packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs
  5. 4
      packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx
  6. 16
      packages/grafana-ui/src/components/AutoSaveField/EllipsisAnimated.tsx
  7. 4
      packages/grafana-ui/src/components/BrowserLabel/Label.tsx
  8. 16
      packages/grafana-ui/src/components/Card/CardContainer.tsx
  9. 1
      packages/grafana-ui/src/components/Collapse/Collapse.tsx
  10. 8
      packages/grafana-ui/src/components/ColorPicker/ColorSwatch.tsx
  11. 40
      packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx
  12. 4
      packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx
  13. 17
      packages/grafana-ui/src/components/Dropdown/Dropdown.tsx
  14. 5
      packages/grafana-ui/src/components/IconButton/IconButton.tsx
  15. 32
      packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx
  16. 4
      packages/grafana-ui/src/components/PanelChrome/HoverWidget.tsx
  17. 4
      packages/grafana-ui/src/components/Table/styles.ts
  18. 8
      packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx
  19. 6
      packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx
  20. 10
      packages/grafana-ui/src/components/transitions/FadeTransition.tsx
  21. 16
      packages/grafana-ui/src/components/transitions/SlideOutTransition.tsx
  22. 16
      packages/grafana-ui/src/utils/handleReducedMotion.ts
  23. 1
      packages/grafana-ui/src/utils/index.ts
  24. 4
      packages/grafana-ui/src/utils/tooltipUtils.ts
  25. 6
      public/app/core/components/AppChrome/AppChromeMenu.tsx
  26. 46
      public/app/core/components/BouncingLoader/BouncingLoader.tsx
  27. 14
      public/app/core/components/Login/LoginLayout.tsx
  28. 24
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx
  29. 2
      public/app/features/canvas/elements/droneFront.tsx
  30. 2
      public/app/features/canvas/elements/droneSide.tsx
  31. 4
      public/app/features/canvas/elements/droneTop.tsx
  32. 4
      public/app/features/canvas/elements/server/server.tsx
  33. 8
      public/app/features/dashboard/components/AddLibraryPanelWidget/AddLibraryPanelWidget.tsx
  34. 4
      public/app/features/dashboard/components/DashboardLoading/DashboardLoading.tsx
  35. 4
      public/app/features/explore/ExploreDrawer.tsx
  36. 4
      public/app/features/explore/Logs/LogsTableWrap.tsx
  37. 8
      public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx
  38. 8
      public/app/features/plugins/admin/components/PluginListItem.tsx
  39. 4
      public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx

@ -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…
Cancel
Save