fix(ui): harden download properties window

This commit is contained in:
NimBold
2026-08-06 07:08:29 +03:30
parent ee2448d084
commit 6f9a9e0638
15 changed files with 557 additions and 57 deletions
+30
View File
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';
import {
getPropertiesTabIndex,
getPropertiesTabs,
PROPERTIES_TABS_OVERFLOW_BREAKPOINT,
shouldUsePropertiesTabOverflow,
} from './propertiesTabs';
describe('Properties tabs', () => {
it('keeps torrent and normal-download sections concise and stable', () => {
expect(getPropertiesTabs(false)).toEqual(['overview', 'transfer', 'advanced']);
expect(getPropertiesTabs(true)).toEqual(['overview', 'files', 'trackers', 'peers', 'options']);
});
it('uses the responsive overflow control at narrow widths', () => {
expect(shouldUsePropertiesTabOverflow(PROPERTIES_TABS_OVERFLOW_BREAKPOINT)).toBe(true);
expect(shouldUsePropertiesTabOverflow(PROPERTIES_TABS_OVERFLOW_BREAKPOINT + 1)).toBe(false);
expect(shouldUsePropertiesTabOverflow(Number.NaN)).toBe(false);
});
it('moves through tabs according to physical direction', () => {
const tabs = getPropertiesTabs(true);
expect(getPropertiesTabIndex(tabs, 0, 'ArrowRight', 'ltr')).toBe(1);
expect(getPropertiesTabIndex(tabs, 0, 'ArrowLeft', 'ltr')).toBe(tabs.length - 1);
expect(getPropertiesTabIndex(tabs, 0, 'ArrowRight', 'rtl')).toBe(tabs.length - 1);
expect(getPropertiesTabIndex(tabs, 0, 'ArrowLeft', 'rtl')).toBe(1);
expect(getPropertiesTabIndex(tabs, 2, 'Home', 'ltr')).toBe(0);
expect(getPropertiesTabIndex(tabs, 2, 'End', 'ltr')).toBe(tabs.length - 1);
});
});
+40
View File
@@ -0,0 +1,40 @@
export type PropertiesTab = 'overview' | 'files' | 'trackers' | 'peers' | 'options' | 'transfer' | 'advanced';
const TORRENT_PROPERTIES_TABS: readonly PropertiesTab[] = [
'overview',
'files',
'trackers',
'peers',
'options',
];
const DOWNLOAD_PROPERTIES_TABS: readonly PropertiesTab[] = [
'overview',
'transfer',
'advanced',
];
export const PROPERTIES_TABS_OVERFLOW_BREAKPOINT = 620;
export const getPropertiesTabs = (isTorrent: boolean): readonly PropertiesTab[] =>
isTorrent ? TORRENT_PROPERTIES_TABS : DOWNLOAD_PROPERTIES_TABS;
export const shouldUsePropertiesTabOverflow = (width: number): boolean =>
Number.isFinite(width) && width <= PROPERTIES_TABS_OVERFLOW_BREAKPOINT;
export const getPropertiesTabIndex = (
tabs: readonly PropertiesTab[],
currentIndex: number,
key: string,
direction: 'ltr' | 'rtl',
): number => {
if (tabs.length === 0) return -1;
if (key === 'Home') return 0;
if (key === 'End') return tabs.length - 1;
if (key !== 'ArrowLeft' && key !== 'ArrowRight') return -1;
const step = key === 'ArrowRight'
? (direction === 'rtl' ? -1 : 1)
: (direction === 'rtl' ? 1 : -1);
return (currentIndex + step + tabs.length) % tabs.length;
};
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import {
PROPERTIES_URL_PREVIEW_MAX_LENGTH,
shouldOfferPropertiesUrlExpansion,
shouldResetPropertiesUrlExpansion,
} from './propertiesUrl';
describe('Properties URL disclosure', () => {
it('offers expansion only for URLs longer than the preview budget', () => {
expect(shouldOfferPropertiesUrlExpansion('x'.repeat(PROPERTIES_URL_PREVIEW_MAX_LENGTH))).toBe(false);
expect(shouldOfferPropertiesUrlExpansion('x'.repeat(PROPERTIES_URL_PREVIEW_MAX_LENGTH + 1))).toBe(true);
});
it('resets expansion when the displayed download changes', () => {
expect(shouldResetPropertiesUrlExpansion('download-1', 'download-1')).toBe(false);
expect(shouldResetPropertiesUrlExpansion('download-1', 'download-2')).toBe(true);
expect(shouldResetPropertiesUrlExpansion(null, 'download-1')).toBe(true);
});
it('resets expansion when the displayed address changes', () => {
expect(shouldResetPropertiesUrlExpansion('download-1', 'download-1', 'magnet:?xt=old', 'magnet:?xt=old')).toBe(false);
expect(shouldResetPropertiesUrlExpansion('download-1', 'download-1', 'magnet:?xt=old', 'magnet:?xt=new')).toBe(true);
});
});
+11
View File
@@ -0,0 +1,11 @@
export const PROPERTIES_URL_PREVIEW_MAX_LENGTH = 180;
export const shouldOfferPropertiesUrlExpansion = (url: string): boolean =>
url.length > PROPERTIES_URL_PREVIEW_MAX_LENGTH;
export const shouldResetPropertiesUrlExpansion = (
previousDownloadId: string | null,
nextDownloadId: string | null,
previousUrl?: string | null,
nextUrl?: string | null,
): boolean => previousDownloadId !== nextDownloadId || previousUrl !== nextUrl;
+8
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
getWindowControlRailWidth,
getWindowControlRevealOffset,
resolveWindowControlSide,
resolveWindowControlStyle,
@@ -37,6 +38,13 @@ describe('resolveWindowControlStyle', () => {
expect(getWindowControlRevealOffset('minimal')).toBe(104);
});
it('reports the shared titlebar rail width for each control style', () => {
expect(getWindowControlRailWidth('macos')).toBe(60);
expect(getWindowControlRailWidth('windows')).toBe(138);
expect(getWindowControlRailWidth('gnome')).toBe(104);
expect(getWindowControlRailWidth('minimal')).toBe(74);
});
it('resolves automatic control placement from the effective document direction', () => {
expect(resolveWindowControlSide('auto', 'ltr')).toBe('left');
expect(resolveWindowControlSide('auto', 'rtl')).toBe('right');
+10
View File
@@ -14,9 +14,19 @@ const WINDOW_CONTROL_REVEAL_OFFSETS: Record<ResolvedWindowControlStyle, number>
minimal: 104,
};
const WINDOW_CONTROL_RAIL_WIDTHS: Record<ResolvedWindowControlStyle, number> = {
macos: 60,
windows: 138,
gnome: 104,
minimal: 74,
};
export const getWindowControlRevealOffset = (style: ResolvedWindowControlStyle): number =>
WINDOW_CONTROL_REVEAL_OFFSETS[style];
export const getWindowControlRailWidth = (style: ResolvedWindowControlStyle): number =>
WINDOW_CONTROL_RAIL_WIDTHS[style];
export const resolveWindowControlSide = (
sidebarPosition: SidebarPosition,
direction: 'ltr' | 'rtl',