feat: implement BackButton component for consistent navigation
- Added BackButton component to standardize back navigation across pages. - Integrated BackButton into various pages, replacing custom back buttons for consistency. - Updated PageHeader to accept backTo prop for displaying BackButton when navigating from subpages. - Created useGoBack hook to handle navigation logic, determining whether to go back in history or redirect to a fallback page. - Added tests for BackButton and its integration with PageHeader to ensure expected behavior.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { screen, fireEvent, render } from '@testing-library/react';
|
||||
import { MemoryRouter, Route, Routes, Link } from 'react-router-dom';
|
||||
import BackButton from './BackButton';
|
||||
import PageHeader from './PageHeader';
|
||||
|
||||
/**
|
||||
* رفتار «بازگشت» باید در همهٔ صفحات یکی باشد: یک قدم عقب در تاریخچهٔ پنل، و وقتی
|
||||
* صفحه مستقیم باز شده (تاریخچهای نیست) رفتن به صفحهٔ والد.
|
||||
*/
|
||||
function Detail({ fallback = '/admin/clinic-services' }: { fallback?: string }) {
|
||||
return (
|
||||
<>
|
||||
<span>صفحهٔ جزئیات</span>
|
||||
<BackButton fallback={fallback} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function List() {
|
||||
return (
|
||||
<>
|
||||
<span>صفحهٔ لیست</span>
|
||||
<Link to="/admin/clinic-services/x1">باز کردن جزئیات</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAt(initialEntries: string[]) {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<Routes>
|
||||
<Route path="/admin/clinic-services" element={<List />} />
|
||||
<Route path="/admin/clinic-services/:uuid" element={<Detail />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('BackButton', () => {
|
||||
it('ظاهر یکسان دارد: همان دکمهٔ بازگشتِ صفحهٔ سرویسها', () => {
|
||||
renderAt(['/admin/clinic-services/x1']);
|
||||
|
||||
const btn = screen.getByRole('button', { name: /بازگشت/ });
|
||||
expect(btn).toHaveClass('cp-btn-secondary');
|
||||
expect(btn).toHaveAttribute('type', 'button');
|
||||
});
|
||||
|
||||
it('وقتی از صفحهٔ دیگری آمدهایم، یک قدم به همان صفحه برمیگردد', () => {
|
||||
renderAt(['/admin/clinic-services']);
|
||||
|
||||
fireEvent.click(screen.getByText('باز کردن جزئیات'));
|
||||
expect(screen.getByText('صفحهٔ جزئیات')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /بازگشت/ }));
|
||||
expect(screen.getByText('صفحهٔ لیست')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('در ورود مستقیم (بدون تاریخچه) به صفحهٔ والد میرود', () => {
|
||||
renderAt(['/admin/clinic-services/x1']);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /بازگشت/ }));
|
||||
|
||||
expect(screen.getByText('صفحهٔ لیست')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PageHeader — backTo', () => {
|
||||
it('با backTo دکمهٔ بازگشت را نشان میدهد', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/admin/claims/p1']}>
|
||||
<PageHeader title="پروندهٔ بیمه" backTo="/admin/claims" />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: /بازگشت/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('بدون backTo دکمهای رندر نمیشود (صفحات سطحاول)', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<PageHeader title="مطالبات بیمه" />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: /بازگشت/ })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
import { useGoBack } from '../../hooks/useGoBack';
|
||||
|
||||
interface Props {
|
||||
/** مقصد وقتی تاریخچهای برای برگشتن نیست (ورود مستقیم/رفرش) — معمولاً صفحهٔ لیستِ همان بخش. */
|
||||
fallback: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* دکمهٔ «بازگشت» — یک ظاهر و یک رفتار در همهٔ صفحاتی که از دل صفحهٔ دیگری باز میشوند.
|
||||
* ظاهر مرجع، همان دکمهٔ بازگشتِ صفحهٔ سرویسهاست (`cp-btn-secondary` با ارتفاع ۳۶).
|
||||
* در RTL، فلشِ «قبلی» راستسو است.
|
||||
*/
|
||||
export default function BackButton({ fallback, label = 'بازگشت' }: Props) {
|
||||
const goBack = useGoBack(fallback);
|
||||
|
||||
return (
|
||||
<button type="button" className="cp-btn-secondary" style={{ height: 36 }} onClick={goBack}>
|
||||
<ChevronRightIcon style={{ width: 16 }} /> {label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ChevronLeftIcon } from '@heroicons/react/24/outline';
|
||||
import BackButton from './BackButton';
|
||||
|
||||
interface Crumb {
|
||||
label: string;
|
||||
@@ -12,12 +13,22 @@ interface Props {
|
||||
breadcrumbs?: Crumb[];
|
||||
action?: React.ReactNode;
|
||||
description?: string;
|
||||
/**
|
||||
* صفحه از دل صفحهٔ دیگری باز میشود → دکمهٔ «بازگشت» بالای عنوان.
|
||||
* مقدار، مقصدِ fallback است وقتی تاریخچهای برای برگشتن نیست.
|
||||
*/
|
||||
backTo?: string;
|
||||
}
|
||||
|
||||
export default function PageHeader({ title, breadcrumbs, action, description }: Props) {
|
||||
export default function PageHeader({ title, breadcrumbs, action, description, backTo }: Props) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 24 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
{backTo && (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<BackButton fallback={backTo} />
|
||||
</div>
|
||||
)}
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
<nav style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 6, fontSize: 12, color: 'var(--text-3)' }}>
|
||||
{breadcrumbs.map((crumb, i) => (
|
||||
|
||||
Reference in New Issue
Block a user