-
Notifications
You must be signed in to change notification settings - Fork 141
fix(mobile): cursor stays on archived thought after drag-to-archive #4405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,7 @@ import getSetting from '../selectors/getSetting' | |
| import getThoughtById from '../selectors/getThoughtById' | ||
| import hasMulticursorSelector from '../selectors/hasMulticursor' | ||
| import rootedParentOf from '../selectors/rootedParentOf' | ||
| import thoughtToPath from '../selectors/thoughtToPath' | ||
| import batchEditingStore from '../stores/batchEditing' | ||
| import editingValueStore from '../stores/editingValue' | ||
| import editingValueUntrimmedStore from '../stores/editingValueUntrimmed' | ||
|
|
@@ -53,6 +54,7 @@ import haptics from '../util/haptics' | |
| import head from '../util/head' | ||
| import isDivider from '../util/isDivider' | ||
| import isDocumentEditable from '../util/isDocumentEditable' | ||
| import isThoughtArchived from '../util/isThoughtArchived' | ||
| import strip from '../util/strip' | ||
| import stripEmptyFormattingTags from '../util/stripEmptyFormattingTags' | ||
| import trimHtml from '../util/trimHtml' | ||
|
|
@@ -212,6 +214,13 @@ const Editable = ({ | |
| dispatch((dispatch, getState) => { | ||
| const state = getState() | ||
|
|
||
| // 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| // do not set cursor if it is unchanged and we are not entering when keyboard is open | ||
| if ((!isKeyboardOpen || state.isKeyboardOpen) && equalPath(state.cursor, path)) return | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { KnownDevices } from 'puppeteer' | ||
| import { ElementHandle } from 'puppeteer' | ||
| import { JSHandle } from 'puppeteer' | ||
| import type { WindowEm } from '../../../initialize' | ||
| import click from '../helpers/click' | ||
| import clickThought from '../helpers/clickThought' | ||
| import emulate from '../helpers/emulate' | ||
|
|
@@ -133,4 +134,95 @@ describe('DropGutter: mobile only', () => { | |
| expect(await isThoughtInDOM('b')).toBe(true) | ||
| expect(await isThoughtInDOM('c')).toBe(true) | ||
| }) | ||
|
|
||
| it('should move the cursor to the previous sibling when a thought is archived via DropGutter (#4077)', async () => { | ||
| await paste(` | ||
| - One | ||
| - Two | ||
| - Three | ||
| `) | ||
|
|
||
| await clickThought('Two') | ||
|
|
||
| await dragToDropGutter(await waitForEditable('Two')) | ||
|
|
||
| await waitForAlertContent('Removed 1 thought') | ||
|
|
||
| // Wait for the browser's asynchronous focus restoration to settle. This is what could override | ||
| // the cursor that archiveThought correctly placed on the previous sibling (#4077). | ||
| await new Promise(resolve => setTimeout(resolve, 1000)) | ||
|
|
||
| // 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed |
||
| if (!cursor) return null | ||
| const id = cursor[cursor.length - 1] | ||
| return em.getThoughtById(id)?.value ?? null | ||
| }) | ||
|
|
||
| expect(cursorValue).toBe('One') | ||
| }) | ||
|
|
||
| it('should keep the cursor on the last known position when a different thought is archived via DropGutter (#4077)', async () => { | ||
| await paste(` | ||
| - One | ||
| - Two | ||
| - Three | ||
| - Four | ||
| - Five | ||
| `) | ||
|
|
||
| // Put the cursor on "Five", then archive a different thought ("Two") by dragging it to the DropGutter. | ||
| await clickThought('Five') | ||
|
|
||
| await dragToDropGutter(await waitForEditable('Two')) | ||
|
|
||
| await waitForAlertContent('Removed 1 thought') | ||
|
|
||
| // Wait for any asynchronous focus restoration to settle. | ||
| await new Promise(resolve => setTimeout(resolve, 1000)) | ||
|
|
||
| // The cursor should remain on "Five" (its last known position), not move to a sibling of the archived thought. | ||
| const cursorValue = await page.evaluate(() => { | ||
| const em = window.em as WindowEm | ||
| const cursor = em.testHelpers.getState().cursor | ||
| if (!cursor) return null | ||
| const id = cursor[cursor.length - 1] | ||
| return em.getThoughtById(id)?.value ?? null | ||
| }) | ||
|
|
||
| expect(cursorValue).toBe('Five') | ||
| }) | ||
|
|
||
| it('should keep the cursor on the last known position when the first thought is archived via DropGutter (#4077)', async () => { | ||
| await paste(` | ||
| - One | ||
| - Two | ||
| - Three | ||
| - Four | ||
| - Five | ||
| `) | ||
|
|
||
| // Put the cursor on "Five", then archive the first thought ("One", which has no previous sibling) via the DropGutter. | ||
| await clickThought('Five') | ||
|
|
||
| await dragToDropGutter(await waitForEditable('One')) | ||
|
|
||
| await waitForAlertContent('Removed 1 thought') | ||
|
|
||
| // Wait for any asynchronous focus restoration to settle. | ||
| await new Promise(resolve => setTimeout(resolve, 1000)) | ||
|
|
||
| // The cursor should remain on "Five" (its last known position), not move to the next sibling of the archived thought. | ||
| const cursorValue = await page.evaluate(() => { | ||
| const em = window.em as WindowEm | ||
| const cursor = em.testHelpers.getState().cursor | ||
| if (!cursor) return null | ||
| const id = cursor[cursor.length - 1] | ||
| return em.getThoughtById(id)?.value ?? null | ||
| }) | ||
|
|
||
| expect(cursorValue).toBe('Five') | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
archiveThoughtis curried so you don't need to passstate. Look at the wayarchiveThoughtis used in the other tests.There was a problem hiding this comment.
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 passingstatetoarchiveThought.