DesignSystem: Menu and popover styling update to use new elevated background token (#100255)

* DesignSystem: Menu and popover styling tweak proposal

* Fix submenu

* Themes: Add elevated prop

* Update themes

* update

* Fixed tests

* Update

* fix markdown lint

* Update packages/grafana-data/src/themes/createColors.ts

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>

* Update contribute/style-guides/themes.md

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>

* Update

* Update

---------

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
Torkel Ödegaard 2025-02-11 12:27:04 +01:00 committed by GitHub
parent d87ef806f0
commit 9bdacf3833
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 90 additions and 91 deletions

View File

@ -564,13 +564,6 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
@ -1348,25 +1341,15 @@ exports[`better eslint`] = {
[0, 0, 0, "\'@grafana/ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup\' import is restricted from being used by a pattern. Import from the public export instead.", "3"],
[0, 0, 0, "\'@grafana/ui/src/components/JSONFormatter/JSONFormatter\' import is restricted from being used by a pattern. Import from the public export instead.", "4"],
[0, 0, 0, "\'@grafana/ui/src/themes\' import is restricted from being used by a pattern. Import from the public export instead.", "5"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"],
[0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "6"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "7"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "8"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "9"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "10"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "11"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "12"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "13"]
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "9"]
],
"public/app/features/actions/ActionEditorModalContent.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/Button\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
[0, 0, 0, "\'@grafana/ui/src/components/Modal/Modal\' import is restricted from being used by a pattern. Import from the public export instead.", "1"]
],
"public/app/features/actions/ActionsInlineEditor.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/Button\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
[0, 0, 0, "\'@grafana/ui/src/components/Modal/Modal\' import is restricted from being used by a pattern. Import from the public export instead.", "1"],
[0, 0, 0, "\'@grafana/ui/src/themes\' import is restricted from being used by a pattern. Import from the public export instead.", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"]
],
"public/app/features/actions/ParamsEditor.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/IconButton/IconButton\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
[0, 0, 0, "\'@grafana/ui/src/components/Input/Input\' import is restricted from being used by a pattern. Import from the public export instead.", "1"],

View File

@ -96,11 +96,12 @@ Example use cases:
### Background colors
| Property | When to use |
| --------------------------------- | ------------------------------------------------------------------------------------------------ |
| theme.colors.background.canvas | Dashboard background. A background surface for panels and panes that use primary background |
| theme.colors.background.primary | The default content background for content panes and panels |
| theme.colors.background.secondary | For cards and other surfaces that need to stand out when placed on top of the primary background |
| Property | When to use |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| theme.colors.background.canvas | Dashboard background. A background surface for panels and panes that use primary background |
| theme.colors.background.primary | The default content background for content panes and panels |
| theme.colors.background.secondary | For cards and other surfaces that need to stand out when placed on top of the primary background |
| theme.colors.background.elevated | For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark themes it has a brighter shade to help give it contrast against the primary background |
### Borders

View File

