Compare commits

...

1 Commits

Author SHA1 Message Date
sadpandajoe
00b5744081 fix(deckgl): hide legend when Legend Position is None
The Legend hide gate only matched a strict null position, but the
modernized Select no longer round-trips the "None" choice (whose value
is null) back as null, handing the layer undefined instead. With the
component also defaulting an unset position to 'tr', "None" left the
legend visible on every deck.gl layer that renders one (surfaced on
Polygon).

Drop the 'tr' default and hide the legend for any falsy position, so
null, undefined, and '' all hide while valid corners still render.

Adds Legend unit tests covering null/undefined/''/valid positions and
makes the Polygon Legend mock honor the hide contract so its "None"
assertions no longer pass vacuously.
2026-07-23 23:38:54 +00:00
3 changed files with 91 additions and 27 deletions

View File

@@ -22,7 +22,7 @@ import { createEvent, fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme';
import type { ReactElement } from 'react';
import Legend from './Legend';
import Legend, { type LegendProps } from './Legend';
const renderWithTheme = (component: ReactElement) =>
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
@@ -31,6 +31,7 @@ test('formats interval-notation labels while preserving brackets', () => {
renderWithTheme(
<Legend
format=",.2f"
position="tr"
categories={{
'[1, 81)': { enabled: true, color: [0, 0, 0] },
'[81, 212)': { enabled: true, color: [0, 0, 0] },
@@ -48,6 +49,7 @@ test('still formats legacy "a - b" delimiter labels', () => {
renderWithTheme(
<Legend
format=",.1f"
position="tr"
categories={{
'0 - 100000': { enabled: true, color: [0, 0, 0] },
'100001 - 200000': { enabled: true, color: [0, 0, 0] },
@@ -63,6 +65,7 @@ test('leaves labels untouched when no format is provided', () => {
renderWithTheme(
<Legend
format={null}
position="tr"
categories={{ '[1, 81)': { enabled: true, color: [0, 0, 0] } }}
/>,
);
@@ -78,6 +81,7 @@ test('clicking a legend item toggles the category without triggering anchor navi
renderWithTheme(
<Legend
format={null}
position="tr"
categories={{
Positive: { enabled: true, color: [0, 255, 0] },
Negative: { enabled: true, color: [255, 0, 0] },
@@ -103,6 +107,7 @@ test('ctrl+clicking a legend item toggles the category without opening a new tab
renderWithTheme(
<Legend
format={null}
position="tr"
categories={{
cat1: { enabled: true, color: [255, 0, 0] },
cat2: { enabled: false, color: [0, 0, 255] },
@@ -124,3 +129,34 @@ test('ctrl+clicking a legend item toggles the category without opening a new tab
expect(toggleCategory).toHaveBeenCalledTimes(1);
expect(toggleCategory).toHaveBeenCalledWith('cat1');
});
// Regression proof for the "Legend Position: None" control not hiding the
// legend. The control's "None" choice has a null value, but the modernized
// Select can hand it back as undefined or an empty string, so the legend must
// hide for any falsy position rather than strictly matching null.
test.each([null, undefined, ''])(
'renders nothing when Legend Position is None (position=%p)',
position => {
renderWithTheme(
<Legend
format={null}
position={position as LegendProps['position']}
categories={{ Alpha: { enabled: true, color: [0, 0, 0] } }}
/>,
);
expect(screen.queryByText('Alpha')).not.toBeInTheDocument();
},
);
test('renders the legend for a valid corner position', () => {
renderWithTheme(
<Legend
format={null}
position="tr"
categories={{ Alpha: { enabled: true, color: [0, 0, 0] } }}
/>,
);
expect(screen.getByText('Alpha')).toBeInTheDocument();
});

View File

@@ -98,7 +98,7 @@ export type LegendProps = {
const Legend = ({
format: d3Format = null,
forceCategorical = false,
position = 'tr',
position,
categories: categoriesObject = {},
toggleCategory = () => {},
showSingleCategory = () => {},
@@ -136,7 +136,12 @@ const Legend = ({
return format(k);
};
if (Object.keys(categoriesObject).length === 0 || position === null) {
// Hide the legend when there are no categories, or when Legend Position is
// "None". The control's "None" choice has a null value, but the Select can
// round-trip it as undefined (or an empty string), so treat every falsy
// position as "hidden" rather than strictly matching null. Valid corner
// values ('tl'/'tr'/'bl'/'br') are truthy and fall through to render.
if (Object.keys(categoriesObject).length === 0 || !position) {
return null;
}

View File

@@ -18,7 +18,7 @@
*/
import type { ReactElement } from 'react';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render, screen } from '@testing-library/react';
import { render } from '@testing-library/react';
// eslint-disable-next-line import/no-extraneous-dependencies
import '@testing-library/jest-dom';
import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme';
@@ -53,15 +53,24 @@ jest.mock('../../utils/mapbox', () => ({
hasMapboxApiKey: () => true,
}));
jest.mock('../../components/Legend', () => ({ categories, position }: any) => (
<div
data-testid="legend"
data-categories={JSON.stringify(categories)}
data-position={position}
>
Legend Mock
</div>
));
// Stand in for the real Legend while preserving its hide contract: a falsy
// position (Legend Position "None") renders nothing. Without this the mock
// would render unconditionally and the "None hides the legend" assertions
// below would pass vacuously.
jest.mock(
'../../components/Legend',
() =>
({ categories, position }: any) =>
position ? (
<div
data-testid="legend"
data-categories={JSON.stringify(categories)}
data-position={position}
>
Legend Mock
</div>
) : null,
);
const mockProps = {
formData: {
@@ -369,20 +378,30 @@ describe('DeckGLPolygon Error Handling and Edge Cases', () => {
expect(mockGetBuckets).not.toHaveBeenCalled();
});
test('handles null legend_position correctly', () => {
const propsWithNullLegendPosition = {
...mockProps,
formData: {
...mockProps.formData,
legend_position: null,
},
};
// The "None" choice can reach the layer as null or (after the Select
// round-trip) undefined; either must hide the legend. queryByTestId is
// configured to match data-test, not the mock's data-testid, so assert
// against the container directly.
test.each([null, undefined])(
'hides the legend when legend_position is None (%p)',
legendPosition => {
const propsWithNoLegend = {
...mockProps,
formData: {
...mockProps.formData,
legend_position: legendPosition,
},
};
renderWithTheme(<DeckGLPolygon {...propsWithNullLegendPosition} />);
const { container } = renderWithTheme(
<DeckGLPolygon {...propsWithNoLegend} />,
);
// Legend should not be rendered when position is null
expect(screen.queryByTestId('legend')).not.toBeInTheDocument();
});
expect(
container.querySelector('[data-testid="legend"]'),
).not.toBeInTheDocument();
},
);
});
describe('DeckGLPolygon Legend Integration', () => {
@@ -421,10 +440,14 @@ describe('DeckGLPolygon Legend Integration', () => {
},
};
renderWithTheme(<DeckGLPolygon {...propsWithoutMetric} />);
const { container } = renderWithTheme(
<DeckGLPolygon {...propsWithoutMetric} />,
);
// Legend should not be rendered when no metric is defined
expect(screen.queryByTestId('legend')).not.toBeInTheDocument();
expect(
container.querySelector('[data-testid="legend"]'),
).not.toBeInTheDocument();
});
});