From 189d50d8152ad83e967c05e9b4808df377011efa Mon Sep 17 00:00:00 2001 From: Alan Martin <53958929+Alan-eMartin@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:59:03 -0500 Subject: [PATCH] UI: Use react-table column header types in InteractiveTable with story and tests (#116091) * feat(InteractiveTable): allow custom header rendering * docs(InteractiveTable): add story for custom header rendering * test(InteractiveTable): add tests for custom header rendering * docs(InteractiveTable): add custom header rendering documentation * fix: test failure from non-a11y code --- .../InteractiveTable/InteractiveTable.mdx | 38 +++++++ .../InteractiveTable.story.tsx | 39 +++++++ .../InteractiveTable.test.tsx | 103 ++++++++++++++++++ .../src/components/InteractiveTable/types.ts | 6 +- 4 files changed, 183 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.mdx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.mdx index c5c04a59cf6..cefa72df356 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.mdx +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.mdx @@ -117,6 +117,44 @@ export const MyComponent = () => { }; ``` +### Custom Header Rendering + +Column headers can be customized using strings, React elements, or renderer functions. The `header` property accepts any value that matches React Table's `Renderer` type. + +**Important:** When using custom header content, prefer inline elements (like ``) over block elements (like `
`) to avoid layout issues. Block-level elements can cause extra spacing and alignment problems in table headers because they disrupt the table's inline flow. Use `display: inline-flex` or `display: inline-block` when you need flexbox or block-like behavior. + +```tsx +const columns: Array> = [ + // React element header + { + id: 'checkbox', + header: ( + <> + + + + ), + cell: () => , + }, + + // Function renderer header + { + id: 'firstName', + header: () => ( + + + First Name + + ), + }, + + // String header + { id: 'lastName', header: 'Last name' }, +]; +``` + ### Custom Cell Rendering Individual cells can be rendered using custom content dy defining a `cell` property on the column definition. diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx index e0df9d9782f..d2b65c531a4 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx @@ -3,8 +3,11 @@ import { useCallback, useMemo, useState } from 'react'; import { CellProps } from 'react-table'; import { LinkButton } from '../Button/Button'; +import { Checkbox } from '../Forms/Checkbox'; import { Field } from '../Forms/Field'; +import { Icon } from '../Icon/Icon'; import { Input } from '../Input/Input'; +import { Text } from '../Text/Text'; import { FetchDataArgs, InteractiveTable, InteractiveTableHeaderTooltip } from './InteractiveTable'; import mdx from './InteractiveTable.mdx'; @@ -297,4 +300,40 @@ export const WithControlledSort: StoryFn = (args) => { return ; }; +export const WithCustomHeader: TableStoryObj = { + args: { + columns: [ + // React element header + { + id: 'checkbox', + header: ( + <> + + + + ), + cell: () => , + }, + // Function renderer header + { + id: 'firstName', + header: () => ( + + + First Name + + ), + sortType: 'string', + }, + // String header + { id: 'lastName', header: 'Last name', sortType: 'string' }, + { id: 'car', header: 'Car', sortType: 'string' }, + { id: 'age', header: 'Age', sortType: 'number' }, + ], + data: pageableData.slice(0, 10), + getRowId: (r) => r.id, + }, +}; export default meta; diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx index 651532409af..d3bc9918ad4 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx @@ -2,6 +2,9 @@ import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; +import { Checkbox } from '../Forms/Checkbox'; +import { Icon } from '../Icon/Icon'; + import { InteractiveTable } from './InteractiveTable'; import { Column } from './types'; @@ -247,4 +250,104 @@ describe('InteractiveTable', () => { expect(fetchData).toHaveBeenCalledWith({ sortBy: [{ id: 'id', desc: false }] }); }); }); + + describe('custom header rendering', () => { + it('should render string headers', () => { + const columns: Array> = [{ id: 'id', header: 'ID' }]; + const data: TableData[] = [{ id: '1', value: '1', country: 'Sweden' }]; + render(); + + expect(screen.getByRole('columnheader', { name: 'ID' })).toBeInTheDocument(); + }); + + it('should render React element headers', () => { + const columns: Array> = [ + { + id: 'checkbox', + header: ( + <> + + + + ), + cell: () => , + }, + ]; + const data: TableData[] = [{ id: '1', value: '1', country: 'Sweden' }]; + render(); + + expect(screen.getByTestId('header-checkbox')).toBeInTheDocument(); + expect(screen.getByTestId('cell-checkbox')).toBeInTheDocument(); + expect(screen.getByLabelText('Select all rows')).toBeInTheDocument(); + expect(screen.getByLabelText('Select row')).toBeInTheDocument(); + expect(screen.getByText('Select all rows')).toBeInTheDocument(); + }); + + it('should render function renderer headers', () => { + const columns: Array> = [ + { + id: 'firstName', + header: () => ( + + + First Name + + ), + sortType: 'string', + }, + ]; + const data: TableData[] = [{ id: '1', value: '1', country: 'Sweden' }]; + render(); + + expect(screen.getByTestId('header-icon')).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /first name/i })).toBeInTheDocument(); + }); + + it('should render all header types together', () => { + const columns: Array> = [ + { + id: 'checkbox', + header: ( + <> + + + + ), + cell: () => , + }, + { + id: 'id', + header: () => ( + + + ID + + ), + sortType: 'string', + }, + { id: 'country', header: 'Country', sortType: 'string' }, + { id: 'value', header: 'Value' }, + ]; + const data: TableData[] = [ + { id: '1', value: 'Value 1', country: 'Sweden' }, + { id: '2', value: 'Value 2', country: 'Norway' }, + ]; + render(); + + expect(screen.getByTestId('header-checkbox')).toBeInTheDocument(); + expect(screen.getByTestId('header-icon')).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Country' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Value' })).toBeInTheDocument(); + + // Verify data is rendered + expect(screen.getByText('Sweden')).toBeInTheDocument(); + expect(screen.getByText('Norway')).toBeInTheDocument(); + expect(screen.getByText('Value 1')).toBeInTheDocument(); + expect(screen.getByText('Value 2')).toBeInTheDocument(); + }); + }); }); diff --git a/packages/grafana-ui/src/components/InteractiveTable/types.ts b/packages/grafana-ui/src/components/InteractiveTable/types.ts index 5b84f4c568b..f466e7d9c6c 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/types.ts +++ b/packages/grafana-ui/src/components/InteractiveTable/types.ts @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import { CellProps, DefaultSortTypes, IdType, SortByFn } from 'react-table'; +import { CellProps, DefaultSortTypes, HeaderProps, IdType, Renderer, SortByFn } from 'react-table'; export interface Column { /** @@ -11,9 +11,9 @@ export interface Column { */ cell?: (props: CellProps) => ReactNode; /** - * Header name. if `undefined` the header will be empty. Useful for action columns. + * Header name. Can be a string, renderer function, or undefined. If `undefined` the header will be empty. Useful for action columns. */ - header?: string; + header?: Renderer>; /** * Column sort type. If `undefined` the column will not be sortable. * */