Fixed scroll-to methods for RTL lists and grids

This commit is contained in:
Brian Vaughn
2019-05-27 16:41:25 -07:00
parent 37e410c0b8
commit b3fc81cf1f
4 changed files with 155 additions and 51 deletions
+34 -24
View File
@@ -3,7 +3,7 @@
import memoizeOne from 'memoize-one';
import { createElement, PureComponent } from 'react';
import { cancelTimeout, requestTimeout } from './timer';
import { getScrollbarSize } from './domHelpers';
import { getScrollbarSize, isRTLOffsetNegative } from './domHelpers';
import type { TimeoutID } from './timer';
@@ -86,7 +86,6 @@ type State = {|
isScrolling: boolean,
horizontalScrollDirection: ScrollDirection,
scrollLeft: number,
scrollLeftRTLNonStandard: boolean | null,
scrollTop: number,
scrollUpdateWasRequested: boolean,
verticalScrollDirection: ScrollDirection,
@@ -195,7 +194,6 @@ export default function createGridComponent({
typeof this.props.initialScrollLeft === 'number'
? this.props.initialScrollLeft
: 0,
scrollLeftRTLNonStandard: null,
scrollTop:
typeof this.props.initialScrollTop === 'number'
? this.props.initialScrollTop
@@ -326,21 +324,39 @@ export default function createGridComponent({
componentDidMount() {
const { initialScrollLeft, initialScrollTop } = this.props;
if (typeof initialScrollLeft === 'number' && this._outerRef != null) {
((this._outerRef: any): HTMLDivElement).scrollLeft = initialScrollLeft;
}
if (typeof initialScrollTop === 'number' && this._outerRef != null) {
((this._outerRef: any): HTMLDivElement).scrollTop = initialScrollTop;
if (this._outerRef != null) {
const outerRef = ((this._outerRef: any): HTMLElement);
if (typeof initialScrollLeft === 'number') {
outerRef.scrollLeft = initialScrollLeft;
}
if (typeof initialScrollTop === 'number') {
outerRef.scrollTop = initialScrollTop;
}
}
this._callPropsCallbacks();
}
componentDidUpdate() {
const { direction } = this.props;
const { scrollLeft, scrollTop, scrollUpdateWasRequested } = this.state;
if (scrollUpdateWasRequested && this._outerRef !== null) {
((this._outerRef: any): HTMLDivElement).scrollLeft = scrollLeft;
((this._outerRef: any): HTMLDivElement).scrollTop = scrollTop;
if (scrollUpdateWasRequested && this._outerRef != null) {
const outerRef = ((this._outerRef: any): HTMLElement);
if (direction === 'rtl') {
const isNegative = isRTLOffsetNegative();
if (isNegative) {
outerRef.scrollLeft = -scrollLeft;
} else {
const { clientWidth, scrollWidth } = outerRef;
outerRef.scrollLeft = scrollWidth - clientWidth - scrollLeft;
}
} else {
outerRef.scrollLeft = Math.max(0, scrollLeft);
}
outerRef.scrollTop = Math.max(0, scrollTop);
}
this._callPropsCallbacks();
@@ -707,22 +723,17 @@ export default function createGridComponent({
const { direction } = this.props;
let scrollLeftRTLNonStandard = prevState.scrollLeftRTLNonStandard;
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left).
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
let calculatedScrollLeft = scrollLeft;
if (direction === 'rtl') {
// TRICKY It's important that we only set this value once; iOS elastic bounce can calse false positives.
if (scrollLeftRTLNonStandard === null && scrollLeft !== 0) {
scrollLeftRTLNonStandard = scrollLeft > 0;
}
const isNegative = isRTLOffsetNegative();
if (scrollLeftRTLNonStandard) {
calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
} else {
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left).
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
if (isNegative) {
calculatedScrollLeft = -scrollLeft;
} else {
calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
}
}
@@ -741,7 +752,6 @@ export default function createGridComponent({
horizontalScrollDirection:
prevState.scrollLeft < scrollLeft ? 'forward' : 'backward',
scrollLeft: calculatedScrollLeft,
scrollLeftRTLNonStandard,
scrollTop: calculatedScrollTop,
verticalScrollDirection:
prevState.scrollTop < scrollTop ? 'forward' : 'backward',
+26 -23
View File
@@ -3,6 +3,7 @@
import memoizeOne from 'memoize-one';
import { createElement, PureComponent } from 'react';
import { cancelTimeout, requestTimeout } from './timer';
import { isRTLOffsetNegative } from './domHelpers';
import type { TimeoutID } from './timer';
@@ -68,7 +69,6 @@ type State = {|
isScrolling: boolean,
scrollDirection: ScrollDirection,
scrollOffset: number,
scrollOffsetRTLNonStandard: boolean | null,
scrollUpdateWasRequested: boolean,
|};
@@ -161,7 +161,6 @@ export default function createListComponent({
typeof this.props.initialScrollOffset === 'number'
? this.props.initialScrollOffset
: 0,
scrollOffsetRTLNonStandard: null,
scrollUpdateWasRequested: false,
};
@@ -217,14 +216,13 @@ export default function createListComponent({
componentDidMount() {
const { direction, initialScrollOffset, layout } = this.props;
if (typeof initialScrollOffset === 'number' && this._outerRef !== null) {
if (typeof initialScrollOffset === 'number' && this._outerRef != null) {
const outerRef = ((this._outerRef: any): HTMLElement);
// TODO Deprecate direction "horizontal"
if (direction === 'horizontal' || layout === 'horizontal') {
((this
._outerRef: any): HTMLDivElement).scrollLeft = initialScrollOffset;
outerRef.scrollLeft = initialScrollOffset;
} else {
((this
._outerRef: any): HTMLDivElement).scrollTop = initialScrollOffset;
outerRef.scrollTop = initialScrollOffset;
}
}
@@ -235,12 +233,23 @@ export default function createListComponent({
const { direction, layout } = this.props;
const { scrollOffset, scrollUpdateWasRequested } = this.state;
if (scrollUpdateWasRequested && this._outerRef !== null) {
if (scrollUpdateWasRequested && this._outerRef != null) {
const outerRef = ((this._outerRef: any): HTMLElement);
// TODO Deprecate direction "horizontal"
if (direction === 'horizontal' || layout === 'horizontal') {
((this._outerRef: any): HTMLDivElement).scrollLeft = scrollOffset;
if (direction === 'rtl') {
const isNegative = isRTLOffsetNegative();
if (isNegative) {
outerRef.scrollLeft = -scrollOffset;
} else {
const { clientWidth, scrollWidth } = outerRef;
outerRef.scrollLeft = scrollWidth - clientWidth - scrollOffset;
}
} else {
outerRef.scrollLeft = scrollOffset;
}
} else {
((this._outerRef: any): HTMLDivElement).scrollTop = scrollOffset;
outerRef.scrollTop = scrollOffset;
}
}
@@ -498,22 +507,17 @@ export default function createListComponent({
const { direction } = this.props;
let scrollOffsetRTLNonStandard = prevState.scrollOffsetRTLNonStandard;
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left).
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
let scrollOffset = scrollLeft;
if (direction === 'rtl') {
// TRICKY It's important that we only set this value once; iOS elastic bounce can calse false positives.
if (scrollOffsetRTLNonStandard === null && scrollLeft !== 0) {
scrollOffsetRTLNonStandard = scrollLeft > 0;
}
const isNegative = isRTLOffsetNegative();
if (scrollOffsetRTLNonStandard) {
scrollOffset = scrollWidth - clientWidth - scrollLeft;
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left).
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
if (isNegative) {
scrollOffset = -scrollLeft;
} else {
scrollOffset = -scrollOffset;
scrollOffset = scrollWidth - clientWidth - scrollLeft;
}
}
@@ -528,7 +532,6 @@ export default function createListComponent({
scrollDirection:
prevState.scrollOffset < scrollLeft ? 'forward' : 'backward',
scrollOffset,
scrollOffsetRTLNonStandard,
scrollUpdateWasRequested: false,
};
}, this._resetIsScrollingDebounced);
+37
View File
@@ -20,3 +20,40 @@ export function getScrollbarSize(recalculate?: boolean = false): number {
return size;
}
let cachedRTLResult: boolean | null = null;
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left).
// Safari's elastic bounce makes detecting this even more complicated wrt potential false positives.
// The safest way to check this is to intentionally set a negative offset,
// and then verify that the subsequent "scroll" event matches the negative offset.
// If it does not match, then we can assume a non-standard RTL scroll implementation.
export function isRTLOffsetNegative(recalculate?: boolean = false): boolean {
if (cachedRTLResult === null || recalculate) {
const outerDiv = document.createElement('div');
const outerStyle = outerDiv.style;
outerStyle.width = '50px';
outerStyle.height = '50px';
outerStyle.overflow = 'scroll';
outerStyle.direction = 'rtl';
const innerDiv = document.createElement('div');
const innerStyle = innerDiv.style;
innerStyle.width = '100px';
innerStyle.height = '100px';
outerDiv.appendChild(innerDiv);
((document.body: any): HTMLBodyElement).appendChild(outerDiv);
outerDiv.scrollLeft = -10;
cachedRTLResult = outerDiv.scrollLeft === -10;
((document.body: any): HTMLBodyElement).removeChild(outerDiv);
return cachedRTLResult;
}
return cachedRTLResult;
}
+58 -4
View File
@@ -1,4 +1,4 @@
import React, { PureComponent } from 'react';
import React, { PureComponent, useRef } from 'react';
import { FixedSizeGrid, FixedSizeList } from 'react-window';
import CodeBlock from '../../components/CodeBlock';
import ProfiledExample from '../../components/ProfiledExample';
@@ -47,6 +47,22 @@ class Item extends PureComponent {
}
export default function() {
const gridRef = useRef();
const listRef = useRef();
const scrollGridTo500 = () => gridRef.current.scrollTo({ scrollLeft: 500 });
const scrollGridTo1500 = () => gridRef.current.scrollTo({ scrollLeft: 1500 });
const scrollGridToItem8Auto = () =>
gridRef.current.scrollToItem({ columnIndex: 8 });
const scrollGridToItem12Center = () =>
gridRef.current.scrollToItem({ columnIndex: 12, align: 'center' });
const scrollListTo500 = () => listRef.current.scrollTo(500);
const scrollListTo1500 = () => listRef.current.scrollTo(1500);
const scrollListToItem8Auto = () => listRef.current.scrollToItem(8);
const scrollListToItem12Center = () =>
listRef.current.scrollToItem(12, 'center');
return (
<div className={styles.ExampleWrapper}>
<h1 className={styles.ExampleHeader}>RTL List</h1>
@@ -55,13 +71,32 @@ export default function() {
className={styles.ExampleDemo}
sandbox="fixed-size-list-horizontal-rtl"
>
<button
className={styles.ExampleButton}
onClick={scrollListToItem8Auto}
>
Scroll to item 8 (align: auto)
</button>
<button
className={styles.ExampleButton}
onClick={scrollListToItem12Center}
>
Scroll to item 12 (align: center)
</button>
<button className={styles.ExampleButton} onClick={scrollListTo500}>
Scroll to 500
</button>
<button className={styles.ExampleButton} onClick={scrollListTo1500}>
Scroll to 1500
</button>
<FixedSizeList
className={styles.List}
direction="rtl"
height={75}
itemCount={1000}
itemCount={20}
itemSize={100}
layout="horizontal"
ref={listRef}
width={300}
>
{Item}
@@ -77,13 +112,32 @@ export default function() {
className={styles.ExampleDemo}
sandbox="fixed-size-grid-rtl"
>
<button
className={styles.ExampleButton}
onClick={scrollGridToItem8Auto}
>
Scroll to column 8 (align: auto)
</button>
<button
className={styles.ExampleButton}
onClick={scrollGridToItem12Center}
>
Scroll to column 12 (align: center)
</button>
<button className={styles.ExampleButton} onClick={scrollGridTo500}>
Scroll to 500
</button>
<button className={styles.ExampleButton} onClick={scrollGridTo1500}>
Scroll to 1500
</button>
<FixedSizeGrid
className={styles.Grid}
columnCount={1000}
columnCount={20}
columnWidth={100}
direction="rtl"
height={150}
rowCount={1000}
ref={gridRef}
rowCount={20}
rowHeight={35}
width={300}
>