Files
bitvid/js/ui/overlay/popoverPanelDom.js
T
thePR0M3TH3ANandClaude Opus 4.8 1921356558 ui/popover: let panel inputs receive keys; portal in-modal panels
The shared engine treats every panel as an ARIA menu and preventDefault'd
single-char + arrow keys, so typing in a panel with inputs (the zap
comment/amount fields) was swallowed — "you can't type in the popup".
handleMenuKeydown now ignores editable targets.

Also relocate a pre-existing in-host panel (the in-modal zap dialog) into
the body-level portal on open so position:fixed is viewport-relative
instead of anchoring to the modal edge, restoring it on close. Pure DOM
helpers extracted to popoverPanelDom.js to keep the engine under its cap.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-23 14:53:38 -04:00

65 lines
2.4 KiB
JavaScript

// Pure DOM helpers for the popover engine, extracted to keep popoverEngine.js
// under its size budget.
function isElement(node) {
return Boolean(node) && typeof node === "object" && node.nodeType === 1;
}
// True when the event target is a form control / editable element. The engine
// treats panels as ARIA menus (arrow-key nav + single-char typeahead, both
// preventDefault'd); that must NOT hijack keystrokes when the user is typing in a
// panel that contains inputs (e.g. the zap comment/amount fields) — otherwise
// letters and caret keys never reach the field and "you can't type" in the popup.
export function isEditableTarget(target) {
if (!isElement(target)) {
return false;
}
const tag = typeof target.tagName === "string" ? target.tagName.toLowerCase() : "";
if (tag === "input" || tag === "textarea" || tag === "select") {
return true;
}
if (target.isContentEditable === true) {
return true;
}
const role =
typeof target.getAttribute === "function" ? target.getAttribute("role") : "";
return role === "textbox" || role === "searchbox" || role === "combobox";
}
// Move a panel into the portal (body-level overlay root) for the duration of the
// open state, so `position: fixed` is viewport-relative rather than resolving
// against a transformed/contained in-modal ancestor. Fresh render-created panels
// are already in the portal (no-op); this only relocates pre-existing, app-owned
// panels. Returns the origin { parent, nextSibling } to restore later, or null.
export function movePanelIntoPortal(panel, portal) {
if (!panel || !portal || panel.parentNode === portal) {
return null;
}
const origin = panel.parentNode
? { parent: panel.parentNode, nextSibling: panel.nextSibling }
: null;
portal.appendChild(panel);
return origin;
}
// Put a relocated app-owned panel back exactly where it came from so the host
// (e.g. the video modal) keeps an intact DOM and the popover can be re-opened.
export function restorePanelToOrigin(panel, origin) {
if (!panel || !origin) {
return;
}
const { parent, nextSibling } = origin;
if (!parent || typeof parent.insertBefore !== "function") {
return;
}
try {
if (nextSibling && nextSibling.parentNode === parent) {
parent.insertBefore(panel, nextSibling);
} else {
parent.appendChild(panel);
}
} catch (error) {
// Best-effort: a failed restore must not break close().
}
}