mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-12 09:24:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f07220649 |
No files matched your search
@@ -281,6 +281,40 @@ test("duplicate double-click behavior falls back to copying when unavailable", (
|
||||
assert.deepEqual(copiedSessionIds, ["session-1"]);
|
||||
});
|
||||
|
||||
test("draggable top-tab close buttons cancel the drag gesture on mousedown", () => {
|
||||
const closeButtonLabels = [
|
||||
"tabs.closePluginViewAria",
|
||||
"Close editor tab",
|
||||
"tabs.closeSessionAria",
|
||||
"tabs.closeLogViewAria",
|
||||
];
|
||||
|
||||
for (const label of closeButtonLabels) {
|
||||
const labelIndex = topTabItemsSource.indexOf(label);
|
||||
assert.notEqual(labelIndex, -1, `missing close label ${label}`);
|
||||
const buttonStart = topTabItemsSource.lastIndexOf("<button", labelIndex);
|
||||
const buttonEnd = topTabItemsSource.indexOf(">", labelIndex);
|
||||
const openingTag = topTabItemsSource.slice(buttonStart, buttonEnd);
|
||||
assert.match(
|
||||
openingTag,
|
||||
/onMouseDown=\{handleTabCloseButtonMouseDown\}/,
|
||||
`${label} close button must guard mousedown`,
|
||||
);
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
topTabItemsSource.match(/onMouseDown=\{handleTabCloseButtonMouseDown\}/g)?.length,
|
||||
4,
|
||||
);
|
||||
|
||||
const workspaceSource = topTabItemsSource.slice(
|
||||
topTabItemsSource.indexOf("export const WorkspaceTopTab"),
|
||||
topTabItemsSource.indexOf("WorkspaceTopTab.displayName"),
|
||||
);
|
||||
assert.equal(workspaceSource.includes("handleTabCloseButtonMouseDown"), false);
|
||||
assert.equal(workspaceSource.includes("<button"), false);
|
||||
});
|
||||
|
||||
test("session close button double click stays on the close button", () => {
|
||||
let stopPropagationCount = 0;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { cn } from '../../lib/utils';
|
||||
import { Host, TerminalSession, Workspace } from '../../types';
|
||||
import { DISTRO_LOGOS, DISTRO_COLORS } from '../DistroAvatar';
|
||||
import { getShellIconPath, isMonochromeShellIcon } from '../../lib/useDiscoveredShells';
|
||||
import { handleTabMiddleClickClose, handleTabMiddleMouseDown } from '../../lib/tabInteractions';
|
||||
import { handleTabCloseButtonMouseDown, handleTabMiddleClickClose, handleTabMiddleMouseDown } from '../../lib/tabInteractions';
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from '../ui/context-menu';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
import { SessionTabContextMenuContent } from './SessionTabContextMenuContent';
|
||||
@@ -555,7 +555,7 @@ export const PluginViewTopTab: React.FC<PluginViewTopTabProps> = memo(({
|
||||
: <PluginContributionIcon pluginId={tab.pluginId} icon={tab.icon} className="shrink-0" />}
|
||||
<span className="truncate leading-5">{tab.title}</span>
|
||||
</div>
|
||||
<button onClick={close} className="flex h-5 w-5 shrink-0 items-center justify-center rounded hover:bg-muted" aria-label={t('tabs.closePluginViewAria', { title: tab.title })}><X size={12} /></button>
|
||||
<button onMouseDown={handleTabCloseButtonMouseDown} onClick={close} className="flex h-5 w-5 shrink-0 items-center justify-center rounded hover:bg-muted" aria-label={t('tabs.closePluginViewAria', { title: tab.title })}><X size={12} /></button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
@@ -704,6 +704,7 @@ export const EditorTopTab: React.FC<EditorTopTabProps> = memo(({
|
||||
{isDetached && <SquareArrowOutUpRight size={11} className="shrink-0 opacity-70" />}
|
||||
</div>
|
||||
<button
|
||||
onMouseDown={handleTabCloseButtonMouseDown}
|
||||
onClick={handleClose}
|
||||
className="p-1 rounded-full hover:bg-destructive/10 hover:text-destructive transition-colors"
|
||||
aria-label="Close editor tab"
|
||||
@@ -879,6 +880,7 @@ export const SessionTopTab: React.FC<SessionTopTabProps> = memo(({
|
||||
<div className="flex-shrink-0">{sessionStatusDot(session.status, hasActivity)}</div>
|
||||
</div>
|
||||
<button
|
||||
onMouseDown={handleTabCloseButtonMouseDown}
|
||||
onClick={(e) => onCloseSession(session.id, e)}
|
||||
onDoubleClick={stopCloseButtonDoubleClickPropagation}
|
||||
className="p-1 rounded-full hover:bg-destructive/10 hover:text-destructive transition-colors"
|
||||
@@ -1228,6 +1230,7 @@ export const LogViewTopTab: React.FC<LogViewTopTabProps> = memo(({
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onMouseDown={handleTabCloseButtonMouseDown}
|
||||
onClick={handleClose}
|
||||
className="p-1 rounded-full hover:bg-destructive/10 hover:text-destructive transition-colors"
|
||||
aria-label={t('tabs.closeLogViewAria')}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type React from "react";
|
||||
|
||||
import {
|
||||
MIDDLE_MOUSE_BUTTON,
|
||||
handleTabCloseButtonMouseDown,
|
||||
handleTabMiddleClickClose,
|
||||
handleTabMiddleMouseDown,
|
||||
} from "./tabInteractions.ts";
|
||||
@@ -64,3 +65,17 @@ test("handleTabMiddleMouseDown suppresses autoscroll only for the middle button"
|
||||
handleTabMiddleMouseDown(left.event);
|
||||
assert.equal(left.calls.preventDefault, 0);
|
||||
});
|
||||
|
||||
test("handleTabCloseButtonMouseDown blocks ancestor drag only for the primary button", () => {
|
||||
const left = makeEvent(0);
|
||||
handleTabCloseButtonMouseDown(left.event);
|
||||
assert.equal(left.calls.preventDefault, 1);
|
||||
assert.equal(left.calls.stopPropagation, 1);
|
||||
|
||||
for (const button of [MIDDLE_MOUSE_BUTTON, 2]) {
|
||||
const other = makeEvent(button);
|
||||
handleTabCloseButtonMouseDown(other.event);
|
||||
assert.equal(other.calls.preventDefault, 0, `button ${button} must not preventDefault`);
|
||||
assert.equal(other.calls.stopPropagation, 0, `button ${button} must not stopPropagation`);
|
||||
}
|
||||
});
|
||||
@@ -18,6 +18,20 @@ export const handleTabMiddleMouseDown = (e: React.MouseEvent): void => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Keep a primary-button press on a tab close control from starting an HTML5
|
||||
* drag on the ancestor `draggable` tab. Chromium promotes a few pixels of
|
||||
* movement between mousedown and mouseup into `dragstart`, which swallows the
|
||||
* click (#3886). `preventDefault` stops that drag; `stopPropagation` keeps the
|
||||
* tab's own mousedown handler from seeing the press. Middle and right buttons
|
||||
* are left alone so aux-click close and context menus stay unchanged.
|
||||
*/
|
||||
export const handleTabCloseButtonMouseDown = (e: React.MouseEvent): void => {
|
||||
if (e.button !== 0) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
/**
|
||||
* Close a tab when it is middle-clicked. Wire to `onAuxClick`, which fires for
|
||||
* a completed non-primary click. Left clicks (tab activation) and right clicks
|
||||
|
||||
Reference in new issue
Block a user