11import {
22 getCurrentDocument ,
33 getCurrentWindow ,
4+ getDocumentElement ,
45 isIOSWebKit ,
5- isRTLDocument ,
66 isSmoothScrollSupported ,
77} from "./environment.js" ;
88import {
@@ -39,19 +39,18 @@ const debounce = <T extends () => void>(fn: T, ms: number) => {
3939} ;
4040
4141/**
42- * scrollLeft is negative value in rtl direction.
42+ * scrollTop/scrollLeft can be negative value under certain styles.
43+ * - direction: rtl https://github.com/othree/jquery.rtl-scroll-type
44+ * - writing-mode https://people.igalia.com/fwang/scrollable-elements-in-non-default-writing-modes/
45+ * - flex-direction: column-reverse/row-reverse
4346 *
44- * left right
45- * 0 100 spec compliant (ltr)
46- * -100 0 spec compliant (rtl)
47- * https://github.com/othree/jquery.rtl- scroll-type
47+ * top/ left bottom/ right
48+ * 0 100 spec compliant bottom/right overflow, or possibly top/left overflow in Chrome earlier than v85
49+ * -100 0 spec compliant top/left overflow
50+ * https://drafts.csswg.org/cssom-view/# scroll-an-element
4851 */
49- const normalizeOffset = ( offset : number , isHorizontal : boolean ) : number => {
50- if ( isHorizontal && isRTLDocument ( ) ) {
51- return - offset ;
52- } else {
53- return offset ;
54- }
52+ const normalizeScrollOffset = ( offset : number , isNegative : boolean ) : number => {
53+ return isNegative ? - offset : offset ;
5554} ;
5655
5756const createScrollObserver = (
@@ -283,6 +282,7 @@ const createScrollScheduler = (
283282export type Scroller = {
284283 $observe : ( viewportElement : HTMLElement ) => void ;
285284 $dispose ( ) : void ;
285+ $isNegative ( ) : boolean ;
286286 $scrollTo : ( offset : number ) => void ;
287287 $scrollBy : ( offset : number ) => void ;
288288 $scrollToIndex : ( index : number , opts ?: ScrollToIndexOpts ) => void ;
@@ -299,14 +299,15 @@ export const createScroller = (
299299 let viewportElement : HTMLElement | undefined ;
300300 let scrollObserver : ScrollObserver | undefined ;
301301 let initialized = createPromise < boolean > ( ) ;
302+ let isNegative = false ;
302303 const scrollOffsetKey = isHorizontal ? "scrollLeft" : "scrollTop" ;
303304 const overflowKey = isHorizontal ? "overflowX" : "overflowY" ;
304305
305306 const [ scheduleScroll , cancelScroll ] = createScrollScheduler (
306307 store ,
307308 ( ) => initialized [ 0 ] ,
308309 ( offset , smooth ) => {
309- offset = normalizeOffset ( offset , isHorizontal ) ;
310+ offset = normalizeScrollOffset ( offset , isNegative ) ;
310311
311312 if ( smooth ) {
312313 viewportElement ! . scrollTo ( {
@@ -323,11 +324,33 @@ export const createScroller = (
323324 $observe ( viewport ) {
324325 viewportElement = viewport ;
325326
327+ const clean = store . $subscribe ( UPDATE_SIZE_EVENT , ( ) => {
328+ const viewportSize = store . $getViewportSize ( ) ;
329+ if ( viewportSize ) {
330+ const prev = viewport [ scrollOffsetKey ] ;
331+
332+ // Detect overflowed direction after the initial viewport measurement
333+ const dummy = getCurrentDocument ( viewport ) . createElement ( "div" ) ;
334+ dummy . style . cssText = `visibility:hidden;min-${
335+ isHorizontal ? "width" : "height"
336+ } :${ viewportSize + 1 } px`;
337+ viewport . appendChild ( dummy ) ;
338+ viewport [ scrollOffsetKey ] = 1 ;
339+ // It can be positive under some specific situations even if negative mode, so we use `<` for now.
340+ isNegative = viewport [ scrollOffsetKey ] < 1 ;
341+ viewport . removeChild ( dummy ) ;
342+
343+ viewport [ scrollOffsetKey ] = prev ;
344+
345+ clean ( ) ;
346+ }
347+ } ) ;
348+
326349 scrollObserver = createScrollObserver (
327350 store ,
328351 viewport ,
329352 isHorizontal ,
330- ( ) => normalizeOffset ( viewport [ scrollOffsetKey ] , isHorizontal ) ,
353+ ( ) => normalizeScrollOffset ( viewport [ scrollOffsetKey ] , isNegative ) ,
331354 ( jump , shift , isMomentumScrolling ) => {
332355 // If we update scroll position while touching on iOS, the position will be reverted.
333356 // However iOS WebKit fires touch events only once at the beginning of momentum scrolling.
@@ -344,9 +367,9 @@ export const createScroller = (
344367
345368 // Use absolute position not to exceed scrollable bounds
346369 // https://github.com/inokawa/virtua/discussions/475
347- viewport [ scrollOffsetKey ] = normalizeOffset (
370+ viewport [ scrollOffsetKey ] = normalizeScrollOffset (
348371 store . $getScrollOffset ( ) + jump ,
349- isHorizontal
372+ isNegative
350373 ) ;
351374 if ( shift ) {
352375 // https://github.com/inokawa/virtua/issues/357
@@ -363,6 +386,7 @@ export const createScroller = (
363386 // https://github.com/inokawa/virtua/pull/765
364387 initialized = createPromise ( ) ;
365388 } ,
389+ $isNegative : ( ) => isNegative ,
366390 $scrollTo ( offset ) {
367391 scheduleScroll ( ( ) => offset ) ;
368392 } ,
@@ -415,6 +439,7 @@ export const createScroller = (
415439export type WindowScroller = {
416440 $observe ( containerElement : HTMLElement ) : void ;
417441 $dispose ( ) : void ;
442+ $isNegative ( ) : boolean ;
418443 $scrollToIndex : ( index : number , opts ?: ScrollToIndexOpts ) => void ;
419444 $fixScrollJump : ( ) => void ;
420445} ;
@@ -429,13 +454,14 @@ export const createWindowScroller = (
429454 let containerElement : HTMLElement | undefined ;
430455 let scrollObserver : ScrollObserver | undefined ;
431456 let initialized = createPromise < boolean > ( ) ;
457+ let isNegative = false ;
432458 const scrollToKey = isHorizontal ? "left" : "top" ;
433459
434460 const [ scheduleScroll ] = createScrollScheduler (
435461 store ,
436462 ( ) => initialized [ 0 ] ,
437463 ( offset , smooth ) => {
438- offset = normalizeOffset ( offset , isHorizontal ) ;
464+ offset = normalizeScrollOffset ( offset , isNegative ) ;
439465
440466 const window = getCurrentWindow ( getCurrentDocument ( containerElement ! ) ) ;
441467
@@ -463,7 +489,7 @@ export const createWindowScroller = (
463489 const offsetKey = isHorizontal ? "offsetLeft" : "offsetTop" ;
464490 const offsetSum =
465491 offset +
466- ( isHorizontal && isRTLDocument ( )
492+ ( isHorizontal && isNegative
467493 ? window . innerWidth - node [ offsetKey ] - node . offsetWidth
468494 : node [ offsetKey ] ) ;
469495
@@ -489,25 +515,31 @@ export const createWindowScroller = (
489515 const document = getCurrentDocument ( container ) ;
490516 const window = getCurrentWindow ( document ) ;
491517
518+ if ( isHorizontal ) {
519+ // Detect RTL document
520+ isNegative =
521+ getComputedStyle ( getDocumentElement ( document ) ) . direction === "rtl" ;
522+ }
523+
492524 scrollObserver = createScrollObserver (
493525 store ,
494526 window ,
495527 isHorizontal ,
496- ( ) => normalizeOffset ( window [ scrollOffsetKey ] , isHorizontal ) ,
528+ ( ) => normalizeScrollOffset ( window [ scrollOffsetKey ] , isNegative ) ,
497529 ( jump , shift ) => {
498530 // TODO support case two window scrollers exist in the same view
499531 if ( shift ) {
500532 // Use absolute position not to exceed scrollable bounds
501533 window . scroll ( {
502- [ scrollToKey ] : normalizeOffset (
534+ [ scrollToKey ] : normalizeScrollOffset (
503535 store . $getScrollOffset ( ) + jump ,
504- isHorizontal
536+ isNegative
505537 ) ,
506538 } ) ;
507539 } else {
508540 // Use window.scrollBy here, which causes less layout shift for some reason.
509541 window . scrollBy ( {
510- [ scrollToKey ] : normalizeOffset ( jump , isHorizontal ) ,
542+ [ scrollToKey ] : normalizeScrollOffset ( jump , isNegative ) ,
511543 } ) ;
512544 }
513545 } ,
@@ -524,6 +556,7 @@ export const createWindowScroller = (
524556 // https://github.com/inokawa/virtua/pull/765
525557 initialized = createPromise ( ) ;
526558 } ,
559+ $isNegative : ( ) => isNegative ,
527560 $fixScrollJump : ( ) => {
528561 scrollObserver && scrollObserver . _fixScrollJump ( ) ;
529562 } ,
@@ -550,7 +583,7 @@ export const createWindowScroller = (
550583
551584 const document = getCurrentDocument ( containerElement ) ;
552585 const window = getCurrentWindow ( document ) ;
553- const html = document . documentElement ;
586+ const html = getDocumentElement ( document ) ;
554587 const getScrollbarSize = ( ) =>
555588 store . $getViewportSize ( ) -
556589 ( isHorizontal ? html . clientWidth : html . clientHeight ) ;
@@ -587,6 +620,7 @@ export const createWindowScroller = (
587620export type GridScroller = {
588621 $observe : ( viewportElement : HTMLElement ) => void ;
589622 $dispose ( ) : void ;
623+ $isNegative ( ) : boolean ;
590624 $scrollTo : ( offsetX ?: number , offsetY ?: number ) => void ;
591625 $scrollBy : ( offsetX ?: number , offsetY ?: number ) => void ;
592626 $scrollToIndex : ( indexX ?: number , indexY ?: number ) => void ;
@@ -611,6 +645,7 @@ export const createGridScroller = (
611645 rowScroller . $dispose ( ) ;
612646 colScroller . $dispose ( ) ;
613647 } ,
648+ $isNegative : colScroller . $isNegative ,
614649 $scrollTo ( row , col ) {
615650 if ( row != null ) {
616651 rowScroller . $scrollTo ( row ) ;
0 commit comments