unify Semi UI filter workspaces and pagination
This commit is contained in:
@@ -7,14 +7,13 @@ describe('TablePagination', () => {
|
||||
|
||||
test('renders one shared Semi pagination pattern and changes pages', () => {
|
||||
const onPageChange = vi.fn();
|
||||
render(<footer><TablePagination page={2} totalPages={4} info="共 80 条" onPageChange={onPageChange} /></footer>);
|
||||
const { container } = render(<footer><TablePagination page={2} totalPages={4} info="共 80 条" onPageChange={onPageChange} /></footer>);
|
||||
|
||||
expect(screen.getByText('共 80 条')).toHaveClass('v2-table-pagination-info');
|
||||
expect(screen.getByText('2').closest('.v2-table-pagination-current')).toHaveTextContent('2/4');
|
||||
const previous = screen.getByRole('button', { name: '上一页' });
|
||||
const next = screen.getByRole('button', { name: '下一页' });
|
||||
expect(previous).toHaveClass('semi-button');
|
||||
expect(next).toHaveClass('semi-button');
|
||||
expect(container.querySelector('.v2-table-pagination')).toHaveClass('semi-page');
|
||||
expect(container.querySelector('.semi-page-item-small')).toHaveTextContent('2/4');
|
||||
const previous = screen.getByRole('button', { name: 'Previous' });
|
||||
const next = screen.getByRole('button', { name: 'Next' });
|
||||
fireEvent.click(previous);
|
||||
fireEvent.click(next);
|
||||
expect(onPageChange).toHaveBeenNthCalledWith(1, 1);
|
||||
@@ -23,8 +22,8 @@ describe('TablePagination', () => {
|
||||
|
||||
test('clamps invalid pages and disables both boundaries for a single page', () => {
|
||||
render(<footer><TablePagination page={0} totalPages={0} info="暂无数据" onPageChange={vi.fn()} /></footer>);
|
||||
expect(screen.getByRole('button', { name: '上一页' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: '下一页' })).toBeDisabled();
|
||||
expect(document.querySelector('.v2-table-pagination-current')).toHaveTextContent('1/1');
|
||||
expect(screen.getByRole('button', { name: 'Previous' })).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(screen.getByRole('button', { name: 'Next' })).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(document.querySelector('.semi-page-item-small')).toHaveTextContent('1/1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
|
||||
import { Button, Select } from '@douyinfe/semi-ui';
|
||||
import { Pagination, Select } from '@douyinfe/semi-ui';
|
||||
import { useId, type ReactNode } from 'react';
|
||||
|
||||
type PageSizeOption = {
|
||||
@@ -37,30 +36,17 @@ export function TablePagination({
|
||||
return <>
|
||||
<span className="v2-table-pagination-info">{info}</span>
|
||||
<div className="v2-table-pagination-controls">
|
||||
<Button
|
||||
className="v2-table-pagination-button"
|
||||
theme="light"
|
||||
icon={<IconChevronLeft />}
|
||||
disabled={disabled || safePage <= 1}
|
||||
aria-label="上一页"
|
||||
onClick={() => onPageChange(safePage - 1)}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<span className="v2-table-pagination-current" aria-live="polite" aria-atomic="true">
|
||||
<b>{safePage}</b><i>/</i><span>{safeTotalPages}</span>
|
||||
</span>
|
||||
<Button
|
||||
className="v2-table-pagination-button"
|
||||
theme="light"
|
||||
icon={<IconChevronRight />}
|
||||
iconPosition="right"
|
||||
disabled={disabled || safePage >= safeTotalPages}
|
||||
aria-label="下一页"
|
||||
onClick={() => onPageChange(safePage + 1)}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
<Pagination
|
||||
className="v2-table-pagination"
|
||||
size="small"
|
||||
currentPage={safePage}
|
||||
total={safeTotalPages}
|
||||
pageSize={1}
|
||||
disabled={disabled}
|
||||
hideOnSinglePage={false}
|
||||
hoverShowPageSelect={!disabled}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
{pageSize != null && pageSizeOptions?.length && onPageSizeChange ? <>
|
||||
<span className="v2-sr-only" id={pageSizeID}>{pageSizeLabel}</span>
|
||||
<Select
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
import { WorkspaceFilterPanel } from './WorkspaceFilterPanel';
|
||||
|
||||
describe('WorkspaceFilterPanel', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
test('keeps desktop context and mobile disclosure in one shared component', () => {
|
||||
const onToggle = vi.fn();
|
||||
const { container } = render(
|
||||
<WorkspaceFilterPanel
|
||||
title="事件筛选"
|
||||
description="按关键词与状态收窄范围"
|
||||
mobileSummary="已启用 2 项"
|
||||
expanded={false}
|
||||
status="2 项已启用"
|
||||
statusColor="blue"
|
||||
onToggle={onToggle}
|
||||
>
|
||||
<form aria-label="事件筛选表单"><input aria-label="关键词" /></form>
|
||||
</WorkspaceFilterPanel>
|
||||
);
|
||||
|
||||
expect(screen.getByText('按关键词与状态收窄范围')).toBeInTheDocument();
|
||||
expect(screen.getByText('2 项已启用')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '修改事件筛选:已启用 2 项' })).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(container.querySelector('.v2-workspace-filter-panel')).toHaveClass('semi-card', 'is-mobile-collapsed');
|
||||
expect(screen.getByRole('form', { name: '事件筛选表单' })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '修改事件筛选:已启用 2 项' }));
|
||||
expect(onToggle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { IconFilter } from '@douyinfe/semi-icons';
|
||||
import { Card, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { MobileFilterToggle } from './MobileFilterToggle';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
type WorkspaceFilterPanelProps = {
|
||||
title?: string;
|
||||
description: string;
|
||||
mobileSummary: string;
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
status?: ReactNode;
|
||||
statusColor?: ComponentProps<typeof Tag>['color'];
|
||||
collapsedLabel?: string;
|
||||
};
|
||||
|
||||
export function WorkspaceFilterPanel({
|
||||
title = '查询条件',
|
||||
description,
|
||||
mobileSummary,
|
||||
expanded,
|
||||
onToggle,
|
||||
children,
|
||||
className = '',
|
||||
status = '默认范围',
|
||||
statusColor = 'grey',
|
||||
collapsedLabel = '修改'
|
||||
}: WorkspaceFilterPanelProps) {
|
||||
const panelClassName = [
|
||||
'v2-workspace-filter-panel',
|
||||
className,
|
||||
expanded ? '' : 'is-mobile-collapsed'
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return <>
|
||||
<MobileFilterToggle
|
||||
title={title}
|
||||
summary={mobileSummary}
|
||||
expanded={expanded}
|
||||
collapsedLabel={collapsedLabel}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
<Card className={panelClassName} bodyStyle={{ padding: 0 }}>
|
||||
<header className="v2-workspace-filter-heading">
|
||||
<span className="v2-workspace-filter-icon" aria-hidden="true"><IconFilter /></span>
|
||||
<span className="v2-workspace-filter-copy">
|
||||
<Text strong>{title}</Text>
|
||||
<Text type="tertiary" size="small">{description}</Text>
|
||||
</span>
|
||||
<Tag color={statusColor} type="light" size="small">{status}</Tag>
|
||||
</header>
|
||||
{children}
|
||||
</Card>
|
||||
</>;
|
||||
}
|
||||
Reference in New Issue
Block a user