virtua 0.44.2 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -292,12 +292,6 @@ const UPDATE_SCROLL_END_EVENT = 0b1000;
292
292
  const getScrollSize = (store) => {
293
293
  return max(store.$getTotalSize(), store.$getViewportSize());
294
294
  };
295
- /**
296
- * @internal
297
- */
298
- const isInitialMeasurementDone = (store) => {
299
- return !!store.$getViewportSize();
300
- };
301
295
  /**
302
296
  * @internal
303
297
  */
@@ -686,69 +680,81 @@ const createScrollObserver = (store, viewport, isHorizontal, getScrollOffset, up
686
680
  },
687
681
  };
688
682
  };
689
- /**
690
- * @internal
691
- */
692
- const createScroller = (store, isHorizontal) => {
693
- let viewportElement;
694
- let scrollObserver;
683
+ const createScrollScheduler = (store, initialized, scroll) => {
695
684
  let cancelScroll;
696
- let initialized = createPromise();
697
- const scrollOffsetKey = isHorizontal ? "scrollLeft" : "scrollTop";
698
- const overflowKey = isHorizontal ? "overflowX" : "overflowY";
699
685
  // The given offset will be clamped by browser
700
686
  // https://drafts.csswg.org/cssom-view/#dom-element-scrolltop
701
- const scheduleImperativeScroll = async (getTargetOffset, smooth) => {
702
- // Wait for element assign. The element may be undefined if scrollRef prop is used and scroll is scheduled on mount.
703
- // https://github.com/inokawa/virtua/pull/733
704
- // https://github.com/inokawa/virtua/pull/750
705
- if (!(await initialized[0])) {
706
- return;
707
- }
708
- if (cancelScroll) {
709
- // Cancel waiting scrollTo
710
- cancelScroll();
711
- }
712
- const waitForMeasurement = () => {
713
- // Wait for the scroll destination items to be measured.
714
- // The measurement will be done asynchronously and the timing is not predictable so we use promise.
715
- const [promise, resolve] = createPromise();
716
- cancelScroll = () => {
717
- resolve(false);
718
- };
719
- // Resize event may not happen when the window/tab is not visible, or during browser back in Safari.
720
- // We have to wait for the initial measurement to avoid failing imperative scroll on mount.
721
- // https://github.com/inokawa/virtua/issues/450
722
- if (isInitialMeasurementDone(store)) {
723
- // Cancel when items around scroll destination completely measured
724
- timeout(cancelScroll, 150);
687
+ return [
688
+ async (getTargetOffset, smooth) => {
689
+ // Wait for element assign. The element may be undefined if scrollRef prop is used and scroll is scheduled on mount.
690
+ // https://github.com/inokawa/virtua/pull/733
691
+ // https://github.com/inokawa/virtua/pull/750
692
+ if (!(await initialized())) {
693
+ return;
725
694
  }
726
- return [
727
- promise,
728
- store.$subscribe(UPDATE_SIZE_EVENT, () => {
729
- resolve(true);
730
- }),
731
- ];
732
- };
733
- if (smooth && isSmoothScrollSupported()) {
734
- store.$update(ACTION_BEFORE_MANUAL_SMOOTH_SCROLL, getTargetOffset());
735
- // https://github.com/inokawa/virtua/issues/590
736
- microtask(async () => {
737
- while (true) {
738
- let measuring = false;
739
- for (let [i, end] = store.$getRange(); i <= end; i++) {
740
- if (store.$isUnmeasuredItem(i)) {
741
- measuring = true;
695
+ if (cancelScroll) {
696
+ // Cancel waiting scrollTo
697
+ cancelScroll();
698
+ }
699
+ const waitForMeasurement = () => {
700
+ // Wait for the scroll destination items to be measured.
701
+ // The measurement will be done asynchronously and the timing is not predictable so we use promise.
702
+ const [promise, resolve] = createPromise();
703
+ cancelScroll = () => {
704
+ resolve(false);
705
+ };
706
+ // Resize event may not happen when the window/tab is not visible, or during browser back in Safari.
707
+ // We have to wait for the initial measurement to avoid failing imperative scroll on mount.
708
+ // https://github.com/inokawa/virtua/issues/450
709
+ if (store.$getViewportSize()) {
710
+ // Cancel when items around scroll destination completely measured
711
+ timeout(cancelScroll, 150);
712
+ }
713
+ return [
714
+ promise,
715
+ store.$subscribe(UPDATE_SIZE_EVENT, () => {
716
+ resolve(true);
717
+ }),
718
+ ];
719
+ };
720
+ if (smooth && isSmoothScrollSupported()) {
721
+ store.$update(ACTION_BEFORE_MANUAL_SMOOTH_SCROLL, getTargetOffset());
722
+ // https://github.com/inokawa/virtua/issues/590
723
+ microtask(async () => {
724
+ while (true) {
725
+ let done = true;
726
+ for (let [i, end] = store.$getRange(); i <= end; i++) {
727
+ if (store.$isUnmeasuredItem(i)) {
728
+ done = false;
729
+ break;
730
+ }
731
+ }
732
+ if (done) {
742
733
  break;
743
734
  }
735
+ const [promise, unsubscribe] = waitForMeasurement();
736
+ try {
737
+ if (!(await promise)) {
738
+ // canceled
739
+ return;
740
+ }
741
+ }
742
+ finally {
743
+ unsubscribe();
744
+ }
744
745
  }
745
- if (!measuring) {
746
- break;
747
- }
746
+ store.$update(ACTION_MANUAL_SCROLL);
747
+ scroll(getTargetOffset(), smooth);
748
+ });
749
+ }
750
+ else {
751
+ while (true) {
748
752
  const [promise, unsubscribe] = waitForMeasurement();
749
753
  try {
754
+ store.$update(ACTION_MANUAL_SCROLL);
755
+ scroll(getTargetOffset());
750
756
  if (!(await promise)) {
751
- // canceled
757
+ // canceled or finished
752
758
  return;
753
759
  }
754
760
  }
@@ -756,30 +762,34 @@ const createScroller = (store, isHorizontal) => {
756
762
  unsubscribe();
757
763
  }
758
764
  }
759
- store.$update(ACTION_MANUAL_SCROLL);
760
- viewportElement.scrollTo({
761
- [isHorizontal ? "left" : "top"]: normalizeOffset(getTargetOffset(), isHorizontal),
762
- behavior: "smooth",
763
- });
765
+ }
766
+ },
767
+ () => {
768
+ cancelScroll && cancelScroll();
769
+ },
770
+ ];
771
+ };
772
+ /**
773
+ * @internal
774
+ */
775
+ const createScroller = (store, isHorizontal) => {
776
+ let viewportElement;
777
+ let scrollObserver;
778
+ let initialized = createPromise();
779
+ const scrollOffsetKey = isHorizontal ? "scrollLeft" : "scrollTop";
780
+ const overflowKey = isHorizontal ? "overflowX" : "overflowY";
781
+ const [scheduleScroll, cancelScroll] = createScrollScheduler(store, () => initialized[0], (offset, smooth) => {
782
+ offset = normalizeOffset(offset, isHorizontal);
783
+ if (smooth) {
784
+ viewportElement.scrollTo({
785
+ [isHorizontal ? "left" : "top"]: offset,
786
+ behavior: "smooth",
764
787
  });
765
788
  }
766
789
  else {
767
- while (true) {
768
- const [promise, unsubscribe] = waitForMeasurement();
769
- try {
770
- store.$update(ACTION_MANUAL_SCROLL);
771
- viewportElement[scrollOffsetKey] = normalizeOffset(getTargetOffset(), isHorizontal);
772
- if (!(await promise)) {
773
- // canceled or finished
774
- return;
775
- }
776
- }
777
- finally {
778
- unsubscribe();
779
- }
780
- }
790
+ viewportElement[scrollOffsetKey] = offset;
781
791
  }
782
- };
792
+ });
783
793
  return {
784
794
  $observe(viewport) {
785
795
  viewportElement = viewport;
@@ -801,7 +811,7 @@ const createScroller = (store, isHorizontal) => {
801
811
  viewport[scrollOffsetKey] = store.$getScrollOffset() + jump;
802
812
  if (shift) {
803
813
  // https://github.com/inokawa/virtua/issues/357
804
- cancelScroll && cancelScroll();
814
+ cancelScroll();
805
815
  }
806
816
  });
807
817
  initialized[1](true);
@@ -813,11 +823,11 @@ const createScroller = (store, isHorizontal) => {
813
823
  initialized = createPromise();
814
824
  },
815
825
  $scrollTo(offset) {
816
- scheduleImperativeScroll(() => offset);
826
+ scheduleScroll(() => offset);
817
827
  },
818
828
  $scrollBy(offset) {
819
829
  offset += store.$getScrollOffset();
820
- scheduleImperativeScroll(() => offset);
830
+ scheduleScroll(() => offset);
821
831
  },
822
832
  $scrollToIndex(index, { align, smooth, offset = 0 } = {}) {
823
833
  index = clamp(index, 0, store.$getItemsLength() - 1);
@@ -836,7 +846,7 @@ const createScroller = (store, isHorizontal) => {
836
846
  return;
837
847
  }
838
848
  }
839
- scheduleImperativeScroll(() => {
849
+ scheduleScroll(() => {
840
850
  return (offset +
841
851
  store.$getStartSpacerSize() +
842
852
  store.$getItemOffset(index) +
@@ -858,8 +868,23 @@ const createScroller = (store, isHorizontal) => {
858
868
  const createWindowScroller = (store, isHorizontal) => {
859
869
  let containerElement;
860
870
  let scrollObserver;
861
- let cancelScroll;
862
871
  let initialized = createPromise();
872
+ const scrollToKey = isHorizontal ? "left" : "top";
873
+ const [scheduleScroll] = createScrollScheduler(store, () => initialized[0], (offset, smooth) => {
874
+ offset = normalizeOffset(offset, isHorizontal);
875
+ const window = getCurrentWindow(getCurrentDocument(containerElement));
876
+ if (smooth) {
877
+ window.scroll({
878
+ [scrollToKey]: offset,
879
+ behavior: "smooth",
880
+ });
881
+ }
882
+ else {
883
+ window.scroll({
884
+ [scrollToKey]: offset,
885
+ });
886
+ }
887
+ });
863
888
  const calcOffsetToViewport = (node, viewport, window, isHorizontal, offset = 0) => {
864
889
  // TODO calc offset only when it changes (maybe impossible)
865
890
  const offsetKey = isHorizontal ? "offsetLeft" : "offsetTop";
@@ -873,104 +898,20 @@ const createWindowScroller = (store, isHorizontal) => {
873
898
  }
874
899
  return calcOffsetToViewport(parent, viewport, window, isHorizontal, offsetSum);
875
900
  };
876
- const scheduleImperativeScroll = async (getTargetOffset, smooth) => {
877
- // Wait for element assign. The element may be undefined if scrollRef prop is used and scroll is scheduled on mount.
878
- // https://github.com/inokawa/virtua/pull/733
879
- // https://github.com/inokawa/virtua/pull/750
880
- if (!(await initialized[0])) {
881
- return;
882
- }
883
- if (cancelScroll) {
884
- cancelScroll();
885
- }
886
- const waitForMeasurement = () => {
887
- // Wait for the scroll destination items to be measured.
888
- // The measurement will be done asynchronously and the timing is not predictable so we use promise.
889
- const [promise, resolve] = createPromise();
890
- cancelScroll = () => {
891
- resolve(false);
892
- };
893
- // Resize event may not happen when the window/tab is not visible, or during browser back in Safari.
894
- // We have to wait for the initial measurement to avoid failing imperative scroll on mount.
895
- // https://github.com/inokawa/virtua/issues/450
896
- if (isInitialMeasurementDone(store)) {
897
- // Cancel when items around scroll destination completely measured
898
- timeout(cancelScroll, 150);
899
- }
900
- return [
901
- promise,
902
- store.$subscribe(UPDATE_SIZE_EVENT, () => {
903
- resolve(true);
904
- }),
905
- ];
906
- };
907
- const window = getCurrentWindow(getCurrentDocument(containerElement));
908
- if (smooth && isSmoothScrollSupported()) {
909
- store.$update(ACTION_BEFORE_MANUAL_SMOOTH_SCROLL, getTargetOffset());
910
- // https://github.com/inokawa/virtua/issues/590
911
- microtask(async () => {
912
- while (true) {
913
- let measuring = false;
914
- for (let [i, end] = store.$getRange(); i <= end; i++) {
915
- if (store.$isUnmeasuredItem(i)) {
916
- measuring = true;
917
- break;
918
- }
919
- }
920
- if (!measuring) {
921
- break;
922
- }
923
- const [promise, unsubscribe] = waitForMeasurement();
924
- try {
925
- if (!(await promise)) {
926
- // canceled
927
- return;
928
- }
929
- }
930
- finally {
931
- unsubscribe();
932
- }
933
- }
934
- store.$update(ACTION_MANUAL_SCROLL);
935
- window.scroll({
936
- [isHorizontal ? "left" : "top"]: normalizeOffset(getTargetOffset(), isHorizontal),
937
- behavior: "smooth",
938
- });
939
- });
940
- }
941
- else {
942
- while (true) {
943
- const [promise, unsubscribe] = waitForMeasurement();
944
- try {
945
- store.$update(ACTION_MANUAL_SCROLL);
946
- window.scroll({
947
- [isHorizontal ? "left" : "top"]: normalizeOffset(getTargetOffset(), isHorizontal),
948
- });
949
- if (!(await promise)) {
950
- return;
951
- }
952
- }
953
- finally {
954
- unsubscribe();
955
- }
956
- }
957
- }
958
- };
959
901
  return {
960
902
  $observe(container) {
961
903
  containerElement = container;
962
904
  const scrollOffsetKey = isHorizontal ? "scrollX" : "scrollY";
963
905
  const document = getCurrentDocument(container);
964
906
  const window = getCurrentWindow(document);
965
- const documentBody = document.body;
966
907
  scrollObserver = createScrollObserver(store, window, isHorizontal, () => normalizeOffset(window[scrollOffsetKey], isHorizontal), (jump) => {
967
908
  // Use absolute position not to exceed scrollable bounds
968
909
  // https://github.com/inokawa/virtua/discussions/475
969
910
  // TODO support case two window scrollers exist in the same view
970
911
  window.scroll({
971
- [isHorizontal ? "left" : "top"]: store.$getScrollOffset() + jump,
912
+ [scrollToKey]: store.$getScrollOffset() + jump,
972
913
  });
973
- }, () => calcOffsetToViewport(container, documentBody, window, isHorizontal));
914
+ }, () => calcOffsetToViewport(container, document.body, window, isHorizontal));
974
915
  initialized[1](true);
975
916
  },
976
917
  $dispose() {
@@ -1006,7 +947,7 @@ const createWindowScroller = (store, isHorizontal) => {
1006
947
  const html = document.documentElement;
1007
948
  const getScrollbarSize = () => store.$getViewportSize() -
1008
949
  (isHorizontal ? html.clientWidth : html.clientHeight);
1009
- scheduleImperativeScroll(() => {
950
+ scheduleScroll(() => {
1010
951
  return (offset +
1011
952
  // Calculate target scroll position including container's offset from document
1012
953
  calcOffsetToViewport(containerElement, document.body, window, isHorizontal) +
@@ -1458,7 +1399,7 @@ const Virtualizer = (props) => {
1458
1399
  return (<ListItem _as={props.item} _index={index()} _resizer={resizer.$observeItem} _offset={offset()} _hide={hide()} _children={children()} _isHorizontal={horizontal}/>);
1459
1400
  };
1460
1401
  return (<Dynamic component={props.as} ref={containerRef} style={{
1461
- contain: "strict",
1402
+ contain: "size paint style", // https://github.com/inokawa/virtua/pull/775
1462
1403
  "overflow-anchor": "none", // opt out browser's scroll anchoring because it will conflict to scroll anchoring of virtualizer
1463
1404
  overflow: "clip", // https://github.com/inokawa/virtua/pull/485 https://github.com/inokawa/virtua/issues/717
1464
1405
  flex: "none", // flex style can break layout
@@ -1590,7 +1531,7 @@ const WindowVirtualizer = (props) => {
1590
1531
  return items;
1591
1532
  });
1592
1533
  return (<div ref={containerRef} style={{
1593
- contain: "strict",
1534
+ contain: "size paint style", // https://github.com/inokawa/virtua/pull/775
1594
1535
  "overflow-anchor": "none", // opt out browser's scroll anchoring because it will conflict to scroll anchoring of virtualizer
1595
1536
  overflow: "clip", // https://github.com/inokawa/virtua/pull/485 https://github.com/inokawa/virtua/issues/717
1596
1537
  flex: "none", // flex style can break layout