diff --git a/resources/js/components/transactions/transaction-list.test.tsx b/resources/js/components/transactions/transaction-list.test.tsx new file mode 100644 index 00000000..2bde144b --- /dev/null +++ b/resources/js/components/transactions/transaction-list.test.tsx @@ -0,0 +1,23 @@ +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { TransactionListSkeleton } from './transaction-list'; + +describe('TransactionListSkeleton', () => { + it('renders the table-shaped skeleton shared by the deferred fallback and the internal loading state', () => { + const { container } = render(); + + // The table-shaped skeleton is wrapped in a bordered container, which is + // what distinguishes it from the old plain-bars fallback. Using the same + // component in both loading phases is what removes the double-skeleton jump. + expect( + container.querySelector('.overflow-hidden.rounded-md.border'), + ).not.toBeNull(); + + // Far more placeholders than the old 8-bar fallback: header row, section + // header, and six body rows, each mimicking the real table layout. + expect( + container.querySelectorAll('[data-slot="skeleton"]').length, + ).toBeGreaterThan(8); + }); +}); diff --git a/resources/js/components/transactions/transaction-list.tsx b/resources/js/components/transactions/transaction-list.tsx index a1a5d088..d8685fcb 100644 --- a/resources/js/components/transactions/transaction-list.tsx +++ b/resources/js/components/transactions/transaction-list.tsx @@ -81,6 +81,41 @@ import { UUID } from '@/types/uuid'; const COLUMN_VISIBILITY_KEY = 'transactions-column-visibility'; +export function TransactionListSkeleton() { + return ( +
+
+
+ + + + +
+
+
+ +
+ {Array.from({ length: 6 }).map((_, index) => ( +
+ + +
+ + +
+ +
+ ))} +
+
+
+ + +
+
+ ); +} + interface TransactionRowProps { row: Row; virtualRow: VirtualItem; @@ -278,7 +313,9 @@ export function TransactionList({ const [transactions, setTransactions] = useState( [], ); - const [isLoading, setIsLoading] = useState(true); + const [isLoading, setIsLoading] = useState( + providedTransactions === undefined, + ); const [sorting, setSorting] = useState([ { id: 'transaction_date', desc: true }, ]); @@ -1005,39 +1042,7 @@ export function TransactionList({ /> {isLoading || isSearching ? ( -
-
-
- - - - -
-
-
- -
- {Array.from({ length: 6 }).map((_, index) => ( -
- - -
- - -
- -
- ))} -
-
-
- - -
-
+ ) : ( <> ({ vi.mock('@/components/transactions/transaction-list', () => ({ TransactionList: () => null, + TransactionListSkeleton: () => null, })); vi.mock('@/components/bank-logo', () => ({ diff --git a/resources/js/pages/Accounts/Show.tsx b/resources/js/pages/Accounts/Show.tsx index c8ded843..d534ce39 100644 --- a/resources/js/pages/Accounts/Show.tsx +++ b/resources/js/pages/Accounts/Show.tsx @@ -17,7 +17,10 @@ import HeadingSmall from '@/components/heading-small'; import InputError from '@/components/input-error'; import { MobileBackButton } from '@/components/mobile-back-button'; import { EditTransactionDialog } from '@/components/transactions/edit-transaction-dialog'; -import { TransactionList } from '@/components/transactions/transaction-list'; +import { + TransactionList, + TransactionListSkeleton, +} from '@/components/transactions/transaction-list'; import { AmountDisplay } from '@/components/ui/amount-display'; import { AmountInput } from '@/components/ui/amount-input'; import { Button } from '@/components/ui/button'; @@ -39,7 +42,6 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Skeleton } from '@/components/ui/skeleton'; import { Textarea } from '@/components/ui/textarea'; import { useChartColors } from '@/hooks/use-chart-color-scheme'; import AppSidebarLayout from '@/layouts/app/app-sidebar-layout'; @@ -385,13 +387,7 @@ export default function AccountShow({ {isTransactionalAccount(account) && ( - {Array.from({ length: 8 }).map((_, i) => ( - - ))} - - } + fallback={} >