Table: Fix some styling issues with frozen columns (#109425)

This commit is contained in:
Paul Marbach
2025-08-08 17:27:44 -05:00
committed by GitHub
parent d921872b97
commit a819e977a1
3 changed files with 146 additions and 96 deletions
@@ -11,6 +11,7 @@ import {
getLuminance,
lighten,
asRgbString,
onBackground,
} from './colorManipulator';
describe('utils/colorManipulator', () => {
@@ -423,4 +424,16 @@ describe('utils/colorManipulator', () => {
expect(asRgbString('#000000')).toEqual('rgb(0, 0, 0)');
});
});
describe('onBackground', () => {
it('should calculate the foreground color given a background color', () => {
expect(onBackground('#ffffff', '#000').toHexString()).toBe('#ffffff');
expect(onBackground('#ffffff00', '#000').toHexString()).toBe('#000000');
expect(onBackground('#ffffff77', '#000').toHexString()).toBe('#777777');
expect(onBackground('#262a6d82', '#644242').toHexString()).toBe('#443658');
expect(onBackground('rgba(255,0,0,0.5)', 'rgba(0,255,0,0.5)').toRgbString()).toBe('rgba(170, 85, 0, 0.75)');
expect(onBackground('rgba(255,0,0,0.5)', 'rgba(0,0,255,1)').toRgbString()).toBe('rgb(128, 0, 128)');
expect(onBackground('rgba(0,0,255,1)', 'rgba(0,0,0,0.5)').toRgbString()).toBe('rgb(0, 0, 255)');
});
});
});
@@ -366,6 +366,32 @@ export function lighten(color: string, coefficient: number) {
return recomposeColor(parts);
}
/**
* given foreground and background colors, returns the color of the foreground color on the background color.
* this is valuable for foreground colors with alpha.
*
* adapted from https://github.com/scttcper/tinycolor/blob/2927a9d2aa03e037486a79a295542a7848621691/src/index.ts#L583-L594
*
* @param foreground
* @param background
* @returns a tinycolor instance
*/
export const onBackground = (
foreground: tinycolor.ColorInput,
background: tinycolor.ColorInput
): tinycolor.Instance => {
const fg = tinycolor(foreground).toRgb();
const bg = tinycolor(background).toRgb();
const alpha = fg.a + bg.a * (1 - fg.a);
return tinycolor({
r: (fg.r * fg.a + bg.r * bg.a * (1 - fg.a)) / alpha,
g: (fg.g * fg.a + bg.g * bg.a * (1 - fg.a)) / alpha,
b: (fg.b * fg.a + bg.b * bg.a * (1 - fg.a)) / alpha,
a: alpha,
});
};
interface DecomposeColor {
type: string;
values: any;
@@ -387,4 +413,5 @@ export const colorManipulator = {
alpha,
darken,
lighten,
onBackground,
};
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
import { GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2, colorManipulator } from '@grafana/data';
import { COLUMN, TABLE } from './constants';
import { TableCellStyles } from './types';
@@ -10,109 +10,120 @@ import { getJustifyContent } from './utils';
export const getGridStyles = (
theme: GrafanaTheme2,
{ enablePagination, transparent }: { enablePagination?: boolean; transparent?: boolean }
) => ({
grid: css({
'--rdg-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary,
'--rdg-header-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary,
'--rdg-border-color': theme.colors.border.weak,
'--rdg-color': theme.colors.text.primary,
'--rdg-summary-border-color': theme.colors.border.weak,
'--rdg-summary-border-width': '1px',
) => {
const bgColor = transparent ? theme.colors.background.canvas : theme.colors.background.primary;
// this needs to be pre-calc'd since the theme colors have alpha and the border color becomes
// unpredictable for background color cells
const borderColor = colorManipulator.onBackground(theme.colors.border.weak, bgColor).toHexString();
// note: this cannot have any transparency since default cells that
// overlay/overflow on hover inherit this background and need to occlude cells below
'--rdg-row-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary,
'--rdg-row-hover-background-color': transparent
? theme.colors.background.primary
: theme.colors.background.secondary,
return {
grid: css({
'--rdg-background-color': bgColor,
'--rdg-header-background-color': bgColor,
'--rdg-border-color': borderColor,
'--rdg-color': theme.colors.text.primary,
'--rdg-summary-border-color': borderColor,
'--rdg-summary-border-width': '1px',
// TODO: magic 32px number is unfortunate. it would be better to have the content
// flow using flexbox rather than hard-coding this size via a calc
blockSize: enablePagination ? 'calc(100% - 32px)' : '100%',
scrollbarWidth: 'thin',
scrollbarColor: theme.isDark ? '#fff5 #fff1' : '#0005 #0001',
// note: this cannot have any transparency since default cells that
// overlay/overflow on hover inherit this background and need to occlude cells below
'--rdg-row-background-color': bgColor,
'--rdg-row-hover-background-color': transparent
? theme.colors.background.primary
: theme.colors.background.secondary,
border: 'none',
// TODO: magic 32px number is unfortunate. it would be better to have the content
// flow using flexbox rather than hard-coding this size via a calc
blockSize: enablePagination ? 'calc(100% - 32px)' : '100%',
scrollbarWidth: 'thin',
scrollbarColor: theme.isDark ? '#fff5 #fff1' : '#0005 #0001',
'.rdg-cell': {
padding: TABLE.CELL_PADDING,
border: 'none',
'&:last-child': {
borderInlineEnd: 'none',
},
},
// add a box shadow on hover and selection for all body cells
'& > :not(.rdg-summary-row, .rdg-header-row) > .rdg-cell': {
'&:hover, &[aria-selected=true]': { boxShadow: theme.shadows.z2 },
// selected cells should appear below hovered cells.
'&:hover': { zIndex: theme.zIndex.tooltip - 4 },
'&[aria-selected=true]': { zIndex: theme.zIndex.tooltip - 5 },
},
'.rdg-cell.rdg-cell-frozen': { zIndex: theme.zIndex.tooltip - 2 },
'.rdg-header-row, .rdg-summary-row': {
'.rdg-cell': {
zIndex: theme.zIndex.tooltip - 3,
padding: TABLE.CELL_PADDING,
'&.rdg-cell-frozen': {
zIndex: theme.zIndex.tooltip - 1,
'&:last-child': {
borderInlineEnd: 'none',
},
},
},
}),
gridNested: css({
height: '100%',
width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING * 2 - 1}px)`,
overflow: 'visible',
marginLeft: COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING - 1,
marginBlock: TABLE.CELL_PADDING,
}),
cellNested: css({ '&[aria-selected=true]': { outline: 'none' } }),
noDataNested: css({
height: TABLE.NESTED_NO_DATA_HEIGHT,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: theme.colors.text.secondary,
fontSize: theme.typography.h4.fontSize,
}),
cellActions: css({
display: 'none',
position: 'absolute',
top: 0,
margin: 'auto',
height: '100%',
color: theme.colors.text.primary,
background: theme.isDark ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)',
padding: theme.spacing.x0_5,
paddingInlineStart: theme.spacing.x1,
}),
cellActionsEnd: css({ left: 0 }),
cellActionsStart: css({ right: 0 }),
headerRow: css({
paddingBlockStart: 0,
fontWeight: 'normal',
'& .rdg-cell': { height: '100%', alignItems: 'flex-end' },
}),
displayNone: css({ display: 'none' }),
paginationContainer: css({
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
marginTop: '8px',
width: '100%',
}),
paginationSummary: css({
color: theme.colors.text.secondary,
fontSize: theme.typography.bodySmall.fontSize,
display: 'flex',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1, 0, 2),
}),
menuItem: css({ maxWidth: '200px' }),
});
// add a box shadow on hover and selection for all body cells
'& > :not(.rdg-summary-row, .rdg-header-row) > .rdg-cell': {
'&:hover, &[aria-selected=true]': { boxShadow: theme.shadows.z2 },
// selected cells should appear below hovered cells.
'&:hover': { zIndex: theme.zIndex.tooltip - 7 },
'&[aria-selected=true]': { zIndex: theme.zIndex.tooltip - 6 },
},
'.rdg-cell.rdg-cell-frozen': {
backgroundColor: '--rdg-row-background-color',
zIndex: theme.zIndex.tooltip - 4,
'&:hover': { zIndex: theme.zIndex.tooltip - 2 },
'&[aria-selected=true]': { zIndex: theme.zIndex.tooltip - 3 },
},
'.rdg-header-row, .rdg-summary-row': {
'.rdg-cell': {
zIndex: theme.zIndex.tooltip - 5,
'&.rdg-cell-frozen': {
zIndex: theme.zIndex.tooltip - 1,
},
},
},
}),
gridNested: css({
height: '100%',
width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING * 2 - 1}px)`,
overflow: 'visible',
marginLeft: COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING - 1,
marginBlock: TABLE.CELL_PADDING,
}),
cellNested: css({ '&[aria-selected=true]': { outline: 'none' } }),
noDataNested: css({
height: TABLE.NESTED_NO_DATA_HEIGHT,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: theme.colors.text.secondary,
fontSize: theme.typography.h4.fontSize,
}),
cellActions: css({
display: 'none',
position: 'absolute',
top: 0,
margin: 'auto',
height: '100%',
color: theme.colors.text.primary,
background: theme.isDark ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)',
padding: theme.spacing.x0_5,
paddingInlineStart: theme.spacing.x1,
}),
cellActionsEnd: css({ left: 0 }),
cellActionsStart: css({ right: 0 }),
headerRow: css({
paddingBlockStart: 0,
fontWeight: 'normal',
'& .rdg-cell': { height: '100%', alignItems: 'flex-end' },
}),
displayNone: css({ display: 'none' }),
paginationContainer: css({
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
marginTop: '8px',
width: '100%',
}),
paginationSummary: css({
color: theme.colors.text.secondary,
fontSize: theme.typography.bodySmall.fontSize,
display: 'flex',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1, 0, 2),
}),
menuItem: css({ maxWidth: '200px' }),
};
};
export const getFooterStyles = (justifyContent: Property.JustifyContent) => ({
footerCellCountRows: css({ display: 'flex', justifyContent: 'space-between' }),
@@ -135,7 +146,6 @@ export const getDefaultCellStyles: TableCellStyles = (theme, { textAlign, should
display: 'flex',
alignItems: 'center',
textAlign,
backgroundClip: 'padding-box !important', // helps when cells have a bg color
justifyContent: getJustifyContent(textAlign),
...(shouldOverflow && { minHeight: '100%' }),
'&:hover, &[aria-selected=true]': {