Files
Multi-Debrid-Downloader/src/renderer/selection.ts
T
Sucukdeluxe 90fc77f203 fix(accounts): release row focus after Escape
Blur the focused account row only when Escape clears an active account selection. This prevents Chromium focus-visible styling from leaving a thick clipped blue outline while retaining accessible focus indicators during normal keyboard navigation.
2026-08-15 02:00:54 +02:00

66 lines
2.3 KiB
TypeScript

import type { SessionState } from "../shared/types";
const DOWNLOAD_SELECTION_PRESERVE_SELECTOR = ".downloads-package-card, .downloads-item-row, .downloads-selection-cell, .package-card, .ctx-menu, .modal-backdrop, .modal-card";
export function shouldClearDownloadSelection(target: Pick<Element, "closest">): boolean {
return !target.closest(DOWNLOAD_SELECTION_PRESERVE_SELECTOR);
}
export function shouldClearDownloadSelectionOnEscape(tagName: string, inputType = ""): boolean {
const normalizedTag = tagName.toUpperCase();
if (normalizedTag === "TEXTAREA") return false;
if (normalizedTag !== "INPUT") return true;
return ["checkbox", "radio", "button"].includes(inputType.toLowerCase());
}
export function resolveEscapeSelectionScope(
view: string,
settingsSection: string,
tagName: string,
inputType = ""
): "downloads" | "history" | "accounts" | null {
if (!shouldClearDownloadSelectionOnEscape(tagName, inputType)) {
return null;
}
if (view === "downloads" || view === "history") {
return view;
}
return view === "settings" && settingsSection === "accounts" ? "accounts" : null;
}
export function releaseAccountSelectionFocus(
activeElement: { closest: (selector: string) => unknown; blur: () => void } | null
): boolean {
if (!activeElement?.closest(".settings-account-row")) {
return false;
}
activeElement.blur();
return true;
}
/**
* Drop selected ids whose package OR item no longer exists in the session.
* The selection set mixes package and item ids; when entries vanish (delta
* removal, backup-driven session swap, completed-cleanup) a stale id would
* otherwise inflate the selection count and the "(N)" action labels and keep
* "multi" styling alive for ghosts.
*
* Returns the SAME set instance when nothing changed, so callers can use it
* directly as a React state updater without forcing a re-render.
*/
export function pruneSelection(
selected: ReadonlySet<string>,
session: Pick<SessionState, "packages" | "items">
): Set<string> {
if (selected.size === 0) {
return selected as Set<string>;
}
const next = new Set<string>();
for (const id of selected) {
if (session.packages[id] || session.items[id]) {
next.add(id);
}
}
return next.size === selected.size ? (selected as Set<string>) : next;
}