diff --git a/src/components/BalanceList.tsx b/src/components/BalanceList.tsx index 5f9f419..9885082 100644 --- a/src/components/BalanceList.tsx +++ b/src/components/BalanceList.tsx @@ -1,7 +1,7 @@ import { useSorokit } from "@/context/useSorokit"; import { Badge } from "@/components/ui/Badge"; import { AssetBadge } from "@/components/AssetBadge"; -import { SkeletonRow } from "@/components/ui/Skeleton"; +import { AssetRowSkeleton } from "@/components/ui/Skeleton"; import type { Balance } from "@/lib/client"; function AssetRow({ b }: { b: Balance }) { @@ -38,9 +38,9 @@ export function BalanceList() { Connect your wallet to view assets

) : isLoadingAccount ? ( -
+
{[1, 2, 3].map((i) => ( - + ))}
) : balances.length === 0 ? ( diff --git a/src/components/index.ts b/src/components/index.ts index e175083..2899fac 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,6 +10,7 @@ export { } from "./ui/Card"; export { Badge } from "./ui/Badge"; export { Input } from "./ui/Input"; +export { Skeleton, SkeletonRow, SkeletonCard, AssetRowSkeleton } from "./ui/Skeleton"; export { Skeleton, SkeletonRow, diff --git a/src/components/ui/Skeleton.test.tsx b/src/components/ui/Skeleton.test.tsx index df4460d..e25f99f 100644 --- a/src/components/ui/Skeleton.test.tsx +++ b/src/components/ui/Skeleton.test.tsx @@ -1,5 +1,50 @@ import { render, screen } from "@testing-library/react"; import { describe, it, expect } from "vitest"; +import { Skeleton, SkeletonRow, SkeletonCard, AssetRowSkeleton } from "./Skeleton"; + +describe("Skeleton components accessibility and structure", () => { + it("Skeleton has role='presentation'", () => { + const { container } = render(); + const el = container.firstChild as HTMLElement; + expect(el).toHaveAttribute("role", "presentation"); + }); + + it("SkeletonRow has role='presentation'", () => { + const { container } = render(); + const el = container.firstChild as HTMLElement; + expect(el).toHaveAttribute("role", "presentation"); + }); + + it("AssetRowSkeleton has role='presentation'", () => { + const { container } = render(); + const el = container.firstChild as HTMLElement; + expect(el).toHaveAttribute("role", "presentation"); + }); + + it("SkeletonCard has role='status', aria-busy='true', and aria-label='Loading content'", () => { + render(); + const el = screen.getByRole("status"); + expect(el).toHaveAttribute("aria-busy", "true"); + expect(el).toHaveAttribute("aria-label", "Loading content"); + }); + + it("SkeletonCard accepts custom children override", () => { + render( + +
Custom Content
+
+ ); + expect(screen.getByTestId("custom-child")).toBeInTheDocument(); + expect(screen.getByRole("status")).toHaveAttribute("aria-label", "Loading content"); + // Default header should not render + expect(screen.queryByText("Stellar account details")).not.toBeInTheDocument(); + }); + + it("SkeletonCard accepts custom structure override", () => { + render( + Custom Structure
} /> + ); + expect(screen.getByTestId("custom-structure")).toBeInTheDocument(); import { Skeleton, SkeletonRow, diff --git a/src/components/ui/Skeleton.tsx b/src/components/ui/Skeleton.tsx index fef49e0..63e89eb 100644 --- a/src/components/ui/Skeleton.tsx +++ b/src/components/ui/Skeleton.tsx @@ -20,8 +20,13 @@ export function Skeleton({ circle, className, ...props }: SkeletonProps) { } /** Pre-composed row skeleton: icon + two lines of text */ -export function SkeletonRow({ className }: { className?: string }) { +export function SkeletonRow({ className, ...props }: { className?: string; [key: string]: any }) { return ( +
@@ -32,6 +37,10 @@ export function SkeletonRow({ className }: { className?: string }) { ); } +interface SkeletonCardProps extends React.HTMLAttributes { + rows?: number; + structure?: React.ReactNode; + children?: React.ReactNode; /** * Pre-composed asset-row skeleton mirroring AssetRow's layout: an icon + two * text lines on the left, and a right-aligned balance/amount placeholder. @@ -58,9 +67,57 @@ export function AssetRowSkeleton({ className }: { className?: string }) { } /** Pre-composed card skeleton: header + body lines */ -export function SkeletonCard({ rows = 3 }: { rows?: number }) { +export function SkeletonCard({ + rows = 3, + structure, + children, + className, + ...props +}: SkeletonCardProps) { + return ( +
+ {structure || children ? ( + structure || children + ) : ( + <> +
+ + +
+
+ {Array.from({ length: rows }).map((_, i) => ( + + ))} +
+ + )} +
+ ); +} + +/** Pre-composed row skeleton matching AssetRow layout exactly */ +export function AssetRowSkeleton({ className, ...props }: { className?: string; [key: string]: any }) { return (
+
+ +
+ + +
aria-busy="true" className="rounded-xl border border-line bg-surface overflow-hidden" > @@ -73,6 +130,7 @@ export function SkeletonCard({ rows = 3 }: { rows?: number }) { ))}
+
); }