diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx
new file mode 100644
index 00000000000..131133bcdfb
--- /dev/null
+++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx
@@ -0,0 +1,78 @@
+import { render, screen } from '@testing-library/react';
+
+import { VizLegendTable } from './VizLegendTable';
+import { VizLegendItem } from './types';
+
+describe('VizLegendTable', () => {
+ const mockItems: VizLegendItem[] = [
+ { label: 'Series 1', color: 'red', yAxis: 1 },
+ { label: 'Series 2', color: 'blue', yAxis: 1 },
+ { label: 'Series 3', color: 'green', yAxis: 1 },
+ ];
+
+ it('renders without crashing', () => {
+ const { container } = render( );
+ expect(container.querySelector('table')).toBeInTheDocument();
+ });
+
+ it('renders all items', () => {
+ render( );
+ expect(screen.getByText('Series 1')).toBeInTheDocument();
+ expect(screen.getByText('Series 2')).toBeInTheDocument();
+ expect(screen.getByText('Series 3')).toBeInTheDocument();
+ });
+
+ it('renders table headers when items have display values', () => {
+ const itemsWithStats: VizLegendItem[] = [
+ {
+ label: 'Series 1',
+ color: 'red',
+ yAxis: 1,
+ getDisplayValues: () => [
+ { numeric: 100, text: '100', title: 'Max' },
+ { numeric: 50, text: '50', title: 'Min' },
+ ],
+ },
+ ];
+ render( );
+ expect(screen.getByText('Max')).toBeInTheDocument();
+ expect(screen.getByText('Min')).toBeInTheDocument();
+ });
+
+ it('renders sort icon when sorted', () => {
+ const { container } = render(
+
+ );
+ expect(container.querySelector('svg')).toBeInTheDocument();
+ });
+
+ it('calls onToggleSort when header is clicked', () => {
+ const onToggleSort = jest.fn();
+ render( );
+ const header = screen.getByText('Name');
+ header.click();
+ expect(onToggleSort).toHaveBeenCalledWith('Name');
+ });
+
+ it('does not call onToggleSort when not sortable', () => {
+ const onToggleSort = jest.fn();
+ render( );
+ const header = screen.getByText('Name');
+ header.click();
+ expect(onToggleSort).not.toHaveBeenCalled();
+ });
+
+ it('renders with long labels', () => {
+ const itemsWithLongLabels: VizLegendItem[] = [
+ {
+ label: 'This is a very long series name that should be scrollable within its table cell',
+ color: 'red',
+ yAxis: 1,
+ },
+ ];
+ render( );
+ expect(
+ screen.getByText('This is a very long series name that should be scrollable within its table cell')
+ ).toBeInTheDocument();
+ });
+});
diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx
index b654a2d3ac6..0c2859453eb 100644
--- a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx
+++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx
@@ -119,7 +119,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
table: css({
width: '100%',
'th:first-child': {
- width: '100%',
borderBottom: `1px solid ${theme.colors.border.weak}`,
},
}),
diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx
new file mode 100644
index 00000000000..4ca95aa395c
--- /dev/null
+++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx
@@ -0,0 +1,112 @@
+import { render, screen } from '@testing-library/react';
+
+import { LegendTableItem } from './VizLegendTableItem';
+import { VizLegendItem } from './types';
+
+describe('LegendTableItem', () => {
+ const mockItem: VizLegendItem = {
+ label: 'Series 1',
+ color: 'red',
+ yAxis: 1,
+ };
+
+ it('renders without crashing', () => {
+ const { container } = render(
+
+ );
+ expect(container.querySelector('tr')).toBeInTheDocument();
+ });
+
+ it('renders label text', () => {
+ render(
+
+ );
+ expect(screen.getByText('Series 1')).toBeInTheDocument();
+ });
+
+ it('renders with long label text', () => {
+ const longLabelItem: VizLegendItem = {
+ ...mockItem,
+ label: 'This is a very long series name that should be scrollable in the table cell',
+ };
+ render(
+
+ );
+ expect(
+ screen.getByText('This is a very long series name that should be scrollable in the table cell')
+ ).toBeInTheDocument();
+ });
+
+ it('renders stat values when provided', () => {
+ const itemWithStats: VizLegendItem = {
+ ...mockItem,
+ getDisplayValues: () => [
+ { numeric: 100, text: '100', title: 'Max' },
+ { numeric: 50, text: '50', title: 'Min' },
+ ],
+ };
+ render(
+
+ );
+ expect(screen.getByText('100')).toBeInTheDocument();
+ expect(screen.getByText('50')).toBeInTheDocument();
+ });
+
+ it('renders right y-axis indicator when yAxis is 2', () => {
+ const rightAxisItem: VizLegendItem = {
+ ...mockItem,
+ yAxis: 2,
+ };
+ render(
+
+ );
+ expect(screen.getByText('(right y-axis)')).toBeInTheDocument();
+ });
+
+ it('calls onLabelClick when label is clicked', () => {
+ const onLabelClick = jest.fn();
+ render(
+
+ );
+ const button = screen.getByRole('button');
+ button.click();
+ expect(onLabelClick).toHaveBeenCalledWith(mockItem, expect.any(Object));
+ });
+
+ it('does not call onClick when readonly', () => {
+ const onLabelClick = jest.fn();
+ render(
+
+ );
+ const button = screen.getByRole('button');
+ expect(button).toBeDisabled();
+ });
+});
diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx
index 335cf4309e9..56ec6cb733e 100644
--- a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx
+++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx
@@ -69,7 +69,7 @@ export const LegendTableItem = ({
return (
-
+
-
- {item.label}{' '}
- {item.yAxis === 2 && (
-
- (right y-axis)
-
- )}
-
+
+
+ {item.label}{' '}
+ {item.yAxis === 2 && (
+
+ (right y-axis)
+
+ )}
+
+
{item.getDisplayValues &&
@@ -128,6 +130,27 @@ const getStyles = (theme: GrafanaTheme2) => {
background: rowHoverBg,
},
}),
+ labelCell: css({
+ label: 'LegendLabelCell',
+ maxWidth: 0,
+ width: '100%',
+ }),
+ labelCellInner: css({
+ label: 'LegendLabelCellInner',
+ display: 'block',
+ flex: 1,
+ minWidth: 0,
+ overflowX: 'auto',
+ overflowY: 'hidden',
+ paddingRight: theme.spacing(3),
+ scrollbarWidth: 'none',
+ msOverflowStyle: 'none',
+ maskImage: `linear-gradient(to right, black calc(100% - ${theme.spacing(3)}), transparent 100%)`,
+ WebkitMaskImage: `linear-gradient(to right, black calc(100% - ${theme.spacing(3)}), transparent 100%)`,
+ '&::-webkit-scrollbar': {
+ display: 'none',
+ },
+ }),
label: css({
label: 'LegendLabel',
whiteSpace: 'nowrap',
@@ -135,9 +158,6 @@ const getStyles = (theme: GrafanaTheme2) => {
border: 'none',
fontSize: 'inherit',
padding: 0,
- maxWidth: '600px',
- textOverflow: 'ellipsis',
- overflow: 'hidden',
userSelect: 'text',
}),
labelDisabled: css({