mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-23 22:47:19 +00:00
fix: route dashboard file menus by input modality, not touch capability
A mouse right-click on a file row in the dashboard Files tab opened the mobile context-menu sheet (centered over the row, nowhere near the cursor) on any touch-capable laptop or desktop, because phone/tablet detection used navigator.maxTouchPoints > 0. The same check applied the .touch-device class, degrading mouse selection and drag on those machines. Route by how the menu was invoked (the row's tracked pointer type) plus isTouchPrimaryDevice() — coarse pointer and no hover — which is false on touch-capable laptops but true on iPads whose UA claims macOS. Also let sidebar-folder and background menus accept taphold on such iPads, where they were previously unreachable (taphold dismissed, and iOS never fires native contextmenu).
This commit is contained in:
@@ -109,9 +109,11 @@ export default class ContextMenuModal {
|
||||
const width = isMobile ? viewportWidth * 0.9 : modalWidth;
|
||||
|
||||
// Center over the target horizontally, clamped to the viewport. (The
|
||||
// old code hardcoded left:300px on non-touch devices — which route here
|
||||
// whenever maxTouchPoints > 0, e.g. touchscreen laptops — so the menu
|
||||
// opened nowhere near the item on a wide screen.)
|
||||
// old code hardcoded left:300px on non-touch devices — which used to
|
||||
// route here whenever maxTouchPoints > 0, e.g. touchscreen laptops —
|
||||
// so the menu opened nowhere near the item on a wide screen. Mouse
|
||||
// interactions now get the desktop UIContextMenu instead; this modal
|
||||
// is only for touch invocations and touch-first devices.)
|
||||
const itemCenter = targetRect.left + (targetRect.width / 2);
|
||||
let left = itemCenter - width / 2;
|
||||
left = Math.max(margin, Math.min(left, viewportWidth - width - margin));
|
||||
|
||||
@@ -30,7 +30,7 @@ import update_title_based_on_uploads from '../../helpers/update_title_based_on_u
|
||||
import item_icon from '../../helpers/item_icon.js';
|
||||
import new_context_menu_item from '../../helpers/new_context_menu_item.js';
|
||||
import publish_as_website from '../../helpers/publish_as_website.js';
|
||||
import ContextMenuModal from './ContextMenu/ContextMenu.js';
|
||||
import ContextMenuModal, { isTouchPrimaryDevice } from './ContextMenu/ContextMenu.js';
|
||||
import UIItemPropertiesModal from './UIItemPropertiesModal.js';
|
||||
import { dedupedName } from './dedupedName.js';
|
||||
import { isEntryVisible, isHiddenName, showHiddenFiles } from './hiddenFiles.js';
|
||||
@@ -306,9 +306,12 @@ const TabFiles = {
|
||||
modified: 120,
|
||||
};
|
||||
|
||||
// Add touch-device class for touch devices to show .item-more button
|
||||
// Use multiple detection methods since user-agent sniffing can miss devices
|
||||
if ( window.isMobile.phone || window.isMobile.tablet || navigator.maxTouchPoints > 0 ) {
|
||||
// Add touch-device class on touch-FIRST devices only (coarse pointer,
|
||||
// no hover — which also catches iPads whose UA claims macOS).
|
||||
// Deliberately not maxTouchPoints: a touch-capable laptop is still
|
||||
// mouse-first, and this class strips pointer-events from the
|
||||
// name/icon drag handles, degrading mouse selection and drag.
|
||||
if ( window.isMobile.phone || window.isMobile.tablet || isTouchPrimaryDevice() ) {
|
||||
$el_window.find('.files-tab').addClass('touch-device');
|
||||
}
|
||||
|
||||
@@ -322,9 +325,11 @@ const TabFiles = {
|
||||
_this.renderDirectory(folderPath);
|
||||
};
|
||||
|
||||
// Context menu for sidebar folders
|
||||
// Context menu for sidebar folders. isTouchPrimaryDevice() covers
|
||||
// touch-first devices the UA misses (iPadOS claims macOS) — both
|
||||
// for accepting the taphold and for picking the touch sheet.
|
||||
$(folderElement).on('contextmenu taphold', async (e) => {
|
||||
if ( e.type === 'taphold' && !window.isMobile.phone && !window.isMobile.tablet ) {
|
||||
if ( e.type === 'taphold' && !window.isMobile.phone && !window.isMobile.tablet && !isTouchPrimaryDevice() ) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
@@ -333,7 +338,7 @@ const TabFiles = {
|
||||
const folderPath = folderElement.getAttribute('data-path');
|
||||
const items = _this.generateFolderContextMenu(folderPath);
|
||||
|
||||
if ( window.isMobile.phone || window.isMobile.tablet ) {
|
||||
if ( window.isMobile.phone || window.isMobile.tablet || isTouchPrimaryDevice() ) {
|
||||
const modal = new ContextMenuModal({
|
||||
onClose: () => $(folderElement).removeClass('context-menu-active'),
|
||||
});
|
||||
@@ -532,8 +537,9 @@ const TabFiles = {
|
||||
|
||||
// Right-click on background shows folder context menu
|
||||
$el_window.find('.files').on('contextmenu taphold', async (e) => {
|
||||
// Dismiss taphold on non-touch devices
|
||||
if ( e.type === 'taphold' && !window.isMobile.phone && !window.isMobile.tablet ) {
|
||||
// Dismiss taphold on non-touch devices (isTouchPrimaryDevice
|
||||
// catches iPads whose UA claims macOS)
|
||||
if ( e.type === 'taphold' && !window.isMobile.phone && !window.isMobile.tablet && !isTouchPrimaryDevice() ) {
|
||||
return;
|
||||
}
|
||||
// Only trigger if clicking directly on .files container (not on a row)
|
||||
@@ -548,7 +554,7 @@ const TabFiles = {
|
||||
});
|
||||
_this.updateFooterStats();
|
||||
const items = await _this.generateFolderContextMenu();
|
||||
if ( window.isMobile.phone || window.isMobile.tablet ) {
|
||||
if ( window.isMobile.phone || window.isMobile.tablet || isTouchPrimaryDevice() ) {
|
||||
const modal = new ContextMenuModal();
|
||||
modal.show(items, e.target.getBoundingClientRect());
|
||||
} else {
|
||||
@@ -2522,12 +2528,14 @@ const TabFiles = {
|
||||
const isPending = () => el_item.getAttribute('data-pending') === '1';
|
||||
|
||||
el_item.onpointerdown = (e) => {
|
||||
// Track pointer type so onclick and the menu handlers can
|
||||
// distinguish touch from mouse — recorded before the early
|
||||
// returns because the '⋯' menu routing needs it too.
|
||||
lastPointerType = e.pointerType;
|
||||
|
||||
if ( e.target.classList.contains('item-more') ) return;
|
||||
if ( el_item.classList.contains('header') ) return;
|
||||
|
||||
// Track pointer type so onclick can distinguish touch from mouse.
|
||||
lastPointerType = e.pointerType;
|
||||
|
||||
// On touch devices, skip all selection logic here.
|
||||
// Taps are handled by onclick (opens item) and taphold (context menu),
|
||||
// so pointerdown never accidentally selects while the user is scrolling.
|
||||
@@ -2666,7 +2674,7 @@ const TabFiles = {
|
||||
el_item.onclick = (e) => {
|
||||
if ( e.target.classList.contains('item-more') ) {
|
||||
if ( isPending() ) return;
|
||||
this.handleMoreClick(el_item, file, e.target);
|
||||
this.handleMoreClick(el_item, file, e.target, lastPointerType === 'touch');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2840,8 +2848,10 @@ const TabFiles = {
|
||||
|
||||
// Right-click context menu handler (desktop) and taphold (touch devices)
|
||||
$(el_item).on('contextmenu taphold', async (e) => {
|
||||
// Dismiss taphold on non-touch devices
|
||||
if ( e.type === 'taphold' && !window.isMobile.phone && !window.isMobile.tablet && !(navigator.maxTouchPoints > 0) ) {
|
||||
// A taphold only counts when it came from an actual touch — the
|
||||
// plugin (helpers.js) also fires it for a 1s mouse-button hold,
|
||||
// which must stay inert, touchscreen or not.
|
||||
if ( e.type === 'taphold' && lastPointerType !== 'touch' && !window.isMobile.phone && !window.isMobile.tablet ) {
|
||||
return;
|
||||
}
|
||||
// On iOS, both contextmenu and taphold can fire for the same long-press.
|
||||
@@ -2866,7 +2876,13 @@ const TabFiles = {
|
||||
items = await _this.generateContextMenuItems(el_item, file);
|
||||
}
|
||||
|
||||
if ( window.isMobile.phone || window.isMobile.tablet || navigator.maxTouchPoints > 0 ) {
|
||||
// The touch sheet is for touch interactions and touch-first
|
||||
// devices. A mouse right-click gets the desktop menu at the
|
||||
// cursor — including on touch-capable laptops, whose right-clicks
|
||||
// used to land in the sheet (centered over the row, nowhere near
|
||||
// the pointer) via a maxTouchPoints check.
|
||||
const touchInvoked = e.type === 'taphold' || lastPointerType === 'touch';
|
||||
if ( window.isMobile.phone || window.isMobile.tablet || isTouchPrimaryDevice() || touchInvoked ) {
|
||||
const modal = new ContextMenuModal();
|
||||
modal.show(items, el_item.getBoundingClientRect(), { title: file.name });
|
||||
} else {
|
||||
@@ -3763,7 +3779,7 @@ const TabFiles = {
|
||||
};
|
||||
},
|
||||
|
||||
async handleMoreClick (rowElement, file, targetElement) {
|
||||
async handleMoreClick (rowElement, file, targetElement, fromTouch) {
|
||||
const selectedRows = document.querySelectorAll('.files-tab .row.selected');
|
||||
|
||||
let items;
|
||||
@@ -3774,8 +3790,10 @@ const TabFiles = {
|
||||
items = await this.generateContextMenuItems(rowElement, file);
|
||||
}
|
||||
|
||||
// Use mobile-friendly context menu on touch devices
|
||||
if ( window.isMobile.phone || window.isMobile.tablet || navigator.maxTouchPoints > 0 ) {
|
||||
// The touch sheet for touch taps and touch-first devices; a mouse
|
||||
// click gets the desktop menu anchored to the button, also on
|
||||
// touch-capable laptops.
|
||||
if ( window.isMobile.phone || window.isMobile.tablet || isTouchPrimaryDevice() || fromTouch ) {
|
||||
const targetRect = targetElement.getBoundingClientRect();
|
||||
const modal = new ContextMenuModal();
|
||||
modal.show(items, targetRect, { title: file.name });
|
||||
|
||||
Reference in New Issue
Block a user