Compare commits

...

1 Commits

Author SHA1 Message Date
Joe Li
97bd247f27 fix(ui): update stale Ant Design v6 internal class selectors
The antd v5->v6 upgrade renamed several generated `.ant-*` DOM classes.
Superset styles those internals directly (styled-components, css literals,
*.styles.ts) and via runtime DOM lookups (getPopupContainer/.closest/
querySelector), so the renamed classes silently stopped matching and
produced visual regressions (mis-sized modals, broken progress bars and
alerts, spinner sizing, step connectors, popups escaping their modal).

Update every stale reference to its v6 name and extend
antdDomContract.test.tsx to pin the classes, so a future antd bump that
renames one fails loudly instead of shipping a silent visual regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:32:22 -07:00
18 changed files with 123 additions and 30 deletions

View File

@@ -84,7 +84,7 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
`
}
.ant-modal-content {
.ant-modal-container {
background-color: ${theme.colorBgContainer};
display: flex;
flex-direction: column;
@@ -196,7 +196,7 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
height: 100%;
}
.ant-modal-content {
.ant-modal-container {
height: 100%;
.ant-modal-body {

View File

@@ -29,10 +29,10 @@ const ProgressBar = styled(({ striped, ...props }: ProgressBarProps) => (
<AntdProgress data-test="progress-bar" {...props} />
))`
position: static;
.ant-progress-inner {
.ant-progress-rail {
position: static;
}
.ant-progress-bg {
.ant-progress-track {
position: static;
${({ striped }) =>
striped &&

View File

@@ -757,7 +757,7 @@ const AsyncSelect = forwardRef(
getPopupContainer={
getPopupContainer ||
((triggerNode: HTMLElement) =>
(triggerNode?.closest('.ant-modal-content') as HTMLElement) ||
(triggerNode?.closest('.ant-modal-container') as HTMLElement) ||
(triggerNode.parentNode as HTMLElement))
}
headerPosition={headerPosition}

View File

@@ -822,7 +822,7 @@ const Select = forwardRef(
getPopupContainer={
getPopupContainer ||
((triggerNode: HTMLElement) =>
(triggerNode?.closest('.ant-modal-content') as HTMLElement) ||
(triggerNode?.closest('.ant-modal-container') as HTMLElement) ||
(triggerNode.parentNode as HTMLElement))
}
headerPosition={headerPosition}

View File

@@ -61,7 +61,7 @@ const StyledTable = styled(AntTable)(
text-overflow: ellipsis;
}
.ant-spin-nested-loading .ant-spin .ant-spin-dot {
.ant-spin .ant-spin-dot {
width: ${theme.sizeUnit * 12}px;
height: unset;
}

View File

@@ -198,7 +198,7 @@ const StyledTable = styled(AntTable as FC<AntTableProps>)<{ height?: number }>(
scrollbar-color: ${theme.colorFillSecondary} ${theme.colorFillQuaternary};
}
.ant-spin-nested-loading .ant-spin .ant-spin-dot {
.ant-spin .ant-spin-dot {
width: ${theme.sizeXXL}px;
height: unset;
}

View File

@@ -32,8 +32,20 @@
* one fails here with a clear pointer instead of shipping a visual regression.
*/
import { render } from '@superset-ui/core/spec';
// eslint-disable-next-line no-restricted-imports
import { Collapse, Modal, Popover, Steps, Tabs, Tag, Tooltip } from 'antd';
import {
// eslint-disable-next-line no-restricted-imports
Alert,
Collapse,
Modal,
Popover,
Progress,
Spin,
Steps,
Tabs,
Tag,
Tooltip,
Upload,
} from 'antd';
import { Select } from './Select';
const antClasses = (root: ParentNode): string[] => {
@@ -176,9 +188,17 @@ test('Steps classes (QueryStatusBar, ChartCreation, SQL Lab loading detection)',
'ant-steps-item-icon',
'ant-steps-item-title',
'ant-steps-item-rail',
'ant-steps-item-content',
]),
);
expect(classes).not.toContain('ant-steps-item-tail');
// antd 6 removed the nested `.ant-steps-item-description`; the description text
// now renders directly inside `.ant-steps-item-content`. ChartCreation styles
// the description by targeting `.ant-steps-item-content` for exactly this reason.
expect(classes).not.toContain('ant-steps-item-description');
expect(container.querySelector('.ant-steps-item-content')).toHaveTextContent(
'd',
);
});
test('Select suffix (arrow) class (plugin-chart-table page-size Select targets it)', () => {
@@ -216,13 +236,87 @@ test('Collapse panel/body classes (Collapse.tsx, VizTypeGallery, config modals)'
expect(classes).not.toContain('ant-collapse-content-box');
});
test('Modal body class (many *.styles.ts modal overrides target it)', () => {
test('Modal container + body classes (many *.styles.ts modal overrides target them)', () => {
render(
<Modal open title="t">
body
</Modal>,
);
expect(antClasses(document.body)).toContain('ant-modal-body');
const classes = antClasses(document.body);
// antd 6 renamed the modal content wrapper `.ant-modal-content` ->
// `.ant-modal-container`. Several `.styles.ts` overrides target it, and Select /
// popup `getPopupContainer`/`.closest()` lookups anchor popups to it so their
// menus stay clipped inside the modal.
expect(classes).toEqual(
expect.arrayContaining(['ant-modal-container', 'ant-modal-body']),
);
expect(classes).not.toContain('ant-modal-content');
});
test('Progress rail/track classes (ProgressBar + DatabaseModal striped overrides target them)', () => {
const { container } = render(<Progress percent={50} />);
const classes = antClasses(container);
// antd 6 renamed `.ant-progress-inner` -> `.ant-progress-rail` and
// `.ant-progress-bg` -> `.ant-progress-track`. ProgressBar's styled wrapper
// paints the striped gradient on the track; DatabaseModal sizes the rail.
expect(classes).toEqual(
expect.arrayContaining(['ant-progress-rail', 'ant-progress-track']),
);
expect(classes).not.toContain('ant-progress-inner');
expect(classes).not.toContain('ant-progress-bg');
});
test('Alert title/actions classes (ImportModal, DatabaseModal, SqlEditor, native-filter overrides target them)', () => {
const { container } = render(
<Alert
type="info"
title="msg"
description="desc"
action={<span>x</span>}
/>,
);
const classes = antClasses(container);
// antd 6 renamed `.ant-alert-message` -> `.ant-alert-title` and pluralised the
// action slot `.ant-alert-action` -> `.ant-alert-actions`. Several `.styles.ts`
// overrides and the SQL Lab / native-filter modal footers target these.
expect(classes).toEqual(
expect.arrayContaining(['ant-alert-title', 'ant-alert-actions']),
);
expect(classes).not.toContain('ant-alert-message');
expect(classes).not.toContain('ant-alert-action');
});
test('Spin nested structure (Table/VirtualTable spinner sizing overrides target it)', () => {
const { container } = render(
<Spin spinning>
<div>content</div>
</Spin>,
);
const classes = antClasses(container);
// antd 6 dropped the `.ant-spin-nested-loading` wrapper: `.ant-spin` is now the
// outer element and `.ant-spin-dot` lives beneath it. Table/VirtualTable size the
// dot via `.ant-spin .ant-spin-dot`, so that descendant chain must hold.
expect(classes).toEqual(
expect.arrayContaining(['ant-spin', 'ant-spin-container', 'ant-spin-dot']),
);
expect(classes).not.toContain('ant-spin-nested-loading');
expect(
container.querySelector('.ant-spin-dot')?.closest('.ant-spin'),
).not.toBeNull();
});
test('Upload list-item actions class (DatabaseModal hides the imported-file action button)', () => {
const { container } = render(
<Upload fileList={[{ uid: '1', name: 'database.zip', status: 'done' }]}>
<button type="button">upload</button>
</Upload>,
);
const classes = antClasses(container);
// antd 6 renamed the upload list-item action-button container
// `.ant-upload-list-item-card-actions` -> `.ant-upload-list-item-actions`.
// DatabaseModal hides that action button on the imported-database file item.
expect(classes).toContain('ant-upload-list-item-actions');
expect(classes).not.toContain('ant-upload-list-item-card-actions');
});
test('Tag keeps its v5 trailing margin (GlobalStyles parity rule)', () => {

View File

@@ -902,7 +902,7 @@ const SqlEditor: FC<Props> = ({
css={css`
margin-bottom: ${theme.sizeUnit * 2}px;
padding-top: ${theme.sizeUnit * 4}px;
.ant-alert-action {
.ant-alert-actions {
align-self: center;
}
`}

View File

@@ -53,7 +53,7 @@ const StyledDatasourceModal = styled(Modal)`
top: ${TOP_MARGIN_VH}vh;
padding-bottom: 0;
&& .ant-modal-content {
&& .ant-modal-container {
max-height: ${MODAL_HEIGHT_VH}vh;
margin-top: 0;
margin-bottom: 0;

View File

@@ -22,7 +22,7 @@ import { css, SupersetTheme } from '@apache-superset/core/theme';
export const antdWarningAlertStyles = (theme: SupersetTheme) => css`
margin: ${theme.sizeUnit * 4}px 0;
.ant-alert-message {
.ant-alert-title {
margin: 0;
}
`;

View File

@@ -32,7 +32,7 @@ const StyledAlert = styled(Alert)`
text-align: left;
flex: 1;
& .ant-alert-action {
& .ant-alert-actions {
align-self: center;
}
`;

View File

@@ -66,7 +66,7 @@ export const BaseModalWrapper = styled(StyledModal)<BaseModalWrapperProps>`
.ant-modal-body {
flex: 1 1 auto;
}
.ant-modal-content {
.ant-modal-container {
height: 100%;
}
`}

View File

@@ -43,7 +43,7 @@ export function CancelConfirmationAlert({
css={{
textAlign: 'left',
flex: 1,
'& .ant-alert-action': { alignSelf: 'center' },
'& .ant-alert-actions': { alignSelf: 'center' },
}}
description={children}
action={

View File

@@ -158,7 +158,7 @@ export const antDErrorAlertStyles = (theme: SupersetTheme) => css`
export const antdWarningAlertStyles = (theme: SupersetTheme) => css`
margin: ${theme.sizeUnit * 4}px 0;
.ant-alert-message {
.ant-alert-title {
margin: 0;
}
`;
@@ -535,11 +535,11 @@ export const StyledCatalogTable = styled.div`
export const StyledUploadWrapper = styled.div`
margin: ${({ theme }) => theme.sizeUnit * 4}px;
.ant-progress-inner {
.ant-progress-rail {
display: none;
}
.ant-upload-list-item-card-actions {
.ant-upload-list-item-actions {
display: none;
}
`;

View File

@@ -140,7 +140,7 @@ function GroupListModal({
value: role.id,
label: role.name,
}))}
getPopupContainer={trigger => trigger.closest('.ant-modal-content')}
getPopupContainer={trigger => trigger.closest('.ant-modal-container')}
/>
</FormItem>
<FormItem name="users" label={t('Users')}>

View File

@@ -60,7 +60,7 @@ export const PermissionsField = ({
placeholder={t('Select permissions')}
options={options}
loading={loading}
getPopupContainer={trigger => trigger.closest('.ant-modal-content')}
getPopupContainer={trigger => trigger.closest('.ant-modal-container')}
data-test="permissions-select"
/>
</FormItem>

View File

@@ -202,7 +202,7 @@ function UserListModal({
label: role.name,
}))}
getPopupContainer={trigger =>
trigger.closest('.ant-modal-content')
trigger.closest('.ant-modal-container')
}
/>
</FormItem>
@@ -221,7 +221,7 @@ function UserListModal({
label: group.name,
}))}
getPopupContainer={trigger =>
trigger.closest('.ant-modal-content')
trigger.closest('.ant-modal-container')
}
/>
</FormItem>

View File

@@ -139,11 +139,10 @@ const StyledContainer = styled.div`
&&&& .ant-steps-item-content {
overflow: unset;
.ant-steps-item-description {
margin-top: ${theme.sizeUnit}px;
padding-bottom: ${theme.sizeUnit}px;
}
/* antd 6 removed .ant-steps-item-description; the description text is now
rendered directly in .ant-steps-item-content. */
margin-top: ${theme.sizeUnit}px;
padding-bottom: ${theme.sizeUnit}px;
}
&&&& .ant-tooltip-open {