Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #163 +/- ##
=======================================
Coverage 77.83% 77.83%
=======================================
Files 68 68
Lines 4502 4502
Branches 725 725
=======================================
Hits 3504 3504
Misses 793 793
Partials 205 205 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
benoit74
left a comment
There was a problem hiding this comment.
Using scrollIntoView is problematic because it causes the page to focus on that element instead of staying where it was. On author details page, it causes the page to scroll down to show the authors caroussel everytime we change author, even if this is done with arrows on the top of the page.
9dc78f5 to
6526687
Compare
|
Confirmed, and thanks for catching it — What changed in the rework:
To stop this from creeping back, the composable's tests now assert Re-checked in headless Chrome with the same page that mirrors the carousel CSS: LTR One thing I want to be explicit about rather than bury: the sign assumption is still there in |
|
I'm a bit lost. What does
Either we solve the whole "negative-range convention" or we just don't care. I'm in favor of not caring because all browsers have converged, but I'm open to merge a PR which solves the issue. But not something "in-the-middle" since it brings too little added value for the added complexity. |
6526687 to
e31a9bd
Compare
e31a9bd to
713a4dd
Compare
|
@benoit74 First, the notation you asked about: those pairs are the arrow state the harness prints after each step, On "the whole thing or nothing" — agreed, and the rebase onto #162 forced exactly that question, because that PR merged three more hand-translations of
To be honest about what that buys a reader today: nothing visible. Every evergreen engine implements the negative-range convention, so Driven through headless Chrome 156 against the carousel CSS (mandatory snap, One site still uses the old pattern: |
|
I'm sorry but this is just way above my concentration bandwidth ATM ; since this is only about chasing a potential bug we've never seen in the wild, I'll park this PR for the time being until I have a better bandwidth or more capabilities. Sorry to waste your time by not merging it, but here I can't accept to merge something I do not really understand and is maybe not driving value by solving a bug we've really encountered in the wild. |
|
Completely understood, and no apology needed — you're right on the cost/benefit. Every engine we can actually test implements the negative-range convention, so this defends against something nobody has hit; and a change you can't follow isn't a change you can maintain, whatever its tests say. I've converted this to draft so it's out of your review queue. If a real engine ever regresses here, everything needed to finish it — the two candidate remedies, the measured-convention approach, and the harness results in both writing directions — is written down in this PR and on this branch, so this isn't lost work. Thanks for the patient review across the rounds. |
…ft convention (openzim#98) The carousels read track.scrollLeft to decide the arrow state, and wrote signed offsets to it to move, which only works if the engine follows the spec's negative-range RTL convention. Arrow state now comes from the cards' getBoundingClientRect() edges, and both the arrow steps and the scroll-to-author alignment ask the engine which way a throwaway probe scroll actually moved the cards, then restore the position and cache the answer per carousel. Under scroll-snap-type: x mandatory a one-pixel probe is refused, so the probe is the intended delta itself, which is a snap point. No scrollIntoView: it scrolls every ancestor, which dragged the page down to the carousel on every author change. The progressive-loading thresholds and the anchor-preserving scroll introduced by openzim#162 read and wrote scrollLeft the same way, so they now use the same measured distances; loadPrevious keeps the card the reader was looking at in place instead of compensating for a scrollWidth delta.
713a4dd to
d2a5764
Compare
|
@benoit74 I've brought this back out of draft — the review request from the earlier round is still standing, so it's in your queue again whenever you have bandwidth, with no urgency at all. If the answer when you get there is still "not worth owning", closing #98 as won't-fix is a fine outcome too; the analysis stays here either way. Two things changed since your last look, both mechanical, and neither adds anything new to understand: it's rebased onto current main (the only conflict was one If it helps to know where to start: |
Summary
Nothing in the author carousels reads or writes
scrollLeftany more, so nothing assumes what it means in RTL. #98 named two possible remedies — feature-detect the convention, or compare positions withgetBoundingClientRect()— and this uses both, because each covers a part the other can't.State (
hasPrevious/hasNext) comes from measured geometry: the track's rect against the outer edges of its children gives the content hanging out past each edge, and the writing direction says which of those two is "ahead".scrollLeftis not read at all, so the negative-range, reversed-positive and legacy-positive readings all produce the same state.Movement has to be expressed as a scroll offset, since
scrollIntoViewscrolls every scrollable ancestor — that is what pulled the whole page down to the carousel when an arrow was pressed, and it is why the movement can't be delegated to the layout engine. So the sign is measured instead of assumed: before the first arrow press on a carousel, the composable writes the delta it intends to apply, reads back how far the engine actually went, measures which way the cards moved, and restores the position. That yields one bit — "does a positive delta advance the content or retreat it" — which is cached per carousel.The probe is a whole card, not a pixel, and that detail is measured rather than guessed: both carousels set
scroll-snap-type: x mandatory, and Chrome refuses a scroll position that is off a snap point — a ±1px write reads back unchanged, so a pixel-sized probe detects nothing on exactly the components that need it (accepted: 0at every position). A card-width write lands on a snap point and is accepted (accepted: 200, reduced: 200, restored: 0).If both directions are refused — a track with nothing to scroll, or an engine that accepts neither — the composable falls back to the spec convention rather than doing nothing. That fallback is not the primary path and is tested as a distinct case.
The "scroll to current author" watch was the third place with a baked-in convention:
scrollTo({ left: -offsetFromStart })with an RTL branch reconstructing the offset fromscrollWidth - (offsetLeft + clientWidth). It now says what it means — align this card to the inline start — throughalignCell, which measures the card's distance from the start edge and applies it with the resolved sign. The mobile/desktop choice of which card to align is unchanged, and it stays an instant jump rather than a smooth scroll, as before.#162 merged more convention assumptions into the same component while this PR was open — the progressive-loading thresholds (
Math.abs(track.scrollLeft)againstscrollWidth - clientWidth, incheckAndLoadMoreand both arrow handlers) and the compensating write inloadPrevious(track.scrollLeft = isRtl ? prevScrollLeft - diff : prevScrollLeft + diff, wherediffis thescrollWidthgrowth from prepending a batch). Rebasing meant reconciling with them rather than reverting them, so they are converted too, and that is what makes this the whole fix rather than half of it:checkAndLoadMoreand the two arrow handlers askdistanceToEnd()/distanceToStart()— the same overflow measurements the arrows' enabled state already uses — instead of translatingscrollLeftinto "distance from the start" by hand.loadPreviousno longer infers how much content appeared below the reader. It remembers the distance of the card at the inline start, prepends the batch, and re-aligns that same card to the distance it had, throughalignCell(anchor, anchorDistance). That reaches the same goal as thescrollWidtharithmetic — the reader keeps looking at the same card — but it is measured on the card the reader is actually on, so it needs no convention to pick its sign and no assumption that the only thing that changed was a uniform batch prepended at the start.CollectionCarousel.vue:42-43still translatesMath.abs(scrollLeft)by hand for its mobile load-more. It is outside the two files #98 names and outside the carousel this PR is about, so I left it alone rather than widen the diff — say the word and it can useuseScrollTrack'sdistanceToEnd()the same way, either here or in a follow-up.Tests
scrollWidth/clientWidth/scrollLeft:stubScrollEngineinstalls ascrollLeftaccessor plus ascrollBymock over one internal number — how far the content has advanced from the logical start — maps that to ascrollLeftreading under the convention under test, clamps writes, and re-stubs the card rects from the new position so measurements and writes stay consistent. One option makes the engine refuse any position that is not a card boundary, reproducing what mandatory scroll-snap does in Chrome; the RTL cases run with it on.scrollBy, under each of the three conventions:shiftNextadvances exactly one card and stops at the last one,shiftPreviousreturns, and the RTL state sequence matches LTR's.alignCellis covered per convention, at the inline start and at an arbitrary distance from it (whatloadPreviousneeds), plus the already-aligned case that must not touch the track.distanceOf/distanceToStart/distanceToEndare covered with a lyingscrollLeftpresent — the reads are geometry-only, so a track that reports 0 in both directions still reports the right distances.scrollWidth/scrollLeft) now stubs card geometry and a scroll engine, so the windowing tests measure what they claim: scrolling near the inline end loads a later batch and near the inline start loads an earlier one, each asserted by which author ends up where rather than by "the cell count went up". The RTL case runs the same two triggers against a mirrored layout. A separate case pinsloadPrevious's anchor: after the batch is prepended, the card the reader was looking at is at the same distance from the inline start as before, and the scroll position moved by exactly the width of the batch.prettier --check,eslint src/andvue-tsc --build --forceclean.overflow-x: auto,overflow-anchor: none,scroll-snap-type: x mandatory,scroll-snap-align: start, 50%-wide cards) inside a document tall enough to scroll, driven through headless Chrome 156, starting atscrollTop 700. The arrow walk movedscrollLeft200 → 400 → 600 → 800 → 804 (the last one clamps at the far edge, 4px past 800 because of the track's border) with stateP,NP,NP,NP,NP,-, then back to 600 and 400; RTL mirrored it exactly at -200 → … → -804 → -600 → -400 with the same state sequence. On top of that, the windowing path was driven the same way, identically in both directions: the initial scroll-to-author put the current card 402px from the inline start (2 cards, the extra 2px being snap rounding); reaching the inline start and prepending a batch kept author 39 at the start with its distance unchanged (2px before, 2px after) while the position advanced by the 3000px the new cards occupy (scrollLeft±3000,distanceToStart2998 after snap rounding), stateP,N; one arrow step then moved it to author 40 at 3198px; and appending a batch at the far end moved nothing at all. The document'sscrollTopdelta was 0 at every step of both runs, in both directions, and each probe recordedrestoredequal to the position it started from.fixes #98