Skip to content

fix(list): water-fill layout engine + anchor relation menus to click position#2239

Open
yasyuk wants to merge 4 commits into
developfrom
fix/js-9573-list-view-bin-vertical-grid
Open

fix(list): water-fill layout engine + anchor relation menus to click position#2239
yasyuk wants to merge 4 commits into
developfrom
fix/js-9573-list-view-bin-vertical-grid

Conversation

@yasyuk

@yasyuk yasyuk commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Menu anchoring (JS-9573)

In list/gallery/board views, relation cells near the right edge of the row were computing the cell's left edge for menu placement (e.g. x=1394 for a Status cell), causing the popup to flip leftward ~400px away from the click. Added recalcRect for noInplace cells in non-grid views: horizontal placement uses the click x-coordinate, vertical uses the cell's bounding rect. Menu now opens near where the user clicked regardless of cell position in the flex layout.

Also fixed coordinate space inconsistency: x/y are now both document-relative (clientX + scrollX, rect.top + scrollY), consistent with the rest of the menu positioning code.

Water-fill layout engine for List view right side

The right side of a List row can contain multiple relation cells (Status, Author, Tags, etc.) with no fixed widths. Without distribution, all cells were full-width and clipped or hidden by overflow. Added a water-fill algorithm that distributes the available right-side space proportionally:

  • Measures each cell's natural content width using an off-screen clone (no live DOM mutation)
  • Caps natural widths at 300px (matching CSS .name { max-width: 300px })
  • Cells whose natural width fits within their equal share keep their exact width; remaining space is split equally among cells that need more
  • If everything fits naturally, no flex is assigned (fast path)
  • Runs via ResizeObserver on the .sides container (fires on container width changes) and a separate useEffect keyed on right-side relation values (fires after in-place edits that change cell content but not container width)

Performance and correctness fixes (post code-review)

  • Use leftSideEl.offsetWidth directly instead of temporarily mutating leftSideEl.style.flex to measure (the mutation was also a TypeScript no-op: flex is not an HTMLElement property)
  • Drive water-fill sort, fit-test, and assignment entirely from naturalCapped — previously naturalCapped was used only for the early-exit total check while the loop used uncapped values, over-allocating space to cells with a 300px visual cap
  • Guard the style.flex = '' clear to only run when flex was previously set, preventing a spurious second ResizeObserver callback on every tick
  • Attach ResizeObserver to .sides instead of the full row to avoid triggering on framer-motion animations, drag handle, and selection state changes
  • Move resizeRef update into useLayoutEffect (post-commit, pre-paint) for React 18 concurrent mode safety
  • Fix sidesEl null check to occur before it is passed to U.Dom.select
  • Fix dead .side.left.s50 hover selector → .s60 (the s50 class is never applied)
  • Guard resizeRef.current?.() in observer callbacks

Test plan

  • Open a Set/Collection in List view
  • Click a relation value on the right side of a row (Status, Author, Select) — menu should open near the click, not far to the left
  • With multiple right-side relations visible, verify cells share space proportionally and don't overflow each other
  • Edit a relation value (e.g. add a tag, change status) — water-fill should recompute to fit the new content
  • Resize the window / panel — water-fill should recompute as container width changes
  • Repeat for Gallery view and Board view relation menus
  • Verify Grid view relation menus are unaffected (still center-aligned)
  • Verify Name cell inline editing still works in List view

🤖 Generated with Claude Code

In list (and gallery/board) views, cells like Author and Status sit near
the right edge of the row. With horizontal:Left, the menu computes the
cell's left edge and flips leftward when it doesn't fit, placing the
popup far from where the user clicked.

For noInplace cells in non-grid views, recalcRect now returns the click's
x coordinate so the menu always opens near the clicked value. The y
coordinate still uses the cell's bounding rect so the menu appears just
below the row.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yasyuk

yasyuk commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA.

@requilence requilence left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review focused on correctness, layout, and perf.

Scope mismatch: the title/description and test plan cover only the menu-anchoring change in cell/index.tsx (16 lines), but this commit also adds a ~110-line water-fill layout engine in row.tsx + new SCSS. The merge base (05dd906b25) has no such layout — it's entirely new here, undocumented and untested. The menu fix itself is correct and low-risk; recommend splitting the layout work into its own PR with its own description/test plan, or expanding this one to cover it.

Tests: none added. Per CLAUDE.md, menu/list changes should get /qa-engineer E2E coverage; SCSS changes should pass /dark-mode-check.

Inline comments below.

// Cap each measured width at the CSS .name max-width (300px).
// Without this, text cells (description) measure at full text width and get
// assigned more space than they can display, creating an empty gap.
const naturalCapped = natural.map(w => Math.min(w, 300));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naturalCapped (300px cap) is computed only for the total <= available early-return — the distribution loop below (L116-130) uses uncapped natural for both the fit-test and assigned[idx] = natural[idx]. So a cell with natural width in (300, share] still gets assigned its full width while .name { max-width: 300px } clips the content → the exact empty gap this comment says it prevents. Either drive the distribution from naturalCapped, or drop it as dead/misleading code.