@ -34,6 +34,11 @@ export interface ThemeColorsBase<TColor> {
primary: string;
/** Cards and elements that need to stand out on the primary background */
secondary: string;
/**
* For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark
* themes it has a brighter shade to help give it contrast against the primary background.
**/
elevated: string;
};
border: {
@ -143,6 +148,7 @@ class DarkColors implements ThemeColorsBase<Partial<ThemeRichColor>> {
canvas: palette.gray05,
primary: palette.gray10,
secondary: palette.gray15,
elevated: palette.gray15,
};
action = {
@ -225,6 +231,7 @@ class LightColors implements ThemeColorsBase<Partial<ThemeRichColor>> {
canvas: palette.gray90,
primary: palette.white,
secondary: palette.gray100,
elevated: palette.white,
};
action = {

View File

@ -83,7 +83,7 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th
background: input.background,
},
tooltip: {
background: colors.background.secondary,
background: colors.background.elevated,
text: colors.text.primary,
},
dashboard: {

View File

@ -28,6 +28,7 @@ const aubergineTheme: NewThemeOptions = {
canvas: '#2E1F2D',
primary: '#3C2136',
secondary: '#4A2D47',
elevated: '#4A2D47',
},
action: {
hover: '#6A3C4B',

View File

@ -16,6 +16,7 @@ const debugTheme: NewThemeOptions = {
canvas: '#000033',
primary: '#000044',
secondary: '#000055',
elevated: '#000055',
},
text: {
primary: '#bbbb00',

View File

@ -52,6 +52,7 @@ const desertBloomTheme: NewThemeOptions = {
canvas: '#FFF8F0',
primary: '#FFFFFF',
secondary: '#f9f3e8',
elevated: '#FFFFFF',
},
action: {
hover: 'rgba(168, 156, 134, 0.12)',

View File

@ -40,6 +40,7 @@ const gildedGroveTheme: NewThemeOptions = {
canvas: '#111614',
primary: '#1d2220',
secondary: '#27312E',
elevated: '#27312E',
},
action: {
hover: 'rgba(200, 200, 180, 0.16)',

View File

@ -52,6 +52,7 @@ const gloomTheme: NewThemeOptions = {
canvas: '#000',
primary: '#0d0b14',
secondary: '#19171f',
elevated: '#19171f',
},
action: {

View File

@ -28,6 +28,7 @@ const marsTheme: NewThemeOptions = {
canvas: '#3C1E1E',
primary: '#522626',
secondary: '#6A2F2F',
elevated: '#6A2F2F',
},
action: {
hover: 'rgba(210, 90, 60, 0.16)',

View File

@ -8,6 +8,7 @@ const matrixTheme: NewThemeOptions = {
canvas: '#000000',
primary: '#020202',
secondary: '#080808',
elevated: '#080808',
},
text: {
primary: '#00c017',

View File

@ -54,6 +54,7 @@ const sapphireDuskTheme: NewThemeOptions = {
canvas: '#1e273d',
primary: '#12192e',
secondary: '#212c47',
elevated: '#212c47',
},
action: {
hover: '#364057',

View File

@ -28,6 +28,7 @@ const synthwaveTheme: NewThemeOptions = {
canvas: '#1A1A2E',
primary: '#16213E',
secondary: '#0F3460',
elevated: '#0F3460',
},
action: {
hover: 'rgba(255, 20, 147, 0.16)',

View File

@ -28,6 +28,7 @@ const tronTheme: NewThemeOptions = {
canvas: '#0A0F18',
primary: '#0F1B2A',
secondary: '#152234',
elevated: '#152234',
},
action: {
hover: 'rgba(0, 255, 255, 0.16)',

View File

@ -28,6 +28,7 @@ const victorianTheme: NewThemeOptions = {
canvas: '#1F1510',
primary: '#2C1A13',
secondary: '#402A21',
elevated: '#402A21',
},
action: {
hover: '#3A2C22',

View File

@ -28,6 +28,7 @@ const zenTheme: NewThemeOptions = {
canvas: '#F4F4F4',
primary: '#E9E9E9',
secondary: '#D8D8D8',
elevated: '#E9E9E9',
},
action: {
hover: '#D1D1D1',

View File

@ -16,8 +16,11 @@ describe('ColorPicker', () => {
mainButton.forEach((button) => expect(button).toHaveAttribute('type', 'button'));
await userEvent.click(mainButton[0]);
const buttons = screen.getAllByRole('button');
expect(buttons.length).toBe(35);
expect(buttons.length).toBe(33);
buttons.forEach((button) => expect(button).toHaveAttribute('type', 'button'));
const tabs = screen.getAllByRole('tab');
expect(tabs.length).toBe(2);
});
it('renders custom trigger when supplied', () => {

View File

@ -11,7 +11,7 @@ describe('ColorPickerPopover', () => {
it('should be tabbable', async () => {
render(<ColorPickerPopover color={'red'} onChange={() => {}} />);
const color = screen.getByRole('button', { name: 'dark-red color' });
const customTab = screen.getByRole('button', { name: 'Custom' });
const customTab = screen.getByRole('tab', { name: 'Custom' });
await userEvent.tab();
expect(customTab).toHaveFocus();

View File

@ -7,7 +7,8 @@ import { GrafanaTheme2, colorManipulator } from '@grafana/data';
import { stylesFactory, withTheme2 } from '../../themes';
import { Themeable2 } from '../../types/theme';
import { Trans } from '../../utils/i18n';
import { t } from '../../utils/i18n';
import { Tab, TabsBar } from '../Tabs';
import { PopoverContentProps } from '../Tooltip';
import { NamedColorsPalette } from './NamedColorsPalette';
@ -18,7 +19,6 @@ export type ColorPickerChangeHandler = (color: string) => void;
export interface ColorPickerProps extends Themeable2 {
color: string;
onChange: ColorPickerChangeHandler;
enableNamedColors?: boolean;
}
@ -47,11 +47,6 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Comp
};
}
getTabClassName = (tabName: PickerType | keyof T) => {
const { activePicker } = this.state;
return `ColorPickerPopover__tab ${activePicker === tabName && 'ColorPickerPopover__tab--active'}`;
};
handleChange = (color: string) => {
const { onChange, enableNamedColors, theme } = this.props;
if (enableNamedColors) {
@ -101,11 +96,7 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Comp
return (
<>
{Object.keys(customPickers).map((key) => {
return (
<button className={this.getTabClassName(key)} onClick={this.onTabChange(key)} key={key} type="button">
{customPickers[key].name}
</button>
);
return <Tab label={customPickers[key].name} onChangeTab={this.onTabChange(key)} key={key} />;
})}
</>
);
@ -113,7 +104,10 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Comp
render() {
const { theme } = this.props;
const { activePicker } = this.state;
const styles = getStyles(theme);
return (
<FocusScope contain restoreFocus autoFocus>
{/*
@ -121,15 +115,19 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Comp
see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668
*/}
<div tabIndex={-1} className={styles.colorPickerPopover}>
<div className={styles.colorPickerPopoverTabs}>
<button className={this.getTabClassName('palette')} onClick={this.onTabChange('palette')} type="button">
<Trans i18nKey="grafana-ui.color-picker-popover.palette-tab">Colors</Trans>
</button>
<button className={this.getTabClassName('spectrum')} onClick={this.onTabChange('spectrum')} type="button">
<Trans i18nKey="grafana-ui.color-picker-popover.spectrum-tab">Custom</Trans>
</button>
<TabsBar>
<Tab
label={t('grafana-ui.color-picker-popover.palette-tab', 'Colors')}
onChangeTab={this.onTabChange('palette')}
active={activePicker === 'palette'}
/>
<Tab
label={t('grafana-ui.color-picker-popover.spectrum-tab', 'Custom')}
onChangeTab={this.onTabChange('spectrum')}
active={activePicker === 'spectrum'}
/>
{this.renderCustomPickerTabs()}
</div>
</TabsBar>
<div className={styles.colorPickerPopoverContent}>{this.renderPicker()}</div>
</div>
</FocusScope>
@ -145,34 +143,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
colorPickerPopover: css({
borderRadius: theme.shape.radius.default,
boxShadow: theme.shadows.z3,
background: theme.colors.background.primary,
background: theme.colors.background.elevated,
padding: theme.spacing(0.5),
border: `1px solid ${theme.colors.border.weak}`,
'.ColorPickerPopover__tab': {
width: '50%',
textAlign: 'center',
padding: theme.spacing(1, 0),
background: theme.colors.background.secondary,
color: theme.colors.text.secondary,
fontSize: theme.typography.bodySmall.fontSize,
cursor: 'pointer',
border: 'none',
'&:focus:not(:focus-visible)': {
outline: 'none',
boxShadow: 'none',
},
':focus-visible': {
position: 'relative',
},
},
'.ColorPickerPopover__tab--active': {
color: theme.colors.text.primary,
fontWeight: theme.typography.fontWeightMedium,
background: theme.colors.background.primary,
},
}),
colorPickerPopoverContent: css({
width: '246px',

View File

@ -54,7 +54,7 @@ const getStyles = (theme: GrafanaTheme2) => {
},
}),
colorLabel: css({
paddingLeft: theme.spacing(2),
paddingLeft: theme.spacing(1),
display: 'flex',
alignItems: 'center',
}),

View File

@ -277,7 +277,7 @@ const getStyles = (
isFullscreen?: boolean
) => ({
container: css({
background: theme.colors.background.primary,
background: theme.colors.background.elevated,
boxShadow: theme.shadows.z3,
width: `${isFullscreen ? '546px' : '262px'}`,
borderRadius: theme.shape.radius.default,

View File

@ -35,7 +35,7 @@ const MenuComp = React.forwardRef<HTMLDivElement, MenuProps>(
<Box
{...otherProps}
aria-label={ariaLabel}
backgroundColor="primary"
backgroundColor="elevated"
borderRadius="default"
boxShadow="z3"
display="inline-block"

View File

@ -77,7 +77,7 @@ const getStyles = (theme: GrafanaTheme2) => {
color: theme.colors.text.secondary,
}),
itemsWrapper: css({
background: theme.colors.background.primary,
background: theme.colors.background.elevated,
padding: theme.spacing(0.5),
boxShadow: theme.shadows.z3,
display: 'inline-block',

View File

@ -15,7 +15,7 @@ import { InlineFieldRow } from '../Forms/InlineFieldRow';
import { RadioButtonGroup } from '../Forms/RadioButtonGroup/RadioButtonGroup';
import { Icon } from '../Icon/Icon';
import { Input } from '../Input/Input';
import { BackgroundColor, BorderColor, Box } from '../Layout/Box/Box';
import { BackgroundColor, BorderColor, Box, BoxShadow } from '../Layout/Box/Box';
import { Stack } from '../Layout/Stack/Stack';
import { Select } from '../Select/Select';
import { Switch } from '../Switch/Switch';
@ -24,12 +24,20 @@ import { Text, TextProps } from '../Text/Text';
interface DemoBoxProps {
bg?: BackgroundColor;
border?: BorderColor;
shadow?: BoxShadow;
textColor?: TextProps['color'];
}
const DemoBox = ({ bg, border, children }: React.PropsWithChildren<DemoBoxProps>) => {
const DemoBox = ({ bg, border, children, shadow }: React.PropsWithChildren<DemoBoxProps>) => {
return (
<Box backgroundColor={bg ? bg : undefined} padding={2} borderStyle={border ? 'solid' : undefined}>
<Box
backgroundColor={bg ? bg : undefined}
padding={2}
borderStyle={border ? 'solid' : undefined}
borderColor={border}
boxShadow={shadow}
borderRadius={'default'}
>
{children}
</Box>
);
@ -91,8 +99,14 @@ export const ThemeDemo = () => {
<DemoBox bg="primary" border="weak">
<DemoText>t.colors.background.primary is the main & preferred content </DemoText>
<DemoBox bg="secondary" border="weak">
<DemoText>t.colors.background.secondary and t.colors.border.layer1</DemoText>
<DemoText>t.colors.background.secondary (Used for cards)</DemoText>
</DemoBox>
<Box padding={4}>
<DemoText>t.colors.background.elevated</DemoText>
<DemoBox bg="elevated" border="weak" shadow="z3">
This elevated color should be used for menus and popovers.
</DemoBox>
</Box>
</DemoBox>
</CollapsableSection>
<CollapsableSection label="Text colors" isOpen={true}>

View File

@ -60,7 +60,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
flexDirection: 'column',
flex: 1,
gap: 2,
borderTop: `1px solid ${theme.colors.border.medium}`,
borderTop: `1px solid ${theme.colors.border.weak}`,
padding: theme.spacing(1),
}),
});

View File

@ -732,7 +732,7 @@ const getStyles = (theme: GrafanaTheme2, maxWidth?: number) => ({
whiteSpace: 'pre',
borderRadius: theme.shape.radius.default,
position: 'fixed',
background: theme.colors.background.primary,
background: theme.colors.background.elevated,
border: `1px solid ${theme.colors.border.weak}`,
boxShadow: theme.shadows.z2,
userSelect: 'text',

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { render, renderHook } from '@testing-library/react';
import { GrafanaTheme2 } from '@grafana/data';
import { createTheme, GrafanaTheme2 } from '@grafana/data';
import { mockThemeContext, useStyles2 } from './ThemeContext';
@ -45,7 +45,7 @@ describe('useStyles', () => {
const { rerender, result } = renderHook(() => useStyles2(stylesCreator));
const storedReference = result.current;
const restoreThemeContext = mockThemeContext({});
const restoreThemeContext = mockThemeContext(createTheme());
rerender();
expect(storedReference).not.toBe(result.current);
restoreThemeContext();

View File

@ -118,6 +118,13 @@ export function useStyles2<T extends unknown[], CSSReturnValue>(
): CSSReturnValue {
const theme = useTheme2();
// Grafana ui can be bundled and used in older versions of Grafana where the theme doesn't have elevated background
// This can be removed post G12
if (!theme.colors.background.elevated) {
theme.colors.background.elevated =
theme.colors.mode === 'light' ? theme.colors.background.primary : theme.colors.background.secondary;
}
let memoizedStyleCreator: typeof getStyles = memoizedStyleCreators.get(getStyles);
if (!memoizedStyleCreator) {

View File

@ -75,7 +75,7 @@ export function getFocusStyles(theme: GrafanaTheme2) {
// max-width is set up based on .grafana-tooltip class that's used in dashboard
export const getTooltipContainerStyles = (theme: GrafanaTheme2) => ({
overflow: 'hidden',
background: theme.colors.background.secondary,
background: theme.colors.background.elevated,
boxShadow: theme.shadows.z2,
maxWidth: '800px',
padding: theme.spacing(1),

View File

@ -129,8 +129,7 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth
const getStyles = (theme: GrafanaTheme2) => {
return {
editor: css({
// zIndex: theme.zIndex.tooltip,
background: theme.colors.background.primary,
background: theme.colors.background.elevated,
border: `1px solid ${theme.colors.border.weak}`,
borderRadius: theme.shape.radius.default,
boxShadow: theme.shadows.z3,

View File

@ -106,9 +106,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
zIndex: theme.zIndex.tooltip,
whiteSpace: 'initial',
borderRadius: theme.shape.radius.default,
background: theme.colors.background.primary,
background: theme.colors.background.elevated,
border: `1px solid ${theme.colors.border.weak}`,
boxShadow: theme.shadows.z2,
boxShadow: theme.shadows.z3,
userSelect: 'text',
}),
header: css({