Prevent focus on non-cursor thoughts when tapping the edge of the editable#4407
Prevent focus on non-cursor thoughts when tapping the edge of the editable#4407ethan-james wants to merge 37 commits into
Conversation
|
@ethan-james is attempting to deploy a commit to the Cybersemics Institute Team on Vercel. A member of the Team first needs to authorize it. |
|
@raineorshine This issue is also related to events failing to dispatch when a tap is on the edge of an editable. In this case it's |
|
Thanks. A couple of points of clarification:
As for the BrowserStack tests, that's going to be a critical piece for us to get test coverage. Feel free to reach out to Faiz, as he has gained a lot of experience recently getting the BrowserStack tests to work for our agents. It's usually a matter of which event interaction API to use. |
Not that I know of. If the
I experimented with extending padding-right, and it seemed to behave normally with a padding of 5em or 10em. It seems like the problem occurs when the touch occurs to the right (on the edge) of whatever padding is set. I'll try to profile it a bit better to get a better answer. Hopefully that will be helpful in figuring out how to construct a failing test. My hypothesis is that BrowserStack is dispatching all events at the exact same pixel coordinate, and that the real touch events are not quite so precise on a device. It's also possible that it's not dispatching
|
Okay. And I don't believe we're calling Keep working on identifying why
Yeah, I'm curious about that. In my testing it didn't seem like it was confined to a single 1px edge. So I'm wondering what exactly the bounds are and what determines them. Maybe native mobile Safari is somewhat forgiving about how close the touch has to be to the editable to trigger a focus?
Sounds like a plan 👍 |
Note: As mentioned above, this bug was fixed by a mostly-unrelated PR that was merged into Hi @fbmcipher, I am trying to write a test that taps slightly past the right edge of an editable in order to reproduce this bug. I was using the
So I need to figure out how to tap in a position past the edge of the editable while getting iOS Safari to retarget the event onto the editable. Do you know how I could do that? Here is some more info from Copilot. Sounds pretty reasonable, but I didn't get much info about "compatibility-event behavior" from Google. What you are likely seeing is a WebKit compatibility-event behavior, not a bug in your math:
So the pattern “touch says X, mouse on editable says edge X” is plausible and consistent with Safari’s behavior around editable text. Practical takeaway: on iOS Safari, trust touch coordinates (touchstart/touchend.changedTouches) for caret placement, and treat compatibility mouse coordinates as potentially normalized/retargeted. |
I got the test to fail, so I merged in |
The 'Focus is prevented after clearing the cursor' test tapped the home button with a synthetic tap() (performActions pointer events), which does not fire the real click event that HomeLink's fastClick onClick handler relies on. As a result the cursor was never cleared and the test only passed as an artifact of leftover focus from newThought. Use a native WebDriver click on the inner anchor to genuinely clear the cursor, and tap the editable's right edge at x: 6, y: 60. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@fbmcipher I managed to get Appium and @wdio/mcp running and managed to figure out a bug in the test where tapping the home button wasn't clearing the cursor. However, I am trying to get this test to fail in #4482 and it just won't happen. Copilot eventually threw in the towel: FindingsYou were right that the home tap was broken — that was the key discovery. tap(homeNodeHandle) uses synthetic WebDriver pointer actions that don't fire a real click event, so HomeLink's fastClick onClickhandler never ran and the cursor was never cleared. The fix is to use WebDriver's native .click() on the inner (like the existing clickThought helper does). But fixing the home tap revealed the deeper problem: once the cursor is genuinely cleared, the bug doesn't reproduce locally on either branch at any tap offset. The elementFromPoint probe showed why — locally the contenteditable is shrink-wrapped to the text (65px) and the void area beside it is covered by an absolutely-positioned overlay (pos_absolute us_none). Taps in the void hit that overlay, not the editable, so the fix's guarded code path (onMouseDown/onFocus on the editable) never executes. The apparent "x:6 fails / x:26 passes" split I saw earlier was an artifact of the broken home tap leaving the editable focused from newThought, not the actual bug. Bottom line: this focus bug is specific to BrowserStack's rendering/layering and does not reproduce on the local iOS 18.6 simulator. I'm about ready to recommend that we move on from this, since I believe the issue to be fixed and I also have a reasonable Browserstack test that passes in this branch even though I can't get it to fail in the other branch. I thought I'd solicit your help one more time to see if you have any ideas for how to proceed. Thanks! |
|
Hey @ethan-james! I'll take a quick look at this tonight... will try and update in the next few hours. |
|
Hey @ethan-james – I dug into this in a background terminal while working on other things tonight. Ran a few tests to try and get a bit more insight. The following is pretty much verbatim output from Claude, but maybe it helps. I'm a bit skeptical about its claim (2) that the root issue isn't fixed. I almost wanted to hold off on posting this until I could figure out why it was so sure about that, but didn't want to leave you hanging waiting for a response. Figured an imperfect analysis is better than none at all. It's probably worth reading the agent's reasoning/thinking process below, in any case. Maybe it'll help you figure out what's going on. I'm also skeptical about claim (3) – like your Copilot, it's also pretty confident that "the bug needs a real finger to reproduce", but something tells me that just cannot be true. It wouldn't be the first time AI insists on a conclusion that is vacuous and provably false. And it doesn't make sense – we're not doing anything that exotic in the app... I have some confidence that I could get to the bottom of it with more time, but we need to make sure claim (2) is addressed first. If, indeed, the bug is not fixed, that would explain the issues. If the bug is fixed (and the AI is hallucinating, or I am just confused) then I'd support you in just moving on if Raine's cool with it. The following is agent output from Opus 4.8 + Fable 5 working together:
Here's what I found. Where you're spot on ✅
One correction on the mechanismCopilot's conclusion — "taps hit the overlay, not the editable, so onMouseDown/onFocus never execute … specific to BrowserStack's rendering/layering" — isn't quite right. What actually happens (with the cursor confirmed cleared), everywhereA touch lands on the
In every row the mouse event retargets onto the editable, but no Why I don't think it's fixedTwo findings: 1. #4371 doesn't change this behavior. I reverted only 2. The code behind #4394 hasn't changed since Raine filed it. #4394 was filed June 14. Since then, the only commits touching the relevant files are #4371 (just shown to be neutral here) and #4253 ("Remove useMultiline" — a 2-line change nowhere near the focus/keyboard logic). And the So the explanation that fits all the evidence: on a real device the focus fires anyway, despite the How I'd proceed
|
|
Thanks @fbmcipher! It wouldn't hurt for me to dig a little to provide a better understanding of why #4371 fixed the bug, since it was targeting a different issue, and I couldn't quickly ascertain what is different about the path that the events take post-#4371. As for claim (3), I wouldn't be too surprised to learn that was true, given the quirkiness of this issue. In case I haven't provided enough context, the issue is that a tap outside the boundaries of the editable will trigger mouse events on the editable even though touch events are registered on the element that is actually being tapped. Generally, that's the thought annotation. This seems to be an iOS Safari trick to improve the accessibility of input elements, but unfortunately, touch events are key to prevent focus for editables other than the cursor thought.
I wish it said more about what it meant by that. I don't know for sure that #4371 fixed it, just that it appears fixed now. |
Replace the weaker home-link/activeElement variant of the "Focus is prevented after clearing the cursor" test with a faithful reproduction of cybersemics#4394 in a dedicated caretFocus.ts spec: create a thought, dismiss the keyboard, clear the cursor via cursorBack, then fat-tap just past the right edge of the non-cursor thought and assert the keyboard stays hidden. The bug only reproduces on iOS 18 (Safari touch-adjustment retargets the synthesized mousedown into the editable), so the BrowserStack config is split into two capabilities: the suite runs on iOS 17 (excluding this spec) and caretFocus.ts runs on a dedicated iOS 18 device. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Inlining the two capabilities in the array literal triggered TS2352: the extra `exclude`/`specs` keys fail the excess-property check when the array is cast to WebdriverIO.Config['capabilities']. Extracting each capability to a standalone const widens its type before the cast, so tsc passes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
b6b861e to
525b4bb
Compare
The 'Focus is prevented after clearing the cursor' test tapped the home button with a synthetic tap() (performActions pointer events), which does not fire the real click event that HomeLink's fastClick onClick handler relies on. As a result the cursor was never cleared and the test only passed as an artifact of leftover focus from newThought. Use a native WebDriver click on the inner anchor to genuinely clear the cursor, and tap the editable's right edge at x: 6, y: 60. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…b.com:ethan-james/em into 4394-bug-prevent-focus-on-non-cursor-thoughts
Adopts the touch techniques proven in #4407 to give the #4173 repro a real chance of failing on hardware: - doubleTap now dispatches from the NATIVE_APP context using a finger-sized contact area (width/height/pressure). A zero-radius webview-context tap does not trigger Safari's touch-adjustment / rapid-tap focus handling and cannot reproduce the bug. Both element centers are resolved in the webview context before switching, so the sub-second interval stays deterministic. - Moves the #4173 test into its own spec (caretAdjacentTap.ts) pinned to a dedicated iOS 18 capability (iPhone 16 Pro Max), mirroring #4407's split. The retargeting behavior fires on iOS 18 but not the suite's default iOS 17, which is why the earlier iOS 17 run gave a false pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fixes #4394 and #4291
How the regression test was built
src/e2e/iOS/__tests__/caretFocus.tsreproduces #4394 — the virtual keyboard incorrectly opening when a non-cursor thought is tapped just past its right edge. It passes on this branch (#4407) and fails on the older pre-fix commit. Many approaches were tried this session; these are the ones that turned out to be load-bearing for the final test:width: 40, height: 40, pressure: 0.9). A zero-radius synthetic tap does not trigger Safari's touch-adjustment heuristic; only a finger-sized contact does.x = rect.x + rect.width + 4,y = vertical center). This lands the touch on the#thought-annotationoverlay — so the editable'sonTouchEnd/preventDefaultnever runs — while staying close enough that Safari retargets the synthesizedmousedowninto the editable.cursorBack(gesture('r')) so "Hello" becomes a non-cursor thought — the exact path [iOS] Keyboard incorrectly opens when tapping right edge of thought #4394 is about.performActionstap, not a webviewelement.click(). On real hardware only a native touch focuses the editable and opens the keyboard; the priming tap lets the later synthetic edge-tap win the timing race a real finger triggers on its own.Discarded along the way (did not contribute): a sim-only
clickThoughtwebview priming (does not open the keyboard on real devices), areleaseActions()call afterperformActions(unsupported on Safari/XCUITest), and earlier point-tap / x-offset tuning that the fat-tap superseded.