refactor(Radio): Upgrade Radio Component to Ant Design 5 (#32004)

This commit is contained in:
Enzo Martellucci 2025-01-31 17:45:06 +01:00 committed by GitHub
parent 1c1494d3e0
commit 468bb5f47a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 293 additions and 199 deletions

View File

@ -19,8 +19,8 @@
import { useCallback, useState, FormEvent } from 'react';
import { Radio } from 'src/components/Radio';
import { RadioChangeEvent, AsyncSelect } from 'src/components';
import { Radio, RadioChangeEvent } from 'src/components/Radio';
import { AsyncSelect } from 'src/components';
import { Input } from 'src/components/Input';
import StyledModal from 'src/components/Modal';
import Button from 'src/components/Button';

View File

@ -32,33 +32,21 @@ export const useDisplayModeToggle = () => {
<div
css={(theme: SupersetTheme) => css`
margin-bottom: ${theme.gridUnit * 6}px;
.ant-radio-button-wrapper-checked:not(
.ant-radio-button-wrapper-disabled
):focus-within {
box-shadow: none;
}
`}
data-test="drill-by-display-toggle"
>
<Radio.Group
<Radio.GroupWrapper
onChange={({ target: { value } }) => {
setDrillByDisplayMode(value);
}}
defaultValue={DrillByType.Chart}
>
<Radio.Button
value={DrillByType.Chart}
data-test="drill-by-chart-radio"
>
{t('Chart')}
</Radio.Button>
<Radio.Button
value={DrillByType.Table}
data-test="drill-by-table-radio"
>
{t('Table')}
</Radio.Button>
</Radio.Group>
options={[
{ label: t('Chart'), value: DrillByType.Chart },
{ label: t('Table'), value: DrillByType.Table },
]}
optionType="button"
buttonStyle="outline"
/>
</div>
),
[],

View File

@ -16,40 +16,139 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useArgs } from '@storybook/preview-api';
import { Radio } from './index';
import { Space } from 'src/components/Space';
import {
BarChartOutlined,
DotChartOutlined,
LineChartOutlined,
PieChartOutlined,
} from '@ant-design/icons';
import { Radio, RadioProps, RadioGroupWrapperProps } from './index';
export default {
title: 'Radio',
component: Radio,
parameters: {
controls: { hideNoControlsWarning: true },
tags: ['autodocs'],
};
const RadioArgsType = {
value: {
control: 'text',
description: 'The value of the radio button.',
},
argTypes: {
theme: {
table: {
disable: true,
},
},
checked: { control: 'boolean' },
disabled: { control: 'boolean' },
disabled: {
control: 'boolean',
description: 'Whether the radio button is disabled or not.',
},
checked: {
control: 'boolean',
description: 'The checked state of the radio button.',
},
};
export const SupersetRadio = () => {
const [{ checked, ...rest }, updateArgs] = useArgs();
return (
<Radio
checked={checked}
onChange={() => updateArgs({ checked: !checked })}
{...rest}
>
Example
</Radio>
);
const radioGroupWrapperArgsType = {
onChange: { action: 'changed' },
disabled: { control: 'boolean' },
size: {
control: 'select',
options: ['small', 'middle', 'large'],
},
options: { control: 'object' },
'spaceConfig.direction': {
control: 'select',
options: ['horizontal', 'vertical'],
description: 'Direction of the Space layout',
if: { arg: 'enableSpaceConfig', truthy: true },
},
'spaceConfig.size': {
control: 'select',
options: ['small', 'middle', 'large'],
description: 'Layout size Space',
if: { arg: 'enableSpaceConfig', truthy: true },
},
'spaceConfig.align': {
control: 'select',
options: ['start', 'center', 'end'],
description: 'Alignment of the Space layout',
if: { arg: 'enableSpaceConfig', truthy: true },
},
'spaceConfig.wrap': {
control: 'boolean',
description:
'Controls whether the items inside the Space component should wrap to the next line when space is insufficient',
if: { arg: 'enableSpaceConfig', truthy: true },
},
};
SupersetRadio.args = {
export const RadioStory = {
args: {
value: 'radio1',
disabled: false,
checked: false,
children: 'Radio',
},
argTypes: RadioArgsType,
};
export const RadioButtonStory = (args: RadioProps) => (
<Radio.Button {...args}>Radio Button</Radio.Button>
);
RadioButtonStory.args = {
value: 'button1',
disabled: false,
checked: false,
};
RadioButtonStory.argTypes = RadioArgsType;
export const RadioGroupWithOptionsStory = (args: RadioGroupWrapperProps) => (
<Radio.GroupWrapper {...args} />
);
RadioGroupWithOptionsStory.args = {
spaceConfig: {
direction: 'vertical',
size: 'middle',
align: 'center',
wrap: false,
},
size: 'middle',
options: [
{
value: 1,
label: (
<Space align="center" direction="vertical">
<LineChartOutlined style={{ fontSize: 18 }} />
LineChart
</Space>
),
},
{
value: 2,
label: (
<Space align="center" direction="vertical">
<DotChartOutlined style={{ fontSize: 18 }} />
DotChart
</Space>
),
},
{
value: 3,
label: (
<Space align="center" direction="vertical">
<BarChartOutlined style={{ fontSize: 18 }} />
BarChart
</Space>
),
},
{
value: 4,
label: (
<Space align="center" direction="vertical">
<PieChartOutlined style={{ fontSize: 18 }} />
PieChart
</Space>
),
},
],
disabled: false,
};
RadioGroupWithOptionsStory.argTypes = radioGroupWrapperArgsType;

View File

@ -16,46 +16,48 @@
* specific language governing permissions and limitations
* under the License.
*/
import { styled } from '@superset-ui/core';
import { Radio as AntdRadio } from 'antd';
import { Radio as Antd5Radio, CheckboxOptionType } from 'antd-v5';
import type {
RadioChangeEvent,
RadioProps,
RadioGroupProps,
} from 'antd-v5/lib/radio';
const StyledRadio = styled(AntdRadio)`
.ant-radio-inner {
top: -1px;
left: 2px;
width: ${({ theme }) => theme.gridUnit * 4}px;
height: ${({ theme }) => theme.gridUnit * 4}px;
border-width: 2px;
border-color: ${({ theme }) => theme.colors.grayscale.light2};
}
import { Space, SpaceProps } from 'src/components/Space';
.ant-radio.ant-radio-checked {
.ant-radio-inner {
border-width: ${({ theme }) => theme.gridUnit + 1}px;
border-color: ${({ theme }) => theme.colors.primary.base};
}
export type RadioGroupWrapperProps = RadioGroupProps & {
spaceConfig?: {
direction?: SpaceProps['direction'];
size?: SpaceProps['size'];
align?: SpaceProps['align'];
wrap?: SpaceProps['wrap'];
};
options: CheckboxOptionType[];
};
.ant-radio-inner::after {
background-color: ${({ theme }) => theme.colors.grayscale.light5};
top: 0;
left: 0;
width: ${({ theme }) => theme.gridUnit + 2}px;
height: ${({ theme }) => theme.gridUnit + 2}px;
}
}
.ant-radio:hover,
.ant-radio:focus {
.ant-radio-inner {
border-color: ${({ theme }) => theme.colors.primary.dark1};
}
}
`;
const StyledGroup = styled(AntdRadio.Group)`
font-size: inherit;
`;
export const Radio = Object.assign(StyledRadio, {
Group: StyledGroup,
Button: AntdRadio.Button,
const RadioGroup = ({
spaceConfig,
options,
...props
}: RadioGroupWrapperProps) => {
const content = options.map((option: CheckboxOptionType) => (
<Radio key={option.value} value={option.value}>
{option.label}
</Radio>
));
return (
<Radio.Group {...props}>
{spaceConfig ? <Space {...spaceConfig}>{content}</Space> : content}
</Radio.Group>
);
};
export type {
RadioChangeEvent,
RadioGroupProps,
RadioProps,
CheckboxOptionType,
};
export const Radio = Object.assign(Antd5Radio, {
GroupWrapper: RadioGroup,
Button: Antd5Radio.Button,
});

View File

@ -19,7 +19,6 @@
import { useState } from 'react';
import { css, useTheme } from '@superset-ui/core';
import { Radio } from 'src/components/Radio';
import { Space } from 'src/components/Space';
import Icons from 'src/components/Icons';
import Popover from 'src/components/Popover';
@ -56,21 +55,20 @@ function HeaderWithRadioGroup(props: HeaderWithRadioGroupProps) {
>
{groupTitle}
</div>
<Radio.Group
<Radio.GroupWrapper
spaceConfig={{
direction: 'vertical',
size: 4,
wrap: false,
align: 'start',
}}
value={value}
onChange={e => {
onChange(e.target.value);
setPopoverVisible(false);
}}
>
<Space direction="vertical">
{groupOptions.map(option => (
<Radio key={option.value} value={option.value}>
{option.label}
</Radio>
))}
</Space>
</Radio.Group>
options={groupOptions}
/>
</div>
}
placement="bottomLeft"

View File

@ -1106,15 +1106,16 @@ const FiltersConfigForm = (
initialValue={sort}
label={<StyledLabel>{t('Sort type')}</StyledLabel>}
>
<Radio.Group
<Radio.GroupWrapper
options={[
{ value: true, label: t('Sort ascending') },
{ value: false, label: t('Sort descending') },
]}
onChange={value => {
onSortChanged(value.target.value);
formChanged();
}}
>
<Radio value>{t('Sort ascending')}</Radio>
<Radio value={false}>{t('Sort descending')}</Radio>
</Radio.Group>
/>
</StyledRowFormItem>
{hasMetrics && (
<StyledRowSubFormItem
@ -1181,22 +1182,23 @@ const FiltersConfigForm = (
<StyledLabel>{t('Single value type')}</StyledLabel>
}
>
<Radio.Group
<Radio.GroupWrapper
onChange={value => {
onEnableSingleValueChanged(value.target.value);
formChanged();
}}
>
<Radio value={SingleValueType.Minimum}>
{t('Minimum')}
</Radio>
<Radio value={SingleValueType.Exact}>
{t('Exact')}
</Radio>
<Radio value={SingleValueType.Maximum}>
{t('Maximum')}
</Radio>
</Radio.Group>
options={[
{
label: t('Minimum'),
value: SingleValueType.Minimum,
},
{ label: t('Exact'), value: SingleValueType.Exact },
{
label: t('Maximum'),
value: SingleValueType.Maximum,
},
]}
/>
</StyledRowFormItem>
</CollapsibleControl>
</CleanFormItem>

View File

@ -30,7 +30,6 @@ import {
import { Global } from '@emotion/react';
import { Column } from 'react-table';
import { debounce } from 'lodash';
import { Space } from 'src/components/Space';
import { Input } from 'src/components/Input';
import {
BOOL_FALSE_DISPLAY,
@ -141,12 +140,21 @@ const FormatPicker = ({
onChange: any;
value: FormatPickerValue;
}) => (
<Radio.Group value={value} onChange={onChange}>
<Space direction="vertical">
<Radio value={FormatPickerValue.Formatted}>{t('Formatted date')}</Radio>
<Radio value={FormatPickerValue.Original}>{t('Original value')}</Radio>
</Space>
</Radio.Group>
<Radio.GroupWrapper
spaceConfig={{
direction: 'vertical',
align: 'start',
size: 15,
wrap: false,
}}
size="large"
value={value}
onChange={onChange}
options={[
{ label: t('Formatted date'), value: FormatPickerValue.Formatted },
{ label: t('Original value'), value: FormatPickerValue.Original },
]}
/>
);
const FormatPickerContainer = styled.div`

View File

@ -87,12 +87,6 @@ const ContentStyleWrapper = styled.div`
margin: 8px 0;
}
.vertical-radio {
display: block;
height: 40px;
line-height: 40px;
}
.section-title {
font-style: normal;
font-weight: ${theme.typography.weights.bold};

View File

@ -45,16 +45,18 @@ export function CalendarFrame({ onChange, value }: FrameComponentProps) {
<div className="section-title">
{t('Configure Time Range: Previous...')}
</div>
<Radio.Group
<Radio.GroupWrapper
spaceConfig={{
direction: 'vertical',
size: 15,
align: 'start',
wrap: false,
}}
size="large"
value={value}
onChange={(e: any) => onChange(e.target.value)}
>
{CALENDAR_RANGE_OPTIONS.map(({ value, label }) => (
<Radio key={value} value={value} className="vertical-radio">
{label}
</Radio>
))}
</Radio.Group>
options={CALENDAR_RANGE_OPTIONS}
/>
</>
);
}

View File

@ -41,16 +41,18 @@ export function CommonFrame(props: FrameComponentProps) {
<div className="section-title" data-test={DateFilterTestKey.CommonFrame}>
{t('Configure Time Range: Last...')}
</div>
<Radio.Group
<Radio.GroupWrapper
spaceConfig={{
direction: 'vertical',
size: 15,
align: 'start',
wrap: false,
}}
size="large"
value={commonRange}
onChange={(e: any) => props.onChange(e.target.value)}
>
{COMMON_RANGE_OPTIONS.map(({ value, label }) => (
<Radio key={value} value={value} className="vertical-radio">
{label}
</Radio>
))}
</Radio.Group>
options={COMMON_RANGE_OPTIONS}
/>
</>
);
}

View File

@ -41,25 +41,22 @@ export function CurrentCalendarFrame({ onChange, value }: FrameComponentProps) {
<div className="section-title">
{t('Configure Time Range: Current...')}
</div>
<Radio.Group
value={value}
<Radio.GroupWrapper
spaceConfig={{
direction: 'vertical',
size: 15,
align: 'start',
wrap: true,
}}
size="large"
onChange={(e: any) => {
let newValue = e.target.value;
// Sanitization: Trim whitespace
newValue = newValue.trim();
// Validation: Check if the value is non-empty
if (newValue === '') {
return;
}
if (newValue === '') return;
onChange(newValue);
}}
>
{CURRENT_RANGE_OPTIONS.map(({ value, label }) => (
<Radio key={value} value={value} className="vertical-radio">
{label}
</Radio>
))}
</Radio.Group>
options={CURRENT_RANGE_OPTIONS}
/>
</>
);
}

View File

@ -238,18 +238,15 @@ export function CustomFrame(props: FrameComponentProps) {
<div className="control-label">{t('Anchor to')}</div>
<Row align="middle">
<Col>
<Radio.Group
<Radio.GroupWrapper
options={[
{ value: 'now', label: t('Now') },
{ value: 'specific', label: t('Date/Time') },
]}
onChange={onAnchorMode}
defaultValue="now"
value={anchorMode}
>
<Radio key="now" value="now">
{t('NOW')}
</Radio>
<Radio key="specific" value="specific">
{t('Date/Time')}
</Radio>
</Radio.Group>
/>
</Col>
{anchorMode !== 'now' && (
<Col>

View File

@ -42,7 +42,7 @@ describe('CalendarFrame', () => {
const radios = screen.getAllByRole('radio');
expect(radios).toHaveLength(CALENDAR_RANGE_OPTIONS.length);
CALENDAR_RANGE_OPTIONS.forEach(option => {
expect(screen.getByText(option.label)).toBeInTheDocument();
expect(screen.getByText(option.label as string)).toBeInTheDocument();
});
});
@ -56,7 +56,7 @@ describe('CalendarFrame', () => {
);
const secondOption = CALENDAR_RANGE_OPTIONS[1];
const radio = screen.getByLabelText(secondOption.label);
const radio = screen.getByLabelText(secondOption.label as string);
fireEvent.click(radio);
expect(mockOnChange).toHaveBeenCalledWith(secondOption.value);
@ -85,6 +85,8 @@ describe('CalendarFrame', () => {
const thirdOption = CALENDAR_RANGE_OPTIONS[2];
expect(thirdOption.value).toBe(PreviousCalendarQuarter);
expect(screen.getByLabelText(thirdOption.label)).toBeInTheDocument();
expect(
screen.getByLabelText(thirdOption.label as string),
).toBeInTheDocument();
});
});

View File

@ -167,8 +167,8 @@ test('renders anchor with now option', async () => {
);
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(screen.getByText('Anchor to')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'NOW' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Date/Time' })).toBeInTheDocument();
expect(screen.getByLabelText('Now')).toBeInTheDocument();
expect(screen.getByLabelText('Date/Time')).toBeInTheDocument();
expect(screen.queryByPlaceholderText('Select date')).not.toBeInTheDocument();
});
@ -180,8 +180,8 @@ test('renders anchor with date/time option', async () => {
);
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(screen.getByText('Anchor to')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'NOW' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Date/Time' })).toBeInTheDocument();
expect(screen.getByLabelText('Now')).toBeInTheDocument();
expect(screen.getByLabelText('Date/Time')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Select date')).toBeInTheDocument();
});

View File

@ -32,6 +32,7 @@ import {
CurrentQuarter,
CurrentDay,
} from 'src/explore/components/controls/DateFilterControl/types';
import { CheckboxOptionType } from 'src/components/Radio';
import { extendedDayjs } from 'src/utils/dates';
export const FRAME_OPTIONS: SelectOptionType[] = [
@ -43,7 +44,7 @@ export const FRAME_OPTIONS: SelectOptionType[] = [
{ value: 'No filter', label: t('No filter') },
];
export const COMMON_RANGE_OPTIONS: SelectOptionType[] = [
export const COMMON_RANGE_OPTIONS: CheckboxOptionType[] = [
{ value: 'Last day', label: t('Last day') },
{ value: 'Last week', label: t('Last week') },
{ value: 'Last month', label: t('Last month') },
@ -51,20 +52,20 @@ export const COMMON_RANGE_OPTIONS: SelectOptionType[] = [
{ value: 'Last year', label: t('Last year') },
];
export const COMMON_RANGE_VALUES_SET = new Set(
COMMON_RANGE_OPTIONS.map(({ value }) => value),
COMMON_RANGE_OPTIONS.map(value => value.value),
);
export const CALENDAR_RANGE_OPTIONS: SelectOptionType[] = [
export const CALENDAR_RANGE_OPTIONS: CheckboxOptionType[] = [
{ value: PreviousCalendarWeek, label: t('previous calendar week') },
{ value: PreviousCalendarMonth, label: t('previous calendar month') },
{ value: PreviousCalendarQuarter, label: t('previous calendar quarter') },
{ value: PreviousCalendarYear, label: t('previous calendar year') },
];
export const CALENDAR_RANGE_VALUES_SET = new Set(
CALENDAR_RANGE_OPTIONS.map(({ value }) => value),
CALENDAR_RANGE_OPTIONS.map(value => value.value),
);
export const CURRENT_RANGE_OPTIONS: SelectOptionType[] = [
export const CURRENT_RANGE_OPTIONS: CheckboxOptionType[] = [
{ value: CurrentDay, label: t('Current day') },
{ value: CurrentWeek, label: t('Current week') },
{ value: CurrentMonth, label: t('Current month') },
@ -72,7 +73,7 @@ export const CURRENT_RANGE_OPTIONS: SelectOptionType[] = [
{ value: CurrentYear, label: t('Current year') },
];
export const CURRENT_RANGE_VALUES_SET = new Set(
CURRENT_RANGE_OPTIONS.map(({ value }) => value),
CURRENT_RANGE_OPTIONS.map(value => value.value),
);
const GRAIN_OPTIONS = [

View File

@ -41,7 +41,7 @@ import TimezoneSelector from 'src/components/TimezoneSelector';
import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput';
import Icons from 'src/components/Icons';
import { CronError } from 'src/components/CronPicker';
import { RadioChangeEvent } from 'src/components';
import { Radio, RadioChangeEvent } from 'src/components/Radio';
import { Input } from 'src/components/Input';
import withToasts from 'src/components/MessageToasts/withToasts';
import { ChartState } from 'src/explore/types';
@ -68,8 +68,6 @@ import {
TimezoneHeaderStyle,
SectionHeaderStyle,
StyledMessageContentTitle,
StyledRadio,
StyledRadioGroup,
} from './styles';
interface ReportProps {
@ -257,24 +255,32 @@ function ReportModal({
<h4>{t('Message content')}</h4>
</StyledMessageContentTitle>
<div className="inline-container">
<StyledRadioGroup
<Radio.GroupWrapper
spaceConfig={{
direction: 'vertical',
size: 'middle',
align: 'start',
wrap: false,
}}
onChange={(event: RadioChangeEvent) => {
setCurrentReport({ report_format: event.target.value });
}}
value={currentReport.report_format || defaultNotificationFormat}
>
{isTextBasedChart && (
<StyledRadio value={NotificationFormats.Text}>
{t('Text embedded in email')}
</StyledRadio>
)}
<StyledRadio value={NotificationFormats.PNG}>
{t('Image (PNG) embedded in email')}
</StyledRadio>
<StyledRadio value={NotificationFormats.CSV}>
{t('Formatted CSV attached in email')}
</StyledRadio>
</StyledRadioGroup>
options={[
{
label: t('Text embedded in email'),
value: NotificationFormats.Text,
},
{
label: t('Image (PNG) embedded in email'),
value: NotificationFormats.PNG,
},
{
label: t('Formatted CSV attached in email'),
value: NotificationFormats.CSV,
},
]}
/>
</div>
</>
);

View File

@ -108,10 +108,6 @@ export const StyledRadio = styled(Radio)`
line-height: ${({ theme }) => theme.gridUnit * 8}px;
`;
export const StyledRadioGroup = styled(Radio.Group)`
margin-left: ${({ theme }) => theme.gridUnit * 0.5}px;
`;
export const antDErrorAlertStyles = (theme: SupersetTheme) => css`
margin: ${theme.gridUnit * 4}px;
margin-top: 0;