Skip to content

✨ Added long-press to open the posts list context menu on touch devices#28602

Merged
9larsons merged 10 commits into
mainfrom
posts-list-longpress-context-menu
Jun 16, 2026
Merged

✨ Added long-press to open the posts list context menu on touch devices#28602
9larsons merged 10 commits into
mainfrom
posts-list-longpress-context-menu

Conversation

@9larsons

@9larsons 9larsons commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Why

Two mobile gaps in the posts list:

  1. The context menu (Duplicate, Feature, Delete, etc.) only opened via right-click, which touch devices can't produce — so mobile users had no way to duplicate a post.
  2. On phones the list rendered wider than the viewport, so rows and the action button ran off the right edge.

What

1. Long-press opens the context menu on touch. A long-press on a post row opens the same context menu through the existing dropdown.toggleDropdown('context-menu', …) path — the natural touch equivalent of a right-click. No new visible UI; the menu and all its actions are reused unchanged. Details:

  • 500ms press with a 10px movement threshold, so a scroll doesn't trigger it.
  • Suppresses the synthetic "ghost" click fired on release (armed at touchend, so it works regardless of how long the press is held) and the browser's native long-press contextmenu during the hold — both of which would otherwise close the menu via the dropdown service's outside-click / onContextMenuOutside handlers.
  • Suppresses the iOS link-callout / selection magnifier on rows, scoped to touch via @media (hover: none) and (pointer: coarse).

2. Posts list fits the viewport on mobile. The title/meta block is a flex child with no min-width, so a long title or tag name held the row open and pushed the row's action button (and the whole list) past the viewport. Giving that block min-width: 0 lets it collapse; the title breaks long words and the meta line truncates with an ellipsis (≤800px).

Testing

ember test --filter "context menu" → all green, including new acceptance tests that long-press to open + duplicate a post, assert the menu survives both a native contextmenu mid-hold and the release click, and that a touch which becomes a scroll does not open the menu.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The multi-list ItemComponent gains touch long-press support for opening the context menu on touch devices. Two constants control timing and drift tolerance. New class fields track the pending timer, fired state, and touch start coordinates. Three new handlers (onTouchStart, onTouchMove, onTouchEnd) manage the timer lifecycle; onClick and onContextMenu are updated to suppress conflicting events that fire after a long-press. The template wires the four touch events with passive=true. A CSS media query suppresses native browser callout and text selection on .gh-posts-list-item-group during long-press on touch devices. Two acceptance tests cover the successful long-press path and the scroll-cancellation path.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding long-press functionality to open the posts list context menu on touch devices, which is the primary objective of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly describes the mobile context menu gap and the implementation approach, including technical details about long-press timing, ghost-click suppression, and viewport fit.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch posts-list-longpress-context-menu

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/core/test/unit/frontend/services/rss/generate-feed.test.js`:
- Around line 225-227: The comment block starting with "// The card markup we
care about lives in <content:encoded>" incorrectly states that the plain-text
<description> excerpt is generated before the card cleanup runs. Update this
comment to accurately reflect the current implementation where the excerpt is
generated after the card cleanup runs, not before. This will prevent misleading
future developers who read this test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d0eb950b-0974-4267-b831-4c52e844399a

📥 Commits

Reviewing files that changed from the base of the PR and between 46d19eb and f609874.

📒 Files selected for processing (6)
  • ghost/admin/app/components/multi-list/item.hbs
  • ghost/admin/app/components/multi-list/item.js
  • ghost/admin/app/styles/layouts/content.css
  • ghost/admin/tests/acceptance/content-test.js
  • ghost/core/core/frontend/services/rss/generate-feed.js
  • ghost/core/test/unit/frontend/services/rss/generate-feed.test.js

Comment on lines +225 to +227
// The card markup we care about lives in <content:encoded>. The feed also
// emits a plain-text <description> excerpt that is generated before the
// card cleanup runs, so assertions are scoped to content:encoded.

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the stale comment about excerpt timing.

This comment says <description> is generated before card cleanup, but the current implementation generates the excerpt after cleanup. Keeping this comment will mislead future test updates.

Suggested comment correction
-        // The card markup we care about lives in <content:encoded>. The feed also
-        // emits a plain-text <description> excerpt that is generated before the
-        // card cleanup runs, so assertions are scoped to content:encoded.
+        // The card markup we care about lives in <content:encoded>. The feed also
+        // emits a plain-text <description> excerpt, now generated from cleaned
+        // card markup, so we assert both encoded content and excerpt behavior.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The card markup we care about lives in <content:encoded>. The feed also
// emits a plain-text <description> excerpt that is generated before the
// card cleanup runs, so assertions are scoped to content:encoded.
// The card markup we care about lives in <content:encoded>. The feed also
// emits a plain-text <description> excerpt, now generated from cleaned
// card markup, so we assert both encoded content and excerpt behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/test/unit/frontend/services/rss/generate-feed.test.js` around
lines 225 - 227, The comment block starting with "// The card markup we care
about lives in <content:encoded>" incorrectly states that the plain-text
<description> excerpt is generated before the card cleanup runs. Update this
comment to accurately reflect the current implementation where the excerpt is
generated after the card cleanup runs, not before. This will prevent misleading
future developers who read this test.

