feat(table): Gradient Toggle (#36280)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Mohammad Al-Qasem
2025-12-04 15:31:44 -05:00
committed by GitHub
parent e1a8886d32
commit 4479614754
7 changed files with 308 additions and 0 deletions

View File

@@ -484,6 +484,7 @@ export type ConditionalFormattingConfig = {
colorScheme?: string;
toAllRow?: boolean;
toTextColor?: boolean;
useGradient?: boolean;
};
export type ColorFormatters = {

View File

@@ -69,6 +69,7 @@ export const getColorFunction = (
targetValueLeft,
targetValueRight,
colorScheme,
useGradient,
}: ConditionalFormattingConfig,
columnValues: number[] | string[] | (boolean | null)[],
alpha?: boolean,
@@ -256,6 +257,13 @@ export const getColorFunction = (
const compareResult = comparatorFunction(value, columnValues);
if (compareResult === false) return undefined;
const { cutoffValue, extremeValue } = compareResult;
// If useGradient is explicitly false, return solid color
if (useGradient === false) {
return colorScheme;
}
// Otherwise apply gradient (default behavior for backward compatibility)
if (alpha === undefined || alpha) {
return addAlpha(
colorScheme,

View File

@@ -596,6 +596,104 @@ test('correct column string config', () => {
expect(colorFormatters[3].getColorFromValue('Carlos')).toEqual('#FF0000FF');
});
test('getColorFunction with useGradient false returns solid color', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterOrEqual,
targetValue: 50,
colorScheme: '#FF0000',
column: 'count',
useGradient: false,
},
countValues,
);
// When useGradient is false, should return solid color without opacity
expect(colorFunction(50)).toEqual('#FF0000');
expect(colorFunction(100)).toEqual('#FF0000');
expect(colorFunction(0)).toBeUndefined();
});
test('getColorFunction with useGradient true returns gradient color', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterOrEqual,
targetValue: 50,
colorScheme: '#FF0000',
column: 'count',
useGradient: true,
},
countValues,
);
// When useGradient is true, should return gradient color with opacity
expect(colorFunction(50)).toEqual('#FF00000D');
expect(colorFunction(100)).toEqual('#FF0000FF');
expect(colorFunction(0)).toBeUndefined();
});
test('getColorFunction with useGradient undefined defaults to gradient (backward compatibility)', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterOrEqual,
targetValue: 50,
colorScheme: '#FF0000',
column: 'count',
// useGradient is undefined
},
countValues,
);
// When useGradient is undefined, should default to gradient for backward compatibility
expect(colorFunction(50)).toEqual('#FF00000D');
expect(colorFunction(100)).toEqual('#FF0000FF');
expect(colorFunction(0)).toBeUndefined();
});
test('getColorFunction with useGradient false and None operator returns solid color', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
useGradient: false,
},
countValues,
);
// When useGradient is false, all matching values should return solid color
expect(colorFunction(20)).toBeUndefined();
expect(colorFunction(50)).toEqual('#FF0000');
expect(colorFunction(75)).toEqual('#FF0000');
expect(colorFunction(100)).toEqual('#FF0000');
expect(colorFunction(120)).toBeUndefined();
});
test('getColorFormatters with useGradient flag', () => {
const columnConfig = [
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: '#FF0000',
column: 'count',
useGradient: false,
},
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: '#00FF00',
column: 'count',
useGradient: true,
},
];
const colorFormatters = getColorFormatters(columnConfig, mockData);
expect(colorFormatters.length).toEqual(2);
// First formatter with useGradient: false should return solid color
expect(colorFormatters[0].column).toEqual('count');
expect(colorFormatters[0].getColorFromValue(100)).toEqual('#FF0000');
// Second formatter with useGradient: true should return gradient color
expect(colorFormatters[1].column).toEqual('count');
expect(colorFormatters[1].getColorFromValue(100)).toEqual('#00FF00FF');
});
test('correct column boolean config', () => {
const columnConfigBoolean = [
{

View File

@@ -1216,6 +1216,136 @@ describe('plugin-chart-table', () => {
);
});
test('render color with useGradient false returns solid color', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '>',
targetValue: 2467,
useGradient: false,
},
],
},
})}
/>
),
}),
);
// When useGradient is false, should return solid color (no opacity variation)
// The color should be the same for all matching values
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'rgb(172, 225, 196)',
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
});
test('render color with useGradient true returns gradient color', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '>',
targetValue: 2467,
useGradient: true,
},
],
},
})}
/>
),
}),
);
// When useGradient is true, should return gradient color with opacity
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
});
test('render color with useGradient undefined defaults to gradient (backward compatibility)', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '>',
targetValue: 2467,
// useGradient is undefined
},
],
},
})}
/>
),
}),
);
// When useGradient is undefined, should default to gradient for backward compatibility
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
});
test('render color with useGradient false and None operator returns solid color', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: 'None',
useGradient: false,
},
],
},
})}
/>
),
}),
);
// When useGradient is false with None operator, all values should have solid color
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'rgb(172, 225, 196)',
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(
'rgb(172, 225, 196)',
);
});
it('recalculates totals when user filters data', async () => {
const formDataWithTotals = {
...testData.basic.formData,

View File

@@ -203,3 +203,53 @@ test('Not displays the toAllRow and toTextColor flags', () => {
expect(screen.queryByText('To entire row')).not.toBeInTheDocument();
expect(screen.queryByText('To text color')).not.toBeInTheDocument();
});
test('displays Use gradient checkbox', () => {
render(
<FormattingPopoverContent onChange={mockOnChange} columns={columns} />,
);
expect(screen.getByText('Use gradient')).toBeInTheDocument();
});
// Helper function to find the "Use gradient" checkbox
// The checkbox and text are in sibling columns within the same row
const findUseGradientCheckbox = (): HTMLInputElement => {
const useGradientText = screen.getByText('Use gradient');
// Find the common parent row that contains both the text and checkbox
let rowElement: HTMLElement | null = useGradientText.parentElement;
while (rowElement) {
const checkbox = rowElement.querySelector('input[type="checkbox"]');
if (checkbox && rowElement.textContent?.includes('Use gradient')) {
return checkbox as HTMLInputElement;
}
rowElement = rowElement.parentElement;
}
throw new Error('Could not find Use gradient checkbox');
};
test('Use gradient checkbox defaults to checked', () => {
render(
<FormattingPopoverContent onChange={mockOnChange} columns={columns} />,
);
const checkbox = findUseGradientCheckbox();
expect(checkbox).toBeChecked();
});
test('Use gradient checkbox can be toggled', async () => {
render(
<FormattingPopoverContent onChange={mockOnChange} columns={columns} />,
);
const checkbox = findUseGradientCheckbox();
expect(checkbox).toBeChecked();
// Uncheck the checkbox
fireEvent.click(checkbox);
expect(checkbox).not.toBeChecked();
// Check the checkbox again
fireEvent.click(checkbox);
expect(checkbox).toBeChecked();
});

View File

@@ -292,6 +292,9 @@ export const FormattingPopoverContent = ({
const [toTextColor, setToTextColor] = useState(() =>
Boolean(config?.toTextColor),
);
const [useGradient, setUseGradient] = useState(() =>
config?.useGradient !== undefined ? config.useGradient : true,
);
const useConditionalFormattingFlag = (
flagKey: 'toAllRowCheck' | 'toColorTextCheck',
@@ -406,6 +409,23 @@ export const FormattingPopoverContent = ({
</FormItem>
</Col>
</Row>
<Row gutter={20}>
<Col span={1}>
<FormItem
name="useGradient"
valuePropName="checked"
initialValue={useGradient}
>
<Checkbox
onChange={event => setUseGradient(event.target.checked)}
checked={useGradient}
/>
</FormItem>
</Col>
<Col>
<FormItem required>{t('Use gradient')}</FormItem>
</Col>
</Row>
<FormItem noStyle shouldUpdate={shouldFormItemUpdate}>
{showOperatorFields ? (
(props: GetFieldValue) => renderOperatorFields(props, columnType)

View File

@@ -31,6 +31,7 @@ export type ConditionalFormattingConfig = {
colorScheme?: string;
toAllRow?: boolean;
toTextColor?: boolean;
useGradient?: boolean;
};
export type ConditionalFormattingControlProps = ControlComponentProps<