From 3dc30b5acbddfa61f17a0e131b3b308abe3a995a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 19 Sep 2025 16:10:33 +0200 Subject: [PATCH] Buttons: Active style for buttons (#111235) * chore: initial tweaks * chore: remove shadow * chore: add reduced motion support, clean up * chore: clean * chore: active states tweaks * Buttons: Add active state / style * Update ToolbarButton and IconButton * Update * Update * Get rid of important on disabled styles * Only color for active * Remove transform bits --------- Co-authored-by: Galen --- .../src/components/Button/Button.tsx | 57 ++++++++++++------- .../src/components/IconButton/IconButton.tsx | 33 +++++++---- .../ToolbarButton/ToolbarButton.tsx | 16 ++++-- packages/grafana-ui/src/themes/mixins.ts | 7 +++ 4 files changed, 78 insertions(+), 35 deletions(-) diff --git a/packages/grafana-ui/src/components/Button/Button.tsx b/packages/grafana-ui/src/components/Button/Button.tsx index 83b1c5825c0..c596904a39b 100644 --- a/packages/grafana-ui/src/components/Button/Button.tsx +++ b/packages/grafana-ui/src/components/Button/Button.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { GrafanaTheme2, ThemeRichColor } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; -import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; +import { getButtonFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; import { IconName, IconSize, IconType } from '../../types/icon'; import { ComponentSize } from '../../types/size'; import { getPropertiesForButtonSize } from '../Forms/commonStyles'; @@ -234,7 +234,7 @@ export const getButtonStyles = (props: StyleProps) => { const { height, padding, fontSize } = getPropertiesForButtonSize(size, theme); const variantStyles = getPropertiesForVariant(theme, variant, fill); const disabledStyles = getPropertiesForDisabled(theme, variant, fill); - const focusStyle = getFocusStyles(theme); + const focusStyle = getButtonFocusStyles(theme); const paddingMinusBorder = theme.spacing.gridSize * padding - 1; return { @@ -263,6 +263,12 @@ export const getButtonStyles = (props: StyleProps) => { ...variantStyles, ':disabled': disabledStyles, '&[disabled]': disabledStyles, + + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background-color', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), + }, }), disabled: css(disabledStyles, { '&:hover': css(disabledStyles), @@ -290,12 +296,18 @@ export const getButtonStyles = (props: StyleProps) => { }; }; -function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fill: ButtonFill) { +export function getActiveButtonStyles(color: ThemeRichColor, fill: ButtonFill) { + return { + background: fill === 'solid' ? color.main : 'transparent', + }; +} + +export function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fill: ButtonFill) { let outlineBorderColor = color.border; let borderColor = 'transparent'; let hoverBorderColor = 'transparent'; - // Secondary button has some special rules as we lack theem color token to + // Secondary button has some special rules as we lack the color token to // specify border color for normal button vs border color for outline button if (color.name === 'secondary') { borderColor = color.border; @@ -308,15 +320,16 @@ function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fil background: 'transparent', color: color.text, border: `1px solid ${outlineBorderColor}`, - transition: theme.transitions.create(['background-color', 'border-color', 'color'], { - duration: theme.transitions.duration.short, - }), - '&:hover': { + '&:hover, &:focus': { background: color.transparent, borderColor: theme.colors.emphasize(outlineBorderColor, 0.25), color: color.text, }, + + '&:active': { + ...getActiveButtonStyles(color, fill), + }, }; } @@ -325,18 +338,15 @@ function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fil background: 'transparent', color: color.text, border: '1px solid transparent', - transition: theme.transitions.create(['background-color', 'color'], { - duration: theme.transitions.duration.short, - }), - '&:focus': { - outline: 'none', - textDecoration: 'none', - }, - - '&:hover': { + '&:hover, &:focus': { background: color.transparent, textDecoration: 'none', + outline: 'none', + }, + + '&:active': { + ...getActiveButtonStyles(color, fill), }, }; } @@ -345,9 +355,6 @@ function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fil background: color.main, color: color.contrastText, border: `1px solid ${borderColor}`, - transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { - duration: theme.transitions.duration.short, - }), '&:hover': { background: color.shade, @@ -355,6 +362,15 @@ function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fil boxShadow: theme.shadows.z1, borderColor: hoverBorderColor, }, + + '&:focus': { + background: color.shade, + color: color.contrastText, + }, + + '&:active': { + ...getActiveButtonStyles(color, fill), + }, }; } @@ -364,6 +380,7 @@ function getPropertiesForDisabled(theme: GrafanaTheme2, variant: ButtonVariant, boxShadow: 'none', color: theme.colors.text.disabled, transition: 'none', + background: theme.colors.action.disabledBackground, }; if (fill === 'text') { diff --git a/packages/grafana-ui/src/components/IconButton/IconButton.tsx b/packages/grafana-ui/src/components/IconButton/IconButton.tsx index 2964143b331..bbc4519f71f 100644 --- a/packages/grafana-ui/src/components/IconButton/IconButton.tsx +++ b/packages/grafana-ui/src/components/IconButton/IconButton.tsx @@ -1,13 +1,13 @@ import { css, cx } from '@emotion/css'; import * as React from 'react'; -import { GrafanaTheme2, colorManipulator, deprecationWarning } from '@grafana/data'; +import { GrafanaTheme2, deprecationWarning } from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; import { IconName, IconSize, IconType } from '../../types/icon'; import { ComponentSize } from '../../types/size'; -import { IconRenderer } from '../Button/Button'; +import { getActiveButtonStyles, IconRenderer } from '../Button/Button'; import { getSvgSize } from '../Icon/utils'; import { Tooltip } from '../Tooltip/Tooltip'; import { PopoverContent, TooltipPlacement } from '../Tooltip/types'; @@ -107,13 +107,17 @@ const getStyles = (theme: GrafanaTheme2, size: IconSize, variant: IconButtonVari // overall size of the IconButton on hover // theme.spacing.gridSize originates from 2*4px for padding and letting the IconSize generally decide on the hoverSize const hoverSize = getSvgSize(size) + theme.spacing.gridSize; + const activeButtonStyle = getActiveButtonStyles(theme.colors.secondary, 'text'); - let iconColor = theme.colors.text.primary; + let iconColor = theme.colors.primary.text; + let hoverColor = theme.colors.primary.transparent; - if (variant === 'primary') { - iconColor = theme.colors.primary.text; + if (variant === 'secondary') { + iconColor = theme.colors.secondary.text; + hoverColor = theme.colors.secondary.transparent; } else if (variant === 'destructive') { iconColor = theme.colors.error.text; + hoverColor = theme.colors.error.transparent; } return { @@ -129,11 +133,21 @@ const getStyles = (theme: GrafanaTheme2, size: IconSize, variant: IconButtonVari alignItems: 'center', padding: 0, color: iconColor, + borderRadius: theme.shape.radius.default, + + '&:active': { + '&:before, &:hover:before': { + backgroundColor: activeButtonStyle.background, + }, + }, '&[disabled], &:disabled': { cursor: 'not-allowed', color: theme.colors.action.disabledText, opacity: 0.65, + '&:hover:before': { + backgroundColor: 'transparent', + }, }, '&:before': { @@ -155,12 +169,9 @@ const getStyles = (theme: GrafanaTheme2, size: IconSize, variant: IconButtonVari '&:focus:not(:focus-visible)': getMouseFocusStyles(theme), - '&:hover': { - '&:before': { - backgroundColor: - variant === 'secondary' ? theme.colors.action.hover : colorManipulator.alpha(iconColor, 0.12), - opacity: 1, - }, + '&:hover:before': { + backgroundColor: hoverColor, + opacity: 1, }, }), icon: css({ diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx index a866ccefc9e..376b1b3fc38 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -8,7 +8,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../themes/ThemeContext'; import { getFocusStyles, getMouseFocusStyles, mediaUp } from '../../themes/mixins'; import { IconSize } from '../../types/icon'; -import { getPropertiesForVariant } from '../Button/Button'; +import { getActiveButtonStyles, getPropertiesForVariant } from '../Button/Button'; import { Icon } from '../Icon/Icon'; import { Tooltip } from '../Tooltip/Tooltip'; @@ -134,11 +134,15 @@ const getStyles = (theme: GrafanaTheme2) => { color: theme.colors.text.primary, background: theme.colors.secondary.main, - '&:hover': { + '&:hover, &:focus': { color: theme.colors.text.primary, background: theme.colors.secondary.shade, border: `1px solid ${theme.colors.border.medium}`, }, + + '&:active': { + ...getActiveButtonStyles(theme.colors.secondary, 'solid'), + }, }); return { @@ -155,7 +159,7 @@ const getStyles = (theme: GrafanaTheme2) => { border: `1px solid ${theme.colors.secondary.border}`, whiteSpace: 'nowrap', [theme.transitions.handleMotion('no-preference', 'reduce')]: { - transition: theme.transitions.create(['background', 'box-shadow', 'border-color', 'color'], { + transition: theme.transitions.create(['background-color', 'border-color', 'color'], { duration: theme.transitions.duration.short, }), }, @@ -189,10 +193,14 @@ const getStyles = (theme: GrafanaTheme2) => { background: 'transparent', border: `1px solid transparent`, - '&:hover': { + '&:hover, &:focus': { color: theme.colors.text.primary, background: theme.colors.action.hover, }, + + '&:active': { + ...getActiveButtonStyles(theme.colors.secondary, 'solid'), + }, }), canvas: defaultOld, active: cx( diff --git a/packages/grafana-ui/src/themes/mixins.ts b/packages/grafana-ui/src/themes/mixins.ts index 5127047ada0..39bb7b19c83 100644 --- a/packages/grafana-ui/src/themes/mixins.ts +++ b/packages/grafana-ui/src/themes/mixins.ts @@ -72,6 +72,13 @@ export function getFocusStyles(theme: GrafanaTheme2) { }; } +export function getButtonFocusStyles(theme: GrafanaTheme2) { + return { + ...getFocusStyles(theme), + transitionProperty: undefined, + }; +} + // max-width is set up based on .grafana-tooltip class that's used in dashboard export const getTooltipContainerStyles = (theme: GrafanaTheme2) => ({ overflow: 'hidden',