fix(ui): selectstart blocker handles Text node event targets (#718)

* fix(ui): handle Text node targets in global selectstart blocker

selectstart can target a Text node without closest(); resolve to the
parent Element before checking inputs and data-selectable regions.

* docs(changelog): note PR #718 selectstart Text node fix

* fix(ui): narrow selectstart target to Node before parentNode

Satisfies tsc: EventTarget has no parentElement/parentNode until
narrowed with instanceof Node.
This commit is contained in:
cucadmuh
2026-05-15 16:21:38 +03:00
committed by GitHub
parent 45e0e1206f
commit e1283f4068
2 changed files with 30 additions and 5 deletions
+6
View File
@@ -498,6 +498,12 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa
* Opening **Now Playing** (sidebar, mobile route, or queue info panel) switches to the queue server before Subsonic metadata loads, with per-server fetch caches so artist/album cards do not show the wrong library.
* **Scrobble**, **now-playing report**, and **savePlayQueue** sync use the queue server as well; removing that server profile clears `queueServerId`; the mini-player bridge receives `queueServerId`; enqueue/play-next from another browsed server is blocked with a toast.
### UI — selectstart blocker no longer throws on Text node targets
**By [@cucadmuh](https://github.com/cucadmuh), PR [#718](https://github.com/Psychotoxical/psysonic/pull/718)**
* The global **`selectstart`** handler assumed `event.target` was always an **`Element`**. Starting selection inside text (e.g. Now Playing **`[data-selectable]`** copy) could pass a **Text** node and crash with **`target.closest is not a function`**. The handler now resolves Text nodes to their parent element before applying allow/deny rules.
## [1.45.0] - 2026-05-04
## Added
+24 -5
View File
@@ -1,5 +1,20 @@
import { useEffect } from 'react';
/** `Event.target` may be a Text node — only Elements expose `closest` / `tagName`. */
function eventTargetElement(e: Event): Element | null {
const t = e.target;
if (!t) return null;
if (t instanceof Element) return t;
if (!(t instanceof Node)) return null;
const parent = t.parentNode;
return parent instanceof Element ? parent : null;
}
function isTextInputElement(el: Element): boolean {
const tag = el.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || (el as HTMLElement).isContentEditable;
}
/**
* Globally tame Linux/WebKitGTK + Wayland behaviour that the page-level
* CSS / Tauri config can't reach:
@@ -29,16 +44,20 @@ export function useGlobalDndAndSelectionBlockers(): void {
const blockSelectAll = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
const target = e.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return;
const el = eventTargetElement(e);
if (el && isTextInputElement(el)) return;
e.preventDefault();
}
};
const blockSelectStart = (e: Event) => {
const target = e.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return;
if ((target as HTMLElement).closest('[data-selectable]')) return;
const el = eventTargetElement(e);
if (!el) {
e.preventDefault();
return;
}
if (isTextInputElement(el)) return;
if (el.closest('[data-selectable]')) return;
e.preventDefault();
};