Improve Bridge Flow types (#352)

* Updated local fork of react-window
* Updated Fow 97 -> 103
* Lint ignore NPM dist
* Improved Bridge Flow types
This commit is contained in:
Brian Vaughn 2019-07-20 14:08:23 -07:00 committed by GitHub
parent 39ad101ea2
commit 4b34a77d29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1070 additions and 383 deletions

View File

@ -4,6 +4,7 @@ shells/browser/chrome/build
shells/browser/firefox/build
shells/browser/shared/build
shells/dev/dist
packages/react-devtools-core/dist
vendor
*.js.snap

View File

@ -63,7 +63,7 @@
"test:chrome": "node ./shells/browser/chrome/test",
"test:firefox": "node ./shells/browser/firefox/test",
"test:standalone": "cd packages/react-devtools && yarn start",
"typecheck": "flow check"
"typecheck": "flow check --show-all-errors"
},
"devEngines": {
"node": "10.x || 11.x"
@ -119,7 +119,7 @@
"fbjs": "0.5.1",
"fbjs-scripts": "0.7.0",
"firefox-profile": "^1.0.2",
"flow-bin": "^0.97.0",
"flow-bin": "^0.103.0",
"fs-extra": "^3.0.1",
"gh-pages": "^1.0.0",
"html2canvas": "^1.0.0-alpha.12",

View File

@ -8,6 +8,7 @@ import { __DEBUG__ } from 'src/constants';
import setupNativeStyleEditor from 'src/backend/NativeStyleEditor/setupNativeStyleEditor';
import { getDefaultComponentFilters } from 'src/utils';
import type { BackendBridge } from 'src/bridge';
import type { ComponentFilter } from 'src/types';
import type { DevToolsHook } from 'src/backend/types';
import type { ResolveNativeStyle } from 'src/backend/NativeStyleEditor/setupNativeStyleEditor';
@ -64,7 +65,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
return;
}
let bridge: Bridge | null = null;
let bridge: BackendBridge | null = null;
const messageListeners = [];
const uri = 'ws://' + host + ':' + port;

View File

