Skip to content

fix(mobile): cursor stays on archived thought after drag-to-archive#4405

Open
Copilot wants to merge 3 commits into
mainfrom
copilot/fix-cursor-on-archived-thought
Open

fix(mobile): cursor stays on archived thought after drag-to-archive#4405
Copilot wants to merge 3 commits into
mainfrom
copilot/fix-cursor-on-archived-thought

Conversation

Copilot AI commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

#4077

After dragging a thought to the scroll zone (DropGutter) to archive it, the cursor behaved incorrectly: it remained on the archived thought (STR A/B), or jumped off its last known position when a different thought was archived (STR C/D).

Root cause (STR A/B): archiveThought correctly sets the cursor to the previous sibling. However, on mobile a spurious post-drag focus event fires on the now-hidden Editable of the archived thought, going through setCursorOnThought and overriding the cursor back onto the archived thought.

Root cause (STR C/D): archiveThought was unconditionally relocating the cursor to a sibling of the archived thought, even when a different thought (not the cursor's) was archived.

Changes

  • src/components/Editable.tsx — Prevent the spurious focus from re-cursoring at its source: onFocus only sets the cursor when isVisible, so a focus event on the now-hidden archived thought no longer overrides the cursor. When hidden thoughts are shown, isVisible is true and the cursor can still be set on them.
  • src/actions/archiveThought.ts — Only relocate the cursor when it is on (or within) the thought being archived (isDescendantPath(state.cursor, path)); otherwise preserve the cursor's last known position.
  • src/actions/__tests__/archiveThought.ts — Added reducer tests for STR C/D using the curried archiveThought form.
  • src/e2e/puppeteer/__tests__/DropGutter.ts — Added regression e2e tests for the STR A and STR C/D drag flows, asserting the cursor via user-observable DOM (getEditingText()) rather than Redux state.

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
em Ready Ready Preview, Comment Jun 17, 2026 3:59am
em-ai Ready Ready Preview, Comment Jun 17, 2026 3:59am

@BayuAri BayuAri left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot
Please resume the work, you should be unblocked now

Copilot AI requested a review from BayuAri June 18, 2026 02:01
Copilot AI changed the title [WIP] Fix cursor staying on archived thought after deletion fix: launch Chrome with remote debugging in copilot-setup-steps Jun 19, 2026
Copilot AI changed the title fix: launch Chrome with remote debugging in copilot-setup-steps fix(mobile): cursor stays on archived thought after drag-to-archive Jun 19, 2026
@BayuAri BayuAri force-pushed the copilot/fix-cursor-on-archived-thought branch from 1d83798 to 6979171 Compare June 23, 2026 16:11
@BayuAri BayuAri added the skip-tdd-puppeteer Use this if you are extending test overage and you expect the new puppeteer tests to pass on main. label Jun 23, 2026
#4077)

When a focused thought is archived by dragging it to the DropGutter, a spurious post-drag tap/focus event fires on the now-hidden Editable and re-sets the cursor onto the archived thought, overriding the previous sibling that archiveThought correctly selected.

Guard setCursorOnThought (the chokepoint for both onFocus and handleTapBehavior) to skip archived thoughts, resolving the thought's real path via thoughtToPath since the Editable path may no longer include the =archive ancestor.

Adds a regression e2e test in DropGutter.ts.
@BayuAri

BayuAri commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator
- One
- Two
- Three
- Four
- Five

STR C: Cursor is moved to the above of the archived thought.

Step to Reproduce

  1. Put the cursor on Five
  2. Without moving the cursor, drag and drop Two to the right side of the screen to archive

Current behavior

Cursor is moved to One

Expected behavior

Cursor should stay on Five as the last known position

@BayuAri

BayuAri commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator
- One
- Two
- Three
- Four
- Five

STR D: Cursor is moved to the below of the archived thought when the archived thought is first

Step to Reproduce

  1. Put the cursor on Five
  2. Without moving the cursor, drag and drop One to the right side of the screen to archive

Current behavior

Cursor is moved to Two

Expected behavior

Cursor should stay on Five as the last known position

@BayuAri BayuAri left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…ferent thought (#4077)

archiveThought unconditionally moved the cursor to a sibling of the archived thought. When a thought is dragged to the DropGutter while the cursor is on a different thought, the cursor now stays on its last known position. Only relocate the cursor when it is on or within the thought being archived.
@BayuAri

BayuAri commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Update — cursor-on-archive fix (#4077)

Pushed 8758e893cf on top of 99cfec5a58. The PR now addresses all four reported scenarios.

What was fixed

STR A / STR B — cursor stays on the archived thought (commit 99cfec5a58)
When you archive the thought the cursor is on by dragging it to the DropGutter, a spurious post-drag tap/focus fired on the now-hidden Editable, going through setCursorOnThought and re-selecting the archived thought. Added a guard in Editable.tsx (setCursorOnThought — the chokepoint for both onFocus and handleTapBehavior) that resolves the thought's real path via thoughtToPath and bails if it is archived. The cursor now correctly moves to the previous sibling (or next, if none).

STR C / STR D — cursor jumps off the last known position (commit 8758e893cf)
When you archive a different thought (drag Two/One to the DropGutter while the cursor is on Five), archiveThought was unconditionally moving the cursor to a sibling of the archived thought. DropGutter dispatches archiveThought({ path }) with the dragged thought, not the cursor. Fixed archiveThought to only relocate the cursor when it is on (or within) the thought being archived:

const cursorOnArchived = isDescendantPath(state.cursor, path)
// …
cursorOnArchived ? setCursor({ path: cursorNew,}) : null

The cursor now stays on its last known position (Five) when an unrelated thought is archived.

These are two distinct, complementary root causes (spurious focus vs. unconditional cursor relocation) with no overlap.

Tests

  • 2 deterministic reducer tests in src/actions/__tests__/archiveThought.ts for STR C/D (verified red-first: previously cursor → One/Two, now stays on Five).
  • 2 e2e tests in src/e2e/puppeteer/__tests__/DropGutter.ts mirroring the STR C/D drag flow, plus the existing STR A e2e test.
  • All 19 archiveThought reducer tests pass; yarn lint:tsc clean.

Note on the skip-tdd-puppeteer label

This PR carries the skip-tdd-puppeteer label. It is required because of the STR A e2e test: that bug only reproduces on a real mobile browser (the spurious focus event does not fire in the browserless CI harness), so the STR A test passes on the base branch — which the TDD workflow flags as "not red-first". The label tells the TDD workflow not to require the puppeteer tests to fail on base.

The STR C/D tests do fail on base (the bug is pure reducer logic), so they are genuine red-first regressions — but since they live in the same file as the STR A test, the label remains necessary while this PR is open. The label can be removed after merge (it has no effect on a merged PR).

@BayuAri BayuAri marked this pull request as ready for review June 24, 2026 01:39
@BayuAri BayuAri requested a review from raineorshine June 24, 2026 01:40
Comment thread src/actions/__tests__/archiveThought.ts Outdated
`,
}),
setCursor(['Five']),
(state: State) => archiveThought({ path: contextToPath(state, ['Two'])! })(state),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

archiveThought is curried so you don't need to pass state. Look at the way archiveThought is used in the other tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — both tests now use the curried form, resolving the path from state first and calling archiveThought({ path })(state) without passing state to archiveThought.

Comment thread src/components/Editable.tsx Outdated
Comment on lines +217 to +222
// Do not set the cursor on an archived thought. After a focused thought is archived by
// dragging it to the DropGutter, a spurious tap/focus event can fire on its (now hidden)
// Editable, which would otherwise override the cursor that archiveThought placed on the
// previous sibling. The Editable's path may no longer include the =archive ancestor, so the
// thought's real path is resolved to determine whether it is archived. (#4077)
if (isThoughtArchived(state, thoughtToPath(state, head(path)))) return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right to me. If hidden thoughts are shown, then we very well may want to set the cursor on the archived thought.

A better solution would be to prevent the spurious tap/focus event to begin with.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the archived-path guard. The override actually came from a spurious post-drag focus on the now-hidden Editable, so onFocus now only sets the cursor when isVisible. This prevents the spurious focus from re-cursoring without ever blocking archived thoughts: when hidden thoughts are shown, isVisible is true and the cursor sets normally.

// The cursor should be placed on the previous sibling ("One"), not remain on the archived thought.
const cursorValue = await page.evaluate(() => {
const em = window.em as WindowEm
const cursor = em.testHelpers.getState().cursor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getState() is definitely not allowed in puppeteer tests. This is an integration test and should not have direct access to Redux state. It needs to interact with the browser like the user.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed getState() from all three DropGutter tests; they now read the cursor value via getEditingText() ([data-editing=true] [data-editable]).

@raineorshine

Copy link
Copy Markdown
Contributor

@copilot Please address the feedback.

… feedback (#4077)

Co-authored-by: raineorshine <750276+raineorshine@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Vercel preview: https://em-jeb2nae0m-cybersemics.vercel.app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-tdd-puppeteer Use this if you are extending test overage and you expect the new puppeteer tests to pass on main.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Mobile] Cursor stays on the archived thought

3 participants