Skip to content

fix(table): a table is scrolled sideways from its header too; dragToScroll pulls it with the mouse — 0.78.3 - #710

Closed
davidnich wants to merge 9 commits into
developfrom
bugfix/table-header-scrolls-the-table
Closed

davidnich wants to merge 9 commits into
developfrom
bugfix/table-header-scrolls-the-table

Conversation

@davidnich

@davidnich davidnich commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

From David's review of qorus#646.

Stacked on #708: #708 is merged into this branch, so its focus-popover and rich-text fixes show in this diff until #708 lands. Merge #708 first; this PR then shows only its own change.

Symptom. On a phone, a swipe on a wide table's header did not scroll the table, though the same swipe on its rows did. With the mouse, a table wider than its frame could only be scrolled by its scrollbar or Shift+wheel; the IDE had built its own drag-to-scroll around reqore tables, which could not reach the header either.

Root cause. The header is scrolled with the body (the body syncs headerRef.scrollLeft), but it is not a scroller itself (overflow: hidden). The browser has nothing to pan under a finger there, and the header forwarded only the wheel (deltaX) to the body.

Fix

  • Header swipes: the header takes sideways touch swipes (touch-action: pan-y, so the page still pans up and down) and scrolls the body with them.
  • New dragToScroll prop (default false): a mouse or pen presses and pulls the table by its body or its header.
    • A press that does not move is still a click: a header cell keeps opening its options and runs its column's onClick.
    • The click that ends a real drag is not delivered.
    • A press on a text field is the field's.
  • One gesture implementation: the gesture is ReqoreFadeScroller's dragToScroll, moved into a shared useDragToScroll hook that both components use. The hook rebinds when the element it scrolls appears after mount, since a table renders no body until it has rows.

Also in this PR

  • Shift with a plain wheel over the header scrolls the table sideways, as it does over the body. Before, only a wheel's deltaX was forwarded.
  • With dragToScroll the body shows a grab cursor, and grabbing with no text selection while it is pulled.
  • A finger's swipe kept going: a real swipe on a phone moved the table about 5px and stopped. A touch pointer is captured by the element it lands on; taking the capture for the header made that element report lostpointercapture, and the gesture ended on it. Now only the capture the gesture itself took ends it. The dispatched-event tests could not see this; it was found with real touch events, and a test now covers it.

Tests

  • __tests__/table-drag-to-scroll.test.tsx: 9 tests. 4 of the first 6 fail against develop's Table: the touch swipe on the header, the header's touch-action, and mouse pulls by the header and by the body.
  • The FadeScroller drag tests are unchanged and pass.
  • New story Collections/Table · Drag To Scroll: it pulls the table by its header with the mouse and with a finger, and checks the header follows the body.
  • yarn precheck passes.

Docs: COMPONENTS.md, ReqoreTable row.

Version: 0.78.3, because #708 is 0.78.1 and #709 is to be 0.78.2. The new prop is a patch bump.

🤖 Generated with Claude Code

…mounts — 0.77.9

A popover with handler='focus' listened for focusin only. A field focused
as it mounts - an editor that opens with the cursor in its field, as a
read-first form row does when it is clicked - was focused before the
listener was attached, so no focusin came and its list stayed closed. The
user then clicked in the field, which already had the focus, and still
saw nothing until they left it and came back. In the Qorus IDE this hid
every template a typed field offers (qorus#646: an added field of type
Whole number showed no fields of the record to refer to).

When the listener is attached, a focus popover whose field already has the
focus opens.

0.77.9: 0.77.8 is taken by #696.
… committed, so a key typed after it is kept

The editor compared a value it was given with its document in a passive
effect, which the browser runs after painting - under load, after the next
key. A value that matched the document when it was rendered was then
compared with a document that had a key more, taken as a value from
outside, and put in its place: the key was lost and the caret went to the
end. reqraft's SmartEditor › With Markdown Docs typed "arr.for" and got
"arr.fr" in CI; under 6x CPU throttling it lost a key in most runs.

The comparison runs in a layout effect, before the browser handles another
key. A value from outside still replaces the document.

richTextEditorValueSync.test.tsx types a key between a value's commit and
its passive effects: lost before, kept now.
…s a focus list again — 0.78.1

Foxhoundn's review of #708: the check for a field focused as it mounts ran
on every re-run of the listener effect, and the effect re-runs whenever
the list opens or closes (it depends on `open`, which depends on
`isOpen`). Closing the list with the cursor still in the field - Escape,
or a click on it with closeOnTargetClick - re-ran it, found the field
focused, and reopened the list at once. The check is made once per
trigger, remembered in a ref, as the listener is first attached.

Test: a list closed with Escape stays closed while the field keeps the
focus (fails without the change).

0.78.1: #696 goes in first as 0.78.0.
…croll pulls it with the mouse — 0.78.3