@ -17,6 +17,7 @@ import DevTools from 'src/devtools/views/DevTools';
import launchEditor from './launchEditor';
import { __DEBUG__ } from 'src/constants';
import type { FrontendBridge } from 'src/bridge';
import type { InspectedElement } from 'src/devtools/views/Components/types';
installHook(window);
@ -46,7 +47,7 @@ function setStatusListener(value: StatusListener) {
return DevtoolsUI;
}
let bridge: Bridge | null = null;
let bridge: FrontendBridge | null = null;
let store: Store | null = null;
let root = null;
@ -83,7 +84,7 @@ function reload() {
root = createRoot(node);
root.render(
createElement(DevTools, {
bridge: ((bridge: any): Bridge),
bridge: ((bridge: any): FrontendBridge),
showTabBar: true,
store: ((store: any): Store),
warnIfLegacyBackendDetected: true,
@ -166,7 +167,7 @@ function initialize(socket: WebSocket) {
}
},
});
((bridge: any): Bridge).addListener('shutdown', () => {
((bridge: any): FrontendBridge).addListener('shutdown', () => {
socket.close();
});

View File

@ -1,17 +1,17 @@
// @flow
import React, { Suspense, useState } from 'react';
import React, { Fragment, Suspense, useState } from 'react';
function SuspenseTree() {
return (
<>
<Fragment>
<h1>Suspense</h1>
<h4>Primary to Fallback Cycle</h4>
<PrimaryFallbackTest initialSuspend={false} />
<h4>Fallback to Primary Cycle</h4>
<PrimaryFallbackTest initialSuspend={true} />
<NestedSuspenseTest />
</>
</Fragment>
);
}
@ -20,7 +20,7 @@ function PrimaryFallbackTest({ initialSuspend }) {
const fallbackStep = useTestSequence('fallback', Fallback1, Fallback2);
const primaryStep = useTestSequence('primary', Primary1, Primary2);
return (
<>
<Fragment>
<label>
<input
checked={suspend}
@ -33,7 +33,7 @@ function PrimaryFallbackTest({ initialSuspend }) {
<Suspense fallback={fallbackStep}>
{suspend ? <Never /> : primaryStep}
</Suspense>
</>
</Fragment>
);
}
@ -45,32 +45,32 @@ function useTestSequence(label, T1, T2) {
</button>
);
let allSteps = [
<>{next}</>,
<>
<Fragment>{next}</Fragment>,
<Fragment>
{next} <T1 prop={step}>mount</T1>
</>,
<>
</Fragment>,
<Fragment>
{next} <T1 prop={step}>update</T1>
</>,
<>
</Fragment>,
<Fragment>
{next} <T2 prop={step}>several</T2> <T1 prop={step}>different</T1>{' '}
<T2 prop={step}>children</T2>
</>,
<>
</Fragment>,
<Fragment>
{next} <T2 prop={step}>goodbye</T2>
</>,
</Fragment>,
];
return allSteps[step];
}
function NestedSuspenseTest() {
return (
<>
<Fragment>
<h3>Nested Suspense</h3>
<Suspense fallback={<Fallback1>Loading outer</Fallback1>}>
<Parent />
</Suspense>
</>
</Fragment>
);
}
@ -118,19 +118,19 @@ function Never() {
throw new Promise(resolve => {});
}
function Fallback1({ prop, ...rest }) {
function Fallback1({ prop, ...rest }: any) {
return <span {...rest} />;
}
function Fallback2({ prop, ...rest }) {
function Fallback2({ prop, ...rest }: any) {
return <span {...rest} />;
}
function Primary1({ prop, ...rest }) {
function Primary1({ prop, ...rest }: any) {
return <span {...rest} />;
}
function Primary2({ prop, ...rest }) {
function Primary2({ prop, ...rest }: any) {
return <span {...rest} />;
}

View File

@ -15,9 +15,9 @@ describe('Bridge', () => {
const bridge = new Bridge(wall);
// Check that we're wired up correctly.
bridge.send('init');
bridge.send('reloadAppForProfiling');
jest.runAllTimers();
expect(wall.send).toHaveBeenCalledWith('init', undefined, undefined);
expect(wall.send).toHaveBeenCalledWith('reloadAppForProfiling');
// Should flush pending messages and then shut down.
wall.send.mockClear();
@ -25,9 +25,9 @@ describe('Bridge', () => {
bridge.send('update', '2');
bridge.shutdown();
jest.runAllTimers();
expect(wall.send).toHaveBeenCalledWith('update', '1', undefined);
expect(wall.send).toHaveBeenCalledWith('update', '2', undefined);
expect(wall.send).toHaveBeenCalledWith('shutdown', undefined, undefined);
expect(wall.send).toHaveBeenCalledWith('update', '1');
expect(wall.send).toHaveBeenCalledWith('update', '2');
expect(wall.send).toHaveBeenCalledWith('shutdown');
// Verify that the Bridge doesn't send messages after shutdown.
spyOn(console, 'warn');

View File

@ -2,14 +2,14 @@
import typeof ReactTestRenderer from 'react-test-renderer';
import type { GetInspectedElementPath } from 'src/devtools/views/Components/InspectedElementContext';
import type Bridge from 'src/bridge';
import type { FrontendBridge } from 'src/bridge';
import type Store from 'src/devtools/store';
describe('InspectedElementContext', () => {
let React;
let ReactDOM;
let TestRenderer: ReactTestRenderer;
let bridge: Bridge;
let bridge: FrontendBridge;
let store: Store;
let meta;
let utils;

View File

@ -2,7 +2,7 @@
import type { InspectedElementPayload } from 'src/backend/types';
import type { DehydratedData } from 'src/devtools/views/Components/types';
import type Bridge from 'src/bridge';
import type { FrontendBridge } from 'src/bridge';
import type Store from 'src/devtools/store';
describe('InspectedElementContext', () => {
@ -10,7 +10,7 @@ describe('InspectedElementContext', () => {
let ReactDOM;
let hydrate;
let meta;
let bridge: Bridge;
let bridge: FrontendBridge;
let store: Store;
const act = (callback: Function) => {

View File

@ -2,14 +2,14 @@
import typeof ReactTestRenderer from 'react-test-renderer';
import type { Element } from 'src/devtools/views/Components/types';
import type Bridge from 'src/bridge';
import type { FrontendBridge } from 'src/bridge';
import type Store from 'src/devtools/store';
describe('OwnersListContext', () => {
let React;
let ReactDOM;
let TestRenderer: ReactTestRenderer;
let bridge: Bridge;
let bridge: FrontendBridge;
let store: Store;
let utils;

View File

@ -1,7 +1,7 @@
// @flow
import typeof ReactTestRenderer from 'react-test-renderer';
import type Bridge from 'src/bridge';
import type { FrontendBridge } from 'src/bridge';
import type { Context } from 'src/devtools/views/Profiler/ProfilerContext';
import type { DispatcherContext } from 'src/devtools/views/Components/TreeContext';
import type Store from 'src/devtools/store';
@ -10,7 +10,7 @@ describe('ProfilerContext', () => {
let React;
let ReactDOM;
let TestRenderer: ReactTestRenderer;
let bridge: Bridge;
let bridge: FrontendBridge;
let store: Store;
let utils;

View File

@ -1,7 +1,7 @@
// @flow
import typeof ReactTestRenderer from 'react-test-renderer';
import type Bridge from 'src/bridge';
import type { FrontendBridge } from 'src/bridge';
import type Store from 'src/devtools/store';
describe('ProfilingCache', () => {
@ -11,7 +11,7 @@ describe('ProfilingCache', () => {
let Scheduler;
let SchedulerTracing;
let TestRenderer: ReactTestRenderer;
let bridge: Bridge;
let bridge: FrontendBridge;
let store: Store;
let utils;

View File

@ -29,7 +29,7 @@ describe('profiling charts', () => {
describe('flamegraph chart', () => {
it('should contain valid data', () => {
const Parent = ({ count }) => {
const Parent = (_: {||}) => {
Scheduler.unstable_advanceTime(10);
return (
<React.Fragment>
@ -105,7 +105,7 @@ describe('profiling charts', () => {
describe('ranked chart', () => {
it('should contain valid data', () => {
const Parent = ({ count }) => {
const Parent = (_: {||}) => {
Scheduler.unstable_advanceTime(10);
return (
<React.Fragment>
@ -177,7 +177,7 @@ describe('profiling charts', () => {
describe('interactions', () => {
it('should contain valid data', () => {
const Parent = ({ count }) => {
const Parent = (_: {||}) => {
Scheduler.unstable_advanceTime(10);
return (
<React.Fragment>

View File

@ -1,5 +1,7 @@
// @flow
import type { BackendBridge, FrontendBridge } from 'src/bridge';
const env = jasmine.getEnv();
env.beforeEach(() => {
// These files should be required (and re-reuired) before each test,
@ -51,13 +53,13 @@ env.beforeEach(() => {
},
});
const agent = new Agent(bridge);
const agent = new Agent(((bridge: any): BackendBridge));
const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
initBackend(hook, agent, global);
const store = new Store(bridge);
const store = new Store(((bridge: any): FrontendBridge));
global.agent = agent;
global.bridge = bridge;

View File

@ -1,6 +1,6 @@
// @flow
import type Bridge from 'src/bridge';
import type { FrontendBridge } from 'src/bridge';
import type Store from 'src/devtools/store';
describe('Store component filters', () => {
@ -8,7 +8,7 @@ describe('Store component filters', () => {
let ReactDOM;
let TestUtils;
let Types;
let bridge: Bridge;
let bridge: FrontendBridge;
let store: Store;
let utils;

View File

@ -1,7 +1,7 @@
// @flow
import typeof ReactTestRenderer from 'react-test-renderer';
import type Bridge from 'src/bridge';
import type { FrontendBridge } from 'src/bridge';
import type Store from 'src/devtools/store';
import type {
DispatcherContext,
@ -12,7 +12,7 @@ describe('TreeListContext', () => {
let React;
let ReactDOM;
let TestRenderer: ReactTestRenderer;
let bridge: Bridge;
let bridge: FrontendBridge;
let store: Store;
let utils;

View File

@ -2,7 +2,7 @@
import typeof ReactTestRenderer from 'react-test-renderer';
import type Bridge from 'src/bridge';
import type { FrontendBridge } from 'src/bridge';
import type Store from 'src/devtools/store';
import type { ProfilingDataFrontend } from 'src/devtools/views/Profiler/types';
import type { ElementType } from 'src/types';
@ -161,7 +161,7 @@ export function requireTestRenderer(): ReactTestRenderer {
}
}
export function exportImportHelper(bridge: Bridge, store: Store): void {
export function exportImportHelper(bridge: FrontendBridge, store: Store): void {
const { act } = require('./utils');
const {
prepareProfilingDataExport,

View File

@ -1,16 +1,16 @@
// @flow
import Agent from 'src/backend/agent';
import Bridge from 'src/bridge';
import resolveBoxStyle from './resolveBoxStyle';
import type { BackendBridge } from 'src/bridge';
import type { RendererID } from '../types';
import type { StyleAndLayout } from './types';
export type ResolveNativeStyle = (stylesheetID: number) => ?Object;
export default function setupNativeStyleEditor(
bridge: Bridge,
bridge: BackendBridge,
agent: Agent,
resolveNativeStyle: ResolveNativeStyle,
validAttributes?: $ReadOnlyArray<string> | null
@ -81,7 +81,7 @@ const componentIDToStyleOverrides: Map<number, Object> = new Map();
function measureStyle(
agent: Agent,
bridge: Bridge,
bridge: BackendBridge,
resolveNativeStyle: ResolveNativeStyle,
id: number,
rendererID: RendererID

View File

@ -2,7 +2,6 @@
import EventEmitter from 'events';
import throttle from 'lodash.throttle';
import Bridge from 'src/bridge';
import {
SESSION_STORAGE_LAST_SELECTION_KEY,
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
@ -17,6 +16,7 @@ import {
import setupHighlighter from './views/Highlighter';
import { patch as patchConsole, unpatch as unpatchConsole } from './console';
import type { BackendBridge } from 'src/bridge';
import type {
InstanceAndStyle,
NativeType,
@ -81,14 +81,14 @@ export default class Agent extends EventEmitter<{|
showNativeHighlight: [NativeType],
shutdown: [],
|}> {
_bridge: Bridge;
_bridge: BackendBridge;
_isProfiling: boolean = false;
_recordChangeDescriptions: boolean = false;
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
_persistedSelection: PersistedSelection | null = null;
_persistedSelectionMatch: PathMatch | null = null;
constructor(bridge: Bridge) {
constructor(bridge: BackendBridge) {
super();
if (
@ -259,7 +259,7 @@ export default class Agent extends EventEmitter<{|
dataURL: string,
rootID: number,
|}) => {
this._bridge.send('screenshotCaptured', { commitIndex, dataURL });
this._bridge.send('screenshotCaptured', { commitIndex, dataURL, rootID });
};
selectElement = ({ id, rendererID }: ElementAndRendererID) => {

View File

@ -11,6 +11,8 @@ type Rect = {
width: number,
};
type Box = {| top: number, left: number, width: number, height: number |};
// Note that the Overlay components are not affected by the active Theme,
// because they highlight elements in the main Chrome window (outside of devtools).
// The colors below were chosen to roughly match those used by Chrome devtools.
@ -21,7 +23,7 @@ class OverlayRect {
padding: HTMLElement;
content: HTMLElement;
constructor(doc, container) {
constructor(doc: Document, container: HTMLElement) {
this.node = doc.createElement('div');
this.border = doc.createElement('div');
this.padding = doc.createElement('div');
@ -51,7 +53,7 @@ class OverlayRect {
}
}
update(box, dims) {
update(box: Rect, dims: any) {
boxWrap(dims, 'margin', this.node);
boxWrap(dims, 'border', this.border);
boxWrap(dims, 'padding', this.padding);
@ -85,7 +87,7 @@ class OverlayTip {
nameSpan: HTMLElement;
dimSpan: HTMLElement;
constructor(doc, container) {
constructor(doc: Document, container: HTMLElement) {
this.tip = doc.createElement('div');
assign(this.tip.style, {
display: 'flex',
@ -126,13 +128,13 @@ class OverlayTip {
}
}
updateText(name, width, height) {
updateText(name: string, width: number, height: number) {
this.nameSpan.textContent = name;
this.dimSpan.textContent =
Math.round(width) + 'px × ' + Math.round(height) + 'px';
}
updatePosition(dims, bounds) {
updatePosition(dims: Box, bounds: Box) {
const tipRect = this.tip.getBoundingClientRect();
const tipPos = findTipPos(dims, bounds, {
width: tipRect.width,
@ -247,6 +249,7 @@ export default class Overlay {
this.tipBoundsWindow.document.documentElement,
this.window
);
this.tip.updatePosition(
{
top: outerBox.top,

View File

@ -2,11 +2,15 @@
import memoize from 'memoize-one';
import throttle from 'lodash.throttle';
import Bridge from 'src/bridge';
import Agent from 'src/backend/agent';
import { hideOverlay, showOverlay } from './Highlighter';
export default function setup(bridge: Bridge, agent: Agent): void {
import type { BackendBridge } from 'src/bridge';
export default function setupHighlighter(
bridge: BackendBridge,
agent: Agent
): void {
bridge.addListener(
'clearNativeElementHighlight',
clearNativeElementHighlight
@ -50,7 +54,7 @@ export default function setup(bridge: Bridge, agent: Agent): void {
rendererID,
scrollIntoView,
}: {
displayName: string,
displayName: string | null,
hideAfterTimeout: boolean,
id: number,
openNativeElementsPanel: boolean,

View File

@ -22,7 +22,7 @@ type Message = {|
type HighlightElementInDOM = {|
...ElementAndRendererID,
displayName: string,
displayName: string | null,
hideAfterTimeout: boolean,
openNativeElementsPanel: boolean,
scrollIntoView: boolean,
@ -62,33 +62,48 @@ type NativeStyleEditor_SetValueParams = {|
value: string,
|};
export default class Bridge extends EventEmitter<{|
type BackendEvents = {|
captureScreenshot: [{| commitIndex: number, rootID: number |}],
inspectedElement: [InspectedElementPayload],
isBackendStorageAPISupported: [boolean],
operations: [Array<number>],
ownersList: [OwnersList],
overrideComponentFilters: [Array<ComponentFilter>],
profilingData: [ProfilingDataBackend],
profilingStatus: [boolean],
reloadAppForProfiling: [],
screenshotCaptured: [
{| commitIndex: number, dataURL: string, rootID: number |},
],
selectFiber: [number],
shutdown: [],
stopInspectingNative: [boolean],
syncSelectionFromNativeElementsPanel: [],
syncSelectionToNativeElementsPanel: [],
// React Native style editor plug-in.
isNativeStyleEditorSupported: [
{| isSupported: boolean, validAttributes: ?$ReadOnlyArray<string> |},
],
NativeStyleEditor_styleAndLayout: [StyleAndLayoutPayload],
|};
type FrontendEvents = {|
captureScreenshot: [{| commitIndex: number, rootID: number |}],
clearNativeElementHighlight: [],
getOwnersList: [ElementAndRendererID],
getProfilingData: [{| rendererID: RendererID |}],
getProfilingStatus: [],
highlightNativeElement: [HighlightElementInDOM],
init: [],
inspectElement: [InspectElementParams],
inspectedElement: [InspectedElementPayload],
isBackendStorageAPISupported: [boolean],
logElementToConsole: [ElementAndRendererID],
operations: [Array<number>],
ownersList: [OwnersList],
overrideComponentFilters: [Array<ComponentFilter>],
overrideContext: [OverrideValue],
overrideHookState: [OverrideHookState],
overrideProps: [OverrideValue],
overrideState: [OverrideValue],
overrideSuspense: [OverrideSuspense],
profilingData: [ProfilingDataBackend],
profilingStatus: [boolean],
reloadAndProfile: [boolean],
reloadAppForProfiling: [],
screenshotCaptured: [
{| commitIndex: number, dataURL: string, rootID: number |},
],
selectElement: [ElementAndRendererID],
selectFiber: [number],
shutdown: [],
@ -96,20 +111,22 @@ export default class Bridge extends EventEmitter<{|
startProfiling: [boolean],
stopInspectingNative: [boolean],
stopProfiling: [],
syncSelectionFromNativeElementsPanel: [],
syncSelectionToNativeElementsPanel: [],
updateAppendComponentStack: [boolean],
updateComponentFilters: [Array<ComponentFilter>],
viewElementSource: [ElementAndRendererID],
// React Native style editor plug-in.
isNativeStyleEditorSupported: [
{| isSupported: boolean, validAttributes: $ReadOnlyArray<string> |},
],
NativeStyleEditor_measure: [ElementAndRendererID],
NativeStyleEditor_renameAttribute: [NativeStyleEditor_RenameAttributeParams],
NativeStyleEditor_setValue: [NativeStyleEditor_SetValueParams],
NativeStyleEditor_styleAndLayout: [StyleAndLayoutPayload],
|};
class Bridge<
OutgoingEvents: Object,
IncomingEvents: Object
> extends EventEmitter<{|
...IncomingEvents,
...OutgoingEvents,
|}> {
_isShutdown: boolean = false;
_messageQueue: Array<any> = [];
@ -134,7 +151,10 @@ export default class Bridge extends EventEmitter<{|
return this._wall;
}
send(event: string, payload: any, transferable?: Array<any>) {
send<EventName: $Keys<OutgoingEvents>>(
event: EventName,
...payload: $ElementType<OutgoingEvents, EventName>
) {
if (this._isShutdown) {
console.warn(
`Cannot send message "${event}" through a Bridge that has been shutdown.`
@ -150,7 +170,7 @@ export default class Bridge extends EventEmitter<{|
// - if there *has* been a message flushed in the last BATCH_DURATION ms
// (or we're waiting for our setTimeout-0 to fire), then _timeoutID will
// be set, and we'll simply add to the queue and wait for that
this._messageQueue.push(event, payload, transferable);
this._messageQueue.push(event, payload);
if (!this._timeoutID) {
this._timeoutID = setTimeout(this._flush, 0);
}
@ -204,12 +224,8 @@ export default class Bridge extends EventEmitter<{|
this._timeoutID = null;
if (this._messageQueue.length) {
for (let i = 0; i < this._messageQueue.length; i += 3) {
this._wall.send(
this._messageQueue[i],
this._messageQueue[i + 1],
this._messageQueue[i + 2]
);
for (let i = 0; i < this._messageQueue.length; i += 2) {
this._wall.send(this._messageQueue[i], ...this._messageQueue[i + 1]);
}
this._messageQueue.length = 0;
@ -220,3 +236,8 @@ export default class Bridge extends EventEmitter<{|
}
};
}
export type BackendBridge = Bridge<BackendEvents, FrontendEvents>;
export type FrontendBridge = Bridge<FrontendEvents, BackendEvents>;
export default Bridge;

View File

@ -3,11 +3,11 @@
import EventEmitter from 'events';
import memoize from 'memoize-one';
import throttle from 'lodash.throttle';
import Bridge from 'src/bridge';
import { prepareProfilingDataFrontendFromBackendAndStore } from './views/Profiler/utils';
import ProfilingCache from './ProfilingCache';
import Store from './store';
import type { FrontendBridge } from 'src/bridge';
import type { ProfilingDataBackend } from 'src/backend/types';
import type {
CommitDataFrontend,
@ -23,7 +23,7 @@ export default class ProfilerStore extends EventEmitter<{|
isProfiling: [],
profilingData: [],
|}> {
_bridge: Bridge;
_bridge: FrontendBridge;
// Suspense cache for lazily calculating derived profiling data.
_cache: ProfilingCache;
@ -79,7 +79,11 @@ export default class ProfilerStore extends EventEmitter<{|
_store: Store;
constructor(bridge: Bridge, store: Store, defaultIsProfiling: boolean) {
constructor(
bridge: FrontendBridge,
store: Store,
defaultIsProfiling: boolean
) {
super();
this._bridge = bridge;

View File

@ -1,6 +1,6 @@
// @flow
import Bridge from 'src/bridge';
import type { FrontendBridge } from 'src/bridge';
type Shell = {|
connect: (callback: Function) => void,
@ -8,7 +8,7 @@ type Shell = {|
|};
export function initDevTools(shell: Shell) {
shell.connect((bridge: Bridge) => {
shell.connect((bridge: FrontendBridge) => {
// TODO ...
});
}

View File

@ -2,7 +2,6 @@
import EventEmitter from 'events';
import { inspect } from 'util';
import Bridge from 'src/bridge';
import {
TREE_OPERATION_ADD,
TREE_OPERATION_REMOVE,
@ -24,6 +23,7 @@ import ProfilerStore from './ProfilerStore';
import type { Element } from './views/Components/types';
import type { ComponentFilter, ElementType } from '../types';
import type { FrontendBridge } from 'src/bridge';
const debug = (methodName, ...args) => {
if (__DEBUG__) {
@ -71,7 +71,7 @@ export default class Store extends EventEmitter<{|
supportsProfiling: [],
supportsReloadAndProfile: [],
|}> {
_bridge: Bridge;
_bridge: FrontendBridge;
_captureScreenshots: boolean = false;
@ -129,7 +129,7 @@ export default class Store extends EventEmitter<{|
// Used for windowing purposes.
_weightAcrossRoots: number = 0;
constructor(bridge: Bridge, config?: Config) {
constructor(bridge: FrontendBridge, config?: Config) {
super();
if (__DEBUG__) {
@ -701,7 +701,7 @@ export default class Store extends EventEmitter<{|
validAttributes,
}: {|
isSupported: boolean,
validAttributes: $ReadOnlyArray<string>,
validAttributes: ?$ReadOnlyArray<string>,
|}) => {
this._isNativeStyleEditorSupported = isSupported;
this._nativeStyleEditorValidAttributes = validAttributes || null;

View File

@ -221,17 +221,19 @@ function HookView({
if (canEditHooks && isStateEditable) {
overrideValueFn = (absolutePath: Array<string | number>, value: any) => {
const rendererID = store.getRendererIDForElement(id);
bridge.send('overrideHookState', {
id,
hookID,
// Hooks override function expects a relative path for the specified hook (id),
// starting with its id within the (flat) hooks list structure.
// This relative path does not include the fake tree structure DevTools uses for display,
// so it's important that we remove that part of the path before sending the update.
path: absolutePath.slice(path.length + 1),
rendererID,
value,
});
if (rendererID !== null) {
bridge.send('overrideHookState', {
id,
hookID,
// Hooks override function expects a relative path for the specified hook (id),
// starting with its id within the (flat) hooks list structure.
// This relative path does not include the fake tree structure DevTools uses for display,
// so it's important that we remove that part of the path before sending the update.
path: absolutePath.slice(path.length + 1),
rendererID,
value,
});
}
};
}

View File

@ -85,7 +85,9 @@ function InspectedElementContextController({ children }: Props) {
const getInspectedElementPath = useCallback<GetInspectedElementPath>(
(id: number, path: Array<string | number>) => {
const rendererID = store.getRendererIDForElement(id);
bridge.send('inspectElement', { id, path, rendererID });
if (rendererID !== null) {
bridge.send('inspectElement', { id, path, rendererID });
}
},
[bridge, store]
);
@ -232,7 +234,9 @@ function InspectedElementContextController({ children }: Props) {
const sendRequest = () => {
timeoutID = null;
bridge.send('inspectElement', { id: selectedElementID, rendererID });
if (rendererID !== null) {
bridge.send('inspectElement', { id: selectedElementID, rendererID });
}
};
// Send the initial inspection request.
@ -240,7 +244,9 @@ function InspectedElementContextController({ children }: Props) {
sendRequest();
// Update the $r variable.
bridge.send('selectElement', { id: selectedElementID, rendererID });
if (rendererID !== null) {
bridge.send('selectElement', { id: selectedElementID, rendererID });
}
const onInspectedElement = (data: InspectedElementPayload) => {
// If this is the element we requested, wait a little bit and then ask for another update.

View File

@ -25,22 +25,28 @@ export default function StyleEditor({ id, style }: Props) {
const store = useContext(StoreContext);
const changeAttribute = (oldName: string, newName: string, value: any) => {
bridge.send('NativeStyleEditor_renameAttribute', {
id,
rendererID: store.getRendererIDForElement(id),
oldName,
newName,
value,
});
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('NativeStyleEditor_renameAttribute', {
id,
rendererID,
oldName,
newName,
value,
});
}
};
const changeValue = (name: string, value: any) => {
bridge.send('NativeStyleEditor_setValue', {
id,
rendererID: store.getRendererIDForElement(id),
name,
value,
});
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('NativeStyleEditor_setValue', {
id,
rendererID,
name,
value,
});
}
};
const keys = useMemo(() => Array.from(Object.keys(style)), [style]);

View File

@ -136,10 +136,12 @@ function NativeStyleContextController({ children }: Props) {
const sendRequest = () => {
timeoutID = null;
bridge.send('NativeStyleEditor_measure', {
id: selectedElementID,
rendererID,
});
if (rendererID !== null) {
bridge.send('NativeStyleEditor_measure', {
id: selectedElementID,
rendererID,
});
}
};
// Send the initial measurement request.

View File

@ -106,8 +106,9 @@ function OwnersListContextController({ children }: Props) {
useEffect(() => {
if (ownerID !== null) {
const rendererID = store.getRendererIDForElement(ownerID);
bridge.send('getOwnersList', { id: ownerID, rendererID });
if (rendererID !== null) {
bridge.send('getOwnersList', { id: ownerID, rendererID });
}
}
return () => {};

View File

@ -140,12 +140,18 @@ export default function SelectedElement(_: Props) {
});
}
const rendererID = store.getRendererIDForElement(
nearestSuspenseElementID
);
// Toggle suspended
bridge.send('overrideSuspense', {
id: nearestSuspenseElementID,
rendererID: store.getRendererIDForElement(nearestSuspenseElementID),
forceFallback: !isSuspended,
});
if (rendererID !== null) {
bridge.send('overrideSuspense', {
id: nearestSuspenseElementID,
rendererID,
forceFallback: !isSuspended,
});
}
}
}, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store]);
@ -280,15 +286,21 @@ function InspectedElementView({
if (type === ElementTypeClass) {
overrideContextFn = (path: Array<string | number>, value: any) => {
const rendererID = store.getRendererIDForElement(id);
bridge.send('overrideContext', { id, path, rendererID, value });
if (rendererID !== null) {
bridge.send('overrideContext', { id, path, rendererID, value });
}
};
overridePropsFn = (path: Array<string | number>, value: any) => {
const rendererID = store.getRendererIDForElement(id);
bridge.send('overrideProps', { id, path, rendererID, value });
if (rendererID !== null) {
bridge.send('overrideProps', { id, path, rendererID, value });
}
};
overrideStateFn = (path: Array<string | number>, value: any) => {
const rendererID = store.getRendererIDForElement(id);
bridge.send('overrideState', { id, path, rendererID, value });
if (rendererID !== null) {
bridge.send('overrideState', { id, path, rendererID, value });
}
};
} else if (
(type === ElementTypeFunction ||
@ -298,7 +310,9 @@ function InspectedElementView({
) {
overridePropsFn = (path: Array<string | number>, value: any) => {
const rendererID = store.getRendererIDForElement(id);
bridge.send('overrideProps', { id, path, rendererID, value });
if (rendererID !== null) {
bridge.send('overrideProps', { id, path, rendererID, value });
}
};
} else if (type === ElementTypeSuspense && canToggleSuspense) {
overrideSuspenseFn = (path: Array<string | number>, value: boolean) => {
@ -306,7 +320,13 @@ function InspectedElementView({
throw new Error('Unexpected path.');
}
const rendererID = store.getRendererIDForElement(id);
bridge.send('overrideSuspense', { id, rendererID, forceFallback: value });
if (rendererID !== null) {
bridge.send('overrideSuspense', {
id,
rendererID,
forceFallback: value,
});
}
};
}

View File

@ -192,7 +192,7 @@ export default function Tree(props: Props) {
(id: number) => {
const element = store.getElementByID(id);
const rendererID = store.getRendererIDForElement(id);
if (element !== null) {
if (element !== null && rendererID !== null) {
bridge.send('highlightNativeElement', {
displayName: element.displayName,
hideAfterTimeout: false,

View File

@ -6,7 +6,6 @@ import '@reach/menu-button/styles.css';
import '@reach/tooltip/styles.css';
import React, { useMemo, useState } from 'react';
import Bridge from 'src/bridge';
import Store from '../store';
import { BridgeContext, StoreContext } from './context';
import Components from './Components/Components';
@ -25,6 +24,7 @@ import styles from './DevTools.css';
import './root.css';
import type { InspectedElement } from 'src/devtools/views/Components/types';
import type { FrontendBridge } from 'src/bridge';
export type BrowserTheme = 'dark' | 'light';
export type TabID = 'components' | 'profiler' | 'settings';
@ -34,7 +34,7 @@ export type ViewElementSource = (
) => void;
export type Props = {|
bridge: Bridge,
bridge: FrontendBridge,
browserTheme?: BrowserTheme,
defaultTab?: TabID,
showTabBar?: boolean,

View File

@ -1,11 +1,13 @@
// @flow
import { createContext } from 'react';
import Bridge from 'src/bridge';
import Store from '../store';
export const BridgeContext = createContext<Bridge>(((null: any): Bridge));
import type { FrontendBridge } from 'src/bridge';
export const BridgeContext = createContext<FrontendBridge>(
((null: any): FrontendBridge)
);
BridgeContext.displayName = 'BridgeContext';
export const StoreContext = createContext<Store>(((null: any): Store));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -59,6 +59,50 @@ function getScrollbarSize(recalculate) {
return size;
}
var cachedRTLResult = 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.
function getRTLOffsetType(recalculate) {
if (recalculate === void 0) {
recalculate = false;
}
if (cachedRTLResult === null || recalculate) {
var outerDiv = document.createElement('div');
var outerStyle = outerDiv.style;
outerStyle.width = '50px';
outerStyle.height = '50px';
outerStyle.overflow = 'scroll';
outerStyle.direction = 'rtl';
var innerDiv = document.createElement('div');
var innerStyle = innerDiv.style;
innerStyle.width = '100px';
innerStyle.height = '100px';
outerDiv.appendChild(innerDiv);
document.body.appendChild(outerDiv);
if (outerDiv.scrollLeft > 0) {
cachedRTLResult = 'positive-descending';
} else {
outerDiv.scrollLeft = 1;
if (outerDiv.scrollLeft === 0) {
cachedRTLResult = 'negative';
} else {
cachedRTLResult = 'positive-ascending';
}
}
document.body.removeChild(outerDiv);
return cachedRTLResult;
}
return cachedRTLResult;
}
var IS_SCROLLING_DEBOUNCE_INTERVAL = 150;
@ -72,6 +116,7 @@ var defaultItemKey = function defaultItemKey(_ref) {
var devWarningsOverscanCount = null;
var devWarningsOverscanRowsColumnsCount = null;
var devWarningsTagName = null;
if (process.env.NODE_ENV !== 'production') {
@ -79,6 +124,9 @@ if (process.env.NODE_ENV !== 'production') {
devWarningsOverscanCount =
/*#__PURE__*/
new WeakSet();
devWarningsOverscanRowsColumnsCount =
/*#__PURE__*/
new WeakSet();
devWarningsTagName =
/*#__PURE__*/
new WeakSet();
@ -183,9 +231,11 @@ function createGridComponent(_ref2) {
_this._onScroll = function (event) {
var _event$currentTarget = event.currentTarget,
clientHeight = _event$currentTarget.clientHeight,
clientWidth = _event$currentTarget.clientWidth,
scrollLeft = _event$currentTarget.scrollLeft,
scrollTop = _event$currentTarget.scrollTop,
scrollHeight = _event$currentTarget.scrollHeight,
scrollWidth = _event$currentTarget.scrollWidth;
_this.setState(function (prevState) {
@ -196,25 +246,33 @@ function createGridComponent(_ref2) {
return null;
}
var direction = _this.props.direction; // HACK 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
var direction = _this.props.direction; // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// It's also easier for this component if we convert offsets to the same format as they would be in for ltr.
// So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it.
var calculatedScrollLeft = scrollLeft;
if (direction === 'rtl') {
if (scrollLeft <= 0) {
calculatedScrollLeft = -scrollLeft;
} else {
calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
}
}
switch (getRTLOffsetType()) {
case 'negative':
calculatedScrollLeft = -scrollLeft;
break;
case 'positive-descending':
calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
break;
}
} // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
calculatedScrollLeft = Math.max(0, Math.min(calculatedScrollLeft, scrollWidth - clientWidth));
var calculatedScrollTop = Math.max(0, Math.min(scrollTop, scrollHeight - clientHeight));
return {
isScrolling: true,
horizontalScrollDirection: prevState.scrollLeft < scrollLeft ? 'forward' : 'backward',
scrollLeft: calculatedScrollLeft,
scrollTop: scrollTop,
scrollTop: calculatedScrollTop,
verticalScrollDirection: prevState.scrollTop < scrollTop ? 'forward' : 'backward',
scrollUpdateWasRequested: false
};
@ -339,26 +397,55 @@ function createGridComponent(_ref2) {
initialScrollLeft = _this$props3.initialScrollLeft,
initialScrollTop = _this$props3.initialScrollTop;
if (typeof initialScrollLeft === 'number' && this._outerRef != null) {
this._outerRef.scrollLeft = initialScrollLeft;
}
if (this._outerRef != null) {
var outerRef = this._outerRef;
if (typeof initialScrollTop === 'number' && this._outerRef != null) {
this._outerRef.scrollTop = initialScrollTop;
if (typeof initialScrollLeft === 'number') {
outerRef.scrollLeft = initialScrollLeft;
}
if (typeof initialScrollTop === 'number') {
outerRef.scrollTop = initialScrollTop;
}
}
this._callPropsCallbacks();
};
_proto.componentDidUpdate = function componentDidUpdate() {
var direction = this.props.direction;
var _this$state2 = this.state,
scrollLeft = _this$state2.scrollLeft,
scrollTop = _this$state2.scrollTop,
scrollUpdateWasRequested = _this$state2.scrollUpdateWasRequested;
if (scrollUpdateWasRequested && this._outerRef !== null) {
this._outerRef.scrollLeft = scrollLeft;
this._outerRef.scrollTop = scrollTop;
if (scrollUpdateWasRequested && this._outerRef != null) {
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// So we need to determine which browser behavior we're dealing with, and mimic it.
var outerRef = this._outerRef;
if (direction === 'rtl') {
switch (getRTLOffsetType()) {
case 'negative':
outerRef.scrollLeft = -scrollLeft;
break;
case 'positive-ascending':
outerRef.scrollLeft = scrollLeft;
break;
default:
var clientWidth = outerRef.clientWidth,
scrollWidth = outerRef.scrollWidth;
outerRef.scrollLeft = scrollWidth - clientWidth - scrollLeft;
break;
}
} else {
outerRef.scrollLeft = Math.max(0, scrollLeft);
}
outerRef.scrollTop = Math.max(0, scrollTop);
}
this._callPropsCallbacks();
@ -442,7 +529,7 @@ function createGridComponent(_ref2) {
ref: innerRef,
style: {
height: estimatedTotalHeight,
pointerEvents: isScrolling ? 'none' : '',
pointerEvents: isScrolling ? 'none' : undefined,
width: estimatedTotalWidth
}
}));
@ -492,6 +579,7 @@ function createGridComponent(_ref2) {
_proto._getHorizontalRangeToRender = function _getHorizontalRangeToRender() {
var _this$props6 = this.props,
columnCount = _this$props6.columnCount,
overscanColumnCount = _this$props6.overscanColumnCount,
overscanColumnsCount = _this$props6.overscanColumnsCount,
overscanCount = _this$props6.overscanCount,
rowCount = _this$props6.rowCount;
@ -499,7 +587,7 @@ function createGridComponent(_ref2) {
horizontalScrollDirection = _this$state4.horizontalScrollDirection,
isScrolling = _this$state4.isScrolling,
scrollLeft = _this$state4.scrollLeft;
var overscanCountResolved = overscanColumnsCount || overscanCount || 1;
var overscanCountResolved = overscanColumnCount || overscanColumnsCount || overscanCount || 1;
if (columnCount === 0 || rowCount === 0) {
return [0, 0, 0, 0];
@ -518,13 +606,14 @@ function createGridComponent(_ref2) {
var _this$props7 = this.props,
columnCount = _this$props7.columnCount,
overscanCount = _this$props7.overscanCount,
overscanRowCount = _this$props7.overscanRowCount,
overscanRowsCount = _this$props7.overscanRowsCount,
rowCount = _this$props7.rowCount;
var _this$state5 = this.state,
isScrolling = _this$state5.isScrolling,
verticalScrollDirection = _this$state5.verticalScrollDirection,
scrollTop = _this$state5.scrollTop;
var overscanCountResolved = overscanRowsCount || overscanCount || 1;
var overscanCountResolved = overscanRowCount || overscanRowsCount || overscanCount || 1;
if (columnCount === 0 || rowCount === 0) {
return [0, 0, 0, 0];
@ -553,7 +642,9 @@ var validateSharedProps = function validateSharedProps(_ref5, _ref6) {
height = _ref5.height,
innerTagName = _ref5.innerTagName,
outerTagName = _ref5.outerTagName,
overscanColumnsCount = _ref5.overscanColumnsCount,
overscanCount = _ref5.overscanCount,
overscanRowsCount = _ref5.overscanRowsCount,
width = _ref5.width;
var instance = _ref6.instance;
@ -561,7 +652,14 @@ var validateSharedProps = function validateSharedProps(_ref5, _ref6) {
if (typeof overscanCount === 'number') {
if (devWarningsOverscanCount && !devWarningsOverscanCount.has(instance)) {
devWarningsOverscanCount.add(instance);
console.warn('The overscanCount prop has been deprecated. ' + 'Please use the overscanColumnsCount and overscanRowsCount props instead.');
console.warn('The overscanCount prop has been deprecated. ' + 'Please use the overscanColumnCount and overscanRowCount props instead.');
}
}
if (typeof overscanColumnsCount === 'number' || typeof overscanRowsCount === 'number') {
if (devWarningsOverscanRowsColumnsCount && !devWarningsOverscanRowsColumnsCount.has(instance)) {
devWarningsOverscanRowsColumnsCount.add(instance);
console.warn('The overscanColumnsCount and overscanRowsCount props have been deprecated. ' + 'Please use the overscanColumnCount and overscanRowCount props instead.');
}
}
@ -770,7 +868,11 @@ var getOffsetForIndexAndAlignment = function getOffsetForIndexAndAlignment(itemT
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset;
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
} else if (minOffset > maxOffset) {
// Because we only take into account the scrollbar size when calculating minOffset
// this value can be larger than maxOffset when at the end of the list
return minOffset;
} else if (scrollOffset < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -1037,20 +1139,27 @@ function createListComponent(_ref) {
return null;
}
var direction = _this.props.direction; // HACK According to the spec, scrollLeft should be negative for RTL aligned elements.
// Chrome does not seem to adhere; its scrolLeft values are positive (measured relative to the left).
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
var direction = _this.props.direction;
var scrollOffset = scrollLeft;
if (direction === 'rtl') {
if (scrollLeft <= 0) {
scrollOffset = -scrollOffset;
} else {
scrollOffset = scrollWidth - clientWidth - scrollLeft;
}
}
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// It's also easier for this component if we convert offsets to the same format as they would be in for ltr.
// So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it.
switch (getRTLOffsetType()) {
case 'negative':
scrollOffset = -scrollLeft;
break;
case 'positive-descending':
scrollOffset = scrollWidth - clientWidth - scrollLeft;
break;
}
} // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
scrollOffset = Math.max(0, Math.min(scrollOffset, scrollWidth - clientWidth));
return {
isScrolling: true,
scrollDirection: prevState.scrollOffset < scrollLeft ? 'forward' : 'backward',
@ -1061,7 +1170,10 @@ function createListComponent(_ref) {
};
_this._onScrollVertical = function (event) {
var scrollTop = event.currentTarget.scrollTop;
var _event$currentTarget2 = event.currentTarget,
clientHeight = _event$currentTarget2.clientHeight,
scrollHeight = _event$currentTarget2.scrollHeight,
scrollTop = _event$currentTarget2.scrollTop;
_this.setState(function (prevState) {
if (prevState.scrollOffset === scrollTop) {
@ -1069,12 +1181,14 @@ function createListComponent(_ref) {
// In which case we don't need to trigger another render,
// And we don't want to update state.isScrolling.
return null;
}
} // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
var scrollOffset = Math.max(0, Math.min(scrollTop, scrollHeight - clientHeight));
return {
isScrolling: true,
scrollDirection: prevState.scrollOffset < scrollTop ? 'forward' : 'backward',
scrollOffset: scrollTop,
scrollDirection: prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',
scrollOffset: scrollOffset,
scrollUpdateWasRequested: false
};
}, _this._resetIsScrollingDebounced);
@ -1154,12 +1268,13 @@ function createListComponent(_ref) {
initialScrollOffset = _this$props2.initialScrollOffset,
layout = _this$props2.layout;
if (typeof initialScrollOffset === 'number' && this._outerRef !== null) {
// TODO Deprecate direction "horizontal"
if (typeof initialScrollOffset === 'number' && this._outerRef != null) {
var outerRef = this._outerRef; // TODO Deprecate direction "horizontal"
if (direction === 'horizontal' || layout === 'horizontal') {
this._outerRef.scrollLeft = initialScrollOffset;
outerRef.scrollLeft = initialScrollOffset;
} else {
this._outerRef.scrollTop = initialScrollOffset;
outerRef.scrollTop = initialScrollOffset;
}
}
@ -1174,12 +1289,34 @@ function createListComponent(_ref) {
scrollOffset = _this$state.scrollOffset,
scrollUpdateWasRequested = _this$state.scrollUpdateWasRequested;
if (scrollUpdateWasRequested && this._outerRef !== null) {
// TODO Deprecate direction "horizontal"
if (scrollUpdateWasRequested && this._outerRef != null) {
var outerRef = this._outerRef; // TODO Deprecate direction "horizontal"
if (direction === 'horizontal' || layout === 'horizontal') {
this._outerRef.scrollLeft = scrollOffset;
if (direction === 'rtl') {
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// So we need to determine which browser behavior we're dealing with, and mimic it.
switch (getRTLOffsetType()) {
case 'negative':
outerRef.scrollLeft = -scrollOffset;
break;
case 'positive-ascending':
outerRef.scrollLeft = scrollOffset;
break;
default:
var clientWidth = outerRef.clientWidth,
scrollWidth = outerRef.scrollWidth;
outerRef.scrollLeft = scrollWidth - clientWidth - scrollOffset;
break;
}
} else {
outerRef.scrollLeft = scrollOffset;
}
} else {
this._outerRef.scrollTop = scrollOffset;
outerRef.scrollTop = scrollOffset;
}
}
@ -1255,7 +1392,7 @@ function createListComponent(_ref) {
ref: innerRef,
style: {
height: isHorizontal ? '100%' : estimatedTotalSize,
pointerEvents: isScrolling ? 'none' : '',
pointerEvents: isScrolling ? 'none' : undefined,
width: isHorizontal ? estimatedTotalSize : '100%'
}
}));
@ -1537,7 +1674,7 @@ createListComponent({
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset;
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
} else if (scrollOffset < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -1642,7 +1779,8 @@ createGridComponent({
var columnCount = _ref7.columnCount,
columnWidth = _ref7.columnWidth,
width = _ref7.width;
var maxOffset = Math.max(0, Math.min(columnCount * columnWidth - width, columnIndex * columnWidth));
var lastColumnOffset = Math.max(0, columnCount * columnWidth - width);
var maxOffset = Math.min(lastColumnOffset, columnIndex * columnWidth);
var minOffset = Math.max(0, columnIndex * columnWidth - width + scrollbarSize + columnWidth);
if (align === 'smart') {
@ -1661,13 +1799,27 @@ createGridComponent({
return minOffset;
case 'center':
return Math.round(minOffset + (maxOffset - minOffset) / 2);
// "Centered" offset is usually the average of the min and max.
// But near the edges of the list, this doesn't hold true.
var middleOffset = Math.round(minOffset + (maxOffset - minOffset) / 2);
if (middleOffset < Math.ceil(width / 2)) {
return 0; // near the beginning
} else if (middleOffset > lastColumnOffset + Math.floor(width / 2)) {
return lastColumnOffset; // near the end
} else {
return middleOffset;
}
case 'auto':
default:
if (scrollLeft >= minOffset && scrollLeft <= maxOffset) {
return scrollLeft;
} else if (scrollLeft - minOffset < maxOffset - scrollLeft) {
} else if (minOffset > maxOffset) {
// Because we only take into account the scrollbar size when calculating minOffset
// this value can be larger than maxOffset when at the end of the list
return minOffset;
} else if (scrollLeft < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -1679,7 +1831,8 @@ createGridComponent({
var rowHeight = _ref8.rowHeight,
height = _ref8.height,
rowCount = _ref8.rowCount;
var maxOffset = Math.max(0, Math.min(rowCount * rowHeight - height, rowIndex * rowHeight));
var lastRowOffset = Math.max(0, rowCount * rowHeight - height);
var maxOffset = Math.min(lastRowOffset, rowIndex * rowHeight);
var minOffset = Math.max(0, rowIndex * rowHeight - height + scrollbarSize + rowHeight);
if (align === 'smart') {
@ -1698,13 +1851,27 @@ createGridComponent({
return minOffset;
case 'center':
return Math.round(minOffset + (maxOffset - minOffset) / 2);
// "Centered" offset is usually the average of the min and max.
// But near the edges of the list, this doesn't hold true.
var middleOffset = Math.round(minOffset + (maxOffset - minOffset) / 2);
if (middleOffset < Math.ceil(height / 2)) {
return 0; // near the beginning
} else if (middleOffset > lastRowOffset + Math.floor(height / 2)) {
return lastRowOffset; // near the end
} else {
return middleOffset;
}
case 'auto':
default:
if (scrollTop >= minOffset && scrollTop <= maxOffset) {
return scrollTop;
} else if (scrollTop - minOffset < maxOffset - scrollTop) {
} else if (minOffset > maxOffset) {
// Because we only take into account the scrollbar size when calculating minOffset
// this value can be larger than maxOffset when at the end of the list
return minOffset;
} else if (scrollTop < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -1722,7 +1889,9 @@ createGridComponent({
columnCount = _ref10.columnCount,
width = _ref10.width;
var left = startIndex * columnWidth;
return Math.max(0, Math.min(columnCount - 1, startIndex + Math.floor((width + (scrollLeft - left)) / columnWidth)));
var numVisibleColumns = Math.ceil((width + scrollLeft - left) / columnWidth);
return Math.max(0, Math.min(columnCount - 1, startIndex + numVisibleColumns - 1 // -1 is because stop index is inclusive
));
},
getRowStartIndexForOffset: function getRowStartIndexForOffset(_ref11, scrollTop) {
var rowHeight = _ref11.rowHeight,
@ -1733,8 +1902,10 @@ createGridComponent({
var rowHeight = _ref12.rowHeight,
rowCount = _ref12.rowCount,
height = _ref12.height;
var left = startIndex * rowHeight;
return Math.max(0, Math.min(rowCount - 1, startIndex + Math.floor((height + (scrollTop - left)) / rowHeight)));
var top = startIndex * rowHeight;
var numVisibleRows = Math.ceil((height + scrollTop - top) / rowHeight);
return Math.max(0, Math.min(rowCount - 1, startIndex + numVisibleRows - 1 // -1 is because stop index is inclusive
));
},
initInstanceProps: function initInstanceProps(props) {// Noop
},
@ -1759,13 +1930,11 @@ var FixedSizeList =
/*#__PURE__*/
createListComponent({
getItemOffset: function getItemOffset(_ref, index) {
var itemSize = _ref.itemSize,
size = _ref.size;
var itemSize = _ref.itemSize;
return index * itemSize;
},
getItemSize: function getItemSize(_ref2, index) {
var itemSize = _ref2.itemSize,
size = _ref2.size;
var itemSize = _ref2.itemSize;
return itemSize;
},
getEstimatedTotalSize: function getEstimatedTotalSize(_ref3) {
@ -1783,7 +1952,8 @@ createListComponent({
// TODO Deprecate direction "horizontal"
var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
var size = isHorizontal ? width : height;
var maxOffset = Math.max(0, Math.min(itemCount * itemSize - size, index * itemSize));
var lastItemOffset = Math.max(0, itemCount * itemSize - size);
var maxOffset = Math.min(lastItemOffset, index * itemSize);
var minOffset = Math.max(0, index * itemSize - size + itemSize);
if (align === 'smart') {
@ -1802,13 +1972,25 @@ createListComponent({
return minOffset;
case 'center':
return Math.round(minOffset + (maxOffset - minOffset) / 2);
{
// "Centered" offset is usually the average of the min and max.
// But near the edges of the list, this doesn't hold true.
var middleOffset = Math.round(minOffset + (maxOffset - minOffset) / 2);
if (middleOffset < Math.ceil(size / 2)) {
return 0; // near the beginning
} else if (middleOffset > lastItemOffset + Math.floor(size / 2)) {
return lastItemOffset; // near the end
} else {
return middleOffset;
}
}
case 'auto':
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset;
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
} else if (scrollOffset < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -1832,7 +2014,9 @@ createListComponent({
var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
var offset = startIndex * itemSize;
var size = isHorizontal ? width : height;
return Math.max(0, Math.min(itemCount - 1, startIndex + Math.floor((size + (scrollOffset - offset)) / itemSize)));
var numVisibleItems = Math.ceil((size + scrollOffset - offset) / itemSize);
return Math.max(0, Math.min(itemCount - 1, startIndex + numVisibleItems - 1 // -1 is because stop index is inclusive
));
},
initInstanceProps: function initInstanceProps(props) {// Noop
},
@ -1892,3 +2076,4 @@ exports.FixedSizeGrid = FixedSizeGrid;
exports.FixedSizeList = FixedSizeList;
exports.areEqual = areEqual;
exports.shouldComponentUpdate = shouldComponentUpdate;
//# sourceMappingURL=index.cjs.js.map

View File

@ -54,6 +54,50 @@ function getScrollbarSize(recalculate) {
return size;
}
var cachedRTLResult = 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.
function getRTLOffsetType(recalculate) {
if (recalculate === void 0) {
recalculate = false;
}
if (cachedRTLResult === null || recalculate) {
var outerDiv = document.createElement('div');
var outerStyle = outerDiv.style;
outerStyle.width = '50px';
outerStyle.height = '50px';
outerStyle.overflow = 'scroll';
outerStyle.direction = 'rtl';
var innerDiv = document.createElement('div');
var innerStyle = innerDiv.style;
innerStyle.width = '100px';
innerStyle.height = '100px';
outerDiv.appendChild(innerDiv);
document.body.appendChild(outerDiv);
if (outerDiv.scrollLeft > 0) {
cachedRTLResult = 'positive-descending';
} else {
outerDiv.scrollLeft = 1;
if (outerDiv.scrollLeft === 0) {
cachedRTLResult = 'negative';
} else {
cachedRTLResult = 'positive-ascending';
}
}
document.body.removeChild(outerDiv);
return cachedRTLResult;
}
return cachedRTLResult;
}
var IS_SCROLLING_DEBOUNCE_INTERVAL = 150;
@ -67,6 +111,7 @@ var defaultItemKey = function defaultItemKey(_ref) {
var devWarningsOverscanCount = null;
var devWarningsOverscanRowsColumnsCount = null;
var devWarningsTagName = null;
if (process.env.NODE_ENV !== 'production') {
@ -74,6 +119,9 @@ if (process.env.NODE_ENV !== 'production') {
devWarningsOverscanCount =
/*#__PURE__*/
new WeakSet();
devWarningsOverscanRowsColumnsCount =
/*#__PURE__*/
new WeakSet();
devWarningsTagName =
/*#__PURE__*/
new WeakSet();
@ -178,9 +226,11 @@ function createGridComponent(_ref2) {
_this._onScroll = function (event) {
var _event$currentTarget = event.currentTarget,
clientHeight = _event$currentTarget.clientHeight,
clientWidth = _event$currentTarget.clientWidth,
scrollLeft = _event$currentTarget.scrollLeft,
scrollTop = _event$currentTarget.scrollTop,
scrollHeight = _event$currentTarget.scrollHeight,
scrollWidth = _event$currentTarget.scrollWidth;
// Force flush sync for scroll updates to reduce visual checkerboarding.
@ -193,25 +243,33 @@ function createGridComponent(_ref2) {
return null;
}
var direction = _this.props.direction; // HACK 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
var direction = _this.props.direction; // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// It's also easier for this component if we convert offsets to the same format as they would be in for ltr.
// So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it.
var calculatedScrollLeft = scrollLeft;
if (direction === 'rtl') {
if (scrollLeft <= 0) {
calculatedScrollLeft = -scrollLeft;
} else {
calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
}
}
switch (getRTLOffsetType()) {
case 'negative':
calculatedScrollLeft = -scrollLeft;
break;
case 'positive-descending':
calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
break;
}
} // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
calculatedScrollLeft = Math.max(0, Math.min(calculatedScrollLeft, scrollWidth - clientWidth));
var calculatedScrollTop = Math.max(0, Math.min(scrollTop, scrollHeight - clientHeight));
return {
isScrolling: true,
horizontalScrollDirection: prevState.scrollLeft < scrollLeft ? 'forward' : 'backward',
scrollLeft: calculatedScrollLeft,
scrollTop: scrollTop,
scrollTop: calculatedScrollTop,
verticalScrollDirection: prevState.scrollTop < scrollTop ? 'forward' : 'backward',
scrollUpdateWasRequested: false
};
@ -337,26 +395,55 @@ function createGridComponent(_ref2) {
initialScrollLeft = _this$props3.initialScrollLeft,
initialScrollTop = _this$props3.initialScrollTop;
if (typeof initialScrollLeft === 'number' && this._outerRef != null) {
this._outerRef.scrollLeft = initialScrollLeft;
}
if (this._outerRef != null) {
var outerRef = this._outerRef;
if (typeof initialScrollTop === 'number' && this._outerRef != null) {
this._outerRef.scrollTop = initialScrollTop;
if (typeof initialScrollLeft === 'number') {
outerRef.scrollLeft = initialScrollLeft;
}
if (typeof initialScrollTop === 'number') {
outerRef.scrollTop = initialScrollTop;
}
}
this._callPropsCallbacks();
};
_proto.componentDidUpdate = function componentDidUpdate() {
var direction = this.props.direction;
var _this$state2 = this.state,
scrollLeft = _this$state2.scrollLeft,
scrollTop = _this$state2.scrollTop,
scrollUpdateWasRequested = _this$state2.scrollUpdateWasRequested;
if (scrollUpdateWasRequested && this._outerRef !== null) {
this._outerRef.scrollLeft = scrollLeft;
this._outerRef.scrollTop = scrollTop;
if (scrollUpdateWasRequested && this._outerRef != null) {
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// So we need to determine which browser behavior we're dealing with, and mimic it.
var outerRef = this._outerRef;
if (direction === 'rtl') {
switch (getRTLOffsetType()) {
case 'negative':
outerRef.scrollLeft = -scrollLeft;
break;
case 'positive-ascending':
outerRef.scrollLeft = scrollLeft;
break;
default:
var clientWidth = outerRef.clientWidth,
scrollWidth = outerRef.scrollWidth;
outerRef.scrollLeft = scrollWidth - clientWidth - scrollLeft;
break;
}
} else {
outerRef.scrollLeft = Math.max(0, scrollLeft);
}
outerRef.scrollTop = Math.max(0, scrollTop);
}
this._callPropsCallbacks();
@ -440,7 +527,7 @@ function createGridComponent(_ref2) {
ref: innerRef,
style: {
height: estimatedTotalHeight,
pointerEvents: isScrolling ? 'none' : '',
pointerEvents: isScrolling ? 'none' : undefined,
width: estimatedTotalWidth
}
}));
@ -490,6 +577,7 @@ function createGridComponent(_ref2) {
_proto._getHorizontalRangeToRender = function _getHorizontalRangeToRender() {
var _this$props6 = this.props,
columnCount = _this$props6.columnCount,
overscanColumnCount = _this$props6.overscanColumnCount,
overscanColumnsCount = _this$props6.overscanColumnsCount,
overscanCount = _this$props6.overscanCount,
rowCount = _this$props6.rowCount;
@ -497,7 +585,7 @@ function createGridComponent(_ref2) {
horizontalScrollDirection = _this$state4.horizontalScrollDirection,
isScrolling = _this$state4.isScrolling,
scrollLeft = _this$state4.scrollLeft;
var overscanCountResolved = overscanColumnsCount || overscanCount || 1;
var overscanCountResolved = overscanColumnCount || overscanColumnsCount || overscanCount || 1;
if (columnCount === 0 || rowCount === 0) {
return [0, 0, 0, 0];
@ -516,13 +604,14 @@ function createGridComponent(_ref2) {
var _this$props7 = this.props,
columnCount = _this$props7.columnCount,
overscanCount = _this$props7.overscanCount,
overscanRowCount = _this$props7.overscanRowCount,
overscanRowsCount = _this$props7.overscanRowsCount,
rowCount = _this$props7.rowCount;
var _this$state5 = this.state,
isScrolling = _this$state5.isScrolling,
verticalScrollDirection = _this$state5.verticalScrollDirection,
scrollTop = _this$state5.scrollTop;
var overscanCountResolved = overscanRowsCount || overscanCount || 1;
var overscanCountResolved = overscanRowCount || overscanRowsCount || overscanCount || 1;
if (columnCount === 0 || rowCount === 0) {
return [0, 0, 0, 0];
@ -551,7 +640,9 @@ var validateSharedProps = function validateSharedProps(_ref5, _ref6) {
height = _ref5.height,
innerTagName = _ref5.innerTagName,
outerTagName = _ref5.outerTagName,
overscanColumnsCount = _ref5.overscanColumnsCount,
overscanCount = _ref5.overscanCount,
overscanRowsCount = _ref5.overscanRowsCount,
width = _ref5.width;
var instance = _ref6.instance;
@ -559,7 +650,14 @@ var validateSharedProps = function validateSharedProps(_ref5, _ref6) {
if (typeof overscanCount === 'number') {
if (devWarningsOverscanCount && !devWarningsOverscanCount.has(instance)) {
devWarningsOverscanCount.add(instance);
console.warn('The overscanCount prop has been deprecated. ' + 'Please use the overscanColumnsCount and overscanRowsCount props instead.');
console.warn('The overscanCount prop has been deprecated. ' + 'Please use the overscanColumnCount and overscanRowCount props instead.');
}
}
if (typeof overscanColumnsCount === 'number' || typeof overscanRowsCount === 'number') {
if (devWarningsOverscanRowsColumnsCount && !devWarningsOverscanRowsColumnsCount.has(instance)) {
devWarningsOverscanRowsColumnsCount.add(instance);
console.warn('The overscanColumnsCount and overscanRowsCount props have been deprecated. ' + 'Please use the overscanColumnCount and overscanRowCount props instead.');
}
}
@ -768,7 +866,11 @@ var getOffsetForIndexAndAlignment = function getOffsetForIndexAndAlignment(itemT
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset;
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
} else if (minOffset > maxOffset) {
// Because we only take into account the scrollbar size when calculating minOffset
// this value can be larger than maxOffset when at the end of the list
return minOffset;
} else if (scrollOffset < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -1037,20 +1139,27 @@ function createListComponent(_ref) {
return null;
}
var direction = _this.props.direction; // HACK According to the spec, scrollLeft should be negative for RTL aligned elements.
// Chrome does not seem to adhere; its scrolLeft values are positive (measured relative to the left).
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
var direction = _this.props.direction;
var scrollOffset = scrollLeft;
if (direction === 'rtl') {
if (scrollLeft <= 0) {
scrollOffset = -scrollOffset;
} else {
scrollOffset = scrollWidth - clientWidth - scrollLeft;
}
}
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// It's also easier for this component if we convert offsets to the same format as they would be in for ltr.
// So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it.
switch (getRTLOffsetType()) {
case 'negative':
scrollOffset = -scrollLeft;
break;
case 'positive-descending':
scrollOffset = scrollWidth - clientWidth - scrollLeft;
break;
}
} // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
scrollOffset = Math.max(0, Math.min(scrollOffset, scrollWidth - clientWidth));
return {
isScrolling: true,
scrollDirection: prevState.scrollOffset < scrollLeft ? 'forward' : 'backward',
@ -1058,11 +1167,14 @@ function createListComponent(_ref) {
scrollUpdateWasRequested: false
};
}, _this._resetIsScrollingDebounced);
});
};
};
});
_this._onScrollVertical = function (event) {
var scrollTop = event.currentTarget.scrollTop;
var _event$currentTarget2 = event.currentTarget,
clientHeight = _event$currentTarget2.clientHeight,
scrollHeight = _event$currentTarget2.scrollHeight,
scrollTop = _event$currentTarget2.scrollTop;
// Force flush sync for scroll updates to reduce visual checkerboarding.
flushSync(() => {
@ -1072,17 +1184,19 @@ function createListComponent(_ref) {
// In which case we don't need to trigger another render,
// And we don't want to update state.isScrolling.
return null;
}
} // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
var scrollOffset = Math.max(0, Math.min(scrollTop, scrollHeight - clientHeight));
return {
isScrolling: true,
scrollDirection: prevState.scrollOffset < scrollTop ? 'forward' : 'backward',
scrollOffset: scrollTop,
scrollDirection: prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',
scrollOffset: scrollOffset,
scrollUpdateWasRequested: false
};
}, _this._resetIsScrollingDebounced);
});
};
};
});
_this._outerRefSetter = function (ref) {
var outerRef = _this.props.outerRef;
@ -1158,12 +1272,13 @@ function createListComponent(_ref) {
initialScrollOffset = _this$props2.initialScrollOffset,
layout = _this$props2.layout;
if (typeof initialScrollOffset === 'number' && this._outerRef !== null) {
// TODO Deprecate direction "horizontal"
if (typeof initialScrollOffset === 'number' && this._outerRef != null) {
var outerRef = this._outerRef; // TODO Deprecate direction "horizontal"
if (direction === 'horizontal' || layout === 'horizontal') {
this._outerRef.scrollLeft = initialScrollOffset;
outerRef.scrollLeft = initialScrollOffset;
} else {
this._outerRef.scrollTop = initialScrollOffset;
outerRef.scrollTop = initialScrollOffset;
}
}
@ -1178,12 +1293,34 @@ function createListComponent(_ref) {
scrollOffset = _this$state.scrollOffset,
scrollUpdateWasRequested = _this$state.scrollUpdateWasRequested;
if (scrollUpdateWasRequested && this._outerRef !== null) {
// TODO Deprecate direction "horizontal"
if (scrollUpdateWasRequested && this._outerRef != null) {
var outerRef = this._outerRef; // TODO Deprecate direction "horizontal"
if (direction === 'horizontal' || layout === 'horizontal') {
this._outerRef.scrollLeft = scrollOffset;
if (direction === 'rtl') {
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// So we need to determine which browser behavior we're dealing with, and mimic it.
switch (getRTLOffsetType()) {
case 'negative':
outerRef.scrollLeft = -scrollOffset;
break;
case 'positive-ascending':
outerRef.scrollLeft = scrollOffset;
break;
default:
var clientWidth = outerRef.clientWidth,
scrollWidth = outerRef.scrollWidth;
outerRef.scrollLeft = scrollWidth - clientWidth - scrollOffset;
break;
}
} else {
outerRef.scrollLeft = scrollOffset;
}
} else {
this._outerRef.scrollTop = scrollOffset;
outerRef.scrollTop = scrollOffset;
}
}
@ -1259,7 +1396,7 @@ function createListComponent(_ref) {
ref: innerRef,
style: {
height: isHorizontal ? '100%' : estimatedTotalSize,
pointerEvents: isScrolling ? 'none' : '',
pointerEvents: isScrolling ? 'none' : undefined,
width: isHorizontal ? estimatedTotalSize : '100%'
}
}));
@ -1541,7 +1678,7 @@ createListComponent({
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset;
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
} else if (scrollOffset < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -1646,7 +1783,8 @@ createGridComponent({
var columnCount = _ref7.columnCount,
columnWidth = _ref7.columnWidth,
width = _ref7.width;
var maxOffset = Math.max(0, Math.min(columnCount * columnWidth - width, columnIndex * columnWidth));
var lastColumnOffset = Math.max(0, columnCount * columnWidth - width);
var maxOffset = Math.min(lastColumnOffset, columnIndex * columnWidth);
var minOffset = Math.max(0, columnIndex * columnWidth - width + scrollbarSize + columnWidth);
if (align === 'smart') {
@ -1665,13 +1803,27 @@ createGridComponent({
return minOffset;
case 'center':
return Math.round(minOffset + (maxOffset - minOffset) / 2);
// "Centered" offset is usually the average of the min and max.
// But near the edges of the list, this doesn't hold true.
var middleOffset = Math.round(minOffset + (maxOffset - minOffset) / 2);
if (middleOffset < Math.ceil(width / 2)) {
return 0; // near the beginning
} else if (middleOffset > lastColumnOffset + Math.floor(width / 2)) {
return lastColumnOffset; // near the end
} else {
return middleOffset;
}
case 'auto':
default:
if (scrollLeft >= minOffset && scrollLeft <= maxOffset) {
return scrollLeft;
} else if (scrollLeft - minOffset < maxOffset - scrollLeft) {
} else if (minOffset > maxOffset) {
// Because we only take into account the scrollbar size when calculating minOffset
// this value can be larger than maxOffset when at the end of the list
return minOffset;
} else if (scrollLeft < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -1683,7 +1835,8 @@ createGridComponent({
var rowHeight = _ref8.rowHeight,
height = _ref8.height,
rowCount = _ref8.rowCount;
var maxOffset = Math.max(0, Math.min(rowCount * rowHeight - height, rowIndex * rowHeight));
var lastRowOffset = Math.max(0, rowCount * rowHeight - height);
var maxOffset = Math.min(lastRowOffset, rowIndex * rowHeight);
var minOffset = Math.max(0, rowIndex * rowHeight - height + scrollbarSize + rowHeight);
if (align === 'smart') {
@ -1702,13 +1855,27 @@ createGridComponent({
return minOffset;
case 'center':
return Math.round(minOffset + (maxOffset - minOffset) / 2);
// "Centered" offset is usually the average of the min and max.
// But near the edges of the list, this doesn't hold true.
var middleOffset = Math.round(minOffset + (maxOffset - minOffset) / 2);
if (middleOffset < Math.ceil(height / 2)) {
return 0; // near the beginning
} else if (middleOffset > lastRowOffset + Math.floor(height / 2)) {
return lastRowOffset; // near the end
} else {
return middleOffset;
}
case 'auto':
default:
if (scrollTop >= minOffset && scrollTop <= maxOffset) {
return scrollTop;
} else if (scrollTop - minOffset < maxOffset - scrollTop) {
} else if (minOffset > maxOffset) {
// Because we only take into account the scrollbar size when calculating minOffset
// this value can be larger than maxOffset when at the end of the list
return minOffset;
} else if (scrollTop < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -1726,7 +1893,9 @@ createGridComponent({
columnCount = _ref10.columnCount,
width = _ref10.width;
var left = startIndex * columnWidth;
return Math.max(0, Math.min(columnCount - 1, startIndex + Math.floor((width + (scrollLeft - left)) / columnWidth)));
var numVisibleColumns = Math.ceil((width + scrollLeft - left) / columnWidth);
return Math.max(0, Math.min(columnCount - 1, startIndex + numVisibleColumns - 1 // -1 is because stop index is inclusive
));
},
getRowStartIndexForOffset: function getRowStartIndexForOffset(_ref11, scrollTop) {
var rowHeight = _ref11.rowHeight,
@ -1737,8 +1906,10 @@ createGridComponent({
var rowHeight = _ref12.rowHeight,
rowCount = _ref12.rowCount,
height = _ref12.height;
var left = startIndex * rowHeight;
return Math.max(0, Math.min(rowCount - 1, startIndex + Math.floor((height + (scrollTop - left)) / rowHeight)));
var top = startIndex * rowHeight;
var numVisibleRows = Math.ceil((height + scrollTop - top) / rowHeight);
return Math.max(0, Math.min(rowCount - 1, startIndex + numVisibleRows - 1 // -1 is because stop index is inclusive
));
},
initInstanceProps: function initInstanceProps(props) {// Noop
},
@ -1763,13 +1934,11 @@ var FixedSizeList =
/*#__PURE__*/
createListComponent({
getItemOffset: function getItemOffset(_ref, index) {
var itemSize = _ref.itemSize,
size = _ref.size;
var itemSize = _ref.itemSize;
return index * itemSize;
},
getItemSize: function getItemSize(_ref2, index) {
var itemSize = _ref2.itemSize,
size = _ref2.size;
var itemSize = _ref2.itemSize;
return itemSize;
},
getEstimatedTotalSize: function getEstimatedTotalSize(_ref3) {
@ -1787,7 +1956,8 @@ createListComponent({
// TODO Deprecate direction "horizontal"
var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
var size = isHorizontal ? width : height;
var maxOffset = Math.max(0, Math.min(itemCount * itemSize - size, index * itemSize));
var lastItemOffset = Math.max(0, itemCount * itemSize - size);
var maxOffset = Math.min(lastItemOffset, index * itemSize);
var minOffset = Math.max(0, index * itemSize - size + itemSize);
if (align === 'smart') {
@ -1806,13 +1976,25 @@ createListComponent({
return minOffset;
case 'center':
return Math.round(minOffset + (maxOffset - minOffset) / 2);
{
// "Centered" offset is usually the average of the min and max.
// But near the edges of the list, this doesn't hold true.
var middleOffset = Math.round(minOffset + (maxOffset - minOffset) / 2);
if (middleOffset < Math.ceil(size / 2)) {
return 0; // near the beginning
} else if (middleOffset > lastItemOffset + Math.floor(size / 2)) {
return lastItemOffset; // near the end
} else {
return middleOffset;
}
}
case 'auto':
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset;
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
} else if (scrollOffset < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -1836,7 +2018,9 @@ createListComponent({
var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
var offset = startIndex * itemSize;
var size = isHorizontal ? width : height;
return Math.max(0, Math.min(itemCount - 1, startIndex + Math.floor((size + (scrollOffset - offset)) / itemSize)));
var numVisibleItems = Math.ceil((size + scrollOffset - offset) / itemSize);
return Math.max(0, Math.min(itemCount - 1, startIndex + numVisibleItems - 1 // -1 is because stop index is inclusive
));
},
initInstanceProps: function initInstanceProps(props) {// Noop
},
@ -1891,3 +2075,4 @@ function shouldComponentUpdate(nextProps, nextState) {
}
export { VariableSizeGrid, VariableSizeList, FixedSizeGrid, FixedSizeList, areEqual, shouldComponentUpdate };
//# sourceMappingURL=index.esm.js.map

View File

@ -1,6 +1,6 @@
{
"name": "react-window",
"version": "1.8.0",
"version": "1.8.5",
"description":
"React components for efficiently rendering large, scrollable lists and tabular data",
"author":
@ -91,7 +91,7 @@
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-react": "^7.7.0",
"eslint-plugin-standard": "^3.0.1",
"flow-bin": "^0.80.0",
"flow-bin": "^0.103.0",
"gh-pages": "^1.1.0",
"lint-staged": "^7.0.5",
"prettier": "^1.12.1",
@ -103,6 +103,8 @@
"rollup": "^1.4.1",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^9.2.1",
"rollup-plugin-node-resolve": "^4.0.1"
"rollup-plugin-node-resolve": "^4.0.1",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^5.1.0"
}
}

View File

@ -31,12 +31,13 @@ const FixedSizeGrid = createGridComponent({
instanceProps: typeof undefined,
scrollbarSize: number
): number => {
const maxOffset = Math.max(
const lastColumnOffset = Math.max(
0,
Math.min(
columnCount * ((columnWidth: any): number) - width,
columnIndex * ((columnWidth: any): number)
)
columnCount * ((columnWidth: any): number) - width
);
const maxOffset = Math.min(
lastColumnOffset,
columnIndex * ((columnWidth: any): number)
);
const minOffset = Math.max(
0,
@ -60,12 +61,27 @@ const FixedSizeGrid = createGridComponent({
case 'end':
return minOffset;
case 'center':
return Math.round(minOffset + (maxOffset - minOffset) / 2);
// "Centered" offset is usually the average of the min and max.
// But near the edges of the list, this doesn't hold true.
const middleOffset = Math.round(
minOffset + (maxOffset - minOffset) / 2
);
if (middleOffset < Math.ceil(width / 2)) {
return 0; // near the beginning
} else if (middleOffset > lastColumnOffset + Math.floor(width / 2)) {
return lastColumnOffset; // near the end
} else {
return middleOffset;
}
case 'auto':
default:
if (scrollLeft >= minOffset && scrollLeft <= maxOffset) {
return scrollLeft;
} else if (scrollLeft - minOffset < maxOffset - scrollLeft) {
} else if (minOffset > maxOffset) {
// Because we only take into account the scrollbar size when calculating minOffset
// this value can be larger than maxOffset when at the end of the list
return minOffset;
} else if (scrollLeft < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -81,12 +97,13 @@ const FixedSizeGrid = createGridComponent({
instanceProps: typeof undefined,
scrollbarSize: number
): number => {
const maxOffset = Math.max(
const lastRowOffset = Math.max(
0,
Math.min(
rowCount * ((rowHeight: any): number) - height,
rowIndex * ((rowHeight: any): number)
)
rowCount * ((rowHeight: any): number) - height
);
const maxOffset = Math.min(
lastRowOffset,
rowIndex * ((rowHeight: any): number)
);
const minOffset = Math.max(
0,
@ -110,12 +127,27 @@ const FixedSizeGrid = createGridComponent({
case 'end':
return minOffset;
case 'center':
return Math.round(minOffset + (maxOffset - minOffset) / 2);
// "Centered" offset is usually the average of the min and max.
// But near the edges of the list, this doesn't hold true.
const middleOffset = Math.round(
minOffset + (maxOffset - minOffset) / 2
);
if (middleOffset < Math.ceil(height / 2)) {
return 0; // near the beginning
} else if (middleOffset > lastRowOffset + Math.floor(height / 2)) {
return lastRowOffset; // near the end
} else {
return middleOffset;
}
case 'auto':
default:
if (scrollTop >= minOffset && scrollTop <= maxOffset) {
return scrollTop;
} else if (scrollTop - minOffset < maxOffset - scrollTop) {
} else if (minOffset > maxOffset) {
// Because we only take into account the scrollbar size when calculating minOffset
// this value can be larger than maxOffset when at the end of the list
return minOffset;
} else if (scrollTop < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -141,14 +173,14 @@ const FixedSizeGrid = createGridComponent({
scrollLeft: number
): number => {
const left = startIndex * ((columnWidth: any): number);
const numVisibleColumns = Math.ceil(
(width + scrollLeft - left) / ((columnWidth: any): number)
);
return Math.max(
0,
Math.min(
columnCount - 1,
startIndex +
Math.floor(
(width + (scrollLeft - left)) / ((columnWidth: any): number)
)
startIndex + numVisibleColumns - 1 // -1 is because stop index is inclusive
)
);
},
@ -167,13 +199,15 @@ const FixedSizeGrid = createGridComponent({
startIndex: number,
scrollTop: number
): number => {
const left = startIndex * ((rowHeight: any): number);
const top = startIndex * ((rowHeight: any): number);
const numVisibleRows = Math.ceil(
(height + scrollTop - top) / ((rowHeight: any): number)
);
return Math.max(
0,
Math.min(
rowCount - 1,
startIndex +
Math.floor((height + (scrollTop - left)) / ((rowHeight: any): number))
startIndex + numVisibleRows - 1 // -1 is because stop index is inclusive
)
);
},

View File

@ -5,10 +5,10 @@ import createListComponent from './createListComponent';
import type { Props, ScrollToAlign } from './createListComponent';
const FixedSizeList = createListComponent({
getItemOffset: ({ itemSize, size }: Props<any>, index: number): number =>
getItemOffset: ({ itemSize }: Props<any>, index: number): number =>
index * ((itemSize: any): number),
getItemSize: ({ itemSize, size }: Props<any>, index: number): number =>
getItemSize: ({ itemSize }: Props<any>, index: number): number =>
((itemSize: any): number),
getEstimatedTotalSize: ({ itemCount, itemSize }: Props<any>) =>
@ -23,12 +23,13 @@ const FixedSizeList = createListComponent({
// TODO Deprecate direction "horizontal"
const isHorizontal = direction === 'horizontal' || layout === 'horizontal';
const size = (((isHorizontal ? width : height): any): number);
const maxOffset = Math.max(
const lastItemOffset = Math.max(
0,
Math.min(
itemCount * ((itemSize: any): number) - size,
index * ((itemSize: any): number)
)
itemCount * ((itemSize: any): number) - size
);
const maxOffset = Math.min(
lastItemOffset,
index * ((itemSize: any): number)
);
const minOffset = Math.max(
0,
@ -51,13 +52,25 @@ const FixedSizeList = createListComponent({
return maxOffset;
case 'end':
return minOffset;
case 'center':
return Math.round(minOffset + (maxOffset - minOffset) / 2);
case 'center': {
// "Centered" offset is usually the average of the min and max.
// But near the edges of the list, this doesn't hold true.
const middleOffset = Math.round(
minOffset + (maxOffset - minOffset) / 2
);
if (middleOffset < Math.ceil(size / 2)) {
return 0; // near the beginning
} else if (middleOffset > lastItemOffset + Math.floor(size / 2)) {
return lastItemOffset; // near the end
} else {
return middleOffset;
}
}
case 'auto':
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset;
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
} else if (scrollOffset < minOffset) {
return minOffset;
} else {
return maxOffset;
@ -83,14 +96,14 @@ const FixedSizeList = createListComponent({
const isHorizontal = direction === 'horizontal' || layout === 'horizontal';
const offset = startIndex * ((itemSize: any): number);
const size = (((isHorizontal ? width : height): any): number);
const numVisibleItems = Math.ceil(
(size + scrollOffset - offset) / ((itemSize: any): number)
);
return Math.max(
0,
Math.min(
itemCount - 1,
startIndex +
Math.floor(
(size + (scrollOffset - offset)) / ((itemSize: any): number)
)
startIndex + numVisibleItems - 1 // -1 is because stop index is inclusive
)
);
},

View File

@ -274,7 +274,11 @@ const getOffsetForIndexAndAlignment = (
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset;
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
} else if (minOffset > maxOffset) {
// Because we only take into account the scrollbar size when calculating minOffset
// this value can be larger than maxOffset when at the end of the list
return minOffset;
} else if (scrollOffset < minOffset) {
return minOffset;
} else {
return maxOffset;

View File

@ -227,7 +227,7 @@ const VariableSizeList = createListComponent({
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset;
} else if (scrollOffset - minOffset < maxOffset - scrollOffset) {
} else if (scrollOffset < minOffset) {
return minOffset;
} else {
return maxOffset;

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, getRTLOffsetType } from './domHelpers';
import type { TimeoutID } from './timer';
@ -46,6 +46,22 @@ type OnScrollCallback = ({
type ScrollEvent = SyntheticEvent<HTMLDivElement>;
type ItemStyleCache = { [key: string]: Object };
type OuterProps = {|
children: React$Node,
className: string | void,
onScroll: ScrollEvent => void,
style: {
[string]: mixed,
},
|};
type InnerProps = {|
children: React$Node,
style: {
[string]: mixed,
},
|};
export type Props<T> = {|
children: RenderComponent<T>,
className?: string,
@ -56,7 +72,7 @@ export type Props<T> = {|
initialScrollLeft?: number,
initialScrollTop?: number,
innerRef?: any,
innerElementType?: React$ElementType,
innerElementType?: string | React$AbstractComponent<InnerProps, any>,
innerTagName?: string, // deprecated
itemData: T,
itemKey?: (params: {|
@ -67,11 +83,13 @@ export type Props<T> = {|
onItemsRendered?: OnItemsRenderedCallback,
onScroll?: OnScrollCallback,
outerRef?: any,
outerElementType?: React$ElementType,
outerElementType?: string | React$AbstractComponent<OuterProps, any>,
outerTagName?: string, // deprecated
overscanColumnsCount?: number,
overscanColumnCount?: number,
overscanColumnsCount?: number, // deprecated
overscanCount?: number, // deprecated
overscanRowsCount?: number,
overscanRowCount?: number,
overscanRowsCount?: number, // deprecated
rowCount: number,
rowHeight: itemSize,
style?: Object,
@ -130,10 +148,12 @@ const defaultItemKey = ({ columnIndex, data, rowIndex }) =>
// In DEV mode, this Set helps us only log a warning once per component instance.
// This avoids spamming the console every time a render happens.
let devWarningsOverscanCount = null;
let devWarningsOverscanRowsColumnsCount = null;
let devWarningsTagName = null;
if (process.env.NODE_ENV !== 'production') {
if (typeof window !== 'undefined' && typeof window.WeakSet !== 'undefined') {
devWarningsOverscanCount = new WeakSet();
devWarningsOverscanRowsColumnsCount = new WeakSet();
devWarningsTagName = new WeakSet();
}
}
@ -320,21 +340,47 @@ 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) {
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// So we need to determine which browser behavior we're dealing with, and mimic it.
const outerRef = ((this._outerRef: any): HTMLElement);
if (direction === 'rtl') {
switch (getRTLOffsetType()) {
case 'negative':
outerRef.scrollLeft = -scrollLeft;
break;
case 'positive-ascending':
outerRef.scrollLeft = scrollLeft;
break;
default:
const { clientWidth, scrollWidth } = outerRef;
outerRef.scrollLeft = scrollWidth - clientWidth - scrollLeft;
break;
}
} else {
outerRef.scrollLeft = Math.max(0, scrollLeft);
}
outerRef.scrollTop = Math.max(0, scrollTop);
}
this._callPropsCallbacks();
@ -432,7 +478,7 @@ export default function createGridComponent({
ref: innerRef,
style: {
height: estimatedTotalHeight,
pointerEvents: isScrolling ? 'none' : '',
pointerEvents: isScrolling ? 'none' : undefined,
width: estimatedTotalWidth,
},
})
@ -586,6 +632,7 @@ export default function createGridComponent({
_getHorizontalRangeToRender(): [number, number, number, number] {
const {
columnCount,
overscanColumnCount,
overscanColumnsCount,
overscanCount,
rowCount,
@ -593,7 +640,7 @@ export default function createGridComponent({
const { horizontalScrollDirection, isScrolling, scrollLeft } = this.state;
const overscanCountResolved: number =
overscanColumnsCount || overscanCount || 1;
overscanColumnCount || overscanColumnsCount || overscanCount || 1;
if (columnCount === 0 || rowCount === 0) {
return [0, 0, 0, 0];
@ -634,13 +681,14 @@ export default function createGridComponent({
const {
columnCount,
overscanCount,
overscanRowCount,
overscanRowsCount,
rowCount,
} = this.props;
const { isScrolling, verticalScrollDirection, scrollTop } = this.state;
const overscanCountResolved: number =
overscanRowsCount || overscanCount || 1;
overscanRowCount || overscanRowsCount || overscanCount || 1;
if (columnCount === 0 || rowCount === 0) {
return [0, 0, 0, 0];
@ -679,9 +727,11 @@ export default function createGridComponent({
_onScroll = (event: ScrollEvent): void => {
const {
clientHeight,
clientWidth,
scrollLeft,
scrollTop,
scrollHeight,
scrollWidth,
} = event.currentTarget;
this.setState(prevState => {
@ -697,24 +747,38 @@ export default function createGridComponent({
const { direction } = this.props;
// HACK 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
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// It's also easier for this component if we convert offsets to the same format as they would be in for ltr.
// So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it.
let calculatedScrollLeft = scrollLeft;
if (direction === 'rtl') {
if (scrollLeft <= 0) {
calculatedScrollLeft = -scrollLeft;
} else {
calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
switch (getRTLOffsetType()) {
case 'negative':
calculatedScrollLeft = -scrollLeft;
break;
case 'positive-descending':
calculatedScrollLeft = scrollWidth - clientWidth - scrollLeft;
break;
}
}
// Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
calculatedScrollLeft = Math.max(
0,
Math.min(calculatedScrollLeft, scrollWidth - clientWidth)
);
const calculatedScrollTop = Math.max(
0,
Math.min(scrollTop, scrollHeight - clientHeight)
);
return {
isScrolling: true,
horizontalScrollDirection:
prevState.scrollLeft < scrollLeft ? 'forward' : 'backward',
scrollLeft: calculatedScrollLeft,
scrollTop,
scrollTop: calculatedScrollTop,
verticalScrollDirection:
prevState.scrollTop < scrollTop ? 'forward' : 'backward',
scrollUpdateWasRequested: false,
@ -768,7 +832,9 @@ const validateSharedProps = (
height,
innerTagName,
outerTagName,
overscanColumnsCount,
overscanCount,
overscanRowsCount,
width,
}: Props<any>,
{ instance }: State
@ -779,7 +845,23 @@ const validateSharedProps = (
devWarningsOverscanCount.add(instance);
console.warn(
'The overscanCount prop has been deprecated. ' +
'Please use the overscanColumnsCount and overscanRowsCount props instead.'
'Please use the overscanColumnCount and overscanRowCount props instead.'
);
}
}
if (
typeof overscanColumnsCount === 'number' ||
typeof overscanRowsCount === 'number'
) {
if (
devWarningsOverscanRowsColumnsCount &&
!devWarningsOverscanRowsColumnsCount.has(instance)
) {
devWarningsOverscanRowsColumnsCount.add(instance);
console.warn(
'The overscanColumnsCount and overscanRowsCount props have been deprecated. ' +
'Please use the overscanColumnCount and overscanRowCount props instead.'
);
}
}

View File

@ -3,6 +3,7 @@
import memoizeOne from 'memoize-one';
import { createElement, PureComponent } from 'react';
import { cancelTimeout, requestTimeout } from './timer';
import { getRTLOffsetType } from './domHelpers';
import type { TimeoutID } from './timer';
@ -38,6 +39,22 @@ type onScrollCallback = ({
type ScrollEvent = SyntheticEvent<HTMLDivElement>;
type ItemStyleCache = { [index: number]: Object };
type OuterProps = {|
children: React$Node,
className: string | void,
onScroll: ScrollEvent => void,
style: {
[string]: mixed,
},
|};
type InnerProps = {|
children: React$Node,
style: {
[string]: mixed,
},
|};
export type Props<T> = {|
children: RenderComponent<T>,
className?: string,
@ -45,7 +62,7 @@ export type Props<T> = {|
height: number | string,
initialScrollOffset?: number,
innerRef?: any,
innerElementType?: React$ElementType,
innerElementType?: string | React$AbstractComponent<InnerProps, any>,
innerTagName?: string, // deprecated
itemCount: number,
itemData: T,
@ -55,7 +72,7 @@ export type Props<T> = {|
onItemsRendered?: onItemsRenderedCallback,
onScroll?: onScrollCallback,
outerRef?: any,
outerElementType?: React$ElementType,
outerElementType?: string | React$AbstractComponent<OuterProps, any>,
outerTagName?: string, // deprecated
overscanCount: number,
style?: Object,
@ -215,14 +232,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;
}
}
@ -233,12 +249,32 @@ 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') {
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// So we need to determine which browser behavior we're dealing with, and mimic it.
switch (getRTLOffsetType()) {
case 'negative':
outerRef.scrollLeft = -scrollOffset;
break;
case 'positive-ascending':
outerRef.scrollLeft = scrollOffset;
break;
default:
const { clientWidth, scrollWidth } = outerRef;
outerRef.scrollLeft = scrollWidth - clientWidth - scrollOffset;
break;
}
} else {
outerRef.scrollLeft = scrollOffset;
}
} else {
((this._outerRef: any): HTMLDivElement).scrollTop = scrollOffset;
outerRef.scrollTop = scrollOffset;
}
}
@ -326,7 +362,7 @@ export default function createListComponent({
ref: innerRef,
style: {
height: isHorizontal ? '100%' : estimatedTotalSize,
pointerEvents: isScrolling ? 'none' : '',
pointerEvents: isScrolling ? 'none' : undefined,
width: isHorizontal ? estimatedTotalSize : '100%',
},
})
@ -496,18 +532,28 @@ export default function createListComponent({
const { direction } = this.props;
// HACK According to the spec, scrollLeft should be negative for RTL aligned elements.
// Chrome does not seem to adhere; its scrolLeft 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') {
if (scrollLeft <= 0) {
scrollOffset = -scrollOffset;
} else {
scrollOffset = scrollWidth - clientWidth - scrollLeft;
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
// This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
// It's also easier for this component if we convert offsets to the same format as they would be in for ltr.
// So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it.
switch (getRTLOffsetType()) {
case 'negative':
scrollOffset = -scrollLeft;
break;
case 'positive-descending':
scrollOffset = scrollWidth - clientWidth - scrollLeft;
break;
}
}
// Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
scrollOffset = Math.max(
0,
Math.min(scrollOffset, scrollWidth - clientWidth)
);
return {
isScrolling: true,
scrollDirection:
@ -519,7 +565,7 @@ export default function createListComponent({
};
_onScrollVertical = (event: ScrollEvent): void => {
const { scrollTop } = event.currentTarget;
const { clientHeight, scrollHeight, scrollTop } = event.currentTarget;
this.setState(prevState => {
if (prevState.scrollOffset === scrollTop) {
// Scroll position may have been updated by cDM/cDU,
@ -528,11 +574,17 @@ export default function createListComponent({
return null;
}
// Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
const scrollOffset = Math.max(
0,
Math.min(scrollTop, scrollHeight - clientHeight)
);
return {
isScrolling: true,
scrollDirection:
prevState.scrollOffset < scrollTop ? 'forward' : 'backward',
scrollOffset: scrollTop,
prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',
scrollOffset,
scrollUpdateWasRequested: false,
};
}, this._resetIsScrollingDebounced);

View File

@ -20,3 +20,53 @@ export function getScrollbarSize(recalculate?: boolean = false): number {
return size;
}
export type RTLOffsetType =
| 'negative'
| 'positive-descending'
| 'positive-ascending';
let cachedRTLResult: RTLOffsetType | 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 getRTLOffsetType(recalculate?: boolean = false): RTLOffsetType {
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);
if (outerDiv.scrollLeft > 0) {
cachedRTLResult = 'positive-descending';
} else {
outerDiv.scrollLeft = 1;
if (outerDiv.scrollLeft === 0) {
cachedRTLResult = 'negative';
} else {
cachedRTLResult = 'positive-ascending';
}
}
((document.body: any): HTMLBodyElement).removeChild(outerDiv);
return cachedRTLResult;
}
return cachedRTLResult;
}

View File

@ -766,9 +766,9 @@
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.0.0":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
dependencies:
regenerator-runtime "^0.13.2"
@ -5522,10 +5522,10 @@ flatstr@^1.0.9:
resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.9.tgz#0950d56fec02de1030c1311847ecd58c25690eb9"
integrity sha512-qFlJnOBWDfIaunF54/lBqNKmXOI0HqNhu+mHkLmbaBXlS71PUd9OjFOdyevHt/aHoHB1+eW7eKHgRKOG5aHSpw==
flow-bin@^0.97.0:
version "0.97.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.97.0.tgz#036ffcfc27503367a9d906ec9d843a0aa6f6bb83"
integrity sha512-jXjD05gkatLuC4+e28frH1hZoRwr1iASP6oJr61Q64+kR4kmzaS+AdFBhYgoYS5kpoe4UzwDebWK8ETQFNh00w==
flow-bin@^0.103.0:
version "0.103.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.103.0.tgz#7aec510d85e1c1b0f2b912bb988337d70035cb0f"
integrity sha512-Y3yrnE5ICN1Kl/y10BwjA3JSuS+gt4jVPNyUNCZb0RqmkdssMrW8QNNysJYvhgAY/JBJH8Qv7NVUf11MiwfSlA==
fluent-syntax@0.10.0:
version "0.10.0"
@ -8250,9 +8250,9 @@ mem@^4.0.0:
p-is-promise "^2.0.0"
"memoize-one@>=3.1.1 <6":
version "5.0.4"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.4.tgz#005928aced5c43d890a4dfab18ca908b0ec92cbc"
integrity sha512-P0z5IeAH6qHHGkJIXWw0xC2HNEgkx/9uWWBQw64FJj3/ol14VYdfVGWWr0fXfjhhv3TKVIqUq65os6O4GUNksA==
version "5.0.5"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.5.tgz#8cd3809555723a07684afafcd6f756072ac75d7e"
integrity sha512-ey6EpYv0tEaIbM/nTDOpHciXUvd+ackQrJgEzBwemhZZIWZjcyodqEcrmqDy2BKRTM3a65kKBV4WtLXJDt26SQ==
memoize-one@^3.1.1:
version "3.1.1"
@ -10183,9 +10183,9 @@ regenerator-runtime@^0.12.0:
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regenerator-runtime@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
version "0.13.3"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==
regenerator-runtime@^0.9.5:
version "0.9.6"