From 4b96834d19e62056af4f04ef0beba8ecd104d28f Mon Sep 17 00:00:00 2001 From: xarmian Date: Fri, 17 Jul 2026 23:25:24 -0400 Subject: [PATCH] feat(web): click list-column dead space to collapse the item pane (IDEA-2148) (#960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While the split detail pane is open on the desktop split, a primary click on genuinely dead space anywhere in the list column now collapses the pane — the pointer analogue of ESC-to-close. A click on any interactive/meaningful surface (a card, a button, a form control, an ARIA-roled widget, the resize divider) is left to that surface's own handler and never collapses: the handler bails via a denylist `closest()` check, and because those handlers run first on bubbling, re-targeting to another card or firing a button never races the close. Guards skip modifier/aux clicks, drag-to-select, and scrollbar-gutter clicks. Excluded on mobile (the list column is `inert` behind the full-screen overlay). Verified with Playwright across list, board, and table views: dead-space clicks close; card / other-card / header-button / table-row clicks keep the pane open. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra --- .../[workspace]/[collection]/+page.svelte | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/web/src/routes/[username]/[workspace]/[collection]/+page.svelte b/web/src/routes/[username]/[workspace]/[collection]/+page.svelte index d79ae8db..6730a68a 100644 --- a/web/src/routes/[username]/[workspace]/[collection]/+page.svelte +++ b/web/src/routes/[username]/[workspace]/[collection]/+page.svelte @@ -515,6 +515,53 @@ }); } + // Click-outside-to-collapse the split pane (IDEA-2148). While the pane is + // open on the DESKTOP split, a primary click on genuinely DEAD space in the + // list column collapses the pane — the pointer analogue of ESC-to-close. + // A click on ANY interactive/meaningful surface (a card, a control, a menu, + // the resize divider) belongs to that surface's own handler and must NOT + // collapse, so we bail whenever the click lands on — or inside — one; those + // handlers run first (bubbling), so re-targeting to another card or firing a + // button never races the close. Excluded on mobile: the list column is + // `inert` behind the full-screen overlay there so this can't fire anyway, + // but the viewport guard makes the intent explicit. + // + // This is a DENYLIST (bail on interactive) rather than a background + // allowlist: interactive elements are semantic and few (anchors, buttons, + // form controls, ARIA-roled widgets), so enumerating them is smaller and + // more stable than enumerating every background wrapper across list / board + // / table. A `[data-pane-keep]` opt-out lets any future non-semantic click + // surface exclude itself without editing this list. + const PANE_DISMISS_INTERACTIVE = + 'a, button, input, textarea, select, label, summary, ' + + '[role="button"], [role="link"], [role="row"], [role="tab"], ' + + '[role="menuitem"], [role="menu"], [role="menuitemradio"], ' + + '[role="menuitemcheckbox"], [role="listbox"], [role="option"], ' + + '[role="checkbox"], [role="switch"], [role="separator"], ' + + '[contenteditable="true"], [draggable="true"], ' + + '.item-card, .table-row, .pane-divider, [data-pane-keep]'; + + function dismissPaneOnBackgroundClick(e: MouseEvent) { + // Desktop split pane only. + if (!openItemRef || viewport.isMobile) return; + // Primary, unmodified click only. Modifier/aux clicks are for + // selection, context menus, open-in-new-tab, etc. — not "click away". + if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; + const target = e.target as Element | null; + if (!target) return; + // A click on any interactive/meaningful surface is that surface's own. + if (target.closest(PANE_DISMISS_INTERACTIVE)) return; + // A drag that SELECTED text also ends in a `click`; that's a text + // selection, not a dismiss. + const sel = browser ? window.getSelection() : null; + if (sel && !sel.isCollapsed && sel.toString().trim() !== '') return; + // A click on the scroll gutter lands on the list column itself but + // outside its content box — not a background click. + const col = e.currentTarget as HTMLElement | null; + if (col && target === col && e.offsetX > col.clientWidth) return; + closeItemPane(); + } + // Move keyboard focus back to the paned row in the list (TASK-2122). Backs // the desktop ESC "back to the list" step — the pane stays OPEN, this only // relocates focus. Returns false when there's no focused row to land on (e.g. @@ -2779,7 +2826,18 @@ screen-reader accessibility tree — a JS focus trap alone can't constrain an SR virtual cursor, and inert also blocks Cmd+F from focusing the hidden search box. Desktop split leaves the list fully interactive. --> -
+ + + +
{#if loading}
Loading...
{:else if metaError}