From David's review of qorus#646: on a phone, a swipe on a wide table's
header did not scroll it, while the same swipe on its rows did. The header
is scrolled WITH the body (the body syncs it) but is not a scroller of its
own (overflow hidden), so the browser has nothing to pan, and nothing
forwarded the swipe; only the wheel was forwarded.

- The header now takes sideways touch swipes (touch-action: pan-y; the page
  still pans up and down) and scrolls the body with them.
- New `dragToScroll` (default false): a mouse or pen presses and pulls the
  table by its body or its header. A press that does not move is still a
  click (a header cell opens its options), the click that ends a real drag
  is not delivered, and a press on a text field is the field's.
- The gesture is FadeScroller's dragToScroll, moved into a shared
  useDragToScroll hook that both use, so a fix to it reaches both. It
  rebinds when the element it scrolls appears after mount (a table renders
  no body until it has rows).

Tests: table-drag-to-scroll (4 of 6 fail on develop), the FadeScroller
drag tests unchanged and passing; story Collections/Table · Drag To
Scroll; COMPONENTS.md.
@davidnich
davidnich requested a review from Foxhoundn as a code owner October 7, 2026 15:24
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

📦 Prerelease published for this PR:

@qoretechnologies/reqore@0.78.3-pr.710.g30a6e81

Install it in a consumer (updates package.json + lockfile):

yarn add @qoretechnologies/reqore@0.78.3-pr.710.g30a6e81

Published under the pr dist-tag — latest/beta are untouched. Repoint consumers to the released version once this PR merges.

…e-header-scrolls-the-table: stacked on #708

# Conflicts:
#	package.json
… pullable body shows a grab cursor

The header forwarded only a wheel's deltaX to the body, so a plain wheel
with Shift held did nothing over it. With dragToScroll the body shows a
grab cursor, and grabbing with no text selection while pulled - what a
consumer otherwise had to style through reqore's class names.
@qlip-qore-technologies

qlip-qore-technologies Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

qlip visual regression

🔍 0 changed, 1 new of 1126 snapshots.
⏳ Awaiting review.

Review build 20261007-162954 in the dashboard →

…t landed on losing its capture

A touch pointer is captured by the element it lands on (implicit touch
capture). Taking the capture for the header moved it away, the cell
reported lostpointercapture, and the window listener ended the gesture:
a real swipe on a phone moved the table 5px. Only the capture the gesture
itself took ends it. Found driving the IDE's review on a phone with real
touch events; the dispatched-event tests could not see it.
@davidnich

Copy link
Copy Markdown
Member Author

Qlip build 20261007-162954 (30a6e81): 1 pending, explained, not reviewed. I have not accepted, flagged or denied it.

@davidnich

Copy link
Copy Markdown
Member Author

Consolidated into #708 at 7c9c127 (merged in 21ee8c2, with no rebase of this branch's history). Further work goes to #708, which ships everything as 0.78.5. Closing without merging; the branch is kept.

@davidnich davidnich closed this Oct 7, 2026
davidnich added a commit that referenced this pull request Oct 12, 2026
…used

Conflicts:
- package.json: the PR stays 0.78.5, above develop's 0.78.1.
- src/components/FadeScroller/index.tsx: #710 (here) had moved the row's
  drag-to-scroll into useDragToScroll, shared with the table; #714
  (develop) moved it onto usePointerDrag, the drag a sheet, a modal, a
  toast and a tier stack share. Develop's version is taken, with this
  branch's import of Reqore's `styled`. The table keeps useDragToScroll:
  its scrollers appear after mount and its header takes touch swipes
  for the body, which usePointerDrag's spread handlers do not cover.
  The fix #710 made to the shared gesture goes to usePointerDrag in the
  next commit, so it reaches every surface on it.
davidnich added a commit that referenced this pull request Oct 12, 2026
… on loses the capture; the row's drag leaves grab handles alone

Merging develop brought two shared drags: usePointerDrag (#714: a sheet,
a modal, a toast, a tier stack and now ReqoreFadeScroller's row) and
useDragToScroll (#710: the table, which binds to elements that appear
after mount). Two fixes this branch had made to the shared gesture
only reached the second:

- A finger's pointer is captured by the element it lands on. When a
  drag engages, the element that drags takes the capture and the one
  under the finger - a title, a label inside it - reports losing it.
  usePointerDrag ended the gesture on any lostpointercapture for the
  pointer, so a swipe that started on something inside a sheet, a toast
  or a tier stopped after its first few pixels; #710 found and fixed the
  same thing on a table's header with real touch events. Now only the
  capture the gesture took itself ends it.
- ReqoreFadeScroller's row, moved onto usePointerDrag by #714, kept its
  own copy of what a drag never starts on, without be112f0's
  `reqore-no-drag-scroll` (documented for the row and the table). It
  takes the threshold and that list from useDragToScroll again.

Tests: pointerDragImplicitCapture (a swipe that starts on a child keeps
going; the capture the drag took still ends it - the first fails on
develop's hook), fadeScroller-drag (a press on a reqore-no-drag-scroll
element does not drag the row - fails on develop's FadeScroller).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant