From e0a06997029e4950d427d7d3d3c75a9d39c3d5df Mon Sep 17 00:00:00 2001 From: Olamidepy Date: Thu, 25 Jun 2026 15:24:03 +0100 Subject: [PATCH 1/7] fix(responsive): resolve content area collapse, topbar overflow, and pagination touch targets --- src/components/NetworkSwitcher.tsx | 4 ++-- src/components/TopBar.tsx | 2 +- src/components/TransactionHistory.tsx | 2 ++ src/components/WalletConnectButton.tsx | 9 +++++---- src/screens/Dashboard.tsx | 4 ++-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index 2baa071..0afbc55 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -19,11 +19,11 @@ export function NetworkSwitcher() { return ( - ); } return ( - ); } diff --git a/src/screens/Dashboard.tsx b/src/screens/Dashboard.tsx index 1dbde20..40ac64b 100644 --- a/src/screens/Dashboard.tsx +++ b/src/screens/Dashboard.tsx @@ -34,8 +34,8 @@ export function Dashboard() { onMenuToggle={() => setSidebarOpen((o) => !o)} /> -
-
+
+
{SCREENS[active]}
From 37edb58838acbd1a9be02fafd7574c7eb32984dc Mon Sep 17 00:00:00 2001 From: Olamidepy Date: Thu, 25 Jun 2026 16:17:48 +0100 Subject: [PATCH 2/7] fix(responsive): resolve duplicate tags from merge and correct eslint configs --- eslint.config.js | 30 +++++--- package-lock.json | 102 ------------------------- src/components/NetworkSwitcher.tsx | 16 +--- src/components/WalletConnectButton.tsx | 17 +++-- src/screens/Dashboard.tsx | 3 - 5 files changed, 30 insertions(+), 138 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 18cb9d2..fa4b5f0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,23 +3,31 @@ import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' -export default defineConfig([ - globalIgnores(['dist']), +export default tseslint.config( + { + ignores: ['dist'], + }, + js.configs.recommended, + ...tseslint.configs.recommended, { files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], languageOptions: { + parser: tseslint.parser, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, globals: globals.browser, }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, rules: { '@typescript-eslint/no-explicit-any': 'off', + ...reactHooks.configs.recommended.rules, }, - }, -]) + } +) diff --git a/package-lock.json b/package-lock.json index 9289a62..7622346 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1519,21 +1519,6 @@ "node": ">=20" } }, - "node_modules/@coinbase/cdp-sdk/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/@creit.tech/stellar-wallets-kit": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@creit.tech/stellar-wallets-kit/-/stellar-wallets-kit-2.4.0.tgz", @@ -1751,21 +1736,6 @@ "node": ">=20.0.0" } }, - "node_modules/@creit.tech/stellar-wallets-kit/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/@creit.tech/xbull-wallet-connect": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@creit.tech/xbull-wallet-connect/-/xbull-wallet-connect-0.4.0.tgz", @@ -7759,21 +7729,6 @@ "ws": "^7.5.1" } }, - "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { "version": "7.5.11", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", @@ -10852,20 +10807,6 @@ } } }, - "node_modules/html-encoding-sniffer/node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -11243,21 +11184,6 @@ "license": "MIT", "peer": true }, - "node_modules/jayson/node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/jayson/node_modules/ws": { "version": "7.5.11", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", @@ -11373,20 +11299,6 @@ } } }, - "node_modules/jsdom/node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/jsdom/node_modules/lru-cache": { "version": "11.5.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", @@ -14856,20 +14768,6 @@ } } }, - "node_modules/whatwg-url/node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index 658dae6..3de59cc 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -31,26 +31,14 @@ export function NetworkSwitcher() { return ( -
- {!isConnected && error && (
@@ -54,4 +56,3 @@ export function WalletConnectButton({
); } - diff --git a/src/screens/Dashboard.tsx b/src/screens/Dashboard.tsx index de18b88..401c564 100644 --- a/src/screens/Dashboard.tsx +++ b/src/screens/Dashboard.tsx @@ -38,9 +38,6 @@ export function Dashboard() {
- {SCREENS[active]} -
-
From 437852c9d1376d5e872c5c80d01e9e50ed49cf4b Mon Sep 17 00:00:00 2001 From: Olamidepy Date: Thu, 25 Jun 2026 16:32:40 +0100 Subject: [PATCH 3/7] fix: use type-only imports in verify-exports.ts to resolve TS1484 verbatimModuleSyntax error --- src/verify-exports.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/verify-exports.ts b/src/verify-exports.ts index 90f2604..0440abd 100644 --- a/src/verify-exports.ts +++ b/src/verify-exports.ts @@ -7,13 +7,13 @@ import { AssetPill, ContractEventFeed, // Types - AccountData, - Balance, - Transaction, - ClaimableBalance, - ContractEvent, - NetworkInfo, - InvokeParams, + type AccountData, + type Balance, + type Transaction, + type ClaimableBalance, + type ContractEvent, + type NetworkInfo, + type InvokeParams, } from "./components/index"; // Dummy usage to prevent unused warnings if strictly checked From 285c54b272c322a9187543e26f22f24d4184dd87 Mon Sep 17 00:00:00 2001 From: Olamidepy Date: Thu, 25 Jun 2026 16:43:59 +0100 Subject: [PATCH 4/7] fix: use custom tsconfig for test:exports to avoid TS5112 warning/error --- package.json | 2 +- tsconfig.test-exports.json | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 tsconfig.test-exports.json diff --git a/package.json b/package.json index 2ab6ae7..082f3e8 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "lint": "eslint .", "preview": "vite preview", "test": "vitest run", - "test:exports": "tsc --noEmit src/verify-exports.ts" + "test:exports": "tsc -p tsconfig.test-exports.json" }, "dependencies": { "@hugeicons/core-free-icons": "^4.1.2", diff --git a/tsconfig.test-exports.json b/tsconfig.test-exports.json new file mode 100644 index 0000000..93f9303 --- /dev/null +++ b/tsconfig.test-exports.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "noEmit": true + }, + "files": ["src/verify-exports.ts"], + "include": [] +} From 9fef245ed60fb7677c0c36a5695f70476a415913 Mon Sep 17 00:00:00 2001 From: Olamidepy Date: Thu, 25 Jun 2026 20:32:01 +0100 Subject: [PATCH 5/7] fix(a11y,Sidebar): trap focus on mobile, close drawer on Escape, and add nav aria-label --- src/components/Sidebar.test.tsx | 54 +++++++++++++++++++++++++++++ src/components/Sidebar.tsx | 60 ++++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index e298c2e..8780933 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -93,4 +93,58 @@ describe("Sidebar", () => { ); expect(screen.queryByTestId("account-card-compact")).not.toBeInTheDocument(); }); + + it("renders nav element with correct aria-label", () => { + render( + , + ); + const navElement = screen.getByRole("navigation"); + expect(navElement).toHaveAttribute("aria-label", "Main navigation"); + }); + + it("traps focus and handles escape/restoration on mobile", () => { + vi.stubGlobal("innerWidth", 375); + + // Create a dummy trigger element and focus it + const trigger = document.createElement("button"); + trigger.setAttribute("id", "trigger-btn"); + document.body.appendChild(trigger); + trigger.focus(); + expect(document.activeElement).toBe(trigger); + + // Render Sidebar with open={true} + const { rerender } = render( + + ); + + // Verify first nav button is focused + const sidebarContainer = document.querySelector("aside")!; + const sidebarButtons = sidebarContainer.querySelectorAll("button"); + const walletButton = sidebarButtons[0]; + const lastButton = sidebarButtons[sidebarButtons.length - 1]; + expect(document.activeElement).toBe(walletButton); + + // Test Escape key closes the sidebar + fireEvent.keyDown(window, { key: "Escape" }); + expect(onClose).toHaveBeenCalled(); + + // Test Tab trap: Shift+Tab on first element wraps to last element + fireEvent.keyDown(window, { key: "Tab", shiftKey: true }); + expect(document.activeElement).toBe(lastButton); + + // Test Tab trap: Tab on last element wraps to first element + lastButton.focus(); + fireEvent.keyDown(window, { key: "Tab" }); + expect(document.activeElement).toBe(walletButton); + + // Test returning focus when closing + rerender( + + ); + expect(document.activeElement).toBe(trigger); + + // Clean up + document.body.removeChild(trigger); + vi.unstubAllGlobals(); + }); }); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d5880f2..5f8ab64 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,3 +1,4 @@ +import { useRef, useEffect } from "react"; import { cn } from "@/lib/utils"; import { useSorokit } from "@/context/useSorokit"; import { AccountCardCompact } from "@/components/AccountCard"; @@ -38,12 +39,68 @@ interface SidebarProps { export function Sidebar({ active, onNavigate, open, onClose }: SidebarProps) { const { isConnected } = useSorokit(); + const sidebarRef = useRef(null); + const triggerRef = useRef(null); function handleNav(id: NavSection) { onNavigate(id); onClose(); } + useEffect(() => { + const isMobile = window.innerWidth < 1024; + if (!isMobile || !open) return; + + // Keep track of the active element that triggered the open + triggerRef.current = document.activeElement as HTMLElement; + + // Focus first nav item when open transitions to true on mobile + const firstNavItem = sidebarRef.current?.querySelector("button"); + if (firstNavItem) { + (firstNavItem as HTMLElement).focus(); + } + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + + if (e.key === "Tab") { + if (!sidebarRef.current) return; + const focusableElements = sidebarRef.current.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + if (focusableElements.length === 0) return; + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + if (e.shiftKey) { + if (document.activeElement === firstElement) { + lastElement.focus(); + e.preventDefault(); + } + } else { + if (document.activeElement === lastElement) { + firstElement.focus(); + e.preventDefault(); + } + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + // Return focus when sidebar closes + if (triggerRef.current) { + triggerRef.current.focus(); + triggerRef.current = null; + } + }; + }, [open, onClose]); + return ( <> {/* Mobile backdrop */} @@ -56,6 +113,7 @@ export function Sidebar({ active, onNavigate, open, onClose }: SidebarProps) { {/* Sidebar panel */}