const sidesWidth = sidesEl.offsetWidth;
// Respect the CSS min-width:40% on the left side.
const effectiveLeft = Math.max(leftNatural, sidesWidth * 0.4);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded 0.4 assumes the left side is ≥40%, but the non-grid branch renders s60 when left.length > 1, and list.scss pins .side.left.s60 { width: 60% }. When the name isn't the first relation, the real left side is 60% → available is overestimated → right cells are over-allocated and clipped by overflow:hidden. Branch on s60 (or read the rendered left width) instead of a fixed 0.4.

});
};

useEffect(() => resize());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No dependency array → resize() runs after every render of every row. In the non-inline path this is react-virtualized, which re-renders/recycles rows on each scroll tick and on AutoSizer width changes. Each run forces sync reflows and deep-clones the right-side subtree into <body> (L79). That's layout-thrashing on large lists / fast scroll. Gate with deps, a ResizeObserver on width, or debounce.

return;
};

leftSideEl.style.flex = '0 0 auto';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this mutates live DOM (set/read/reset flex) to measure the left side, which both adds 2 synchronous reflows and contradicts the clone rationale at L74-77 ("never mutate live elements"). Also note sidesEl is used in U.Dom.select(..., sidesEl) above before its null check on L60.

Comment thread src/ts/component/cell/index.tsx Outdated
const rect = el?.getBoundingClientRect();
return {
x: clickX,
y: rect ? rect.top + window.scrollY : clickY,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixed coordinate spaces: x: clickX is viewport-relative (no scrollX) while y: rect.top + window.scrollY is document-relative, and the clickY fallback is viewport-relative again. Harmless today only because this list scrolls an inner container (window.scrollY ≈ 0), but inconsistent with the surrounding fallback ox = elRect.left + window.scrollX and a latent bug if window scrolling ever applies. Align all three.

Comment thread src/scss/block/dataview/view/list.scss Outdated
.element { overflow: hidden; max-width: 100%; }
.tagItem { overflow: hidden; max-width: 100%; }
}
.element .iconObject { display: none; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hides object/relation icons (e.g. Author avatars) on the right side of list rows — an undescribed visual change. Per CLAUDE.md, design properties shouldn't change without an explicit spec. Confirm this is intended JS-9573 behavior.

- Fix coordinate spaces in cell recalcRect: align x/y to document coords
- Replace live DOM flex mutation with leftSideEl.offsetWidth (also fixes
  TypeScript bug where leftSideEl.flex reset was a no-op)
- Drive water-fill distribution entirely from naturalCapped (fixes
  over-allocation to cells whose visual cap is 300px)
- Guard style.flex clear to only run when flex was previously set,
  preventing a spurious second ResizeObserver callback on every tick
- Attach ResizeObserver to .sides container instead of the full row to
  avoid triggering on framer-motion animations and drag/selection changes
- Move resizeRef update into useLayoutEffect for React 18 concurrent safety
- Fix dead .side.left.s50 hover rule to .side.left.s60

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yasyuk

yasyuk commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@requilence requilence left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 6863e53b63. All six findings from the previous round are resolved correctly:

  • 300px cap now drives the distribution (naturalCapped used in sort, fit-test and assignment) — no more over-allocation/gap.
  • 40%/60% mismatch removed; available is now derived from the real leftSideEl.offsetWidth (the CSS min-width:40% enforces the floor naturally). Bonus: the dead .side.left.s50 hover selector was corrected to .s60.
  • Perf: render-driven useEffect(resize) replaced with a one-time ResizeObserver + resizeRef, plus a guard so the flex-clear doesn't re-trigger the observer.
  • Live-DOM mutation for left measurement gone; sidesEl null-checked before use.
  • Coordinate spaces aligned (+ window.scrollX / + window.scrollY).
  • .element .iconObject { display:none } reverted.

One new trade-off to confirm + a nit inline. Still open from round 1: no tests (menu/list → /qa-engineer), and the PR description still only covers the menu fix while the layout engine is the bulk of the diff. The CLAUDE.md cleanup in e8fdb7a2bf is unrelated scope (benign).

LGTM once the ResizeObserver trade-off below is confirmed acceptable.

return;
};
const target = (U.Dom.select('.sides', node) as HTMLElement) || node;
const ro = new ResizeObserver(() => resizeRef.current());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trade-off to confirm: .sides is width:100% with a fixed row height, so the ResizeObserver only fires on container width changes — never on content changes inside a stable-width row. The previous useEffect(resize) re-ran on every render and so re-flowed after an in-place value edit (e.g. changing a Status/tag to a wider value). Now that won't recompute the water-fill until the next container resize, so flex-basis can go stale after editing a relation value in List/Gallery/Board. If that's a real scenario here, also re-run on the relevant record/relation changes. If in-place edits can't change right-cell widths in these views, ignore.

Nit: resizeRef.current() has no guard — resizeRef.current?.() is safer.

- Add useEffect keyed on right-side values so water-fill recomputes
  after in-place relation edits (ResizeObserver alone misses this since
  .sides width is stable when only cell content changes)
- Fix resizeRef.current?.() guard in ResizeObserver callback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yasyuk yasyuk changed the title fix(list): anchor relation menus to click position in non-grid views fix(list): water-fill layout engine + anchor relation menus to click position Jun 2, 2026
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.

2 participants