Accessibility: Improve no-unreduced-motion rule and fix violations (#110304)
This commit is contained in:
@@ -584,6 +584,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Cascader/styles.ts": {
|
||||
"@grafana/no-unreduced-motion": {
|
||||
"count": 17
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @ts-check
|
||||
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<string, []>} RuleContext */
|
||||
const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils');
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(
|
||||
@@ -6,9 +7,38 @@ const createRule = ESLintUtils.RuleCreator(
|
||||
);
|
||||
|
||||
const restrictedProperties = ['animation', 'transition'];
|
||||
const excludedProperties = ['transitionProperty'];
|
||||
|
||||
const isRestrictedProperty = (/** @type string */ propertyName) => {
|
||||
return restrictedProperties.some((prop) => propertyName.startsWith(prop));
|
||||
return (
|
||||
!excludedProperties.includes(propertyName) && restrictedProperties.some((prop) => propertyName.startsWith(prop))
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('@typescript-eslint/utils').TSESTree.ObjectExpression} obj
|
||||
* @param {import('@typescript-eslint/utils/ts-eslint').RuleContext<string, []>} context
|
||||
*/
|
||||
const checkProperties = (obj, context) => {
|
||||
for (const property of obj.properties) {
|
||||
if (property.type !== AST_NODE_TYPES.Property) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
property.value.type === AST_NODE_TYPES.ObjectExpression &&
|
||||
property.key.type !== AST_NODE_TYPES.CallExpression
|
||||
) {
|
||||
checkProperties(property.value, context);
|
||||
}
|
||||
|
||||
if (property.key.type === AST_NODE_TYPES.Identifier && isRestrictedProperty(property.key.name)) {
|
||||
context.report({
|
||||
node: property,
|
||||
messageId: 'noUnreducedMotion',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rule = createRule({
|
||||
@@ -29,18 +59,7 @@ const rule = createRule({
|
||||
|
||||
for (const cssObject of cssObjects) {
|
||||
if (cssObject?.type === AST_NODE_TYPES.ObjectExpression) {
|
||||
for (const property of cssObject.properties) {
|
||||
if (
|
||||
property.type === AST_NODE_TYPES.Property &&
|
||||
property.key.type === AST_NODE_TYPES.Identifier &&
|
||||
isRestrictedProperty(property.key.name)
|
||||
) {
|
||||
context.report({
|
||||
node: property,
|
||||
messageId: 'noUnreducedMotion',
|
||||
});
|
||||
}
|
||||
}
|
||||
checkProperties(cssObject, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { RuleTester } from 'eslint';
|
||||
|
||||
import noUnreducedMotion from '../rules/no-unreduced-motion.cjs';
|
||||
|
||||
RuleTester.setDefaultConfig({
|
||||
languageOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('eslint no-unreduced-motion', noUnreducedMotion, {
|
||||
valid: [
|
||||
{
|
||||
name: 'basic case with handled preference',
|
||||
code: `
|
||||
css({
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
},
|
||||
})
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'basic case ignored property',
|
||||
code: `
|
||||
css({
|
||||
transitionProperty: 'opacity',
|
||||
})
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'basic case',
|
||||
code: `
|
||||
css({
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
})
|
||||
`,
|
||||
errors: 1,
|
||||
},
|
||||
{
|
||||
name: 'invalid usage in nested property or pseudo element',
|
||||
code: `
|
||||
css({
|
||||
foo: {
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
},
|
||||
'&:before': {
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
},
|
||||
})
|
||||
`,
|
||||
errors: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -122,7 +122,9 @@ const collapsableSectionStyles = (theme: GrafanaTheme2) => ({
|
||||
'&:focus-visible': {
|
||||
outline: 'none',
|
||||
outlineOffset: 'unset',
|
||||
transition: 'none',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: 'none',
|
||||
},
|
||||
boxShadow: 'none',
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -51,8 +51,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
top: 0,
|
||||
height: '250%',
|
||||
position: 'absolute',
|
||||
animation: 'loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67) 500ms',
|
||||
animationIterationCount: 100,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
animation: 'loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67) 500ms',
|
||||
animationIterationCount: 100,
|
||||
},
|
||||
[theme.transitions.handleMotion('reduce')]: {
|
||||
animationDuration: '10s',
|
||||
animationIterationCount: 20,
|
||||
},
|
||||
left: '-25%',
|
||||
background: theme.colors.primary.main,
|
||||
},
|
||||
|
||||
@@ -196,7 +196,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
'&:hover': {
|
||||
'.thumb-vertical, .thumb-horizontal': {
|
||||
opacity: 1,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
exports[`CustomScrollbar renders correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="css-1bz6b2c"
|
||||
class="css-rbj06h"
|
||||
style="position: relative; overflow: hidden; width: 100%; height: auto; min-height: 0; max-height: 100%;"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -33,7 +33,9 @@ export const getDragStyles = (theme: GrafanaTheme2, handlePosition?: DragHandleP
|
||||
'&:before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
transition: theme.transitions.create('border-color'),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create('border-color'),
|
||||
},
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
@@ -41,7 +43,9 @@ export const getDragStyles = (theme: GrafanaTheme2, handlePosition?: DragHandleP
|
||||
background: baseColor,
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
transition: theme.transitions.create('background'),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create('background'),
|
||||
},
|
||||
transform: 'translate(-50%, -50%)',
|
||||
borderRadius: theme.shape.radius.pill,
|
||||
zIndex: 1,
|
||||
|
||||
@@ -273,12 +273,22 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
}),
|
||||
drawerMotion: css({
|
||||
'&-appear': {
|
||||
transform: 'translateX(100%)',
|
||||
transition: 'none !important',
|
||||
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transform: 'translateX(100%)',
|
||||
transition: 'none !important',
|
||||
},
|
||||
[theme.transitions.handleMotion('reduce')]: {
|
||||
opacity: 0,
|
||||
},
|
||||
'&-active': {
|
||||
transition: `${theme.transitions.create('transform')} !important`,
|
||||
transform: 'translateX(0)',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transform: 'translateX(0)',
|
||||
transition: `${theme.transitions.create('transform')} !important`,
|
||||
},
|
||||
[theme.transitions.handleMotion('reduce')]: {
|
||||
transition: `opacity 0.2s ease-in-out`,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -308,7 +318,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
|
||||
'&-active': {
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create('opacity'),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create('opacity'),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -108,7 +108,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
left: '2px',
|
||||
top: '2px',
|
||||
background: theme.components.input.background,
|
||||
transition: '0.4s',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: '0.4s',
|
||||
},
|
||||
borderRadius: theme.shape.radius.circle,
|
||||
boxShadow: theme.shadows.z1,
|
||||
},
|
||||
|
||||
@@ -137,7 +137,9 @@ const getSwitchStyles = (theme: GrafanaTheme2, transparent?: boolean) => ({
|
||||
borderRadius: theme.shape.radius.pill,
|
||||
background: theme.components.input.background,
|
||||
border: `1px solid ${theme.components.input.borderColor}`,
|
||||
transition: 'all 0.3s ease',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: 'all 0.3s ease',
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
borderColor: theme.components.input.borderHover,
|
||||
@@ -155,7 +157,9 @@ const getSwitchStyles = (theme: GrafanaTheme2, transparent?: boolean) => ({
|
||||
left: 0,
|
||||
top: '50%',
|
||||
transform: `translate3d(${theme.spacing(0.25)}, -50%, 0)`,
|
||||
transition: 'transform 0.2s cubic-bezier(0.19, 1, 0.22, 1)',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: 'transform 0.2s cubic-bezier(0.19, 1, 0.22, 1)',
|
||||
},
|
||||
|
||||
'@media (forced-colors: active)': {
|
||||
border: `1px solid ${theme.colors.primary.contrastText}`,
|
||||
|
||||
@@ -64,6 +64,7 @@ export function getDashboardGridStyles(theme: GrafanaTheme2) {
|
||||
|
||||
// Disable animation on initial rendering and enable it when component has been mounted.
|
||||
'.react-grid-item.cssTransforms': {
|
||||
// eslint-disable-next-line @grafana/no-unreduced-motion
|
||||
transitionProperty: 'none !important',
|
||||
},
|
||||
|
||||
@@ -93,7 +94,9 @@ export function getDashboardGridStyles(theme: GrafanaTheme2) {
|
||||
'.dashboard-canvas-add-button': {
|
||||
display: 'flex',
|
||||
opacity: 0.5,
|
||||
transition: theme.transitions.create('opacity'),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create('opacity'),
|
||||
},
|
||||
filter: `grayscale(100%)`,
|
||||
'&:hover,:focus-within': {
|
||||
opacity: 1,
|
||||
|
||||
@@ -89,7 +89,9 @@ export function getJsonFormatterStyles(theme: GrafanaTheme2) {
|
||||
|
||||
'&::after': {
|
||||
display: 'inline-block',
|
||||
transition: 'transform 100ms ease-in',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: 'transform 100ms ease-in',
|
||||
},
|
||||
content: "'►'",
|
||||
},
|
||||
},
|
||||
@@ -97,7 +99,9 @@ export function getJsonFormatterStyles(theme: GrafanaTheme2) {
|
||||
// Inline preview on hover (optional)
|
||||
'> a > .json-formatter-preview-text': {
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.15s ease-in',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: 'opacity 0.15s ease-in',
|
||||
},
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ export function getSlateStyles(theme: GrafanaTheme2) {
|
||||
backgroundImage: 'none',
|
||||
border: `1px solid ${theme.components.input.borderColor}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
transition: 'all 0.3s',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: 'all 0.3s',
|
||||
},
|
||||
lineHeight: '18px',
|
||||
},
|
||||
|
||||
@@ -67,8 +69,10 @@ export function getSlateStyles(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')]: {
|
||||
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)',
|
||||
},
|
||||
},
|
||||
|
||||
'.typeahead-item__selected': {
|
||||
|
||||
@@ -124,7 +124,6 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive']) => ({
|
||||
boxShadow: 'none',
|
||||
outline: `2px solid ${theme.colors.primary.main}`,
|
||||
outlineOffset: '-2px',
|
||||
transition: 'none',
|
||||
},
|
||||
}),
|
||||
linkContent: css({
|
||||
|
||||
@@ -33,7 +33,10 @@ const LoginBackground: FC<BrandComponentProps> = ({ className, children }) => {
|
||||
backgroundRepeat: 'no-repeat',
|
||||
|
||||
opacity: 0,
|
||||
transition: 'opacity 3s ease-in-out',
|
||||
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: 'opacity 3s ease-in-out',
|
||||
},
|
||||
|
||||
[theme.breakpoints.up('md')]: {
|
||||
backgroundPosition: 'center',
|
||||
|
||||
@@ -153,9 +153,12 @@ export const getLoginStyles = (theme: GrafanaTheme2) => {
|
||||
borderRadius: theme.shape.radius.lg,
|
||||
padding: theme.spacing(2, 0),
|
||||
opacity: 0,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
},
|
||||
[theme.transitions.handleMotion('reduce')]: {
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
minHeight: theme.spacing(40),
|
||||
|
||||
@@ -84,10 +84,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
fullScreenPanel: css({
|
||||
'.react-grid-layout': {
|
||||
height: 'auto !important',
|
||||
// eslint-disable-next-line @grafana/no-unreduced-motion
|
||||
transitionProperty: 'none',
|
||||
},
|
||||
'.react-grid-item': {
|
||||
display: 'none !important',
|
||||
// eslint-disable-next-line @grafana/no-unreduced-motion
|
||||
transitionProperty: 'none !important',
|
||||
|
||||
'&--fullscreen': {
|
||||
|
||||
+6
-2
@@ -17,12 +17,16 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
opacity: 0,
|
||||
transition: 'all 0.8s',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: 'all 0.8s',
|
||||
},
|
||||
},
|
||||
'&:active:after': {
|
||||
margin: 0,
|
||||
opacity: 0.3,
|
||||
transition: '0s',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: '0s',
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -189,7 +189,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
|
||||
},
|
||||
[`& .${nameWrapperClassName}, .${viewClassName}, .${nameWrapperMatchingFilterClassName}`]: {
|
||||
backgroundColor: autoColor(theme, '#cbe7ff'),
|
||||
animation: `${animations.flash} 1s cubic-bezier(0.12, 0, 0.39, 0)`,
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
animation: `${animations.flash} 1s cubic-bezier(0.12, 0, 0.39, 0)`,
|
||||
},
|
||||
},
|
||||
[`& .${spanBarClassName}`]: {
|
||||
opacity: 1,
|
||||
|
||||
@@ -285,7 +285,9 @@ export const getStyles = (theme: GrafanaTheme2, isResizeInProgress: boolean) =>
|
||||
color: theme.colors.text.primary,
|
||||
borderRight: `1px solid ${theme.components.panel.borderColor}`,
|
||||
borderBottom: `1px solid ${theme.components.panel.borderColor}`,
|
||||
transition: 'background-color 200ms',
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transition: 'background-color 200ms',
|
||||
},
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
|
||||
Reference in New Issue
Block a user