no ref

- the posts list context menu (Duplicate, Feature, Delete, etc.) only opened via a right-click, which touch devices can't produce, so mobile users had no way to duplicate a post
- a long-press now opens the same menu through the existing dropdown path; it's the natural touch equivalent of a right-click and there's no row text-selection it conflicts with
- distinguishes a press from a scroll via a movement threshold, guards against the duplicate native contextmenu some touch platforms also emit, and suppresses the click that would otherwise navigate into the post
- suppresses the iOS link-callout / selection magnifier on rows, scoped to touch devices
@9larsons 9larsons force-pushed the posts-list-longpress-context-menu branch from f609874 to da1aeef Compare June 15, 2026 17:11
@nx-cloud

nx-cloud Bot commented Jun 15, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 7d84c1a

Command Status Duration Result
nx run @tryghost/admin:build ✅ Succeeded 3m 36s View ↗
nx run ghost-admin:test ✅ Succeeded 2m 39s View ↗
nx run ghost:build:assets ✅ Succeeded 2s View ↗
nx run ghost:build:tsc ✅ Succeeded 6s View ↗
nx run-many -t lint -p ghost-admin ✅ Succeeded 18s View ↗
nx run-many --target=build --projects=@tryghost... ✅ Succeeded <1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-16 15:24:21 UTC

9larsons added 9 commits June 15, 2026 12:23
no ref

- a long-press opens the menu while the finger is still down; on release the browser fires a synthetic "ghost" click that the dropdown service's body-click handler read as an outside click, closing the menu instantly
- swallow that one ghost click at the document level (capture phase) so the menu stays open; the test now simulates the release click and asserts the menu remains visible
no ref

- the ghost-click suppressor was armed when the menu opened with a fixed timeout, so it expired while the press was held and the release click still closed the menu
- arm it on touchend (release) instead, so it catches the synthetic click regardless of how long the press was held
no ref

- the browser's native long-press also fires a contextmenu event during the hold; it landed on the menu's overlay, whose onContextMenuOutside handler closed the menu mid-press (quick releases escaped it, longer holds didn't)
- suppress the native contextmenu at the document level for the duration of the touch, so only the long-press timer controls the menu
no ref

- the post title/meta block is a flex child with no min-width, so a long title or tag name couldn't shrink and held the row open, pushing the row's action button (and the whole list) past the viewport on phones
- give that block min-width: 0 so it collapses, break long title words, and truncate the meta line with an ellipsis (<=800px)
no ref

- the list container is a CSS table (display: table), which sizes to its content's width, so a long title or tag grew the whole list past the viewport no matter how the row text was constrained
- switch it to a normal block on mobile (the table columns are already hidden at this width) so it tracks the viewport; the row-text shrinking added in the previous commit now actually takes effect
…llbar on mobile

no ref

- the meta line used white-space: nowrap, so a long tag's date laid out past the viewport and clipped tight against the row's action button; let the title and meta wrap and break long tokens instead so nothing in the row can extend sideways
- gave the filter bar's horizontal scroll some bottom padding and a thin scrollbar so the bar sits below the chips rather than over them
no ref

- the macOS overlay scrollbar floated on top of the filter chips and ignores padding (overlay scrollbars reserve no gutter), so hide it; the chip row still scrolls by swipe/trackpad, matching the usual mobile chip-row pattern
…d-word

no ref

- on very narrow viewports the title column got so small that overflow-wrap broke words mid-character; give the list a 300px min-width so the text column stays usable and the page scrolls a little instead (real phones at >=360px still fit with no scroll)
no ref

- the click suppressor armed after a long-press cancelled the next click anywhere within 500ms; if the browser never emitted the synthetic release click, a fast real tap on a menu action could be discarded (review feedback)
- scope it to clicks at the touch point, where the compatibility click fires, so a real tap elsewhere always goes through even with the suppressor armed
- add a regression test that taps a menu action after touchend with no ghost click emitted; this fails on the previous eat-any-click behaviour
@9larsons 9larsons merged commit 9efa69e into main Jun 16, 2026
41 checks passed
@9larsons 9larsons deleted the posts-list-longpress-context-menu branch June 16, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant