bug(frontend): Connect Wallet modal has no obvious close control and does not dismiss on outside click #672

Closed
opened 2026-08-26 04:23:23 +00:00 by PlasticDigits · 12 comments
PlasticDigits commented 2026-08-26 04:23:23 +00:00 (Migrated from gitlab.com)

Summary

The Connect Wallet dialog is hard to dismiss. Retail users report there is no close button, and the only reliable exit they find is clicking the header — not the dimmed page, Escape, or a visible Close control.

This is a dismiss / overlay bug on the shared Modal primitive (Connect Wallet, WalletConnect pairing, Expert Mode). It is not a connect, signing, or quote bug. Bundle header close visibility, backdrop / outside-click, Escape, header Connect Wallet toggle, and short-viewport clipping into one issue. Do not split “add an X” vs “click outside” vs “header toggle.”

Community report (anonymized): “There’s no close button for the wallet modal, and it can only be closed by clicking the header instead of anywhere else on the page.”

Related (do not regress): #187 connected-wallet menu app-menu-dismiss + Escape, #138 first-visit risk modal must stay non-dismissible, #554 pairing z-[10001] above Connect z-[9999] + Cancel / timeout, #160 / #490 connect-row layout, #517 clickwrap after connect, #488 tokens.


Current codebase

How the dialog opens

WalletButton.tsx lives in the sticky header (Layout.tsx). When disconnected, Connect Wallet / Connect only calls setWalletModalOpen(true) — it does not toggle closed if the dialog is already open. In-page CTAs (/portfolio, Swap / Trade / Pool / Pay “Connect Wallet”) call useWalletStore.openWalletModal().

