From 23ac67761315aae89e98334b06d9778ac4dd4574 Mon Sep 17 00:00:00 2001 From: otsimaofficial Date: Sat, 29 Aug 2026 00:47:04 +0100 Subject: [PATCH] feat(loading-skeleton): constrain height and preserve clickability on mobile Cap the skeleton's height on small viewports and let it scroll internally (overscroll-contain) instead of pushing surrounding controls off-screen, while keeping the wrapper free of fixed/absolute positioning so it never traps taps on other elements. Closes #279 --- __tests__/loading-skeleton-mobile.test.tsx | 71 ++++++++++++++++++++++ app/components/LoadingSkeleton.tsx | 61 ++++++++++--------- 2 files changed, 104 insertions(+), 28 deletions(-) create mode 100644 __tests__/loading-skeleton-mobile.test.tsx diff --git a/__tests__/loading-skeleton-mobile.test.tsx b/__tests__/loading-skeleton-mobile.test.tsx new file mode 100644 index 0000000..a4120d2 --- /dev/null +++ b/__tests__/loading-skeleton-mobile.test.tsx @@ -0,0 +1,71 @@ +/** + * Issue #279 – Handle mobile viewports navigation styling in loading_spinner_skeleton + * + * Verifies the skeleton is height-constrained and internally scrollable on + * small screens (rather than pushing surrounding controls off-screen), and + * that it never traps pointer events so other elements stay clickable on + * mobile viewports. + */ +import { render, screen, fireEvent } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import LoadingSkeleton from "@/app/components/LoadingSkeleton"; +import Dashboard from "@/app/dashboard/page"; + +const mockUseWallet = vi.fn(); + +vi.mock("@/app/context/WalletContext", () => ({ + useWallet: () => mockUseWallet(), +})); + +vi.mock("@/app/components/Navbar", () => ({ + default: () =>
, +})); + +describe("LoadingSkeleton – mobile viewport constraints (issue #279)", () => { + it("caps height on mobile with max-h-[70vh] and scrolls internally", () => { + render(); + const wrapper = screen.getByTestId("loading-skeleton-mobile-wrapper"); + expect(wrapper).toHaveClass("max-h-[70vh]"); + expect(wrapper).toHaveClass("overflow-y-auto"); + }); + + it("removes the height cap on larger viewports (sm:max-h-none)", () => { + render(); + expect(screen.getByTestId("loading-skeleton-mobile-wrapper")).toHaveClass( + "sm:max-h-none" + ); + }); + + it("contains overscroll within the wrapper instead of the page (overscroll-contain)", () => { + render(); + expect(screen.getByTestId("loading-skeleton-mobile-wrapper")).toHaveClass( + "overscroll-contain" + ); + }); + + it("does not use fixed/absolute positioning that would trap pointer events", () => { + render(); + const root = screen.getByTestId("loading-skeleton"); + expect(root).not.toHaveClass("fixed"); + expect(root).not.toHaveClass("absolute"); + }); +}); + +describe("Dashboard – surrounding controls stay clickable while the skeleton is visible (issue #279)", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockUseWallet.mockReturnValue({ address: "GCLIENT", signTransaction: vi.fn() }); + vi.stubGlobal("fetch", vi.fn(() => new Promise(() => {}))); + }); + + it("keeps role-filter buttons enabled and clickable while the loading skeleton is displayed", () => { + render(); + expect(screen.getByTestId("loading-skeleton")).toBeInTheDocument(); + + const clientFilter = screen.getByRole("button", { name: "As Client" }); + expect(clientFilter).not.toBeDisabled(); + + fireEvent.click(clientFilter); + expect(clientFilter).toHaveAttribute("aria-pressed", "true"); + }); +}); diff --git a/app/components/LoadingSkeleton.tsx b/app/components/LoadingSkeleton.tsx index 4b42b2c..d6e3e21 100644 --- a/app/components/LoadingSkeleton.tsx +++ b/app/components/LoadingSkeleton.tsx @@ -1,36 +1,41 @@ export default function LoadingSkeleton() { return ( -
+
Loading job data… -