fix(dashboard): size the touch context menu as a card on tablets

The touch menu took 90% of the viewport, which on an iPad meant a ~920px
strip with its labels far from the row they belong to. It now stops at
400px, which every phone stays under. Its height is also measured after
the width is set, so the clamp to the viewport uses the real height.
This commit is contained in:
Nariman Jelveh committed 2026-10-22 15:18:27 -07:00
1 parent 127bf3c827
commit 619e184c14
2 files changed
+55 -4

No files matched your search

@@ -16,6 +16,9 @@ function isTouchPrimaryDevice () {
);
}
/** Widest the touch menu gets, so phones (90% of up to ~440px) are unaffected. */
const MAX_TOUCH_MENU_WIDTH = 400;
/** Labels of the destructive file actions, in the current language. */
const dangerLabels = () => new Set([i18n('delete'), i18n('delete_permanently')]);
@@ -103,13 +106,20 @@ export default class ContextMenuModal {
*/
positionModal (targetRect) {
const isMobile = isTouchPrimaryDevice();
const modalHeight = this.modal.offsetHeight;
const modalWidth = this.modal.offsetWidth;
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const margin = 20; // Minimum margin from viewport edges
const width = isMobile ? viewportWidth * 0.9 : modalWidth;
// Touch: most of a phone's width, but a phone-sized card on a tablet
// rather than a strip spanning the screen. Applied before measuring
// the height, which depends on how the labels wrap at this width.
if ( isMobile ) {
this.modal.style.width = `${Math.min(viewportWidth * 0.9, MAX_TOUCH_MENU_WIDTH)}px`;
} else {
this.modal.style.width = 'auto';
}
const modalHeight = this.modal.offsetHeight;
const width = this.modal.offsetWidth;
// Center over the target horizontally, clamped to the viewport. (The
// old code hardcoded left:300px on non-touch devices — which used to
@@ -131,7 +141,6 @@ export default class ContextMenuModal {
this.modal.style.top = `${top}px`;
this.modal.style.left = `${left}px`;
this.modal.style.width = isMobile ? '90%' : 'auto';
}
/**
@@ -0,0 +1,42 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest';
globalThis.i18n = (key) => key;
const { default: ContextMenuModal } = await import('./ContextMenu.js');
const setTouchViewport = (width, touch = true) => {
window.innerWidth = width;
window.innerHeight = 800;
window.matchMedia = (query) => ({
matches: touch && (query === '(pointer: coarse)' || query === '(hover: none)'),
});
};
const openMenu = () => {
const menu = new ContextMenuModal();
menu.show([{ label: 'Open', action: () => {} }], { left: 100, top: 100, width: 200, height: 30 });
return document.querySelector('.context-menu-modal-dialog');
};
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
describe('ContextMenuModal width', () => {
it('takes 90% of a phone screen', () => {
setTouchViewport(390);
expect(openMenu().style.width).toBe('351px');
});
it('stops at a card width on a tablet instead of spanning the screen', () => {
setTouchViewport(1024);
expect(openMenu().style.width).toBe('400px');
});
it('sizes to its content for a mouse', () => {
setTouchViewport(1024, false);
expect(openMenu().style.width).toBe('auto');
});
});