bug(frontend): Android Keplr token menu taps open the wallet URL bar #632
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#632
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
On Android Keplr in-app browser, tapping a token in the Swap You Pay / You Receive menu often activates the Keplr URL / navigation chrome instead of selecting the coin. Compact phones make this nearly 100% of “fast pick” attempts. Desktop and wide viewports are fine.
Community reports (2026-08, anonymized): multiple Android devices (compact and large) in Keplr’s in-app browser on
https://dex.cl8y.com/. Opening the token menu with the IME up places the last list rows a few pixels above Keplr’s bottom URL bar; a quick tap lands on the bar (keyboard / tab switcher / overflow) rather than the option.This is not a wallet-connect bug. Keplr in-app remains a supported path (WC-M7 / #554). The picker UI does not reserve space for in-app chrome or the visual viewport.
Related (do not regress): #481 (Swap combobox), #498 (mobile CLS), #181 (portal layout), #244 (listbox keyboard), #347 (mobile tab bar), #350 (typed Enter = first hit). Label leak for
uluna/uusdis a separate picker issue (#630).Current codebase
What users tap
Swap pay/receive use
TokenSearchSelect— an<input type="text" role="combobox">.onFocus/onClickopen a portaled<ul role="listbox">ondocument.body. Same control is reused on Create Pair, pay-with-any-token, and Pool one-sided add/withdraw. Trade / Limits use the twinPairSearchSelect(also an input combobox). Mint keeps button-triggerTokenSelect.Tapping the trigger focuses a text field, so Android Gboard opens even when the user only wants to pick a visible row. Viewport meta in
frontend-dapp/index.htmliswidth=device-width, initial-scale=1.0with nointeractive-widget. Chromium’s default isresizes-visual:window.innerHeightstays large; onlyvisualViewportshrinks.How the menu is placed
usePortalListboxreads:Listeners:
windowscroll(capture) +resizeonly. NovisualViewportresize/scroll.computePortalListboxStylesubtractsbottomInsetfrom space-below, then prefers opening below the anchor when that space is ≥ 120px. Preferred max height for token search is 240px. Flip-above exists but is not used wheninnerHeightstill looks roomy under the IME.getMobileBottomNavInsetPxmeasures only.app-mobile-nav-shell(DEX tab bar,#347). It does not include:visualViewport.offsetTop/ height delta vsinnerHeight.token-select-dropdownisposition: fixed; z-index: 200(index.css). The mobile tab bar isz-index: 50. Keplr’s chrome is above the WebView and cannot be covered by any pagez-index. Last options sit flush on that chrome.Why a fast tap opens the URL bar
innerHeight→ list extends into the occluded band.py-2.5(~40px), under the 44px coarse-pointer floor.onMouseDownon options callspreventDefaultto keep focus; combined withonBlurclose (150ms) and a moving IME, the first tap can miss the option and hit WebView chrome.detectWalletInAppBrowser()(detectWalletInAppBrowser.ts) already knows Keplr / Lunc Dash / Galaxy Station UAs. It is unused for picker geometry.Why this is needed
Keplr in-app is the path many Android users actually use (and the documented alternate when Chrome WalletConnect is awkward). If picking LUNC / UST1 / USTR opens the wallet URL bar, Swap is unusable on the devices that need it most.
This is a hit-target / occlusion bug, not “users should tap slower.” Compact phones (≈360×640-class) have almost no gap between the last option and Keplr chrome once the IME is up.
Desktop combobox UX (#481) and mobile CLS (#498) stay; this issue adds visual-viewport + in-app chrome clearance and browse-without-IME on coarse/narrow viewports.
Constraints / guardrails
tokensprop, client-side filter, noGET /api/v1/tokens?q=). Do not revert Swap to scroll-onlyTokenSelect.onChangeonly emits ids in the allowed set; honorexcludeToken. No external token lists..token-select-trigger--with-leading-logowhile open;queryDraft === nulluntil edit; no empty-label flash. Opening/closing must not shove the pay amount field.combobox+listbox+aria-activedescendant.position: fixedondocument.body. Do not put the menu back in document flow (chart/ticket CLS, #181).setStatepass for initial coords (usePortalListboxrender-time read).getMobileBottomNavInsetPx(). In-app / IME insets are additive, not a replacement.innerHeightvsvisualViewportbug).resolveTrustedTokenLogoUrl; query capTOKEN_SEARCH_MAX_QUERY_LENGTH(128). NodangerouslySetInnerHTML.getByRole('combobox', { name: 'Select token you pay|receive' })unless the mobile browse control is intentionally a button that still exposes a combobox name. Updatee2e/helpers/token-select.tsin the same MR if the trigger role changes.e2e-smoke; do not add ane2e-txdependency for this UI fix.Relevant files
Shared positioning (primary)
frontend-dapp/src/components/ui/PortalListbox.tsx— viewport source + listenersfrontend-dapp/src/components/ui/portalListboxPosition.ts— flip / clamp /bottomInsetfrontend-dapp/src/components/ui/__tests__/portalListboxPosition.test.tsfrontend-dapp/src/lib/mobileBottomNav.tsfrontend-dapp/src/utils/detectWalletInAppBrowser.tsfrontend-dapp/index.html— viewport meta (optionalinteractive-widget; measure before changing)Combobox / IME
frontend-dapp/src/components/trade/TokenSearchSelect.tsxfrontend-dapp/src/components/trade/PairSearchSelect.tsxfrontend-dapp/src/components/ui/TokenSelect.tsx(button trigger; positioning only)frontend-dapp/src/components/ui/MenuSelect.tsx(Charts pair, etc.; positioning only)frontend-dapp/src/index.css—.token-select-dropdown,.token-select-option,.token-select-triggerCall sites (must keep working)
frontend-dapp/src/pages/SwapPage.tsxfrontend-dapp/src/components/create/CreatePairTokenField.tsxfrontend-dapp/src/components/payments/PayWithAnyToken.tsxfrontend-dapp/src/components/pool/OneSidedAddCard.tsx,OneSidedWithdrawCard.tsxDocs / playbooks
docs/frontend.md§ Portal listboxes, § Token search, § Pair searchskills/AGENTS_FRONTEND_PORTAL_LISTBOX_CLS.mdskills/AGENTS_FRONTEND_TOKEN_SEARCH.mdQA_TEMPLATE.md(add a Keplr in-app picker row next to 1.2.12)Regression tests to extend
frontend-dapp/src/components/trade/__tests__/TokenSearchSelect.test.tsxfrontend-dapp/e2e/swap-token-select-cls.spec.ts(#498)frontend-dapp/e2e/helpers/token-select.tsfrontend-dapp/e2e/trade-pair-select-cls.spec.tsRecommended direction
Ship both layers in one MR. Positioning alone still opens Gboard and compresses the list onto chrome. IME-off alone still leaves the last row on the URL bar on short screens.
A — Visual viewport + chrome inset (shared)
window.visualViewportexists, pass its width/height (and offset if usingfixedagainst the layout viewport) intocomputePortalListboxStyle.bottomInset= DEX tab bar + IME/visual occluded band + optional in-app chrome reserve (≈48–64px whendetectWalletInAppBrowser().isInAppBrowserand coarse/narrow). Fail closed: extra inset is safer than overlap.visualViewportresizeandscrollinusePortalListbox(same reducer bump as window resize). Keep first-frame sync read.B — Browse without IME on coarse / narrow viewports
Pick one (do not fork two mobile UIs):
readOnly/inputMode="none"until the user taps an explicit search field (or types on a hardware keyboard)<input>lives inside the portaled menuPrefer B3 if A still leaves You Receive options on chrome on a 640px-tall WebView; otherwise B2. Do not use a native
<select>(breaks layout / z-order — seeMenuSelectcomment).On desktop (
pointer: fineand width ≥768) keep today’s focus-opens-list + type-to-filter behavior.C — Hit targets and pointer capture
overscroll-behavior: containon the listbox so scroll does not leak to the WebView.preventDefaulton mouse down only if it does not eat the first Android tap; verify with a real device.Acceptance criteria
visualViewport, the open token listbox is fully inside the visual viewport and does not overlap the DEX tab bar.PairSearchSelect(Trade/Limits) and otherTokenSearchSelectcall sites.TokenSelectstill a button listbox.Test plan (all paths)
Unit
computePortalListboxStylewithheight= visual viewport (e.g. 390×400) +bottomInset56 (tab) + 56 (in-app): menumaxHeight/topstay above the reserved band; flip-above when space-below < 120px.bottomInset0: existing “opens below” numbers unchanged.TokenSearchSelect: coarse/narrow browse-open does not leave the search input as the active text field (orreadOnlyuntil search intent). Desktop click still opens list + allows type-to-filter.excludeToken/ XSS (text-only symbol, allowlisted logo) tests stay green.detectWalletInAppBrowserstill: Keplr UA = in-app; Android Chrome ≠ in-app.Vitest / existing suites (must stay green)
Playwright (
e2e-smoke, 5 workers)e2e/swap-token-select-cls.spec.ts(#498) — trigger/amount Y/X stable on 390×844.getBoundingClientRect().bottomis below the trigger and abovewindow.innerHeight - getMobileBottomNavInsetPx() - simulatedInset(page.evaluate with mockedvisualViewportif the harness can inject it; otherwise assert gap to viewport bottom ≥ tab bar + 44px)./swap.trade-pair-select-cls.spec.tsextended or sibling).Manual (required — Playwright cannot drive Keplr chrome)
/create,/poolone-sided, invoice pay token — menu clears chrome.Test plan (attack, hack, and abuse)
Picker changes sit on the selection path (token id → quote/execute). Treat as a trust-boundary UI.
<li>must not callonChangeunless the id is in the factory-gatedtokenslist. Keep the existingallowedSetguard.symbol/name(HTML,javascript:) still render as text. Logos still passresolveTrustedTokenLogoUrl. No raw HTML in option rows or the sheet title.TOKEN_SEARCH_MAX_QUERY_LENGTH.tokens/filterRetailDiscovery*set as today.z-[9999]/z-[10001]) or Legal gate. Dismiss target must not be a transparent full-screen layer that intercepts Keplr chrome (we cannot cover chrome; do not try). Backdrop click-outside must not generate a phantom tokenonChange.window.open, changelocation, or focus an off-page control. Blur timers must not select a token on outside tap.KeplrUA must only add inset (safer). It must not unlock hidden tokens, skip Legal, or change quotes.visualViewportvalues (0, NaN, huge) must clamp to a usable menu, not throw or cover the full window including the tab bar.Verification criteria
Done when:
e2e-smoke.TokenSearchSelecttests.visualViewport(or equivalent) inusePortalListboxand a coarse/narrow browse path that does not open the IME by default.docs/frontend.md+ QA row updated so the next agent does not revert toinnerHeight-only positioning.Not done if the only change is “tap slower” copy, a Keplr-only CSS hack, or a desktop-only dropdown tweak.
marked as related to #481
marked as related to #498
marked as related to #554
mentioned in merge request !1144
mentioned in commit
6af8261cfd!1144 merged to
main(6af8261c) without waiting for CI. Conflicts with !1142 were docs/Makefile/AGENTS only; TokenSearchSelect auto-merged.make verify-issue-63210/10 (V632-1–V632-8 unit + docs). Playwright chain specs were skipped (VERIFY_ISSUE_632_CHAINunset).Leftover (Playwright cannot drive wallet chrome):
Do not reopen unless a merged invariant is wrong. Manual leftovers go on the post-merge leftover issue.
mentioned in issue #638
marked as related to #638
Post-merge leftovers for the !1140/!1142/!1143/!1144/!1145 pass: #638.
mentioned in issue #651
mentioned in merge request !1153
mentioned in issue #659
mentioned in issue #665
mentioned in issue #711