WalletModal.tsx portals through shared Modal.tsx (title="Connect Wallet", rootTestId="wallet-connect-modal-portal"). Same primitive: WalletConnectPairingModal.tsx (z-[10001]), ExpertModeModal.tsx. RiskAcknowledgementModal.tsx uses dismissible={false} on purpose (#138).

Close controls that already exist (but fail the report)

Modal.tsx when dismissible (default true):

Path Implementation Why users still report “no close”
Header X aria-label="Close modal" + SVG, class btn-muted !min-h-0 !px-2.5 !py-2 Icon-only, muted chrome, generic label. Easy to miss next to the Connect Wallet title. QA shots (docs/qa/issue-488/wallet-modal-dark.jpg) show the X, but it reads as header chrome, not a dismiss CTA.
Backdrop Sibling .app-modal-backdrop (position: absolute; inset: 0) onClick → onClose Portal root is fixed inset-0 flex items-center justify-center p-4 and has no click handler. Clicks that hit the flex root (not the backdrop) are swallowed and do not close. No Vitest for backdrop click.
Escape document keydown Works in unit tests; not discoverable on touch.
Header trigger Disconnected Connect Wallet only sets walletModalOpen: true Does not close. Users who “click the header” are almost certainly hitting the modal title row (invisible/low-contrast X hit box), not a designed toggle.

.app-modal-panel is position: relative; z-index: 10; overflow: hidden and vertically centered. The wallet list is tall (Simulated + 5 production rows + Install). On a short phone / zoomed viewport, items-center can push the header + X above the visible area. The panel does not pin the header or scroll the body independently. Mobile .app-modal-portal-root only adds padding-bottom for the tab bar (index.css).

Connected-wallet dropdown already uses a full-viewport app-menu-dismiss button (#187). The Connect dialog does not reuse that pattern.

Close vs in-flight connect

useWallet.ts closeWalletModal and WalletModal.handleClose: if isConnecting, they call cancelConnection() (aborts WC, closes pairing, sets walletModalOpen: false) and return. cancelConnection already clears connect state. Header Cancel while connecting (#554 WC-M9) is separate and must stay.

When pairing is open, WalletModal returns null so only the pairing sheet is shown (WC-M8). Pairing has Cancel plus the same header X / backdrop.

Test gaps

Modal.test.tsx covers Escape + hiding the X when dismissible={false}. It does not assert backdrop click, visible close control, or “click panel does not close.” WalletModal.test.tsx covers badges / logos / mobile WC rows, not dismiss. useWallet.test.ts does not assert closeWalletModal / trigger toggle.


Why this is needed

Connect Wallet is the first dialog most retail users open. If they cannot find a close control and tapping the dimmed page does nothing, they assume the dApp is stuck. “Click the header” is an accidental hit on the title-row X, not a designed exit. The connected menu already dismisses on outside tap + Escape (#187); Connect should match that expectation without weakening the blocking risk ack (#138).


Constraints / guardrails

  1. Dismiss only. Do not change wallet option lists, extension detection, Install URLs, logos, hybrid quote, or execute paths (#160 / #490 / #554 / #566).
  2. Shared Modal, keep dismissible. Risk acknowledgement (#138) must remain non-dismissible: no X, no backdrop, no Escape, no header-trigger bypass. Expert Mode may stay dismissible (it already has Cancel).
  3. Pairing stack (#554). Pairing portal stays z-[10001] above Connect z-[9999]. Dismiss on pairing must still cancelConnection() (abort pending WC, ignore late session). Do not leave isConnecting stuck.
  4. Do not close on dialog chrome. Clicks on wallet rows, Install (target=_blank), pairing Open / Copy, or Expert Mode inputs must not dismiss. Use stopPropagation on the panel (or equivalent), not a document click that treats the panel as “outside.”
  5. In-flight connect. Backdrop / X / Escape / header toggle while connecting must cancel (same as WC-M9), not leave a hung spinner. Do not treat cancel as a successful connect.
  6. Header trigger. Disconnected Connect Wallet should toggle (open ↔ close / cancel). Do not add a second header close widget. Do not raise header z-index above the portal to “win” clicks (#181 / T527-8).
  7. Short viewport. Header + close control must remain in view at ~375×667 and zoomed desktop. Prefer max-height + scroll the body, pin the header; do not clip the X with overflow: hidden on the whole panel. Keep mobile tab-bar padding; do not cover #trade-pair-select / Settings by raising z-index.
  8. Tokens / a11y (#488, #144, #489). Close control uses existing theme tokens (no *-neo, no gold fill). Visible name: aria-label like Close / Close connect wallet (not only a low-contrast glyph). :focus-visible ring with --focus-ring. No new lecture copy. Icon decorative if a text label is shown.
  9. Legal / TermsGate (#517). Dismiss must not skip clickwrap after a successful connect. Closing without connecting must not look like an accepted session.
  10. No dangerouslySetInnerHTML. Do not bind wallet names into the close control as HTML.

Relevant files

File Role
frontend-dapp/src/components/ui/Modal.tsx Shared dialog: X, backdrop, Escape, dismissible
frontend-dapp/src/index.css .app-modal-backdrop, .app-modal-panel, .app-modal-header, mobile .app-modal-portal-root padding
frontend-dapp/src/components/wallet/WalletModal.tsx Connect list; handleClose vs isConnecting
frontend-dapp/src/components/wallet/WalletButton.tsx Header trigger; double createPortal; no toggle
frontend-dapp/src/hooks/useWallet.ts openWalletModal / closeWalletModal / cancelConnection
frontend-dapp/src/components/wallet/WalletConnectPairingModal.tsx Pairing dismiss + Cancel
frontend-dapp/src/components/legal/RiskAcknowledgementModal.tsx Must stay dismissible={false}
frontend-dapp/src/components/swap/ExpertModeModal.tsx Same primitive; keep Cancel
frontend-dapp/src/components/ui/__tests__/Modal.test.tsx Escape + non-dismissible X hidden; add backdrop / close / panel no-op
frontend-dapp/src/components/wallet/__tests__/WalletModal.test.tsx Connect dismiss + cancel-while-connecting
frontend-dapp/src/components/wallet/__tests__/WalletButton.test.tsx Header toggle / cancel
frontend-dapp/src/hooks/__tests__/useWallet.test.ts Store close / cancel
skills/AGENTS_FRONTEND_WALLET_CONNECT_MODAL.md Connect modal playbook
skills/AGENTS_FRONTEND_WALLETCONNECT_MOBILE.md Pairing / WC-M8–M9
skills/AGENTS_FRONTEND_RISK_DISCLAIMERS.md Blocking risk modal
docs/frontend.md Connect modal + menu dismiss + risk ack

  1. Make the close control obvious on dismissible modals: keep a header button with a visible X (and/or the word Close on sm+), aria-label="Close" or Close connect wallet, contrast that holds in dark and light. Do not rely on btn-muted alone if it blends into --panel-bg-strong.
  2. Make outside-click reliable: treat the portal root as the dismiss surface (same idea as app-menu-dismiss), and stopPropagation on the panel. Do not require users to hit only the absolutely positioned backdrop sibling. Keep role="dialog" / aria-modal / focus trap.
  3. Toggle the header trigger when walletModalOpen (close or cancelConnection if connecting). Same for accessibility: aria-expanded if you expose a disclosure relationship.
  4. Pin + scroll on short viewports: header (title + close) stays visible; wallet rows scroll inside the panel. Re-check 375px with Simulated Wallet visible (dev).
  5. Tests: Vitest for X, backdrop/root click, Escape, panel click no-op, dismissible={false} unchanged, connecting dismiss cancels, header toggle. Optional Playwright: open Connect from header on /, click dimmed page, dialog gone (wallet-connect-modal-portal detached).
  6. Document the dismiss invariants on the Connect modal section of docs/frontend.md / the wallet-connect-modal playbook.

Acceptance criteria

  • D1 Dismissible Connect Wallet shows a visible, labeled close control in the dialog header at 375px and 1280px, dark and light. It is in the viewport (not clipped above the fold).
  • D2 Click / tap on the dimmed page (any side, including below the header and beside the panel) closes the dialog when not connecting.
  • D3 Escape closes when dismissible; focus remains trapped while open.
  • D4 Click on a wallet row, Install, or pairing Open / Copy does not close except after a successful connect (existing success path).
  • D5 Header Connect Wallet while the dialog is open closes it (or cancels an in-flight connect). Second click can open again.
  • D6 While isConnecting / pairing sheet open: X, backdrop, Escape, pairing Cancel, and header Cancel all abort connect and clear isConnecting; no late WC session attaches (#554 WC-M9).
  • D7 Risk acknowledgement (#138): still no X, no backdrop, no Escape. Header Connect must not dismiss the risk gate.
  • D8 Pairing stays above Connect (z-[10001] / z-[9999]). Clickwrap after a real connect is unchanged (#517).
  • D9 make lint-frontend and make test-frontend green. New Vitest covers D1–D4 and D7.

Test plan (all paths)

Idle Connect (not connecting)

  1. / disconnected — open from header Connect Wallet. Confirm D1 (dark + light, 375 + 1280).
  2. Click dimmed area (top, sides, bottom above mobile tab bar) — dialog unmounts (wallet-connect-modal-portal gone). Repeat from /portfolio Connect CTA and Swap Connect Wallet submit.
  3. Click the close control — same.
  4. Escape — same. Tab cycle stays inside the dialog (first ↔ last).
  5. Click Keplr / Install — dialog stays (Install may open a new tab). Click a wallet row without completing connect — stays open; error banner may show.
  6. With dialog open, click header Connect Wallet — closes (D5). Click again — reopens.
  7. Simulated Wallet (dev only) — success still closes (existing path). Production: Simulated absent.

In-flight connect / pairing

  1. Start WalletConnect (mobile or desktop). While Connecting… / pairing sheet: X, backdrop, pairing Cancel, header Cancel — isConnecting false, pairing closed, no address set.
  2. Pairing Open / Copy still work; they must not dismiss unless the user hits Cancel / X / backdrop.

Blocking + other modals

  1. First-visit risk ack (clear localStorage risk key): no X; backdrop and Escape do nothing; Continue still requires checkbox (#138).
  2. Expert Mode: Cancel + X + backdrop still close; phrase field click does not close.
  3. After a successful connect, TermsGate / clickwrap still appears when required (#517). Dismissing Connect without connecting does not unlock swap.

Regression commands

make lint-frontend
make test-frontend
# targeted:
# frontend-dapp: npx vitest run src/components/ui/__tests__/Modal.test.tsx src/components/wallet/__tests__/WalletModal.test.tsx src/components/wallet/__tests__/WalletButton.test.tsx src/hooks/__tests__/useWallet.test.ts

No LocalTerra required for dismiss UX. Do not bind-mount indexer/ for cargo.


Test plan (attack, hack, and abuse)

Dismiss wiring sits on the connect funnel. A broken overlay can trap the user, steal clicks, or look like a connected session.

Vector Why it matters Test
Click-through / overlay steal Portal root pointer-events or a full-page dismiss button with a hole that is too small can steal Swap amount / token-picker / Settings clicks after “close,” or block them while open. After close, Pay amount and token triggers work. While open, page under the dialog does not receive clicks. Do not raise header/z-index above the portal.
Dismiss = connect spoof Closing the dialog must not set address or write cl8y_wallet_connection. Close via X / backdrop / Escape / header toggle; store address stays null; no localStorage wallet key.
Cancel race (WC) Backdrop close during pairing must bump connectAttemptId / abort so a late session cannot attach (#554). Cancel then complete WC in the wallet app; dApp stays disconnected.
Risk-ack bypass A global “click outside closes Modal” would skip NFA (#138). dismissible={false}: backdrop / Escape / missing X. Header Connect does not unmount the risk dialog.
Install / deep-link abuse stopPropagation on Install and pairing <a href> must remain so a parent dismiss does not fire on the same tap as navigation. Allowlist for WC links unchanged (WC-M5). Install still noopener + official store URL. Pairing href still allowlisted; blocked href does not navigate.
Focus trap / keyboard Missing trap leaves focus on Swap fields under a aria-modal dialog (spoofed “page is usable”). Escape-only without an X fails WCAG on some AT. Tab stays in dialog. Screen reader name includes Close.
z-index escalation z-[10001]+ on Connect (not pairing) can hide pairing Open / Copy or cover TermsGate. Pairing still tappable above Connect. Toasts (z-[100]) may stay below — do not lift toasts through the dialog.
HTML injection Wallet / pairing name in the title must stay text. No dangerouslySetInnerHTML on header / close.
Stuck isConnecting Close path that only set({ walletModalOpen: false }) while connecting leaves header on Cancel forever. After dismiss-during-connect, header shows Connect Wallet again.

Out of scope as attack surface: contracts, indexer, fee math.


Verification criteria

Done when a reviewer on make dev can open Connect Wallet from the header and from an in-page CTA, see an obvious close control, and dismiss by (1) that control, (2) clicking the dimmed page, (3) Escape, and (4) clicking header Connect Wallet again — at ~375px and ~1280px, dark and light — and an in-flight WalletConnect cancel still clears connecting state. Risk ack stays blocking.

Not done if the only exit is still the title-row hit box; if outside clicks hit the portal root and do nothing; if the X is off-screen on a phone; or if risk ack / pairing stack / clickwrap regress.

Evidence: before/after crops of the Connect header (dark + light, 375 + 1280) and a note that backdrop click was exercised. No user identity, addresses, or mnemonics in screenshots or issue comments.

## Summary The **Connect Wallet** dialog is hard to dismiss. Retail users report there is **no close button**, and the only reliable exit they find is **clicking the header** — not the dimmed page, Escape, or a visible Close control. This is a **dismiss / overlay** bug on the shared `Modal` primitive (Connect Wallet, WalletConnect pairing, Expert Mode). It is **not** a connect, signing, or quote bug. Bundle header close visibility, backdrop / outside-click, Escape, header **Connect Wallet** toggle, and short-viewport clipping into **one** issue. Do **not** split “add an X” vs “click outside” vs “header toggle.” Community report (anonymized): “There’s no close button for the wallet modal, and it can only be closed by clicking the header instead of anywhere else on the page.” Related (do not regress): [#187](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/187) connected-wallet menu `app-menu-dismiss` + Escape, [#138](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/138) first-visit risk modal **must stay non-dismissible**, [#554](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/554) pairing `z-[10001]` above Connect `z-[9999]` + Cancel / timeout, [#160](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/160) / [#490](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/490) connect-row layout, [#517](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/517) clickwrap after connect, [#488](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/488) tokens. --- ## Current codebase ### How the dialog opens [`WalletButton.tsx`](frontend-dapp/src/components/wallet/WalletButton.tsx) lives in the sticky header ([`Layout.tsx`](frontend-dapp/src/components/common/Layout.tsx)). When disconnected, **Connect Wallet** / **Connect** only calls `setWalletModalOpen(true)` — it does **not** toggle closed if the dialog is already open. In-page CTAs (`/portfolio`, Swap / Trade / Pool / Pay “Connect Wallet”) call `useWalletStore.openWalletModal()`. [`WalletModal.tsx`](frontend-dapp/src/components/wallet/WalletModal.tsx) portals through shared [`Modal.tsx`](frontend-dapp/src/components/ui/Modal.tsx) (`title="Connect Wallet"`, `rootTestId="wallet-connect-modal-portal"`). Same primitive: [`WalletConnectPairingModal.tsx`](frontend-dapp/src/components/wallet/WalletConnectPairingModal.tsx) (`z-[10001]`), [`ExpertModeModal.tsx`](frontend-dapp/src/components/swap/ExpertModeModal.tsx). [`RiskAcknowledgementModal.tsx`](frontend-dapp/src/components/legal/RiskAcknowledgementModal.tsx) uses `dismissible={false}` on purpose ([#138](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/138)). ### Close controls that already exist (but fail the report) [`Modal.tsx`](frontend-dapp/src/components/ui/Modal.tsx) when `dismissible` (default `true`): | Path | Implementation | Why users still report “no close” | |------|----------------|-----------------------------------| | Header X | `aria-label="Close modal"` + SVG, class `btn-muted !min-h-0 !px-2.5 !py-2` | Icon-only, muted chrome, generic label. Easy to miss next to the **Connect Wallet** title. QA shots (`docs/qa/issue-488/wallet-modal-dark.jpg`) show the X, but it reads as header chrome, not a dismiss CTA. | | Backdrop | Sibling `.app-modal-backdrop` (`position: absolute; inset: 0`) `onClick` → `onClose` | Portal root is `fixed inset-0 flex items-center justify-center p-4` and has **no** click handler. Clicks that hit the flex root (not the backdrop) are swallowed and do **not** close. No Vitest for backdrop click. | | Escape | `document` `keydown` | Works in unit tests; not discoverable on touch. | | Header trigger | Disconnected **Connect Wallet** only sets `walletModalOpen: true` | Does **not** close. Users who “click the header” are almost certainly hitting the modal **title row** (invisible/low-contrast X hit box), not a designed toggle. | `.app-modal-panel` is `position: relative; z-index: 10; overflow: hidden` and vertically centered. The wallet list is tall (Simulated + 5 production rows + Install). On a short phone / zoomed viewport, `items-center` can push the **header + X above the visible area**. The panel does not pin the header or scroll the body independently. Mobile `.app-modal-portal-root` only adds `padding-bottom` for the tab bar ([`index.css`](frontend-dapp/src/index.css)). Connected-wallet **dropdown** already uses a full-viewport `app-menu-dismiss` button ([#187](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/187)). The Connect dialog does **not** reuse that pattern. ### Close vs in-flight connect [`useWallet.ts`](frontend-dapp/src/hooks/useWallet.ts) `closeWalletModal` and `WalletModal.handleClose`: if `isConnecting`, they call `cancelConnection()` (aborts WC, closes pairing, sets `walletModalOpen: false`) and return. [`cancelConnection`](frontend-dapp/src/hooks/useWallet.ts) already clears connect state. Header **Cancel** while connecting ([#554](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/554) **WC-M9**) is separate and must stay. When pairing is open, `WalletModal` returns `null` so only the pairing sheet is shown (**WC-M8**). Pairing has **Cancel** plus the same header X / backdrop. ### Test gaps [`Modal.test.tsx`](frontend-dapp/src/components/ui/__tests__/Modal.test.tsx) covers Escape + hiding the X when `dismissible={false}`. It does **not** assert backdrop click, visible close control, or “click panel does not close.” [`WalletModal.test.tsx`](frontend-dapp/src/components/wallet/__tests__/WalletModal.test.tsx) covers badges / logos / mobile WC rows, not dismiss. [`useWallet.test.ts`](frontend-dapp/src/hooks/__tests__/useWallet.test.ts) does not assert `closeWalletModal` / trigger toggle. --- ## Why this is needed Connect Wallet is the first dialog most retail users open. If they cannot find a close control and tapping the dimmed page does nothing, they assume the dApp is stuck. “Click the header” is an accidental hit on the title-row X, not a designed exit. The connected menu already dismisses on outside tap + Escape ([#187](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/187)); Connect should match that expectation without weakening the blocking risk ack ([#138](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/138)). --- ## Constraints / guardrails 1. **Dismiss only.** Do not change wallet option lists, extension detection, Install URLs, logos, hybrid quote, or execute paths ([#160](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/160) / [#490](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/490) / [#554](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/554) / [#566](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/566)). 2. **Shared `Modal`, keep `dismissible`.** Risk acknowledgement ([#138](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/138)) must remain **non-dismissible**: no X, no backdrop, no Escape, no header-trigger bypass. Expert Mode may stay dismissible (it already has Cancel). 3. **Pairing stack ([#554](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/554)).** Pairing portal stays `z-[10001]` above Connect `z-[9999]`. Dismiss on pairing must still `cancelConnection()` (abort pending WC, ignore late session). Do not leave `isConnecting` stuck. 4. **Do not close on dialog chrome.** Clicks on wallet rows, **Install** (`target=_blank`), pairing Open / Copy, or Expert Mode inputs must **not** dismiss. Use `stopPropagation` on the panel (or equivalent), not a document click that treats the panel as “outside.” 5. **In-flight connect.** Backdrop / X / Escape / header toggle while connecting must **cancel** (same as **WC-M9**), not leave a hung spinner. Do not treat cancel as a successful connect. 6. **Header trigger.** Disconnected **Connect Wallet** should **toggle** (open ↔ close / cancel). Do not add a second header close widget. Do not raise header `z-index` above the portal to “win” clicks ([#181](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/181) / **T527-8**). 7. **Short viewport.** Header + close control must remain in view at ~375×667 and zoomed desktop. Prefer `max-height` + scroll the **body**, pin the header; do not clip the X with `overflow: hidden` on the whole panel. Keep mobile tab-bar padding; do not cover `#trade-pair-select` / Settings by raising z-index. 8. **Tokens / a11y ([#488](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/488), [#144](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/144), [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489)).** Close control uses existing theme tokens (no `*-neo`, no gold fill). Visible name: `aria-label` like **Close** / **Close connect wallet** (not only a low-contrast glyph). `:focus-visible` ring with `--focus-ring`. No new lecture copy. Icon decorative if a text label is shown. 9. **Legal / TermsGate ([#517](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/517)).** Dismiss must not skip clickwrap after a successful connect. Closing without connecting must not look like an accepted session. 10. **No `dangerouslySetInnerHTML`.** Do not bind wallet names into the close control as HTML. --- ## Relevant files | File | Role | |------|------| | `frontend-dapp/src/components/ui/Modal.tsx` | Shared dialog: X, backdrop, Escape, `dismissible` | | `frontend-dapp/src/index.css` | `.app-modal-backdrop`, `.app-modal-panel`, `.app-modal-header`, mobile `.app-modal-portal-root` padding | | `frontend-dapp/src/components/wallet/WalletModal.tsx` | Connect list; `handleClose` vs `isConnecting` | | `frontend-dapp/src/components/wallet/WalletButton.tsx` | Header trigger; double `createPortal`; no toggle | | `frontend-dapp/src/hooks/useWallet.ts` | `openWalletModal` / `closeWalletModal` / `cancelConnection` | | `frontend-dapp/src/components/wallet/WalletConnectPairingModal.tsx` | Pairing dismiss + Cancel | | `frontend-dapp/src/components/legal/RiskAcknowledgementModal.tsx` | Must stay `dismissible={false}` | | `frontend-dapp/src/components/swap/ExpertModeModal.tsx` | Same primitive; keep Cancel | | `frontend-dapp/src/components/ui/__tests__/Modal.test.tsx` | Escape + non-dismissible X hidden; **add** backdrop / close / panel no-op | | `frontend-dapp/src/components/wallet/__tests__/WalletModal.test.tsx` | Connect dismiss + cancel-while-connecting | | `frontend-dapp/src/components/wallet/__tests__/WalletButton.test.tsx` | Header toggle / cancel | | `frontend-dapp/src/hooks/__tests__/useWallet.test.ts` | Store close / cancel | | `skills/AGENTS_FRONTEND_WALLET_CONNECT_MODAL.md` | Connect modal playbook | | `skills/AGENTS_FRONTEND_WALLETCONNECT_MOBILE.md` | Pairing / **WC-M8–M9** | | `skills/AGENTS_FRONTEND_RISK_DISCLAIMERS.md` | Blocking risk modal | | `docs/frontend.md` | Connect modal + menu dismiss + risk ack | --- ## Recommended direction 1. **Make the close control obvious** on dismissible modals: keep a header button with a visible X (and/or the word **Close** on `sm+`), `aria-label="Close"` or **Close connect wallet**, contrast that holds in dark and light. Do not rely on `btn-muted` alone if it blends into `--panel-bg-strong`. 2. **Make outside-click reliable:** treat the portal root as the dismiss surface (same idea as `app-menu-dismiss`), and `stopPropagation` on the panel. Do not require users to hit only the absolutely positioned backdrop sibling. Keep `role="dialog"` / `aria-modal` / focus trap. 3. **Toggle the header trigger** when `walletModalOpen` (close or `cancelConnection` if connecting). Same for accessibility: `aria-expanded` if you expose a disclosure relationship. 4. **Pin + scroll on short viewports:** header (title + close) stays visible; wallet rows scroll inside the panel. Re-check 375px with Simulated Wallet visible (dev). 5. **Tests:** Vitest for X, backdrop/root click, Escape, panel click no-op, `dismissible={false}` unchanged, connecting dismiss cancels, header toggle. Optional Playwright: open Connect from header on `/`, click dimmed page, dialog gone (`wallet-connect-modal-portal` detached). 6. Document the dismiss invariants on the Connect modal section of `docs/frontend.md` / the wallet-connect-modal playbook. --- ## Acceptance criteria - [ ] **D1** Dismissible Connect Wallet shows a **visible, labeled** close control in the dialog header at 375px and 1280px, dark and light. It is in the viewport (not clipped above the fold). - [ ] **D2** Click / tap on the dimmed page (any side, including below the header and beside the panel) closes the dialog when not connecting. - [ ] **D3** Escape closes when dismissible; focus remains trapped while open. - [ ] **D4** Click on a wallet row, **Install**, or pairing Open / Copy does **not** close except after a successful connect (existing success path). - [ ] **D5** Header **Connect Wallet** while the dialog is open closes it (or cancels an in-flight connect). Second click can open again. - [ ] **D6** While `isConnecting` / pairing sheet open: X, backdrop, Escape, pairing **Cancel**, and header **Cancel** all abort connect and clear `isConnecting`; no late WC session attaches ([#554](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/554) **WC-M9**). - [ ] **D7** Risk acknowledgement ([#138](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/138)): still no X, no backdrop, no Escape. Header Connect must not dismiss the risk gate. - [ ] **D8** Pairing stays above Connect (`z-[10001]` / `z-[9999]`). Clickwrap after a real connect is unchanged ([#517](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/517)). - [ ] **D9** `make lint-frontend` and `make test-frontend` green. New Vitest covers D1–D4 and D7. --- ## Test plan (all paths) ### Idle Connect (not connecting) 1. `/` disconnected — open from header **Connect Wallet**. Confirm D1 (dark + light, 375 + 1280). 2. Click dimmed area (top, sides, bottom above mobile tab bar) — dialog unmounts (`wallet-connect-modal-portal` gone). Repeat from `/portfolio` Connect CTA and Swap **Connect Wallet** submit. 3. Click the close control — same. 4. Escape — same. Tab cycle stays inside the dialog (first ↔ last). 5. Click **Keplr** / **Install** — dialog stays (Install may open a new tab). Click a wallet row without completing connect — stays open; error banner may show. 6. With dialog open, click header **Connect Wallet** — closes (D5). Click again — reopens. 7. Simulated Wallet (dev only) — success still closes (existing path). Production: Simulated absent. ### In-flight connect / pairing 8. Start WalletConnect (mobile or desktop). While **Connecting…** / pairing sheet: X, backdrop, pairing **Cancel**, header **Cancel** — `isConnecting` false, pairing closed, no address set. 9. Pairing Open / Copy still work; they must not dismiss unless the user hits Cancel / X / backdrop. ### Blocking + other modals 10. First-visit risk ack (clear `localStorage` risk key): no X; backdrop and Escape do nothing; Continue still requires checkbox ([#138](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/138)). 11. Expert Mode: Cancel + X + backdrop still close; phrase field click does not close. 12. After a successful connect, TermsGate / clickwrap still appears when required ([#517](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/517)). Dismissing Connect **without** connecting does not unlock swap. ### Regression commands ```bash make lint-frontend make test-frontend # targeted: # frontend-dapp: npx vitest run src/components/ui/__tests__/Modal.test.tsx src/components/wallet/__tests__/WalletModal.test.tsx src/components/wallet/__tests__/WalletButton.test.tsx src/hooks/__tests__/useWallet.test.ts ``` No LocalTerra required for dismiss UX. Do not bind-mount `indexer/` for cargo. --- ## Test plan (attack, hack, and abuse) Dismiss wiring sits on the connect funnel. A broken overlay can trap the user, steal clicks, or look like a connected session. | Vector | Why it matters | Test | |--------|----------------|------| | **Click-through / overlay steal** | Portal root `pointer-events` or a full-page dismiss button with a hole that is too small can steal Swap amount / token-picker / Settings clicks after “close,” or block them while open. | After close, Pay amount and token triggers work. While open, page under the dialog does not receive clicks. Do not raise header/`z-index` above the portal. | | **Dismiss = connect spoof** | Closing the dialog must not set `address` or write `cl8y_wallet_connection`. | Close via X / backdrop / Escape / header toggle; store address stays null; no localStorage wallet key. | | **Cancel race (WC)** | Backdrop close during pairing must bump `connectAttemptId` / abort so a late session cannot attach ([#554](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/554)). | Cancel then complete WC in the wallet app; dApp stays disconnected. | | **Risk-ack bypass** | A global “click outside closes Modal” would skip NFA ([#138](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/138)). | `dismissible={false}`: backdrop / Escape / missing X. Header Connect does not unmount the risk dialog. | | **Install / deep-link abuse** | `stopPropagation` on **Install** and pairing `<a href>` must remain so a parent dismiss does not fire on the same tap as navigation. Allowlist for WC links unchanged (**WC-M5**). | Install still `noopener` + official store URL. Pairing href still allowlisted; blocked href does not navigate. | | **Focus trap / keyboard** | Missing trap leaves focus on Swap fields under a `aria-modal` dialog (spoofed “page is usable”). Escape-only without an X fails WCAG on some AT. | Tab stays in dialog. Screen reader name includes Close. | | **z-index escalation** | `z-[10001]`+ on Connect (not pairing) can hide pairing Open / Copy or cover TermsGate. | Pairing still tappable above Connect. Toasts (`z-[100]`) may stay below — do not lift toasts through the dialog. | | **HTML injection** | Wallet / pairing `name` in the title must stay text. | No `dangerouslySetInnerHTML` on header / close. | | **Stuck `isConnecting`** | Close path that only `set({ walletModalOpen: false })` while connecting leaves header on **Cancel** forever. | After dismiss-during-connect, header shows **Connect Wallet** again. | Out of scope as attack surface: contracts, indexer, fee math. --- ## Verification criteria **Done when** a reviewer on `make dev` can open Connect Wallet from the header and from an in-page CTA, see an obvious close control, and dismiss by (1) that control, (2) clicking the dimmed page, (3) Escape, and (4) clicking header **Connect Wallet** again — at ~375px and ~1280px, dark and light — and an in-flight WalletConnect cancel still clears connecting state. Risk ack stays blocking. **Not done if** the only exit is still the title-row hit box; if outside clicks hit the portal root and do nothing; if the X is off-screen on a phone; or if risk ack / pairing stack / clickwrap regress. **Evidence:** before/after crops of the Connect header (dark + light, 375 + 1280) and a note that backdrop click was exercised. No user identity, addresses, or mnemonics in screenshots or issue comments.
PlasticDigits commented 2026-08-26 04:23:24 +00:00 (Migrated from gitlab.com)

marked as related to #187

marked as related to #187
PlasticDigits commented 2026-08-26 04:23:25 +00:00 (Migrated from gitlab.com)

marked as related to #138

marked as related to #138
PlasticDigits commented 2026-08-26 04:23:26 +00:00 (Migrated from gitlab.com)

marked as related to #554

marked as related to #554
PlasticDigits commented 2026-08-26 04:23:26 +00:00 (Migrated from gitlab.com)

marked as related to #160

marked as related to #160
PlasticDigits commented 2026-08-26 07:00:42 +00:00 (Migrated from gitlab.com)

mentioned in commit 1ba8c8cf7c

mentioned in commit 1ba8c8cf7cfe19f5352c00dd7c14368422a7855d
PlasticDigits commented 2026-08-26 07:00:44 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1160

mentioned in merge request !1160
PlasticDigits commented 2026-08-26 07:03:07 +00:00 (Migrated from gitlab.com)

Implemented on fix/672-wallet-modal-dismiss — MR !1160.

Acceptance (this MR):

  • D1 Labeled Close in the dialog header (visible text + decorative X). Pinned header + scrollable body; panel max-height.
  • D2 Dimmed page (portal root / backdrop) closes when not connecting.
  • D3 Escape closes when dismissible; focus trap while open.
  • D4 Wallet row / Install / pairing Open-Copy do not dismiss (panel stopPropagation).
  • D5 Header Connect Wallet toggles via closeWalletModal (aria-expanded).
  • D6 Dismiss while connecting calls cancelConnection() (header returns to Connect Wallet).
  • D7 Risk acknowledgement stays dismissible={false}.
  • D8 Pairing z-[10001] unchanged; dismiss without connect does not write wallet storage.
  • D9 Vitest for D1–D7; make lint-frontend + make verify-issue-672 green.

Manual browser (worktree Vite): Close, backdrop, Escape, Galaxy Station row stays then Close-while-connecting. Did not attach screenshots.

Left for reviewer: 375×667 / light theme visual, Playwright VERIFY_ISSUE_672_CHAIN=1, first-visit risk ack with empty localStorage, clickwrap after a real connect.

Implemented on `fix/672-wallet-modal-dismiss` — MR !1160. Acceptance (this MR): - [x] **D1** Labeled **Close** in the dialog header (visible text + decorative X). Pinned header + scrollable body; panel `max-height`. - [x] **D2** Dimmed page (portal root / backdrop) closes when not connecting. - [x] **D3** Escape closes when dismissible; focus trap while open. - [x] **D4** Wallet row / Install / pairing Open-Copy do not dismiss (panel `stopPropagation`). - [x] **D5** Header **Connect Wallet** toggles via `closeWalletModal` (`aria-expanded`). - [x] **D6** Dismiss while connecting calls `cancelConnection()` (header returns to Connect Wallet). - [x] **D7** Risk acknowledgement stays `dismissible={false}`. - [x] **D8** Pairing `z-[10001]` unchanged; dismiss without connect does not write wallet storage. - [x] **D9** Vitest for D1–D7; `make lint-frontend` + `make verify-issue-672` green. Manual browser (worktree Vite): Close, backdrop, Escape, Galaxy Station row stays then Close-while-connecting. Did not attach screenshots. Left for reviewer: 375×667 / light theme visual, Playwright `VERIFY_ISSUE_672_CHAIN=1`, first-visit risk ack with empty localStorage, clickwrap after a real connect.
PlasticDigits commented 2026-08-26 08:13:04 +00:00 (Migrated from gitlab.com)

mentioned in commit bd868e228e

mentioned in commit bd868e228e4d9f4bdb8b884784d6f23ab593240a
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-26 08:13:06 +00:00
PlasticDigits commented 2026-08-26 08:34:16 +00:00 (Migrated from gitlab.com)

mentioned in commit c7b888ff56

mentioned in commit c7b888ff56cc8d6f63668a24307fd8db060a9292
PlasticDigits commented 2026-08-26 09:30:54 +00:00 (Migrated from gitlab.com)

Merged to main via !1160. Wallet dropdown tests from #671 were kept alongside Close.

Leftover: Connect Wallet modal still has a labeled dismissible Close after frontend rebuild. make verify-issue-672.

Merged to `main` via !1160. Wallet dropdown tests from #671 were kept alongside Close. Leftover: Connect Wallet modal still has a labeled dismissible Close after frontend rebuild. `make verify-issue-672`.
PlasticDigits commented 2026-08-26 09:31:03 +00:00 (Migrated from gitlab.com)

mentioned in issue #671

mentioned in issue #671
PlasticDigits commented 2026-08-26 09:31:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #673

mentioned in issue #673
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#672
No description provided.