mirror of
https://github.com/apache/superset.git
synced 2026-04-18 15:44:57 +00:00
fix(dashboard): prevent tab content cutoff and excessive whitespace in empty tabs (#35834)
This commit is contained in:
committed by
GitHub
parent
74a590cb76
commit
78f9debdd4
@@ -20,25 +20,25 @@
|
||||
import { fireEvent, render } from '@superset-ui/core/spec';
|
||||
import Tabs, { EditableTabs, LineEditableTabs } from './Tabs';
|
||||
|
||||
describe('Tabs', () => {
|
||||
const defaultItems = [
|
||||
{
|
||||
key: '1',
|
||||
label: 'Tab 1',
|
||||
children: <div data-testid="tab1-content">Tab 1 content</div>,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Tab 2',
|
||||
children: <div data-testid="tab2-content">Tab 2 content</div>,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: 'Tab 3',
|
||||
children: <div data-testid="tab3-content">Tab 3 content</div>,
|
||||
},
|
||||
];
|
||||
const defaultItems = [
|
||||
{
|
||||
key: '1',
|
||||
label: 'Tab 1',
|
||||
children: <div data-testid="tab1-content">Tab 1 content</div>,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Tab 2',
|
||||
children: <div data-testid="tab2-content">Tab 2 content</div>,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: 'Tab 3',
|
||||
children: <div data-testid="tab3-content">Tab 3 content</div>,
|
||||
},
|
||||
];
|
||||
|
||||
describe('Tabs', () => {
|
||||
describe('Basic Tabs', () => {
|
||||
it('should render tabs with default props', () => {
|
||||
const { getByText, container } = render(<Tabs items={defaultItems} />);
|
||||
@@ -284,6 +284,7 @@ describe('Tabs', () => {
|
||||
describe('Styling Integration', () => {
|
||||
it('should accept and apply custom CSS classes', () => {
|
||||
const { container } = render(
|
||||
// eslint-disable-next-line react/forbid-component-props
|
||||
<Tabs items={defaultItems} className="custom-tabs-class" />,
|
||||
);
|
||||
|
||||
@@ -295,6 +296,7 @@ describe('Tabs', () => {
|
||||
it('should accept and apply custom styles', () => {
|
||||
const customStyle = { minHeight: '200px' };
|
||||
const { container } = render(
|
||||
// eslint-disable-next-line react/forbid-component-props
|
||||
<Tabs items={defaultItems} style={customStyle} />,
|
||||
);
|
||||
|
||||
@@ -304,3 +306,72 @@ describe('Tabs', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('fullHeight prop renders component hierarchy correctly', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} fullHeight />);
|
||||
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
const contentHolder = container.querySelector('.ant-tabs-content-holder');
|
||||
const content = container.querySelector('.ant-tabs-content');
|
||||
const tabPane = container.querySelector('.ant-tabs-tabpane');
|
||||
|
||||
expect(tabsElement).toBeInTheDocument();
|
||||
expect(contentHolder).toBeInTheDocument();
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(tabPane).toBeInTheDocument();
|
||||
expect(tabsElement?.contains(contentHolder as Node)).toBe(true);
|
||||
expect(contentHolder?.contains(content as Node)).toBe(true);
|
||||
expect(content?.contains(tabPane as Node)).toBe(true);
|
||||
});
|
||||
|
||||
test('fullHeight prop maintains structure when content updates', () => {
|
||||
const { container, rerender } = render(
|
||||
<Tabs items={defaultItems} fullHeight />,
|
||||
);
|
||||
|
||||
const initialTabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
const newItems = [
|
||||
...defaultItems,
|
||||
{
|
||||
key: '4',
|
||||
label: 'Tab 4',
|
||||
children: <div data-testid="tab4-content">New tab content</div>,
|
||||
},
|
||||
];
|
||||
|
||||
rerender(<Tabs items={newItems} fullHeight />);
|
||||
|
||||
const updatedTabsElement = container.querySelector('.ant-tabs');
|
||||
const updatedContentHolder = container.querySelector(
|
||||
'.ant-tabs-content-holder',
|
||||
);
|
||||
|
||||
expect(updatedTabsElement).toBeInTheDocument();
|
||||
expect(updatedContentHolder).toBeInTheDocument();
|
||||
expect(initialTabsElement).toBe(updatedTabsElement);
|
||||
});
|
||||
|
||||
test('fullHeight prop works with allowOverflow to handle tall content', () => {
|
||||
const { container } = render(
|
||||
<Tabs items={defaultItems} fullHeight allowOverflow />,
|
||||
);
|
||||
|
||||
const tabsElement = container.querySelector('.ant-tabs') as HTMLElement;
|
||||
const contentHolder = container.querySelector(
|
||||
'.ant-tabs-content-holder',
|
||||
) as HTMLElement;
|
||||
|
||||
expect(tabsElement).toBeInTheDocument();
|
||||
expect(contentHolder).toBeInTheDocument();
|
||||
|
||||
// Verify overflow handling is not restricted
|
||||
const holderStyles = window.getComputedStyle(contentHolder);
|
||||
expect(holderStyles.overflow).not.toBe('hidden');
|
||||
});
|
||||
|
||||
test('fullHeight prop handles empty items array', () => {
|
||||
const { container } = render(<Tabs items={[]} fullHeight />);
|
||||
|
||||
expect(container.querySelector('.ant-tabs')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -25,12 +25,14 @@ import type { SerializedStyles } from '@emotion/react';
|
||||
|
||||
export interface TabsProps extends AntdTabsProps {
|
||||
allowOverflow?: boolean;
|
||||
fullHeight?: boolean;
|
||||
contentStyle?: SerializedStyles;
|
||||
}
|
||||
|
||||
const StyledTabs = ({
|
||||
animated = false,
|
||||
allowOverflow = true,
|
||||
fullHeight = false,
|
||||
tabBarStyle,
|
||||
contentStyle,
|
||||
...props
|
||||
@@ -46,9 +48,17 @@ const StyledTabs = ({
|
||||
tabBarStyle={mergedStyle}
|
||||
css={theme => css`
|
||||
overflow: ${allowOverflow ? 'visible' : 'hidden'};
|
||||
${fullHeight && 'height: 100%;'}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
overflow: ${allowOverflow ? 'visible' : 'auto'};
|
||||
${fullHeight && 'height: 100%;'}
|
||||
}
|
||||
.ant-tabs-content {
|
||||
${fullHeight && 'height: 100%;'}
|
||||
}
|
||||
.ant-tabs-tabpane {
|
||||
${fullHeight && 'height: 100%;'}
|
||||
${contentStyle}
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
|
||||
@@ -58,30 +58,52 @@ jest.mock('src/dashboard/actions/dashboardState', () => ({
|
||||
jest.mock('src/components/ResizableSidebar/useStoredSidebarWidth');
|
||||
|
||||
// mock following dependent components to fix the prop warnings
|
||||
jest.mock('@superset-ui/core/components/Select/Select', () => () => (
|
||||
<div data-test="mock-select" />
|
||||
));
|
||||
jest.mock('@superset-ui/core/components/Select/AsyncSelect', () => () => (
|
||||
<div data-test="mock-async-select" />
|
||||
));
|
||||
jest.mock('@superset-ui/core/components/PageHeaderWithActions', () => ({
|
||||
PageHeaderWithActions: () => (
|
||||
jest.mock('@superset-ui/core/components/Select/Select', () => {
|
||||
const MockSelect = () => <div data-test="mock-select" />;
|
||||
MockSelect.displayName = 'MockSelect';
|
||||
return MockSelect;
|
||||
});
|
||||
jest.mock('@superset-ui/core/components/Select/AsyncSelect', () => {
|
||||
const MockAsyncSelect = () => <div data-test="mock-async-select" />;
|
||||
MockAsyncSelect.displayName = 'MockAsyncSelect';
|
||||
return MockAsyncSelect;
|
||||
});
|
||||
jest.mock('@superset-ui/core/components/PageHeaderWithActions', () => {
|
||||
const MockPageHeaderWithActions = () => (
|
||||
<div data-test="mock-page-header-with-actions" />
|
||||
),
|
||||
}));
|
||||
);
|
||||
MockPageHeaderWithActions.displayName = 'MockPageHeaderWithActions';
|
||||
return {
|
||||
PageHeaderWithActions: MockPageHeaderWithActions,
|
||||
};
|
||||
});
|
||||
jest.mock(
|
||||
'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal',
|
||||
() => () => <div data-test="mock-filters-config-modal" />,
|
||||
() => {
|
||||
const MockFiltersConfigModal = () => (
|
||||
<div data-test="mock-filters-config-modal" />
|
||||
);
|
||||
MockFiltersConfigModal.displayName = 'MockFiltersConfigModal';
|
||||
return MockFiltersConfigModal;
|
||||
},
|
||||
);
|
||||
jest.mock('src/dashboard/components/BuilderComponentPane', () => () => (
|
||||
<div data-test="mock-builder-component-pane" />
|
||||
));
|
||||
jest.mock('src/dashboard/components/nativeFilters/FilterBar', () => () => (
|
||||
<div data-test="mock-filter-bar" />
|
||||
));
|
||||
jest.mock('src/dashboard/containers/DashboardGrid', () => () => (
|
||||
<div data-test="mock-dashboard-grid" />
|
||||
));
|
||||
jest.mock('src/dashboard/components/BuilderComponentPane', () => {
|
||||
const MockBuilderComponentPane = () => (
|
||||
<div data-test="mock-builder-component-pane" />
|
||||
);
|
||||
MockBuilderComponentPane.displayName = 'MockBuilderComponentPane';
|
||||
return MockBuilderComponentPane;
|
||||
});
|
||||
jest.mock('src/dashboard/components/nativeFilters/FilterBar', () => {
|
||||
const MockFilterBar = () => <div data-test="mock-filter-bar" />;
|
||||
MockFilterBar.displayName = 'MockFilterBar';
|
||||
return MockFilterBar;
|
||||
});
|
||||
jest.mock('src/dashboard/containers/DashboardGrid', () => {
|
||||
const MockDashboardGrid = () => <div data-test="mock-dashboard-grid" />;
|
||||
MockDashboardGrid.displayName = 'MockDashboardGrid';
|
||||
return MockDashboardGrid;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('DashboardBuilder', () => {
|
||||
@@ -178,8 +200,8 @@ describe('DashboardBuilder', () => {
|
||||
dashboardLayout: undoableDashboardLayoutWithTabs,
|
||||
});
|
||||
const parentSize = await findByTestId('grid-container');
|
||||
const first_tab = screen.getByText('tab1');
|
||||
expect(first_tab).toBeInTheDocument();
|
||||
const firstTab = screen.getByText('tab1');
|
||||
expect(firstTab).toBeInTheDocument();
|
||||
const tabPanels = within(parentSize).getAllByRole('tabpanel', {
|
||||
// to include invisible tab panels
|
||||
hidden: false,
|
||||
@@ -198,9 +220,9 @@ describe('DashboardBuilder', () => {
|
||||
},
|
||||
});
|
||||
const parentSize = await findByTestId('grid-container');
|
||||
const second_tab = screen.getByText('tab2');
|
||||
expect(second_tab).toBeInTheDocument();
|
||||
fireEvent.click(second_tab);
|
||||
const secondTab = screen.getByText('tab2');
|
||||
expect(secondTab).toBeInTheDocument();
|
||||
fireEvent.click(secondTab);
|
||||
const tabPanels = within(parentSize).getAllByRole('tabpanel', {
|
||||
// to include invisible tab panels
|
||||
hidden: true,
|
||||
@@ -435,3 +457,67 @@ describe('DashboardBuilder', () => {
|
||||
expect(queryByTestId('dashboard-filters-panel')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should render ParentSize wrapper with height 100% for tabs', async () => {
|
||||
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
|
||||
100,
|
||||
jest.fn(),
|
||||
]);
|
||||
(fetchFaveStar as jest.Mock).mockReturnValue({ type: 'mock-action' });
|
||||
(setActiveTab as jest.Mock).mockReturnValue({ type: 'mock-action' });
|
||||
|
||||
const { findByTestId } = render(<DashboardBuilder />, {
|
||||
useRedux: true,
|
||||
store: storeWithState({
|
||||
...mockState,
|
||||
dashboardLayout: undoableDashboardLayoutWithTabs,
|
||||
}),
|
||||
useDnd: true,
|
||||
useTheme: true,
|
||||
});
|
||||
|
||||
const gridContainer = await findByTestId('grid-container');
|
||||
const parentSizeWrapper = gridContainer.querySelector('div');
|
||||
const tabPanels = within(gridContainer).getAllByRole('tabpanel', {
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
expect(gridContainer).toBeInTheDocument();
|
||||
expect(parentSizeWrapper).toBeInTheDocument();
|
||||
expect(tabPanels.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should maintain layout when switching between tabs', async () => {
|
||||
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
|
||||
100,
|
||||
jest.fn(),
|
||||
]);
|
||||
(fetchFaveStar as jest.Mock).mockReturnValue({ type: 'mock-action' });
|
||||
(setActiveTab as jest.Mock).mockReturnValue({ type: 'mock-action' });
|
||||
(setDirectPathToChild as jest.Mock).mockImplementation(arg0 => ({
|
||||
type: 'type',
|
||||
arg0,
|
||||
}));
|
||||
|
||||
const { findByTestId } = render(<DashboardBuilder />, {
|
||||
useRedux: true,
|
||||
store: storeWithState({
|
||||
...mockState,
|
||||
dashboardLayout: undoableDashboardLayoutWithTabs,
|
||||
}),
|
||||
useDnd: true,
|
||||
useTheme: true,
|
||||
});
|
||||
|
||||
const gridContainer = await findByTestId('grid-container');
|
||||
|
||||
fireEvent.click(screen.getByText('tab1'));
|
||||
fireEvent.click(screen.getByText('tab2'));
|
||||
|
||||
const tabPanels = within(gridContainer).getAllByRole('tabpanel', {
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
expect(gridContainer).toBeInTheDocument();
|
||||
expect(tabPanels.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -298,7 +298,7 @@ const StyledDashboardContent = styled.div<{
|
||||
|
||||
/* this is the ParentSize wrapper */
|
||||
& > div:first-child {
|
||||
height: inherit !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -314,6 +314,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
renderTabBar={renderTabBar}
|
||||
animated={false}
|
||||
allowOverflow
|
||||
fullHeight
|
||||
onFocus={handleFocus}
|
||||
items={tabItems}
|
||||
tabBarStyle={{ paddingLeft: 0 }}
|
||||
|
||||
@@ -54,6 +54,9 @@ const DashboardEmptyStateContainer = styled.div`
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const GridContent = styled.div`
|
||||
|
||||
@@ -24,22 +24,22 @@ import newComponentFactory from 'src/dashboard/util/newComponentFactory';
|
||||
import { DASHBOARD_GRID_TYPE } from 'src/dashboard/util/componentTypes';
|
||||
import { GRID_COLUMN_COUNT } from 'src/dashboard/util/constants';
|
||||
|
||||
jest.mock(
|
||||
'src/dashboard/containers/DashboardComponent',
|
||||
() =>
|
||||
({ onResizeStart, onResizeStop }) => (
|
||||
<button
|
||||
type="button"
|
||||
data-test="mock-dashboard-component"
|
||||
onClick={() => onResizeStart()}
|
||||
onBlur={() =>
|
||||
onResizeStop(null, null, null, { width: 1, height: 3 }, 'id')
|
||||
}
|
||||
>
|
||||
Mock
|
||||
</button>
|
||||
),
|
||||
);
|
||||
jest.mock('src/dashboard/containers/DashboardComponent', () => {
|
||||
const MockDashboardComponent = ({ onResizeStart, onResizeStop }) => (
|
||||
<button
|
||||
type="button"
|
||||
data-test="mock-dashboard-component"
|
||||
onClick={() => onResizeStart()}
|
||||
onBlur={() =>
|
||||
onResizeStop(null, null, null, { width: 1, height: 3 }, 'id')
|
||||
}
|
||||
>
|
||||
Mock
|
||||
</button>
|
||||
);
|
||||
MockDashboardComponent.displayName = 'MockDashboardComponent';
|
||||
return MockDashboardComponent;
|
||||
});
|
||||
|
||||
const props = {
|
||||
depth: 1,
|
||||
@@ -106,3 +106,51 @@ test('should call resizeComponent when a child DashboardComponent calls resizeSt
|
||||
height: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('should apply flexbox centering and absolute positioning to empty state', () => {
|
||||
const { container } = setup({
|
||||
gridComponent: { ...props.gridComponent, children: [] },
|
||||
editMode: true,
|
||||
canEdit: true,
|
||||
setEditMode: jest.fn(),
|
||||
dashboardId: 1,
|
||||
});
|
||||
|
||||
const dashboardGrid = container.querySelector('.dashboard-grid');
|
||||
const emptyState = dashboardGrid?.previousElementSibling;
|
||||
|
||||
expect(emptyState).toBeInTheDocument();
|
||||
|
||||
const styles = window.getComputedStyle(emptyState);
|
||||
expect(styles.display).toBe('flex');
|
||||
expect(styles.alignItems).toBe('center');
|
||||
expect(styles.justifyContent).toBe('center');
|
||||
expect(styles.position).toBe('absolute');
|
||||
});
|
||||
|
||||
test('should render empty state in both edit and view modes', () => {
|
||||
const { container: editContainer } = setup({
|
||||
gridComponent: { ...props.gridComponent, children: [] },
|
||||
editMode: true,
|
||||
canEdit: true,
|
||||
setEditMode: jest.fn(),
|
||||
dashboardId: 1,
|
||||
});
|
||||
|
||||
const { container: viewContainer } = setup({
|
||||
gridComponent: { ...props.gridComponent, children: [] },
|
||||
editMode: false,
|
||||
canEdit: true,
|
||||
setEditMode: jest.fn(),
|
||||
dashboardId: 1,
|
||||
});
|
||||
|
||||
const editDashboardGrid = editContainer.querySelector('.dashboard-grid');
|
||||
const editEmptyState = editDashboardGrid?.previousElementSibling;
|
||||
|
||||
const viewDashboardGrid = viewContainer.querySelector('.dashboard-grid');
|
||||
const viewEmptyState = viewDashboardGrid?.previousElementSibling;
|
||||
|
||||
expect(editEmptyState).toBeInTheDocument();
|
||||
expect(viewEmptyState).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -111,6 +111,7 @@ const TabsRenderer = memo<TabsRendererProps>(
|
||||
type={editMode ? 'editable-card' : 'card'}
|
||||
items={tabItems}
|
||||
tabBarStyle={{ paddingLeft: tabBarPaddingLeft }}
|
||||
fullHeight
|
||||
/>
|
||||
</StyledTabsContainer>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user