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:
hamed
2026-07-29 20:26:51 +03:30
parent e6267080b2
commit e0e8fbd1e4
31 changed files with 224 additions and 82 deletions
@@ -1,7 +1,7 @@
import { Link } from 'react-router-dom';
import { formatDate } from '../lib/utils';
import BackButton from './ui/BackButton';
import {
ArrowLeftPH, ArrowLeftD, FilesServicePhone, FilesServiceCalendar,
ArrowLeftD, FilesServicePhone, FilesServiceCalendar,
FilesServiceNotification, FilesServiceMessage,
} from './icons/FilesServiceIcons';
@@ -11,10 +11,7 @@ interface Tag { uuid: string; name: string; color: string }
export function Breadcrumb({ name, backTo }: { name: string; backTo: string }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, marginBottom: 14 }}>
<Link to={backTo} className="bg-[var(--surface)]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer', color: 'var(--text-2)', textDecoration: 'none' }}>
<ArrowLeftPH />
<span>بازگشت</span>
</Link>
<BackButton fallback={backTo} />
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
@@ -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();
});
});
+23
View File
@@ -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>
);
}
+12 -1
View File
@@ -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) => (