From a819e977a1d7535bd2b867e65938f09b684f1fcc Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Fri, 8 Aug 2025 18:27:44 -0400 Subject: [PATCH] Table: Fix some styling issues with frozen columns (#109425) --- .../src/themes/colorManipulator.test.ts | 13 ++ .../src/themes/colorManipulator.ts | 27 +++ .../src/components/Table/TableNG/styles.ts | 202 +++++++++--------- 3 files changed, 146 insertions(+), 96 deletions(-) diff --git a/packages/grafana-data/src/themes/colorManipulator.test.ts b/packages/grafana-data/src/themes/colorManipulator.test.ts index 4f65f50c4d5..5619ddc4928 100644 --- a/packages/grafana-data/src/themes/colorManipulator.test.ts +++ b/packages/grafana-data/src/themes/colorManipulator.test.ts @@ -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)'); + }); + }); }); diff --git a/packages/grafana-data/src/themes/colorManipulator.ts b/packages/grafana-data/src/themes/colorManipulator.ts index 9964e9a09bc..eccb661b7d8 100644 --- a/packages/grafana-data/src/themes/colorManipulator.ts +++ b/packages/grafana-data/src/themes/colorManipulator.ts @@ -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, }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/styles.ts b/packages/grafana-ui/src/components/Table/TableNG/styles.ts index 739652cf92f..983372aeb0b 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/styles.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/styles.ts @@ -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]': {