fix: review round 1 — row press after a rename, related reset, dead items on removal, honest Prev/Next
- A press that begins while a field in the row is edited only ends the editing (rowOpen latches it at pointerdown); the card layout's row press is now tested. - A related video reset in the player forgets the session's stored position. - Removing the playing video passes over known-dead items and steps the way it went. - Prev/Next off only because of dead items stay focusable and say why (HU/EN). - The docked bar's scroller leaves room for focus rings. - A focus-opened About card is not closed by its own scroll into view. - EN "Clear watch progress" for the video reset, distinct from a page's "Reset view". - The signed-out title read goes to the page yt-dlp read; the retitle writes are conditional. - Tests: the cap of three errors, the bar's own Fullscreen, the group glyph, the backfill's reads.
This commit is contained in:
@@ -9,18 +9,25 @@ import { endsTextSelection, movedPastSlop } from "../../lib/textSelection";
|
||||
// - a click that ends a text selection, or a press that travelled (a drag across the row's padding):
|
||||
// DESIGN.md "Text stays selectable" — no play or open fires;
|
||||
// - an event from a portal (a popover a cell opened): React bubbles it through the row, the DOM does
|
||||
// not, so only a target the row really contains counts.
|
||||
// not, so only a target the row really contains counts;
|
||||
// - a press that began while a field inside the row held the focus (its rename): that press ends the
|
||||
// editing — its mousedown blurs the field, which saves — and does nothing more. Read at pointerdown,
|
||||
// because by the click the blur has already put the row back as it is when nothing is edited.
|
||||
// The long-press menu's trailing click is swallowed by its own trigger (`useContextMenu`), before it
|
||||
// reaches the row.
|
||||
|
||||
// A field the focus can be editing in.
|
||||
const FIELD = 'input, textarea, select, [contenteditable="true"]';
|
||||
|
||||
// What counts as a control of its own. `[tabindex]` covers a focusable cell (a hover card's trigger);
|
||||
// the row's own roving tab stop is the row itself, which the containment check below leaves out.
|
||||
const CONTROL =
|
||||
'a, button, input, select, textarea, label, summary, [role="button"], [role="menuitem"], [role="checkbox"], [role="switch"], [contenteditable="true"], [tabindex]';
|
||||
|
||||
// Where each row's press began, for the drag slop. Per element: the table row and its card twin are
|
||||
// two elements, and a press on one must not be measured against the other.
|
||||
const pressedAt = new WeakMap<Element, { x: number; y: number }>();
|
||||
// Where each row's press began, for the drag slop, and whether it began while a field inside the row
|
||||
// was being edited. Per element: the table row and its card twin are two elements, and a press on one
|
||||
// must not be measured against the other.
|
||||
const pressedAt = new WeakMap<Element, { x: number; y: number; editing: boolean }>();
|
||||
|
||||
type Handlers = {
|
||||
onPointerDown?: (e: PointerEvent<HTMLElement>) => void;
|
||||
@@ -39,13 +46,18 @@ export function withRowOpen(
|
||||
return {
|
||||
...base,
|
||||
onPointerDown: (e: PointerEvent<HTMLElement>) => {
|
||||
pressedAt.set(e.currentTarget, { x: e.clientX, y: e.clientY });
|
||||
const row = e.currentTarget;
|
||||
const focused = document.activeElement;
|
||||
const editing = !!focused && row.contains(focused) && focused.matches(FIELD);
|
||||
pressedAt.set(row, { x: e.clientX, y: e.clientY, editing });
|
||||
onPointerDown?.(e);
|
||||
},
|
||||
onClick: (e: MouseEvent<HTMLElement>) => {
|
||||
onClick?.(e);
|
||||
if (e.defaultPrevented || !claims(e)) return;
|
||||
open();
|
||||
const claimed = !e.defaultPrevented && claims(e);
|
||||
// One press, one click: a later keyboard click (no press) is not judged by this one.
|
||||
pressedAt.delete(e.currentTarget);
|
||||
if (claimed) open();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -56,5 +68,6 @@ function claims(e: MouseEvent<HTMLElement>): boolean {
|
||||
if (!(target instanceof Element) || !row.contains(target)) return false;
|
||||
const control = target.closest(CONTROL);
|
||||
if (control && control !== row && row.contains(control)) return false;
|
||||
return !endsTextSelection(e) && !movedPastSlop(pressedAt.get(row) ?? null, e);
|
||||
const pressed = pressedAt.get(row) ?? null;
|
||||
return !pressed?.editing && !endsTextSelection(e) && !movedPastSlop(pressed, e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user