mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(matrixify): add single metric constraint (#37169)
This commit is contained in:
@@ -19,14 +19,20 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { t } from '@apache-superset/core';
|
||||
import { JsonValue } from '@superset-ui/core';
|
||||
import { Radio } from '@superset-ui/core/components';
|
||||
import { Radio, Tooltip, TooltipPlacement } from '@superset-ui/core/components';
|
||||
import { ControlHeader } from '../../components/ControlHeader';
|
||||
|
||||
// [value, label]
|
||||
export type RadioButtonOption = [
|
||||
JsonValue,
|
||||
Exclude<ReactNode, null | undefined | boolean>,
|
||||
];
|
||||
export interface RadioButtonOptionObject {
|
||||
value: JsonValue;
|
||||
label: Exclude<ReactNode, null | undefined | boolean>;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
tooltipPlacement?: TooltipPlacement;
|
||||
}
|
||||
|
||||
export type RadioButtonOption =
|
||||
| [JsonValue, Exclude<ReactNode, null | undefined | boolean>]
|
||||
| RadioButtonOptionObject;
|
||||
|
||||
export interface RadioButtonControlProps {
|
||||
label?: ReactNode;
|
||||
@@ -34,7 +40,17 @@ export interface RadioButtonControlProps {
|
||||
options: RadioButtonOption[];
|
||||
hovered?: boolean;
|
||||
value?: JsonValue;
|
||||
onChange: (opt: RadioButtonOption[0]) => void;
|
||||
onChange: (opt: JsonValue) => void;
|
||||
}
|
||||
|
||||
function normalizeOption(option: RadioButtonOption): RadioButtonOptionObject {
|
||||
if (Array.isArray(option)) {
|
||||
return {
|
||||
value: option[0],
|
||||
label: option[1],
|
||||
};
|
||||
}
|
||||
return option;
|
||||
}
|
||||
|
||||
export default function RadioButtonControl({
|
||||
@@ -43,7 +59,9 @@ export default function RadioButtonControl({
|
||||
onChange,
|
||||
...props
|
||||
}: RadioButtonControlProps) {
|
||||
const currentValue = initialValue || options[0][0];
|
||||
const normalizedOptions = options.map(normalizeOption);
|
||||
const currentValue = initialValue || normalizedOptions[0].value;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@@ -55,29 +73,52 @@ export default function RadioButtonControl({
|
||||
value={currentValue}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
{options.map(([val, label]) => (
|
||||
<Radio.Button
|
||||
// role="tab"
|
||||
key={JSON.stringify(val)}
|
||||
value={val}
|
||||
aria-label={typeof label === 'string' ? label : undefined}
|
||||
id={`tab-${val}`}
|
||||
type="button"
|
||||
aria-selected={val === currentValue}
|
||||
className={`btn btn-default ${
|
||||
val === currentValue ? 'active' : ''
|
||||
}`}
|
||||
onClick={e => {
|
||||
e.currentTarget?.focus();
|
||||
onChange(val);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Radio.Button>
|
||||
))}
|
||||
{normalizedOptions.map(
|
||||
({
|
||||
value: val,
|
||||
label,
|
||||
disabled = false,
|
||||
tooltip,
|
||||
tooltipPlacement = 'top',
|
||||
}) => {
|
||||
const button = (
|
||||
<Radio.Button
|
||||
key={JSON.stringify(val)}
|
||||
value={val}
|
||||
disabled={disabled}
|
||||
aria-label={typeof label === 'string' ? label : undefined}
|
||||
id={`tab-${val}`}
|
||||
type="button"
|
||||
aria-selected={val === currentValue}
|
||||
className={`btn btn-default ${
|
||||
val === currentValue ? 'active' : ''
|
||||
}`}
|
||||
onClick={e => {
|
||||
e.currentTarget?.focus();
|
||||
onChange(val);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Radio.Button>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
key={JSON.stringify(val)}
|
||||
title={tooltip}
|
||||
placement={tooltipPlacement}
|
||||
>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
},
|
||||
)}
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{/* accessibility begin */}
|
||||
<div
|
||||
aria-live="polite"
|
||||
style={{
|
||||
@@ -90,10 +131,10 @@ export default function RadioButtonControl({
|
||||
>
|
||||
{t(
|
||||
'%s tab selected',
|
||||
options.find(([val]) => val === currentValue)?.[1],
|
||||
normalizedOptions.find(({ value: val }) => val === currentValue)
|
||||
?.label,
|
||||
)}
|
||||
</div>
|
||||
{/* accessibility end */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,18 +62,41 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
||||
// Dynamically add axis-specific controls (rows and columns)
|
||||
(['columns', 'rows'] as const).forEach(axisParam => {
|
||||
const axis: 'rows' | 'columns' = axisParam;
|
||||
const otherAxis: 'rows' | 'columns' = axis === 'rows' ? 'columns' : 'rows';
|
||||
|
||||
matrixifyControls[`matrixify_mode_${axis}`] = {
|
||||
type: 'RadioButtonControl',
|
||||
label: t(`Metrics / Dimensions`),
|
||||
default: 'metrics',
|
||||
options: [
|
||||
['metrics', t('Metrics')],
|
||||
['dimensions', t('Dimension members')],
|
||||
],
|
||||
default: axis === 'columns' ? 'metrics' : 'dimensions',
|
||||
renderTrigger: true,
|
||||
tabOverride: 'matrixify',
|
||||
visibility: ({ controls }) => isMatrixifyVisible(controls, axis),
|
||||
mapStateToProps: ({ controls }) => {
|
||||
const otherAxisControlName = `matrixify_mode_${otherAxis}`;
|
||||
|
||||
const otherAxisValue =
|
||||
controls?.[otherAxisControlName]?.value ??
|
||||
(otherAxis === 'columns' ? 'metrics' : 'dimensions');
|
||||
|
||||
const isMetricsDisabled = otherAxisValue === 'metrics';
|
||||
|
||||
return {
|
||||
options: [
|
||||
{
|
||||
value: 'metrics',
|
||||
label: t('Metrics'),
|
||||
disabled: isMetricsDisabled,
|
||||
tooltip: isMetricsDisabled
|
||||
? t(
|
||||
"Metrics can't be used for both rows and columns at the same time",
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
{ value: 'dimensions', label: t('Dimension members') },
|
||||
],
|
||||
};
|
||||
},
|
||||
rerender: [`matrixify_mode_${otherAxis}`, `matrixify_dimension_${axis}`],
|
||||
};
|
||||
|
||||
matrixifyControls[`matrixify_${axis}`] = {
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, render, screen, waitFor } from '@superset-ui/core/spec';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import RadioButtonControl, {
|
||||
RadioButtonControlProps,
|
||||
RadioButtonOption,
|
||||
} from '../../../src/shared-controls/components/RadioButtonControl';
|
||||
|
||||
const defaultProps: RadioButtonControlProps = {
|
||||
label: 'Test Radio Control',
|
||||
options: [
|
||||
['option1', 'Option 1'],
|
||||
['option2', 'Option 2'],
|
||||
['option3', 'Option 3'],
|
||||
],
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
const setup = (props: Partial<RadioButtonControlProps> = {}) =>
|
||||
render(<RadioButtonControl {...defaultProps} {...props} />);
|
||||
|
||||
test('renders with array-based options (legacy format)', () => {
|
||||
const { container } = setup();
|
||||
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 3')).toBeInTheDocument();
|
||||
expect(container.querySelector('[role="tablist"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders with object-based options (new format)', () => {
|
||||
const objectOptions: RadioButtonOption[] = [
|
||||
{ value: 'opt1', label: 'Object Option 1' },
|
||||
{ value: 'opt2', label: 'Object Option 2' },
|
||||
{ value: 'opt3', label: 'Object Option 3' },
|
||||
];
|
||||
|
||||
setup({ options: objectOptions });
|
||||
|
||||
expect(screen.getByText('Object Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Object Option 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Object Option 3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders mixed array and object options', () => {
|
||||
const mixedOptions: RadioButtonOption[] = [
|
||||
['array1', 'Array Option'],
|
||||
{ value: 'obj1', label: 'Object Option' },
|
||||
];
|
||||
|
||||
setup({ options: mixedOptions });
|
||||
|
||||
expect(screen.getByText('Array Option')).toBeInTheDocument();
|
||||
expect(screen.getByText('Object Option')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('defaults to first option when no value provided', () => {
|
||||
const { container } = setup();
|
||||
|
||||
const firstButton = container.querySelector('#tab-option1');
|
||||
expect(firstButton).toBeInTheDocument();
|
||||
expect(firstButton).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
test('respects initial value prop', () => {
|
||||
const { container } = setup({ value: 'option2' });
|
||||
|
||||
const secondButton = container.querySelector('#tab-option2');
|
||||
expect(secondButton).toBeInTheDocument();
|
||||
expect(secondButton).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
test('calls onChange when radio button is clicked', () => {
|
||||
const onChange = jest.fn();
|
||||
setup({ onChange });
|
||||
|
||||
const secondOption = screen.getByText('Option 2');
|
||||
fireEvent.click(secondOption);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('option2');
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles multiple clicks correctly', () => {
|
||||
const onChange = jest.fn();
|
||||
setup({ onChange });
|
||||
|
||||
fireEvent.click(screen.getByText('Option 2'));
|
||||
fireEvent.click(screen.getByText('Option 3'));
|
||||
fireEvent.click(screen.getByText('Option 1'));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('option2');
|
||||
expect(onChange).toHaveBeenCalledWith('option3');
|
||||
expect(onChange).toHaveBeenCalledWith('option1');
|
||||
expect(onChange.mock.calls.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
test('disables specific options when disabled flag is set', () => {
|
||||
const optionsWithDisabled: RadioButtonOption[] = [
|
||||
{ value: 'opt1', label: 'Enabled Option' },
|
||||
{ value: 'opt2', label: 'Disabled Option', disabled: true },
|
||||
{ value: 'opt3', label: 'Another Enabled' },
|
||||
];
|
||||
|
||||
const { container } = setup({ options: optionsWithDisabled });
|
||||
|
||||
const disabledButton = container.querySelector('#tab-opt2');
|
||||
const enabledButton = container.querySelector('#tab-opt1');
|
||||
|
||||
expect(disabledButton).toHaveAttribute('disabled');
|
||||
expect(enabledButton).not.toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
test('disabled options do not trigger onChange when clicked', () => {
|
||||
const onChange = jest.fn();
|
||||
const optionsWithDisabled: RadioButtonOption[] = [
|
||||
{ value: 'opt1', label: 'Enabled' },
|
||||
{ value: 'opt2', label: 'Disabled', disabled: true },
|
||||
];
|
||||
|
||||
const { container } = setup({ options: optionsWithDisabled, onChange });
|
||||
|
||||
const disabledButton = container.querySelector('#tab-opt2');
|
||||
if (disabledButton) {
|
||||
fireEvent.click(disabledButton);
|
||||
}
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('renders ControlHeader with label and description', () => {
|
||||
const { container } = setup({
|
||||
label: 'My Radio Control',
|
||||
description: 'This is a helpful description',
|
||||
});
|
||||
|
||||
const header = container.querySelector('.ControlHeader');
|
||||
expect(header).toBeInTheDocument();
|
||||
expect(screen.getByText('My Radio Control')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('aria-live region updates with current selection', () => {
|
||||
const { container } = setup({ value: 'option1' });
|
||||
|
||||
const ariaLiveRegion = container.querySelector('[aria-live="polite"]');
|
||||
expect(ariaLiveRegion).toBeInTheDocument();
|
||||
expect(ariaLiveRegion?.textContent).toContain('Option 1');
|
||||
});
|
||||
|
||||
test('aria-live region updates when selection changes', () => {
|
||||
const { container, rerender } = setup({ value: 'option1' });
|
||||
|
||||
let ariaLiveRegion = container.querySelector('[aria-live="polite"]');
|
||||
expect(ariaLiveRegion?.textContent).toContain('Option 1');
|
||||
|
||||
rerender(<RadioButtonControl {...defaultProps} value="option2" />);
|
||||
|
||||
ariaLiveRegion = container.querySelector('[aria-live="polite"]');
|
||||
expect(ariaLiveRegion?.textContent).toContain('Option 2');
|
||||
});
|
||||
|
||||
test('aria-live region is visually hidden but accessible', () => {
|
||||
const { container } = setup();
|
||||
|
||||
const ariaLiveRegion = container.querySelector(
|
||||
'[aria-live="polite"]',
|
||||
) as HTMLElement;
|
||||
|
||||
expect(ariaLiveRegion).toBeInTheDocument();
|
||||
expect(ariaLiveRegion?.style.position).toBe('absolute');
|
||||
expect(ariaLiveRegion?.style.left).toBe('-9999px');
|
||||
expect(ariaLiveRegion?.style.height).toBe('1px');
|
||||
expect(ariaLiveRegion?.style.width).toBe('1px');
|
||||
expect(ariaLiveRegion?.style.overflow).toBe('hidden');
|
||||
});
|
||||
|
||||
test('renders tablist with correct aria-label when label is string', () => {
|
||||
const { container } = setup({ label: 'String Label' });
|
||||
|
||||
const tablist = container.querySelector('[role="tablist"]');
|
||||
expect(tablist).toHaveAttribute('aria-label', 'String Label');
|
||||
});
|
||||
|
||||
test('tablist has no aria-label when label is not string', () => {
|
||||
const { container } = setup({ label: <div>JSX Label</div> });
|
||||
|
||||
const tablist = container.querySelector('[role="tablist"]');
|
||||
expect(tablist).not.toHaveAttribute('aria-label');
|
||||
});
|
||||
|
||||
test('each radio button has correct aria-selected state', () => {
|
||||
const { container } = setup({ value: 'option2' });
|
||||
|
||||
expect(container.querySelector('#tab-option1')).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'false',
|
||||
);
|
||||
expect(container.querySelector('#tab-option2')).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
);
|
||||
expect(container.querySelector('#tab-option3')).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'false',
|
||||
);
|
||||
});
|
||||
|
||||
test('radio buttons have correct aria-label when label is string', () => {
|
||||
setup();
|
||||
|
||||
const option1Button = screen.getByLabelText('Option 1');
|
||||
expect(option1Button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('focuses button when clicked', () => {
|
||||
const { container } = setup();
|
||||
|
||||
const button = container.querySelector('#tab-option2') as HTMLElement;
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(document.activeElement).toBe(button);
|
||||
});
|
||||
|
||||
test('handles numeric values in options', () => {
|
||||
const onChange = jest.fn();
|
||||
const numericOptions: RadioButtonOption[] = [
|
||||
[1, 'One'],
|
||||
[2, 'Two'],
|
||||
[3, 'Three'],
|
||||
];
|
||||
|
||||
setup({ options: numericOptions, onChange });
|
||||
|
||||
fireEvent.click(screen.getByText('Two'));
|
||||
expect(onChange).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
test('handles boolean values in options', () => {
|
||||
const onChange = jest.fn();
|
||||
const booleanOptions: RadioButtonOption[] = [
|
||||
[true, 'True'],
|
||||
[false, 'False'],
|
||||
];
|
||||
|
||||
setup({ options: booleanOptions, onChange });
|
||||
|
||||
fireEvent.click(screen.getByText('False'));
|
||||
expect(onChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test('handles null values in options', () => {
|
||||
const onChange = jest.fn();
|
||||
const nullOptions: RadioButtonOption[] = [
|
||||
[null, 'None'],
|
||||
['value', 'Value'],
|
||||
];
|
||||
|
||||
setup({ options: nullOptions, onChange });
|
||||
|
||||
fireEvent.click(screen.getByText('None'));
|
||||
expect(onChange).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
test('generates unique IDs for options', () => {
|
||||
const { container } = setup();
|
||||
|
||||
const button1 = container.querySelector('#tab-option1');
|
||||
const button2 = container.querySelector('#tab-option2');
|
||||
const button3 = container.querySelector('#tab-option3');
|
||||
|
||||
expect(button1).toBeInTheDocument();
|
||||
expect(button2).toBeInTheDocument();
|
||||
expect(button3).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('applies active class to selected button', () => {
|
||||
const { container } = setup({ value: 'option2' });
|
||||
|
||||
const activeButton = container.querySelector('#tab-option2');
|
||||
expect(activeButton).toBeInTheDocument();
|
||||
expect(activeButton).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
test('does not set aria-selected to true for unselected buttons', () => {
|
||||
const { container } = setup({ value: 'option2' });
|
||||
|
||||
const inactiveButton1 = container.querySelector('#tab-option1');
|
||||
const inactiveButton3 = container.querySelector('#tab-option3');
|
||||
|
||||
expect(inactiveButton1).toHaveAttribute('aria-selected', 'false');
|
||||
expect(inactiveButton3).toHaveAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
test('backward compatibility with legacy array format', () => {
|
||||
const onChange = jest.fn();
|
||||
const legacyOptions: RadioButtonOption[] = [
|
||||
['val1', 'Label 1'],
|
||||
['val2', 'Label 2'],
|
||||
];
|
||||
|
||||
setup({ options: legacyOptions, onChange });
|
||||
|
||||
expect(screen.getByText('Label 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Label 2')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Label 2'));
|
||||
expect(onChange).toHaveBeenCalledWith('val2');
|
||||
});
|
||||
|
||||
test('normalizeOption handles array format correctly', () => {
|
||||
const arrayOption: RadioButtonOption = ['value', 'Label'];
|
||||
const onChange = jest.fn();
|
||||
|
||||
setup({ options: [arrayOption], onChange });
|
||||
|
||||
expect(screen.getByText('Label')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Label'));
|
||||
expect(onChange).toHaveBeenCalledWith('value');
|
||||
});
|
||||
|
||||
test('normalizeOption handles object format correctly', () => {
|
||||
const objectOption: RadioButtonOption = {
|
||||
value: 'value',
|
||||
label: 'Label',
|
||||
disabled: false,
|
||||
};
|
||||
const onChange = jest.fn();
|
||||
|
||||
setup({ options: [objectOption], onChange });
|
||||
|
||||
expect(screen.getByText('Label')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Label'));
|
||||
expect(onChange).toHaveBeenCalledWith('value');
|
||||
});
|
||||
|
||||
test('handles empty options array gracefully', () => {
|
||||
const { container } = setup({ options: [], value: 'default' });
|
||||
|
||||
expect(container.querySelector('[role="tablist"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders with hovered prop', () => {
|
||||
const { container } = setup({
|
||||
label: 'Test',
|
||||
description: 'Test description',
|
||||
hovered: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-test="info-tooltip-icon"]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders tooltips for options with tooltip property', async () => {
|
||||
const optionsWithTooltips: RadioButtonOption[] = [
|
||||
{ value: 'opt1', label: 'Option 1', tooltip: 'Tooltip for option 1' },
|
||||
{ value: 'opt2', label: 'Option 2' },
|
||||
{ value: 'opt3', label: 'Option 3', tooltip: 'Tooltip for option 3' },
|
||||
];
|
||||
|
||||
setup({ options: optionsWithTooltips });
|
||||
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 3')).toBeInTheDocument();
|
||||
|
||||
const option1 = screen.getByText('Option 1');
|
||||
userEvent.hover(option1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Tooltip for option 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
userEvent.unhover(option1);
|
||||
|
||||
const option3 = screen.getByText('Option 3');
|
||||
userEvent.hover(option3);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Tooltip for option 3')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('wraps disabled buttons with tooltip in span', () => {
|
||||
const optionsWithDisabledTooltip: RadioButtonOption[] = [
|
||||
{ value: 'opt1', label: 'Enabled with tooltip', tooltip: 'Tooltip text' },
|
||||
{
|
||||
value: 'opt2',
|
||||
label: 'Disabled with tooltip',
|
||||
disabled: true,
|
||||
tooltip: 'Disabled tooltip',
|
||||
},
|
||||
];
|
||||
|
||||
const { container } = setup({ options: optionsWithDisabledTooltip });
|
||||
|
||||
const disabledButton = container.querySelector('#tab-opt2');
|
||||
expect(disabledButton).toHaveAttribute('disabled');
|
||||
expect(disabledButton?.parentElement?.tagName).toBe('SPAN');
|
||||
});
|
||||
@@ -19,10 +19,7 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { HandlerFunction, JsonValue } from '@superset-ui/core';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import {
|
||||
RadioButtonOption,
|
||||
sharedControlComponents,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { sharedControlComponents } from '@superset-ui/chart-controls';
|
||||
import { AreaChartStackControlOptions } from '../constants';
|
||||
|
||||
const { RadioButtonControl } = sharedControlComponents;
|
||||
@@ -60,7 +57,7 @@ export function useExtraControl<
|
||||
}, [area]);
|
||||
|
||||
const extraControlsHandler = useCallback(
|
||||
(value: RadioButtonOption[0]) => {
|
||||
(value: JsonValue) => {
|
||||
if (area) {
|
||||
if (setControlValue) {
|
||||
setControlValue('stack', value);
|
||||
|
||||
Reference in New Issue
Block a user