react-window 1.8.1 → 1.8.5

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.
@@ -227,7 +227,7 @@ const VariableSizeList = createListComponent({
227
227
  default:
228
228
  if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
229
229
  return scrollOffset;
230
- } else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
230
+ } else if (scrollOffset < minOffset) {
231
231
  return minOffset;
232
232
  } else {
233
233
  return maxOffset;
@@ -3,7 +3,7 @@
3
3
  import memoizeOne from 'memoize-one';
4
4
  import { createElement, PureComponent } from 'react';
5
5
  import { cancelTimeout, requestTimeout } from './timer';
6
- import { getScrollbarSize } from './domHelpers';
6
+ import { getScrollbarSize, getRTLOffsetType } from './domHelpers';
7
7
 
8
8
  import type { TimeoutID } from './timer';
9
9
 
@@ -46,6 +46,22 @@ type OnScrollCallback = ({
46
46
  type ScrollEvent = SyntheticEvent<HTMLDivElement>;
47
47
  type ItemStyleCache = { [key: string]: Object };
48
48
 
49
+ type OuterProps = {|
50
+ children: React$Node,
51
+ className: string | void,
52
+ onScroll: ScrollEvent => void,
53
+ style: {
54
+ [string]: mixed,
55
+ },
56
+ |};
57
+
58
+ type InnerProps = {|
59
+ children: React$Node,
60
+ style: {
61
+ [string]: mixed,
62
+ },
63
+ |};
64
+
49
65
  export type Props<T> = {|
50
66
  children: RenderComponent<T>,
51
67
  className?: string,
@@ -56,7 +72,7 @@ export type Props<T> = {|
56
72
  initialScrollLeft?: number,
57
73
  initialScrollTop?: number,
58
74
  innerRef?: any,
59
- innerElementType?: React$ElementType,
75
+ innerElementType?: string | React$AbstractComponent<InnerProps, any>,
60
76
  innerTagName?: string, // deprecated
61
77
  itemData: T,
62
78
  itemKey?: (params: {|
@@ -67,11 +83,13 @@ export type Props<T> = {|
67
83
  onItemsRendered?: OnItemsRenderedCallback,
68
84
  onScroll?: OnScrollCallback,
69
85
  outerRef?: any,
70
- outerElementType?: React$ElementType,
86
+ outerElementType?: string | React$AbstractComponent<OuterProps, any>,
71
87
  outerTagName?: string, // deprecated
72
- overscanColumnsCount?: number,
88
+ overscanColumnCount?: number,
89
+ overscanColumnsCount?: number, // deprecated
73
90
  overscanCount?: number, // deprecated
74
- overscanRowsCount?: number,
91
+ overscanRowCount?: number,
92
+ overscanRowsCount?: number, // deprecated
75
93
  rowCount: number,
76
94
  rowHeight: itemSize,
77
95
  style?: Object,
@@ -130,10 +148,12 @@ const defaultItemKey = ({ columnIndex, data, rowIndex }) =>
130
148
  // In DEV mode, this Set helps us only log a warning once per component instance.
131
149
  // This avoids spamming the console every time a render happens.
132
150
  let devWarningsOverscanCount = null;
151
+ let devWarningsOverscanRowsColumnsCount = null;
133
152
  let devWarningsTagName = null;
134
153
  if (process.env.NODE_ENV !== 'production') {
135
154
  if (typeof window !== 'undefined' && typeof window.WeakSet !== 'undefined') {
136
155
  devWarningsOverscanCount = new WeakSet();
156
+ devWarningsOverscanRowsColumnsCount = new WeakSet();
137
157
  devWarningsTagName = new WeakSet();
138
158
  }
139
159
  }
@@ -320,21 +340,47 @@ export default function createGridComponent({
320
340
 
321
341
  componentDidMount() {
322
342
  const { initialScrollLeft, initialScrollTop } = this.props;
323
- if (typeof initialScrollLeft === 'number' && this._outerRef != null) {
324
- ((this._outerRef: any): HTMLDivElement).scrollLeft = initialScrollLeft;
325
- }
326
- if (typeof initialScrollTop === 'number' && this._outerRef != null) {
327
- ((this._outerRef: any): HTMLDivElement).scrollTop = initialScrollTop;
343
+
344
+ if (this._outerRef != null) {
345
+ const outerRef = ((this._outerRef: any): HTMLElement);
346
+ if (typeof initialScrollLeft === 'number') {
347
+ outerRef.scrollLeft = initialScrollLeft;
348
+ }
349
+ if (typeof initialScrollTop === 'number') {
350
+ outerRef.scrollTop = initialScrollTop;
351
+ }
328
352
  }
329
353
 
330
354
  this._callPropsCallbacks();
331
355
  }
332
356
 
333
357
  componentDidUpdate() {
358
+ const { direction } = this.props;
334
359
  const { scrollLeft, scrollTop, scrollUpdateWasRequested } = this.state;
335
- if (scrollUpdateWasRequested && this._outerRef !== null) {
336
- ((this._outerRef: any): HTMLDivElement).scrollLeft = scrollLeft;
337
- ((this._outerRef: any): HTMLDivElement).scrollTop = scrollTop;
360
+
361
+ if (scrollUpdateWasRequested && this._outerRef != null) {
362
+ // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
363
+ // This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
364
+ // So we need to determine which browser behavior we're dealing with, and mimic it.
365
+ const outerRef = ((this._outerRef: any): HTMLElement);
366
+ if (direction === 'rtl') {
367
+ switch (getRTLOffsetType()) {
368
+ case 'negative':
369
+ outerRef.scrollLeft = -scrollLeft;
370
+ break;
371
+ case 'positive-ascending':
372
+ outerRef.scrollLeft = scrollLeft;
373
+ break;
374
+ default:
375
+ const { clientWidth, scrollWidth } = outerRef;
376
+ outerRef.scrollLeft = scrollWidth - clientWidth - scrollLeft;
377
+ break;
378
+ }
379
+ } else {
380
+ outerRef.scrollLeft = Math.max(0, scrollLeft);
381
+ }
382
+
383
+ outerRef.scrollTop = Math.max(0, scrollTop);
338
384
  }
339
385
 
340
386
  this._callPropsCallbacks();
@@ -586,6 +632,7 @@ export default function createGridComponent({
586
632
  _getHorizontalRangeToRender(): [number, number, number, number] {
587
633
  const {
588
634
  columnCount,
635
+ overscanColumnCount,
589
636
  overscanColumnsCount,
590
637
  overscanCount,
591
638
  rowCount,
@@ -593,7 +640,7 @@ export default function createGridComponent({
593
640
  const { horizontalScrollDirection, isScrolling, scrollLeft } = this.state;
594
641
 
595
642
  const overscanCountResolved: number =
596
- overscanColumnsCount || overscanCount || 1;
643
+ overscanColumnCount || overscanColumnsCount || overscanCount || 1;
597
644
 
598
645
  if (columnCount === 0 || rowCount === 0) {
599
646
  return [0, 0, 0, 0];
@@ -634,13 +681,14 @@ export default function createGridComponent({
634
681
  const {
635
682
  columnCount,
636
683
  overscanCount,
684
+ overscanRowCount,
637
685
  overscanRowsCount,
638
686
  rowCount,
639
687
  } = this.props;
640
688
  const { isScrolling, verticalScrollDirection, scrollTop } = this.state;
641
689
 
642
690
  const overscanCountResolved: number =
643
- overscanRowsCount || overscanCount || 1;
691
+ overscanRowCount || overscanRowsCount || overscanCount || 1;
644
692
 
645
693
  if (columnCount === 0 || rowCount === 0) {
646
694
  return [0, 0, 0, 0];
@@ -679,9 +727,11 @@ export default function createGridComponent({
679
727
 
680
728
  _onScroll = (event: ScrollEvent): void => {
681
729
  const {
730
+ clientHeight,
682
731
  clientWidth,
683
732
  scrollLeft,
684
733
  scrollTop,
734
+ scrollHeight,
685
735
  scrollWidth,
686
736
  } = event.currentTarget;
687
737
  this.setState(prevState => {
@@ -697,24 +747,38 @@ export default function createGridComponent({
697
747
 
698
748
  const { direction } = this.props;
699
749
 
700
- // HACK According to the spec, scrollLeft should be negative for RTL aligned elements.
701
- // Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left).
702
- // See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
750
+ // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
751
+ // This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
752
+ // It's also easier for this component if we convert offsets to the same format as they would be in for ltr.
753
+ // So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it.
703
754
  let calculatedScrollLeft = scrollLeft;
704
755
  if (direction === 'rtl') {
705
- if (scrollLeft <= 0) {
706
- calculatedScrollLeft = -scrollLeft;
707
- } else {
708
- calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
756
+ switch (getRTLOffsetType()) {
757
+ case 'negative':
758
+ calculatedScrollLeft = -scrollLeft;
759
+ break;
760
+ case 'positive-descending':
761
+ calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
762
+ break;
709
763
  }
710
764
  }
711
765
 
766
+ // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
767
+ calculatedScrollLeft = Math.max(
768
+ 0,
769
+ Math.min(calculatedScrollLeft, scrollWidth - clientWidth)
770
+ );
771
+ const calculatedScrollTop = Math.max(
772
+ 0,
773
+ Math.min(scrollTop, scrollHeight - clientHeight)
774
+ );
775
+
712
776
  return {
713
777
  isScrolling: true,
714
778
  horizontalScrollDirection:
715
779
  prevState.scrollLeft < scrollLeft ? 'forward' : 'backward',
716
780
  scrollLeft: calculatedScrollLeft,
717
- scrollTop,
781
+ scrollTop: calculatedScrollTop,
718
782
  verticalScrollDirection:
719
783
  prevState.scrollTop < scrollTop ? 'forward' : 'backward',
720
784
  scrollUpdateWasRequested: false,
@@ -768,7 +832,9 @@ const validateSharedProps = (
768
832
  height,
769
833
  innerTagName,
770
834
  outerTagName,
835
+ overscanColumnsCount,
771
836
  overscanCount,
837
+ overscanRowsCount,
772
838
  width,
773
839
  }: Props<any>,
774
840
  { instance }: State
@@ -779,7 +845,23 @@ const validateSharedProps = (
779
845
  devWarningsOverscanCount.add(instance);
780
846
  console.warn(
781
847
  'The overscanCount prop has been deprecated. ' +
782
- 'Please use the overscanColumnsCount and overscanRowsCount props instead.'
848
+ 'Please use the overscanColumnCount and overscanRowCount props instead.'
849
+ );
850
+ }
851
+ }
852
+
853
+ if (
854
+ typeof overscanColumnsCount === 'number' ||
855
+ typeof overscanRowsCount === 'number'
856
+ ) {
857
+ if (
858
+ devWarningsOverscanRowsColumnsCount &&
859
+ !devWarningsOverscanRowsColumnsCount.has(instance)
860
+ ) {
861
+ devWarningsOverscanRowsColumnsCount.add(instance);
862
+ console.warn(
863
+ 'The overscanColumnsCount and overscanRowsCount props have been deprecated. ' +
864
+ 'Please use the overscanColumnCount and overscanRowCount props instead.'
783
865
  );
784
866
  }
785
867
  }
@@ -3,6 +3,7 @@
3
3
  import memoizeOne from 'memoize-one';
4
4
  import { createElement, PureComponent } from 'react';
5
5
  import { cancelTimeout, requestTimeout } from './timer';
6
+ import { getRTLOffsetType } from './domHelpers';
6
7
 
7
8
  import type { TimeoutID } from './timer';
8
9
 
@@ -38,6 +39,22 @@ type onScrollCallback = ({
38
39
  type ScrollEvent = SyntheticEvent<HTMLDivElement>;
39
40
  type ItemStyleCache = { [index: number]: Object };
40
41
 
42
+ type OuterProps = {|
43
+ children: React$Node,
44
+ className: string | void,
45
+ onScroll: ScrollEvent => void,
46
+ style: {
47
+ [string]: mixed,
48
+ },
49
+ |};
50
+
51
+ type InnerProps = {|
52
+ children: React$Node,
53
+ style: {
54
+ [string]: mixed,
55
+ },
56
+ |};
57
+
41
58
  export type Props<T> = {|
42
59
  children: RenderComponent<T>,
43
60
  className?: string,
@@ -45,7 +62,7 @@ export type Props<T> = {|
45
62
  height: number | string,
46
63
  initialScrollOffset?: number,
47
64
  innerRef?: any,
48
- innerElementType?: React$ElementType,
65
+ innerElementType?: string | React$AbstractComponent<InnerProps, any>,
49
66
  innerTagName?: string, // deprecated
50
67
  itemCount: number,
51
68
  itemData: T,
@@ -55,7 +72,7 @@ export type Props<T> = {|
55
72
  onItemsRendered?: onItemsRenderedCallback,
56
73
  onScroll?: onScrollCallback,
57
74
  outerRef?: any,
58
- outerElementType?: React$ElementType,
75
+ outerElementType?: string | React$AbstractComponent<OuterProps, any>,
59
76
  outerTagName?: string, // deprecated
60
77
  overscanCount: number,
61
78
  style?: Object,
@@ -215,14 +232,13 @@ export default function createListComponent({
215
232
  componentDidMount() {
216
233
  const { direction, initialScrollOffset, layout } = this.props;
217
234
 
218
- if (typeof initialScrollOffset === 'number' && this._outerRef !== null) {
235
+ if (typeof initialScrollOffset === 'number' && this._outerRef != null) {
236
+ const outerRef = ((this._outerRef: any): HTMLElement);
219
237
  // TODO Deprecate direction "horizontal"
220
238
  if (direction === 'horizontal' || layout === 'horizontal') {
221
- ((this
222
- ._outerRef: any): HTMLDivElement).scrollLeft = initialScrollOffset;
239
+ outerRef.scrollLeft = initialScrollOffset;
223
240
  } else {
224
- ((this
225
- ._outerRef: any): HTMLDivElement).scrollTop = initialScrollOffset;
241
+ outerRef.scrollTop = initialScrollOffset;
226
242
  }
227
243
  }
228
244
 
@@ -233,12 +249,32 @@ export default function createListComponent({
233
249
  const { direction, layout } = this.props;
234
250
  const { scrollOffset, scrollUpdateWasRequested } = this.state;
235
251
 
236
- if (scrollUpdateWasRequested && this._outerRef !== null) {
252
+ if (scrollUpdateWasRequested && this._outerRef != null) {
253
+ const outerRef = ((this._outerRef: any): HTMLElement);
254
+
237
255
  // TODO Deprecate direction "horizontal"
238
256
  if (direction === 'horizontal' || layout === 'horizontal') {
239
- ((this._outerRef: any): HTMLDivElement).scrollLeft = scrollOffset;
257
+ if (direction === 'rtl') {
258
+ // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
259
+ // This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
260
+ // So we need to determine which browser behavior we're dealing with, and mimic it.
261
+ switch (getRTLOffsetType()) {
262
+ case 'negative':
263
+ outerRef.scrollLeft = -scrollOffset;
264
+ break;
265
+ case 'positive-ascending':
266
+ outerRef.scrollLeft = scrollOffset;
267
+ break;
268
+ default:
269
+ const { clientWidth, scrollWidth } = outerRef;
270
+ outerRef.scrollLeft = scrollWidth - clientWidth - scrollOffset;
271
+ break;
272
+ }
273
+ } else {
274
+ outerRef.scrollLeft = scrollOffset;
275
+ }
240
276
  } else {
241
- ((this._outerRef: any): HTMLDivElement).scrollTop = scrollOffset;
277
+ outerRef.scrollTop = scrollOffset;
242
278
  }
243
279
  }
244
280
 
@@ -496,18 +532,28 @@ export default function createListComponent({
496
532
 
497
533
  const { direction } = this.props;
498
534
 
499
- // HACK According to the spec, scrollLeft should be negative for RTL aligned elements.
500
- // Chrome does not seem to adhere; its scrolLeft values are positive (measured relative to the left).
501
- // See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
502
535
  let scrollOffset = scrollLeft;
503
536
  if (direction === 'rtl') {
504
- if (scrollLeft <= 0) {
505
- scrollOffset = -scrollOffset;
506
- } else {
507
- scrollOffset = scrollWidth - clientWidth - scrollLeft;
537
+ // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
538
+ // This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
539
+ // It's also easier for this component if we convert offsets to the same format as they would be in for ltr.
540
+ // So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it.
541
+ switch (getRTLOffsetType()) {
542
+ case 'negative':
543
+ scrollOffset = -scrollLeft;
544
+ break;
545
+ case 'positive-descending':
546
+ scrollOffset = scrollWidth - clientWidth - scrollLeft;
547
+ break;
508
548
  }
509
549
  }
510
550
 
551
+ // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
552
+ scrollOffset = Math.max(
553
+ 0,
554
+ Math.min(scrollOffset, scrollWidth - clientWidth)
555
+ );
556
+
511
557
  return {
512
558
  isScrolling: true,
513
559
  scrollDirection:
@@ -519,7 +565,7 @@ export default function createListComponent({
519
565
  };
520
566
 
521
567
  _onScrollVertical = (event: ScrollEvent): void => {
522
- const { scrollTop } = event.currentTarget;
568
+ const { clientHeight, scrollHeight, scrollTop } = event.currentTarget;
523
569
  this.setState(prevState => {
524
570
  if (prevState.scrollOffset === scrollTop) {
525
571
  // Scroll position may have been updated by cDM/cDU,
@@ -528,11 +574,17 @@ export default function createListComponent({
528
574
  return null;
529
575
  }
530
576
 
577
+ // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
578
+ const scrollOffset = Math.max(
579
+ 0,
580
+ Math.min(scrollTop, scrollHeight - clientHeight)
581
+ );
582
+
531
583
  return {
532
584
  isScrolling: true,
533
585
  scrollDirection:
534
- prevState.scrollOffset < scrollTop ? 'forward' : 'backward',
535
- scrollOffset: scrollTop,
586
+ prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',
587
+ scrollOffset,
536
588
  scrollUpdateWasRequested: false,
537
589
  };
538
590
  }, this._resetIsScrollingDebounced);
package/src/domHelpers.js CHANGED
@@ -20,3 +20,53 @@ export function getScrollbarSize(recalculate?: boolean = false): number {
20
20
 
21
21
  return size;
22
22
  }
23
+
24
+ export type RTLOffsetType =
25
+ | 'negative'
26
+ | 'positive-descending'
27
+ | 'positive-ascending';
28
+
29
+ let cachedRTLResult: RTLOffsetType | null = null;
30
+
31
+ // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
32
+ // Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left).
33
+ // Safari's elastic bounce makes detecting this even more complicated wrt potential false positives.
34
+ // The safest way to check this is to intentionally set a negative offset,
35
+ // and then verify that the subsequent "scroll" event matches the negative offset.
36
+ // If it does not match, then we can assume a non-standard RTL scroll implementation.
37
+ export function getRTLOffsetType(recalculate?: boolean = false): RTLOffsetType {
38
+ if (cachedRTLResult === null || recalculate) {
39
+ const outerDiv = document.createElement('div');
40
+ const outerStyle = outerDiv.style;
41
+ outerStyle.width = '50px';
42
+ outerStyle.height = '50px';
43
+ outerStyle.overflow = 'scroll';
44
+ outerStyle.direction = 'rtl';
45
+
46
+ const innerDiv = document.createElement('div');
47
+ const innerStyle = innerDiv.style;
48
+ innerStyle.width = '100px';
49
+ innerStyle.height = '100px';
50
+
51
+ outerDiv.appendChild(innerDiv);
52
+
53
+ ((document.body: any): HTMLBodyElement).appendChild(outerDiv);
54
+
55
+ if (outerDiv.scrollLeft > 0) {
56
+ cachedRTLResult = 'positive-descending';
57
+ } else {
58
+ outerDiv.scrollLeft = 1;
59
+ if (outerDiv.scrollLeft === 0) {
60
+ cachedRTLResult = 'negative';
61
+ } else {
62
+ cachedRTLResult = 'positive-ascending';
63
+ }
64
+ }
65
+
66
+ ((document.body: any): HTMLBodyElement).removeChild(outerDiv);
67
+
68
+ return cachedRTLResult;
69
+ }
70
+
71
+ return cachedRTLResult;
72
+ }